diff --git a/docs/02-architecture/adr/ADR-0016-declarative-template-format.md b/docs/02-architecture/adr/ADR-0016-declarative-template-format.md index d63e5fb7c19..e12223f7243 100644 --- a/docs/02-architecture/adr/ADR-0016-declarative-template-format.md +++ b/docs/02-architecture/adr/ADR-0016-declarative-template-format.md @@ -80,7 +80,7 @@ Schemas. The two MCP scenarios are the conformance fixtures: identical `condition` that is the **shared Decision-7 grammar**, not option-bound logic. **No configuration payload** hangs off an option — the v3 `option.data` / `JSON.parse(option.data)` overload does **not** exist (it is absent from - `question.schema.json`); configuration lives in `descriptor.optionsSchema` and + `questions.schema.json`); configuration lives in `descriptor.optionsSchema` and computed fields in the provider `derived..` namespace (§5). Exactly one of `staticOptions` / `optionsFrom` per option-bearing question. `optionsFrom` names an engine-owned provider diff --git a/docs/02-architecture/scaffolding.backlog.md b/docs/02-architecture/scaffolding.backlog.md index 9c29821ffeb..d50b89b9393 100644 --- a/docs/02-architecture/scaffolding.backlog.md +++ b/docs/02-architecture/scaffolding.backlog.md @@ -146,6 +146,15 @@ decided by ADR-0014 … ADR-0019: (b) is clean but needs two invocations. The ADR-0014 dispatcher *resolution* contract does not depend on which is chosen; only the help UX does. +- **v3 parity gap — DA api-spec route not `KiotaNPMIntegration`-gated + (recorded, not fixed).** v3 `daProjectTypeNode` chooses `apiSpecWithSearchNode()` + (url/file/search split) vs `apiSpecNode()` (single `singleFileOrText`) on + `FeatureFlags.KiotaNPMIntegration` (default `"true"`). The v4 `openapi` + selector route always targets the split-form `da/api-plugin-from-existing-api`. + Default-consistent (flag on → both split); diverges only when the flag is off. + Full parity would add a flag-gated route + a `singleFileOrText` DA api-plugin + variant (mirrors the mcp DT split `da/mcp-server` vs `da/mcp-server-static`). + (Found 2026-07-07.) ## 3. Visual Studio multi-project surface diff --git a/docs/03-specs/operations/scaffolding/collect-create-inputs.md b/docs/03-specs/operations/scaffolding/collect-create-inputs.md index ac535b6067f..399ca7e359a 100644 --- a/docs/03-specs/operations/scaffolding/collect-create-inputs.md +++ b/docs/03-specs/operations/scaffolding/collect-create-inputs.md @@ -154,6 +154,12 @@ flowchart TD walked by the surface-neutral driver, and the common floor questions are operation-owned normalized `QuestionSpec`s in the same walk. Neither is ever rehydrated into a v3 `IQTreeNode` (ADR-0016 decision 6 / collect-inputs INV-1). + A `questions` array item may also be a `{ "use": "" }` reference to a + shared fragment under `v4/_shared/questions/.json`; the loader splices the + fragment's own `questions` in place (recursively; bare-name / cycle guarded) so + the walk always sees one flat `QuestionSpec[]`. This is authoring-time reuse + only (e.g. the `llm-service` fragment shared by the custom-copilot / CEA + templates) — it changes neither the resolved questions nor any behavior. - **INV-3** — A v4 identity-only `OptionItem` carries no configuration payload across the bridge (no v3 `option.data`); only its `id` round-trips (collect-inputs INV-2). diff --git a/docs/03-specs/operations/scaffolding/walk-create-selector.md b/docs/03-specs/operations/scaffolding/walk-create-selector.md index 22eae873b4d..fa5217b1b31 100644 --- a/docs/03-specs/operations/scaffolding/walk-create-selector.md +++ b/docs/03-specs/operations/scaffolding/walk-create-selector.md @@ -47,7 +47,8 @@ else: 1. **The presentation read** — for full-package floor bytes, `openCreateSelectorPresentation(bytes)` projects `v4/create/selector.json` onto a `SelectorPresentation` (each question's `title` / `placeholder` / - `staticOptions[{ id, label, detail?, groupName?, condition? }]`). For a staged + `keyPrefix?` / `staticOptions[{ id, label, detail?, groupName?, keyPrefix?, + condition? }]`). For a staged selector artifact, the same projection runs over the standalone JSON bytes. It is the presentation sibling of `openCreateSelector` / `openSelectorFromJsonBytes` (which keep only `{ name, condition }` for @@ -56,7 +57,10 @@ else: `RouteQuestion`, looks up its presentation by `name`, filters its `staticOptions` by each option's environment `condition` (the shared evaluator over a `{ surface }` scope + the injected feature-flag reader), - renders the survivors via `UserInteraction.selectOption`, and returns the + renders the survivors via `UserInteraction.selectOption` — localizing each + question's `title` / `placeholder` and each option's `label` / `detail` / + `groupName` through its `keyPrefix` (the shared `localizePrefixedText` over + the NLS bundle, with the authored literal as fallback) — and returns the chosen `id`. A surface cancellation surfaces as the `Result` error. 3. **The port assembly + run** — build the shared question-walk port from the prompt face, the shared validator registry, and the shared evaluator; route @@ -68,9 +72,12 @@ else: It does **not** translate the answers to v3 inputs, does **not** ask Q2, does **not** scaffold, and adds **no** routing grammar — every question `condition`, option `condition`, and route `when` is the one shared `evaluate-expression` -grammar (resolve-build-target INV-3). Option labels are the authored English -fallback; `keyPrefix` localization is a tracked follow-up and rides the same -presentation, so it does not change this operation's shape. +grammar (resolve-build-target INV-3). User-visible text localizes through each +question's / option's `keyPrefix` (`.{title|placeholder|label|detail| +groupName}` in the NLS bundle) with the authored English literal as fallback — +the same shared `localizePrefixedText` the Q2 + common-floor bridge uses +([`collect-create-inputs`](collect-create-inputs.md)), so Q1 and Q2/Q3 share one +localization mechanism. ## Inputs @@ -122,6 +129,7 @@ presentation, so it does not change this operation's shape. | WCS-11 | L1 | the floor, `interactive=false`, `prefilled={ projectType:"copilot-agent-type" }` (missing `daTemplate` / `actionSource`), a UI that throws if called | `runCreateSelector` | `err` — a `UserError` naming the missing required dimension; **no** `ui.selectOption` call, **no** silent route coercion (resolve-build-target AC-03b) | | WCS-12 | L1 | the real shipped floor, `surface="vscode"`, `flagReader(TEAMSFX_AGENT_SKILLS)=false`, a scripted UI reaching `daTemplate` (after `projectType=copilot-agent-type`) | `runCreateSelector` (prompt face) | `ui.selectOption` is offered the `daTemplate` options **without** `skill` — the option-level `featureFlag('TEAMSFX_AGENT_SKILLS')` condition filters it; the always-on options (e.g. `no-action`) remain | | WCS-13 | L1 | the floor, `flagReader(TEAMSFX_AGENT_SKILLS)=true`, a scripted UI picking `projectType=copilot-agent-type` → `daTemplate=skill` | `runCreateSelector` | the `skill` option **is offered**; `ok` `BuildTarget` `{ templateId:"da/skill", engine:"v4" }`; the walk ends at `daTemplate` (no `actionSource` — that is `add-action` only) and `answers` carry both picks; the route's `featureFlag('TEAMSFX_AGENT_SKILLS')` gate is honored | +| WCS-23 | L1 | the real shipped floor, `surface="vscode"`, a scripted UI picking `projectType=copilot-agent-type` → `daTemplate=no-action` | `runCreateSelector` (prompt face) | the `projectType` prompt localizes its `title` and each option's `label` / `detail` via `.{title\|label\|detail}` from the NLS bundle — e.g. `blank-app-type` renders its NLS label (which differs from the authored literal `"Blank App"`) with the authored icon preserved — falling back to the authored literal when no key is registered | | WCS-18 | L1 | the floor, a scripted UI picking `projectType=copilot-agent-type` → `daTemplate=typespec` | `runCreateSelector` | `ok` `BuildTarget` `{ templateId:"da/typespec", engine:"v4" }`; the walk ends at `daTemplate` and `answers` carry both picks | | WCS-19 | L1 | the floor, a scripted UI picking `projectType=copilot-agent-type` → `daTemplate=graph-connector` | `runCreateSelector` | `ok` `BuildTarget` `{ templateId:"da/graph-connector", engine:"v4" }`; the walk ends at `daTemplate` and `answers` carry both picks | | WCS-20 | L1 | the floor, a scripted UI picking `projectType=graph-connector-type` | `runCreateSelector` | `ok` `BuildTarget` `{ templateId:"graph-connector", engine:"v4" }`; the walk ends at `projectType` and `answers` carry that pick | diff --git a/packages/fx-core/src/common/secretmasker/feature.ts b/packages/fx-core/src/common/secretmasker/feature.ts index 98a4dcde79e..3f5f8dd6aef 100644 --- a/packages/fx-core/src/common/secretmasker/feature.ts +++ b/packages/fx-core/src/common/secretmasker/feature.ts @@ -359,7 +359,7 @@ export function extractFeatures(text: string): Token[] { const allTokens = tokenize(text); const featureTokens = allTokens.filter((t) => t.type === "feature"); for (let i = 0; i < featureTokens.length; i++) { - const tokenObj = featureTokens[i] as FeatureToken; + const tokenObj = featureTokens[i]; let token = tokenObj.token; if (token.endsWith("")) { diff --git a/packages/fx-core/src/component/coordinator/index.ts b/packages/fx-core/src/component/coordinator/index.ts index facb19ac7de..25b54f4fb19 100644 --- a/packages/fx-core/src/component/coordinator/index.ts +++ b/packages/fx-core/src/component/coordinator/index.ts @@ -286,7 +286,7 @@ class Coordinator { projectModel.registerApp, projectModel.provision, projectModel.configureApp, - ].filter((c) => c !== undefined) as ILifecycle[]; + ].filter((c) => c !== undefined); // 2. check each cycle for (const cycle of cycles) { @@ -330,9 +330,7 @@ class Coordinator { return err(maybeProjectModel.error); } const projectModel = maybeProjectModel.value; - const cycles: ILifecycle[] = [projectModel.provision].filter( - (c) => c !== undefined - ) as ILifecycle[]; + const cycles: ILifecycle[] = [projectModel.provision].filter((c) => c !== undefined); let unresolvedPlaceholders: string[] = []; // 2. check each cycle diff --git a/packages/fx-core/src/component/deps-checker/internal/dotnetChecker.ts b/packages/fx-core/src/component/deps-checker/internal/dotnetChecker.ts index 9138aba95bd..c13a0755ca0 100644 --- a/packages/fx-core/src/component/deps-checker/internal/dotnetChecker.ts +++ b/packages/fx-core/src/component/deps-checker/internal/dotnetChecker.ts @@ -333,7 +333,7 @@ export class DotnetChecker implements DepsChecker { const dotnetSdks: DotnetSDK[] = await this.searchDotnetSdks(dotnetExecPath); const installedVersions = dotnetSdks .map((sdk) => DotnetChecker.parseDotnetVersion(sdk.version)) - .filter((version) => version !== null) as string[]; + .filter((version) => version !== null); return this.isDotnetVersionsInstalled(installedVersions); } catch (error) { const errorMessage = `validate private install failed, error = '${ diff --git a/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts b/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts index eea90d31788..9c568f76157 100644 --- a/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts +++ b/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts @@ -687,7 +687,7 @@ export class CopilotGptManifestUtils { const capability = agentManifest.capabilities.find( (cap) => cap.name === DeclarativeCopilotCapabilityName.OneDriveAndSharePoint - ) as OneDriveAndSharePointCapability | undefined; + ); if (items_by_url) { newCapabilityData.items_by_url = capability ? capability.items_by_url || [] : []; @@ -728,7 +728,7 @@ export class CopilotGptManifestUtils { }; const capability = agentManifest.capabilities.find( (cap) => cap.name === DeclarativeCopilotCapabilityName.WebSearch - ) as WebSearchCapability | undefined; + ); // del capability warning if (items_by_url === null && capability?.sites?.length) { diff --git a/packages/fx-core/src/component/utils/envUtil.ts b/packages/fx-core/src/component/utils/envUtil.ts index a04a830aa05..945c158229f 100644 --- a/packages/fx-core/src/component/utils/envUtil.ts +++ b/packages/fx-core/src/component/utils/envUtil.ts @@ -238,7 +238,7 @@ class EnvUtil { const list = await fs.readdir(envFolderPath); let envs = list .map((fileName) => this.extractEnvNameFromFileName(fileName)) - .filter((env) => env !== undefined) as string[]; + .filter((env) => env !== undefined); if (remoteOnly) { envs = envs.filter((env) => environmentNameManager.isRemoteEnvironment(env)); } @@ -295,8 +295,7 @@ export const envUtil = new EnvUtil(); const NEW_LINE_SPLITTER = /\r?\n/; type DotenvParsedLine = - | string - | { key: string; value: string; comment?: string; quote?: '"' | "'" }; + string | { key: string; value: string; comment?: string; quote?: '"' | "'" }; interface DotenvParseResult { lines?: DotenvParsedLine[]; obj: DotenvOutput; diff --git a/packages/fx-core/src/question/other.ts b/packages/fx-core/src/question/other.ts index 6b5fe54347f..fcf22dba7bc 100644 --- a/packages/fx-core/src/question/other.ts +++ b/packages/fx-core/src/question/other.ts @@ -1132,7 +1132,7 @@ export function apiSpecFromPluginManifestQuestion(): SingleSelectQuestion { const pluginManifest = (await fs.readJson(pluginManifestPath)) as PluginManifestSchema; const specs = pluginManifest .runtimes!.filter((runtime) => runtime.type === "OpenApi") - .map((runtime) => runtime.spec.url as string); + .map((runtime) => runtime.spec.url); return [...new Set(specs)]; }, }; diff --git a/packages/fx-core/src/v4/buildTarget/parseSelector.ts b/packages/fx-core/src/v4/buildTarget/parseSelector.ts index 6d8c1982555..621d28721a2 100644 --- a/packages/fx-core/src/v4/buildTarget/parseSelector.ts +++ b/packages/fx-core/src/v4/buildTarget/parseSelector.ts @@ -131,6 +131,7 @@ export interface PresentationOption { detail?: string; groupName?: string; iconPath?: string; + keyPrefix?: string; condition?: ConditionNode; } @@ -139,6 +140,7 @@ export interface PresentationQuestion { name: string; title?: string; placeholder?: string; + keyPrefix?: string; staticOptions: PresentationOption[]; } @@ -173,6 +175,10 @@ function parsePresentationOption(raw: unknown): Result; inputOptionItem?: OptionItem; inputBoxConfig?: InputBoxConfig; diff --git a/packages/fx-core/src/v4/distribution/createQuestions.ts b/packages/fx-core/src/v4/distribution/createQuestions.ts index a7a7a8e29ff..9a23367fece 100644 --- a/packages/fx-core/src/v4/distribution/createQuestions.ts +++ b/packages/fx-core/src/v4/distribution/createQuestions.ts @@ -3,48 +3,20 @@ import { FxError, SystemError } from "@microsoft/teamsfx-api"; import AdmZip from "adm-zip"; -import { Result, err, ok } from "neverthrow"; +import { Result, err } from "neverthrow"; import { QuestionSpec } from "../collectInputs/collectInputs"; import { DeclarativeLocator } from "../model/dataModel"; +import { resolveQuestions, zipFragmentReader } from "./questionFragments"; /** Load one create template's `questions.json`. See collect-create-inputs spec. */ const SOURCE = "Scaffold"; -/** The native question kinds `questions.json` may declare (collect-inputs `QuestionType`). */ -const QUESTION_TYPES: ReadonlySet = new Set([ - "singleSelect", - "multiSelect", - "text", - "confirm", - "singleFile", - "folder", - "singleFileOrText", -]); - /** The `v4///questions.json` entry this locator resolves to. */ function questionsEntry(locator: DeclarativeLocator): string { return `v4/${locator.kind}/${locator.templateId}/questions.json`; } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -/** Predicate-narrow a parsed `questions` array without an unchecked cast. */ -function isQuestionSpecArray(value: unknown): value is QuestionSpec[] { - return ( - Array.isArray(value) && - value.every( - (item) => - isRecord(item) && - typeof item.name === "string" && - typeof item.type === "string" && - QUESTION_TYPES.has(item.type) - ) - ); -} - export function openCreateQuestions( bytes: Buffer, locator: DeclarativeLocator @@ -98,15 +70,5 @@ export function openCreateQuestions( ); } - if (!isRecord(parsed) || !isQuestionSpecArray(parsed.questions)) { - return err( - new SystemError({ - source: SOURCE, - name: "PackageFileInvalid", - message: `The template package file "${entryPath}" must be an object with a "questions" array.`, - }) - ); - } - - return ok(parsed.questions); + return resolveQuestions(parsed, entryPath, zipFragmentReader(zip)); } diff --git a/packages/fx-core/src/v4/distribution/declarativePackage.ts b/packages/fx-core/src/v4/distribution/declarativePackage.ts index d911a8d395e..e2890da36a8 100644 --- a/packages/fx-core/src/v4/distribution/declarativePackage.ts +++ b/packages/fx-core/src/v4/distribution/declarativePackage.ts @@ -7,6 +7,7 @@ import { Result, err, ok } from "neverthrow"; import { QuestionSpec } from "../collectInputs/collectInputs"; import { DeclarativeLocator, TemplateFileEntry } from "../model/dataModel"; import { LoadedPackage } from "./packageDir"; +import { resolveQuestions, zipFragmentReader } from "./questionFragments"; /** Open one declarative package subtree from channel zip bytes. See open-template-package spec. */ @@ -47,46 +48,6 @@ function missingFile(file: string): FxError { }); } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -const QUESTION_TYPES: ReadonlySet = new Set([ - "singleSelect", - "multiSelect", - "text", - "confirm", - "singleFile", - "folder", - "singleFileOrText", -]); - -function isQuestionSpecArray(value: unknown): value is QuestionSpec[] { - return ( - Array.isArray(value) && - value.every( - (item) => - isRecord(item) && - typeof item.name === "string" && - typeof item.type === "string" && - QUESTION_TYPES.has(item.type) - ) - ); -} - -function parseQuestions(raw: unknown, file: string): Result { - if (!isRecord(raw) || !isQuestionSpecArray(raw.questions)) { - return err( - new SystemError({ - source: SOURCE, - name: "PackageFileInvalid", - message: `The template package file "${file}" must be an object with a "questions" array.`, - }) - ); - } - return ok(raw.questions); -} - interface PackageMetadataEntries { descriptorRaw?: string; questionsRaw?: string; @@ -230,7 +191,11 @@ export function openDeclarativePackageMetadata( if (questionsRaw.isErr()) { return err(questionsRaw.error); } - const questions = parseQuestions(questionsRaw.value, `${root}questions.json`); + const questions = resolveQuestions( + questionsRaw.value, + `${root}questions.json`, + zipFragmentReader(zip.value) + ); if (questions.isErr()) { return err(questions.error); } diff --git a/packages/fx-core/src/v4/distribution/questionFragments.ts b/packages/fx-core/src/v4/distribution/questionFragments.ts new file mode 100644 index 00000000000..bcdf612ba15 --- /dev/null +++ b/packages/fx-core/src/v4/distribution/questionFragments.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { FxError, SystemError } from "@microsoft/teamsfx-api"; +import AdmZip from "adm-zip"; +import { Result, err, ok } from "neverthrow"; +import { QuestionSpec } from "../collectInputs/collectInputs"; + +/** + * Resolve shared question fragments. A `questions` array may contain + * `{ "use": "" }` entries that reference a reusable fragment under + * `v4/_shared/questions/.json`; the loader splices the fragment's own + * `questions` in place (recursively) so `collect-inputs` always sees one flat + * `QuestionSpec[]`. See open-template-package + collect-create-inputs specs. + */ + +const SOURCE = "Scaffold"; + +/** Channel path prefix under which shared question fragments live. */ +const FRAGMENT_DIR = "v4/_shared/questions/"; + +/** A fragment name is a bare identifier — no path separators (Zip-Slip guard). */ +const FRAGMENT_NAME = /^[A-Za-z][A-Za-z0-9-]*$/; + +/** The native question kinds a `questions.json` (or fragment) may declare. */ +const QUESTION_TYPES: ReadonlySet = new Set([ + "singleSelect", + "multiSelect", + "text", + "confirm", + "singleFile", + "folder", + "singleFileOrText", +]); + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** A single native question, narrowed without an unchecked cast. */ +function isQuestionSpec(value: unknown): value is QuestionSpec { + return ( + isRecord(value) && + typeof value.name === "string" && + typeof value.type === "string" && + QUESTION_TYPES.has(value.type) + ); +} + +/** A `{ "use": "" }` fragment reference (no `name`, which marks a question). */ +function isUseRef(value: unknown): value is { use: string } { + return isRecord(value) && typeof value.use === "string" && !("name" in value); +} + +/** Read one fragment's raw JSON by name; the reader owns the source (zip / disk). */ +export type FragmentReader = (name: string) => Result; + +function invalidQuestionsFile(file: string): FxError { + return new SystemError({ + source: SOURCE, + name: "PackageFileInvalid", + message: `The template package file "${file}" must be an object with a "questions" array.`, + }); +} + +/** Resolve one questions array, splicing any `{ "use" }` fragments in place. */ +function resolveArray( + items: unknown[], + read: FragmentReader, + file: string, + seen: ReadonlySet +): Result { + const out: QuestionSpec[] = []; + for (const item of items) { + if (isUseRef(item)) { + const name = item.use; + if (!FRAGMENT_NAME.test(name)) { + return err( + new SystemError({ + source: SOURCE, + name: "QuestionFragmentInvalidName", + message: `Question fragment reference "${name}" in "${file}" is not a bare name.`, + }) + ); + } + if (seen.has(name)) { + return err( + new SystemError({ + source: SOURCE, + name: "QuestionFragmentCycle", + message: `Question fragment "${name}" forms a cycle (referenced from "${file}").`, + }) + ); + } + const raw = read(name); + if (raw.isErr()) { + return err(raw.error); + } + const fragFile = `${FRAGMENT_DIR}${name}.json`; + if (!isRecord(raw.value) || !Array.isArray(raw.value.questions)) { + return err(invalidQuestionsFile(fragFile)); + } + const nested = resolveArray(raw.value.questions, read, fragFile, new Set([...seen, name])); + if (nested.isErr()) { + return err(nested.error); + } + out.push(...nested.value); + } else if (isQuestionSpec(item)) { + out.push(item); + } else { + return err(invalidQuestionsFile(file)); + } + } + return ok(out); +} + +/** Resolve a `{ questions: [...] }` object into a flat `QuestionSpec[]`, expanding fragments. */ +export function resolveQuestions( + raw: unknown, + file: string, + read: FragmentReader +): Result { + if (!isRecord(raw) || !Array.isArray(raw.questions)) { + return err(invalidQuestionsFile(file)); + } + return resolveArray(raw.questions, read, file, new Set()); +} + +/** A `FragmentReader` over an open channel zip. */ +export function zipFragmentReader(zip: AdmZip): FragmentReader { + return (name) => { + const target = `${FRAGMENT_DIR}${name}.json`; + for (const entry of zip.getEntries()) { + if (entry.isDirectory) { + continue; + } + if (entry.entryName.replace(/\\/g, "/") === target) { + try { + return ok(JSON.parse(entry.getData().toString("utf8"))); + } catch { + return err( + new SystemError({ + source: SOURCE, + name: "PackageFileInvalid", + message: `The template package file "${target}" is not valid JSON.`, + }) + ); + } + } + } + return err( + new SystemError({ + source: SOURCE, + name: "PackageFileMissing", + message: `The template package is missing question fragment "${target}".`, + }) + ); + }; +} diff --git a/packages/fx-core/src/v4/providers/createOptionsProviders.ts b/packages/fx-core/src/v4/providers/createOptionsProviders.ts index 914d5145a35..77a454b1ed9 100644 --- a/packages/fx-core/src/v4/providers/createOptionsProviders.ts +++ b/packages/fx-core/src/v4/providers/createOptionsProviders.ts @@ -17,8 +17,16 @@ import { type ODRServer } from "../../component/utils/odrProvider"; import { SearchOpenAPISpecResult } from "../../common/kiotaClient"; import { parseMcpStaticToolsJson } from "../mcp/mcpStaticTools"; -const remoteMcpServerType = { id: "remote", label: "Remote" }; -const localMcpServerType = { id: "local", label: "Local" }; +const remoteMcpServerType = { + id: "remote", + label: "Remote MCP Server", + keyPrefix: "core.createProjectQuestion.mcpServerType.remote", +}; +const localMcpServerType = { + id: "local", + label: "Local MCP Server", + keyPrefix: "core.createProjectQuestion.mcpServerType.local", +}; function createLocalServerCache( listLocalMcpServers: () => Promise diff --git a/packages/fx-core/src/v4/surface/createSelectorWalk.ts b/packages/fx-core/src/v4/surface/createSelectorWalk.ts index 94f3721c3eb..e8084b94394 100644 --- a/packages/fx-core/src/v4/surface/createSelectorWalk.ts +++ b/packages/fx-core/src/v4/surface/createSelectorWalk.ts @@ -26,6 +26,7 @@ import { import { openDeclarativePackage } from "../distribution/declarativePackage"; import { ExpressionRuntimePort, Scope, evaluateExpression } from "../expression/evaluateExpression"; import { readBooleanFeatureFlag } from "../../common/featureFlags"; +import { localizePrefixedText } from "./localizePrompt"; /** Live Q1 create-selector prompt face. See walk-create-selector spec. */ @@ -102,14 +103,17 @@ function buildPort( } const selected = await ui.selectOption({ name: pq.name, - title: pq.title ?? pq.name, - placeholder: pq.placeholder, + title: localizePrefixedText(pq.keyPrefix, "title", pq.title) ?? pq.name, + placeholder: localizePrefixedText(pq.keyPrefix, "placeholder", pq.placeholder), step, options: visible.map((option) => ({ id: option.id, - label: labelWithIcon(option.label, option.iconPath), - detail: option.detail, - groupName: option.groupName, + label: labelWithIcon( + localizePrefixedText(option.keyPrefix, "label", option.label) ?? option.label, + option.iconPath + ), + detail: localizePrefixedText(option.keyPrefix, "detail", option.detail), + groupName: localizePrefixedText(option.keyPrefix, "groupName", option.groupName), })), returnObject: false, }); diff --git a/packages/fx-core/src/v4/surface/localizePrompt.ts b/packages/fx-core/src/v4/surface/localizePrompt.ts new file mode 100644 index 00000000000..529be8c390f --- /dev/null +++ b/packages/fx-core/src/v4/surface/localizePrompt.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { getLocalizedString } from "../../common/localizeUtils"; + +/** + * Shared keyPrefix-based localization for the v4 create prompts. Both the Q1 + * selector walk and the Q2/Q3 collect-inputs bridge resolve user-visible text + * the same way: look up `.` in the NLS bundle, and fall back + * to the authored literal when no key is registered. + */ + +/** Localize a raw string when it is itself an NLS key, else return it verbatim. */ +export function localizeText(text: string | undefined): string | undefined { + if (text === undefined) { + return undefined; + } + const localized = getLocalizedString(text); + return localized.length > 0 ? localized : text; +} + +/** Resolve `.` via NLS, falling back to the literal `fallback`. */ +export function localizePrefixedText( + keyPrefix: string | undefined, + suffix: string, + fallback: string | undefined +): string | undefined { + if (keyPrefix !== undefined) { + const localized = getLocalizedString(`${keyPrefix}.${suffix}`); + if (localized.length > 0) { + return localized; + } + } + return localizeText(fallback); +} diff --git a/packages/fx-core/src/v4/surface/uiPromptUI.ts b/packages/fx-core/src/v4/surface/uiPromptUI.ts index 91d61126343..b14b50feff1 100644 --- a/packages/fx-core/src/v4/surface/uiPromptUI.ts +++ b/packages/fx-core/src/v4/surface/uiPromptUI.ts @@ -14,7 +14,6 @@ import { UserInteraction, } from "@microsoft/teamsfx-api"; import { Result, err, ok } from "neverthrow"; -import { getLocalizedString } from "../../common/localizeUtils"; import { Asked, OptionItem, @@ -23,33 +22,12 @@ import { PromptUI, QuestionSpec, } from "../collectInputs/collectInputs"; +import { localizePrefixedText } from "./localizePrompt"; /** Create-Q2 prompt bridge from v4 `PromptUI` to host `UserInteraction`. */ const SOURCE = "Scaffold"; -function localizeText(text: string | undefined): string | undefined { - if (text === undefined) { - return undefined; - } - const localized = getLocalizedString(text); - return localized.length > 0 ? localized : text; -} - -function localizePrefixedText( - keyPrefix: string | undefined, - suffix: string, - fallback: string | undefined -): string | undefined { - if (keyPrefix !== undefined) { - const localized = getLocalizedString(`${keyPrefix}.${suffix}`); - if (localized.length > 0) { - return localized; - } - } - return localizeText(fallback); -} - function labelWithIcon(label: string, iconPath: string | undefined): string { return iconPath === undefined ? label : `$(${iconPath}) ${label}`; } @@ -156,6 +134,7 @@ export function createUiPromptUI(ui: UserInteraction): PromptUI { ), prompt: localizePrefixedText(question.keyPrefix, "prompt", question.prompt), default: question.default, + password: question.password, step, validation, }; diff --git a/packages/fx-core/tests/v4/distribution/questionFragments.test.ts b/packages/fx-core/tests/v4/distribution/questionFragments.test.ts new file mode 100644 index 00000000000..bb3bfb72354 --- /dev/null +++ b/packages/fx-core/tests/v4/distribution/questionFragments.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { SystemError } from "@microsoft/teamsfx-api"; +import { Result, err, ok } from "neverthrow"; +import { assert } from "vitest"; +import { resolveQuestions } from "../../../src/v4/distribution/questionFragments"; + +/** A `FragmentReader` over an in-memory fragment map. */ +function reader( + fragments: Record +): (name: string) => Result { + return (name) => { + if (Object.prototype.hasOwnProperty.call(fragments, name)) { + return ok(fragments[name]); + } + return err(new SystemError({ source: "Test", name: "FragmentMissing", message: name })); + }; +} + +function names(res: ReturnType): string[] { + return res._unsafeUnwrap().map((q) => q.name); +} + +describe("resolveQuestions (question fragments)", () => { + it("splices a { use } fragment's questions in place", () => { + const fragments = { + llm: { + questions: [ + { name: "a", type: "text" }, + { name: "b", type: "text" }, + ], + }, + }; + const res = resolveQuestions( + { + questions: [{ name: "pre", type: "text" }, { use: "llm" }, { name: "post", type: "text" }], + }, + "q.json", + reader(fragments) + ); + assert.deepEqual(names(res), ["pre", "a", "b", "post"]); + }); + + it("resolves nested fragments (a fragment may use another)", () => { + const fragments = { + outer: { questions: [{ use: "inner" }, { name: "x", type: "text" }] }, + inner: { questions: [{ name: "y", type: "text" }] }, + }; + const res = resolveQuestions({ questions: [{ use: "outer" }] }, "q.json", reader(fragments)); + assert.deepEqual(names(res), ["y", "x"]); + }); + + it("preserves every field on a spliced question (e.g. password)", () => { + const fragments = { + secret: { questions: [{ name: "key", type: "text", password: true, optional: true }] }, + }; + const res = resolveQuestions({ questions: [{ use: "secret" }] }, "q.json", reader(fragments)); + assert.deepEqual(res._unsafeUnwrap(), [ + { name: "key", type: "text", password: true, optional: true }, + ]); + }); + + it("propagates a missing-fragment reader error", () => { + const res = resolveQuestions({ questions: [{ use: "nope" }] }, "q.json", reader({})); + assert.isTrue(res.isErr()); + assert.equal(res._unsafeUnwrapErr().name, "FragmentMissing"); + }); + + it("rejects a cyclic fragment reference", () => { + const fragments = { + a: { questions: [{ use: "b" }] }, + b: { questions: [{ use: "a" }] }, + }; + const res = resolveQuestions({ questions: [{ use: "a" }] }, "q.json", reader(fragments)); + assert.isTrue(res.isErr()); + assert.equal(res._unsafeUnwrapErr().name, "QuestionFragmentCycle"); + }); + + it("rejects an unsafe fragment name before reading it (Zip-Slip guard)", () => { + let read = false; + const res = resolveQuestions({ questions: [{ use: "../evil" }] }, "q.json", () => { + read = true; + return ok({ questions: [] }); + }); + assert.isTrue(res.isErr()); + assert.equal(res._unsafeUnwrapErr().name, "QuestionFragmentInvalidName"); + assert.isFalse(read); + }); + + it("rejects a fragment whose body lacks a questions array", () => { + const res = resolveQuestions( + { questions: [{ use: "bad" }] }, + "q.json", + reader({ bad: { notQuestions: [] } }) + ); + assert.isTrue(res.isErr()); + }); + + it("rejects an item that is neither a question nor a { use } reference", () => { + const res = resolveQuestions({ questions: [{ foo: "bar" }] }, "q.json", reader({})); + assert.isTrue(res.isErr()); + }); +}); diff --git a/packages/fx-core/tests/v4/surface/createInputs.test.ts b/packages/fx-core/tests/v4/surface/createInputs.test.ts index 36438e42c67..4a00166f3b6 100644 --- a/packages/fx-core/tests/v4/surface/createInputs.test.ts +++ b/packages/fx-core/tests/v4/surface/createInputs.test.ts @@ -583,8 +583,9 @@ describe("runCreateInputs (collect-create-inputs)", () => { assert.deepEqual(ui.textNames, ["apiSpecLocation"]); assert.deepEqual(ui.fileNames, []); assert.deepEqual(ui.fileOrInputNames, []); - assert.equal(ui.lastInputConfig?.placeholder, "https://example.com/openapi.yaml"); - assert.equal(ui.lastInputConfig?.prompt, "Enter an OpenAPI description document URL."); + assert.equal(ui.lastInputConfig?.title, "OpenAPI Document"); + assert.equal(ui.lastInputConfig?.placeholder, "Enter OpenAPI Document URL"); + assert.isUndefined(ui.lastInputConfig?.prompt); assert.isFunction(ui.lastInputConfig?.validation); assert.equal( await ui.lastInputConfig?.validation?.("./openapi.yaml"), @@ -614,7 +615,8 @@ describe("runCreateInputs (collect-create-inputs)", () => { assert.deepEqual(ui.fileNames, ["apiSpecLocation"]); assert.deepEqual(ui.textNames, []); assert.deepEqual(ui.fileOrInputNames, []); - assert.equal(ui.lastFileConfig?.placeholder, "Select an OpenAPI description document."); + assert.equal(ui.lastFileConfig?.title, "OpenAPI Document"); + assert.isUndefined(ui.lastFileConfig?.placeholder); assert.deepEqual(ui.lastFileConfig?.filters, { "OpenAPI Description Document": ["json", "yml", "yaml"], }); @@ -1125,8 +1127,6 @@ describe("runCreateInputs (collect-create-inputs)", () => { multi: { apiOperations: ["GET /repairs"] }, text: { azureOpenAIKey: "", - azureOpenAIEndpoint: "", - azureOpenAIDeploymentName: "", }, }); const res = await runCreateInputs( @@ -1141,11 +1141,9 @@ describe("runCreateInputs (collect-create-inputs)", () => { assert.fail(res.error.message); } assert.equal(res.value.apiSpecLocation, OPENAPI_SPEC); - assert.deepEqual(ui.textNames, [ - "azureOpenAIKey", - "azureOpenAIEndpoint", - "azureOpenAIDeploymentName", - ]); + // Empty Azure OpenAI key short-circuits the cascade: the endpoint and + // deployment name questions are never asked. + assert.deepEqual(ui.textNames, ["azureOpenAIKey"]); assert.deepEqual(ui.fileNames, []); assert.deepEqual(ui.fileOrInputNames, ["apiSpecLocation"]); assert.equal(ui.lastFileOrInputConfig?.inputBoxConfig.name, "input-api-spec-url"); @@ -1158,6 +1156,29 @@ describe("runCreateInputs (collect-create-inputs)", () => { assert.isFunction(ui.lastFileOrInputConfig?.inputBoxConfig.validation); }); + it("cascades Azure OpenAI questions: a filled key reveals the endpoint, an empty endpoint skips the deployment name", async () => { + const ui = new ScriptedUserInteraction({ + select: { llmService: "llm-service-azure-openai" }, + text: { + azureOpenAIKey: "fake-azure-openai-key", + azureOpenAIEndpoint: "", + }, + }); + + const res = await runCreateInputs( + buildFloor(), + WEATHER_AGENT, + { language: "typescript" }, + asUI(ui), + { flagReader: () => false } + ); + + assert.isTrue(res.isOk(), res.isErr() ? res.error.message : "expected ok"); + // The filled key reveals the endpoint; the empty endpoint short-circuits + // the deployment name. + assert.deepEqual(ui.textNames, ["azureOpenAIKey", "azureOpenAIEndpoint"]); + }); + it("lists static MCP tools from the provided tools JSON", async () => { const toolsJson = JSON.stringify({ tools: [ diff --git a/packages/fx-core/tests/v4/surface/createSelectorWalk.test.ts b/packages/fx-core/tests/v4/surface/createSelectorWalk.test.ts index 677c47ba610..ee89cf7b7de 100644 --- a/packages/fx-core/tests/v4/surface/createSelectorWalk.test.ts +++ b/packages/fx-core/tests/v4/surface/createSelectorWalk.test.ts @@ -20,6 +20,7 @@ import { resolveCreateTargetByTemplateId, runCreateSelector, } from "../../../src/v4/surface/createSelectorWalk"; +import { getLocalizedString } from "../../../src/common/localizeUtils"; /** * Tests for docs/03-specs/operations/scaffolding/walk-create-selector.md. @@ -155,6 +156,8 @@ describe("runCreateSelector (walk-create-selector)", () => { }); const projectType = ui.configByName.get("projectType"); + // Labels localize via each option's `keyPrefix` (NLS); the authored selector + // literals are kept in sync with the v3 NLS, so they double as the fallback. assert.deepEqual( [ "copilot-agent-type", @@ -169,7 +172,7 @@ describe("runCreateSelector (walk-create-selector)", () => { ["copilot-agent-type", "$(teamsfx-agent) Declarative Agent"], ["custom-engine-agent-type", "$(teamsfx-custom-copilot) Custom Engine Agent"], ["graph-connector-type", "$(teamsfx-graph-connector) Copilot connectors"], - ["blank-app-type", "$(file) Blank App"], + ["blank-app-type", "$(file) Blank Copilot app/agent"], ["teams-agent-and-app-type", "$(microsoft365-agents-toolkit-teams) Teams Agents and Apps"], ["office-meta-os-type", "$(microsoft365-agents-office) Office Add-in"], [ @@ -180,6 +183,54 @@ describe("runCreateSelector (walk-create-selector)", () => { ); }); + it("WCS-23: Q1 prompts localize title, label, and detail via keyPrefix (NLS wins over the authored literal)", async () => { + // A selector whose keyPrefixes point at real shipped NLS keys but whose authored + // literals are deliberately wrong — proving the walk renders the localized value, + // not the literal fallback. (The shipped selector keeps its literals in sync with + // the v3 NLS, so a divergent literal is constructed here to isolate the behavior.) + const selector = { + questions: [ + { + name: "projectType", + type: "singleSelect", + title: "WRONG TITLE LITERAL", + keyPrefix: "template.createProjectQuestion", + staticOptions: [ + { + id: "blank-app-type", + label: "WRONG LABEL LITERAL", + detail: "WRONG DETAIL LITERAL", + keyPrefix: "template.createProjectQuestion.projectType.blankApp", + }, + ], + }, + ], + routes: [{ when: "projectType=='blank-app-type'", engine: "v4", templateId: "x" }], + }; + const ui = new ScriptedUI({ projectType: "blank-app-type" }); + + await runCreateSelector(Buffer.from(JSON.stringify(selector)), asUI(ui), "vscode", { + selectorBytesKind: "json", + }); + + const projectType = ui.configByName.get("projectType"); + // Title resolves `.title` from the NLS bundle, overriding the wrong literal. + assert.equal(projectType?.title, getLocalizedString("template.createProjectQuestion.title")); + assert.notEqual(projectType?.title, "WRONG TITLE LITERAL"); + + // Option label + detail resolve `.{label,detail}`, overriding the wrong literals. + const blankApp = offeredOption(projectType, "blank-app-type"); + assert.equal( + blankApp?.label, + getLocalizedString("template.createProjectQuestion.projectType.blankApp.label") + ); + assert.notEqual(blankApp?.label, "WRONG LABEL LITERAL"); + assert.equal( + blankApp?.detail, + getLocalizedString("template.createProjectQuestion.projectType.blankApp.detail") + ); + }); + it("WCS-00: selector JSON bytes use the selector route registry without opening packages", async () => { const ui = new ScriptedUI({ projectType: "minimal" }); diff --git a/packages/fx-core/tests/v4/surface/uiPromptUI.test.ts b/packages/fx-core/tests/v4/surface/uiPromptUI.test.ts index 792c788e8fe..ebec74900d6 100644 --- a/packages/fx-core/tests/v4/surface/uiPromptUI.test.ts +++ b/packages/fx-core/tests/v4/surface/uiPromptUI.test.ts @@ -141,6 +141,20 @@ describe("createUiPromptUI (collect-create-inputs prompt bridge)", () => { assert.equal(seenFolder, "C:/src"); }); + it("passes password through to the text input config", async () => { + const secret = new ScriptedUi({ text: ok({ type: "success", result: "sk-123" }) }); + const masked = await createUiPromptUI(asUi(secret)).ask( + { name: "openAIKey", type: "text", password: true }, + undefined + ); + assert.deepEqual(masked._unsafeUnwrap(), { kind: "value", value: "sk-123" }); + assert.isTrue(secret.lastTextConfig?.password); + + const plain = new ScriptedUi({ text: ok({ type: "success", result: "x" }) }); + await createUiPromptUI(asUi(plain)).ask({ name: "app-name", type: "text" }, undefined); + assert.isUndefined(plain.lastTextConfig?.password); + }); + it("maps host back and error results without rewriting them", async () => { const back = await createUiPromptUI(asUi(new ScriptedUi({ select: ok({ type: "back" }) }))).ask( { name: "language", type: "singleSelect" }, diff --git a/packages/fx-core/tests/v4/validation/openAiQuestions.test.ts b/packages/fx-core/tests/v4/validation/openAiQuestions.test.ts index eb9e8145401..c24d97d0925 100644 --- a/packages/fx-core/tests/v4/validation/openAiQuestions.test.ts +++ b/packages/fx-core/tests/v4/validation/openAiQuestions.test.ts @@ -7,6 +7,7 @@ import { assert } from "vitest"; const REPO_ROOT = path.resolve(__dirname, "../../../../.."); const CREATE_TEMPLATES_ROOT = path.join(REPO_ROOT, "templates/v4/create"); +const SHARED_QUESTIONS_ROOT = path.join(REPO_ROOT, "templates/v4/_shared/questions"); const OPENAI_RENDER_VARS = new Set([ "openAIKey", "azureOpenAIKey", @@ -51,16 +52,22 @@ function listFiles(root: string, matches: (name: string) => boolean): string[] { } function questionNames(templateDir: string): Set { - const questions = readJsonObject(path.join(templateDir, "questions.json"))["questions"]; const names = new Set(); - if (!Array.isArray(questions)) { - return names; - } - for (const question of questions) { - if (isRecord(question) && typeof question.name === "string") { - names.add(question.name); + const collect = (questionsPath: string): void => { + const questions = readJsonObject(questionsPath)["questions"]; + if (!Array.isArray(questions)) { + return; } - } + for (const question of questions) { + if (isRecord(question) && typeof question.use === "string") { + // Resolve a shared question fragment the same way the loader does. + collect(path.join(SHARED_QUESTIONS_ROOT, `${question.use}.json`)); + } else if (isRecord(question) && typeof question.name === "string") { + names.add(question.name); + } + } + }; + collect(path.join(templateDir, "questions.json")); return names; } diff --git a/templates/scripts/generateV4Zip.js b/templates/scripts/generateV4Zip.js index 854b02f64d7..e0b3c20d35d 100644 --- a/templates/scripts/generateV4Zip.js +++ b/templates/scripts/generateV4Zip.js @@ -70,9 +70,14 @@ function addV4MetadataFiles(zip, sourceRoot, zipRoot) { addV4MetadataFiles(zip, sourcePath, zipPath); continue; } - if ( - ["selector.json", "descriptor.json", "questions.json", "pipeline.json"].includes(entry.name) - ) { + const isTemplateMeta = [ + "selector.json", + "descriptor.json", + "questions.json", + "pipeline.json", + ].includes(entry.name); + const isSharedFragment = zipPath.includes("/_shared/") && entry.name.endsWith(".json"); + if (isTemplateMeta || isSharedFragment) { zip.addLocalFile(sourcePath, path.posix.dirname(zipPath)); } } diff --git a/templates/v4/_shared/questions/llm-service.json b/templates/v4/_shared/questions/llm-service.json new file mode 100644 index 00000000000..25fd927ae78 --- /dev/null +++ b/templates/v4/_shared/questions/llm-service.json @@ -0,0 +1,67 @@ +{ + "$schema": "../../schema/questions.schema.json", + "questions": [ + { + "name": "llmService", + "type": "singleSelect", + "title": "Service for Large Language Model (LLM)", + "placeholder": "Select a service to access LLMs", + "keyPrefix": "core.createProjectQuestion.llmService", + "default": "llm-service-azure-openai", + "staticOptions": [ + { + "id": "llm-service-azure-openai", + "label": "Azure OpenAI", + "detail": "Access powerful LLMs in OpenAI with Azure security and reliability", + "keyPrefix": "core.createProjectQuestion.llmServiceAzureOpenAIOption" + }, + { + "id": "llm-service-openai", + "label": "OpenAI", + "detail": "Access LLMs developed by OpenAI", + "keyPrefix": "core.createProjectQuestion.llmServiceOpenAIOption" + } + ] + }, + { + "name": "openAIKey", + "type": "text", + "password": true, + "title": "OpenAI Key", + "placeholder": "Input OpenAI service key now or set it later in the project", + "keyPrefix": "core.createProjectQuestion.llmService.openAIKey", + "condition": { "equals": { "llmService": "llm-service-openai" } }, + "optional": true + }, + { + "name": "azureOpenAIKey", + "type": "text", + "password": true, + "title": "Azure OpenAI Key", + "placeholder": "Input Azure OpenAI service key now or set it later in the project", + "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", + "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, + "optional": true + }, + { + "name": "azureOpenAIEndpoint", + "type": "text", + "title": "Azure OpenAI Endpoint", + "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", + "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", + "condition": { "expr": "llmService == 'llm-service-azure-openai' && azureOpenAIKey != ''" }, + "optional": true + }, + { + "name": "azureOpenAIDeploymentName", + "type": "text", + "title": "Azure OpenAI Deployment Name", + "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", + "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", + "condition": { + "expr": "llmService == 'llm-service-azure-openai' && azureOpenAIKey != '' && azureOpenAIEndpoint != ''" + }, + "optional": true + } + ] +} diff --git a/templates/v4/create/basic-custom-engine-agent/questions.json b/templates/v4/create/basic-custom-engine-agent/questions.json index d677230c79e..08b53bfdb17 100644 --- a/templates/v4/create/basic-custom-engine-agent/questions.json +++ b/templates/v4/create/basic-custom-engine-agent/questions.json @@ -1,63 +1,6 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ - { - "name": "llmService", - "type": "singleSelect", - "title": "Service for Large Language Model (LLM)", - "placeholder": "Select a service to access LLMs", - "keyPrefix": "core.createProjectQuestion.llmService", - "default": "llm-service-azure-openai", - "staticOptions": [ - { - "id": "llm-service-azure-openai", - "label": "Azure OpenAI", - "detail": "Access powerful LLMs in OpenAI with Azure security and reliability", - "keyPrefix": "core.createProjectQuestion.llmServiceAzureOpenAIOption" - }, - { - "id": "llm-service-openai", - "label": "OpenAI", - "detail": "Access LLMs developed by OpenAI", - "keyPrefix": "core.createProjectQuestion.llmServiceOpenAIOption" - } - ] - }, - { - "name": "openAIKey", - "type": "text", - "title": "OpenAI Key", - "placeholder": "Input OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.openAIKey", - "condition": { "equals": { "llmService": "llm-service-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIKey", - "type": "text", - "title": "Azure OpenAI Key", - "placeholder": "Input Azure OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIEndpoint", - "type": "text", - "title": "Azure OpenAI Endpoint", - "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIDeploymentName", - "type": "text", - "title": "Azure OpenAI Deployment Name", - "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - } + { "use": "llm-service" } ] } \ No newline at end of file diff --git a/templates/v4/create/blank-app/questions.json b/templates/v4/create/blank-app/questions.json index b355f6356e8..0e0048ed7dc 100644 --- a/templates/v4/create/blank-app/questions.json +++ b/templates/v4/create/blank-app/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [] } \ No newline at end of file diff --git a/templates/v4/create/custom-copilot-basic/questions.json b/templates/v4/create/custom-copilot-basic/questions.json index d677230c79e..08b53bfdb17 100644 --- a/templates/v4/create/custom-copilot-basic/questions.json +++ b/templates/v4/create/custom-copilot-basic/questions.json @@ -1,63 +1,6 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ - { - "name": "llmService", - "type": "singleSelect", - "title": "Service for Large Language Model (LLM)", - "placeholder": "Select a service to access LLMs", - "keyPrefix": "core.createProjectQuestion.llmService", - "default": "llm-service-azure-openai", - "staticOptions": [ - { - "id": "llm-service-azure-openai", - "label": "Azure OpenAI", - "detail": "Access powerful LLMs in OpenAI with Azure security and reliability", - "keyPrefix": "core.createProjectQuestion.llmServiceAzureOpenAIOption" - }, - { - "id": "llm-service-openai", - "label": "OpenAI", - "detail": "Access LLMs developed by OpenAI", - "keyPrefix": "core.createProjectQuestion.llmServiceOpenAIOption" - } - ] - }, - { - "name": "openAIKey", - "type": "text", - "title": "OpenAI Key", - "placeholder": "Input OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.openAIKey", - "condition": { "equals": { "llmService": "llm-service-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIKey", - "type": "text", - "title": "Azure OpenAI Key", - "placeholder": "Input Azure OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIEndpoint", - "type": "text", - "title": "Azure OpenAI Endpoint", - "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIDeploymentName", - "type": "text", - "title": "Azure OpenAI Deployment Name", - "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - } + { "use": "llm-service" } ] } \ No newline at end of file diff --git a/templates/v4/create/custom-copilot-rag-azure-ai-search/questions.json b/templates/v4/create/custom-copilot-rag-azure-ai-search/questions.json index d677230c79e..08b53bfdb17 100644 --- a/templates/v4/create/custom-copilot-rag-azure-ai-search/questions.json +++ b/templates/v4/create/custom-copilot-rag-azure-ai-search/questions.json @@ -1,63 +1,6 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ - { - "name": "llmService", - "type": "singleSelect", - "title": "Service for Large Language Model (LLM)", - "placeholder": "Select a service to access LLMs", - "keyPrefix": "core.createProjectQuestion.llmService", - "default": "llm-service-azure-openai", - "staticOptions": [ - { - "id": "llm-service-azure-openai", - "label": "Azure OpenAI", - "detail": "Access powerful LLMs in OpenAI with Azure security and reliability", - "keyPrefix": "core.createProjectQuestion.llmServiceAzureOpenAIOption" - }, - { - "id": "llm-service-openai", - "label": "OpenAI", - "detail": "Access LLMs developed by OpenAI", - "keyPrefix": "core.createProjectQuestion.llmServiceOpenAIOption" - } - ] - }, - { - "name": "openAIKey", - "type": "text", - "title": "OpenAI Key", - "placeholder": "Input OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.openAIKey", - "condition": { "equals": { "llmService": "llm-service-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIKey", - "type": "text", - "title": "Azure OpenAI Key", - "placeholder": "Input Azure OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIEndpoint", - "type": "text", - "title": "Azure OpenAI Endpoint", - "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIDeploymentName", - "type": "text", - "title": "Azure OpenAI Deployment Name", - "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - } + { "use": "llm-service" } ] } \ No newline at end of file diff --git a/templates/v4/create/custom-copilot-rag-custom-api/questions.json b/templates/v4/create/custom-copilot-rag-custom-api/questions.json index 709b316ddb4..d811931ff20 100644 --- a/templates/v4/create/custom-copilot-rag-custom-api/questions.json +++ b/templates/v4/create/custom-copilot-rag-custom-api/questions.json @@ -1,5 +1,5 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ { "name": "apiSpecLocation", @@ -25,70 +25,14 @@ { "name": "apiOperations", "type": "multiSelect", - "title": "API Operations", + "title": "Select Operation(s) Teams Can Interact with", + "keyPrefix": "core.createProjectQuestion.apiSpec.operation", "optionsFrom": "openapi.operations", "optionsFromParams": { "apiSpecLocation": { "expr": "apiSpecLocation" } }, "cliDescription": "API operations to include in the custom API agent." }, - { - "name": "llmService", - "type": "singleSelect", - "title": "Service for Large Language Model (LLM)", - "placeholder": "Select a service to access LLMs", - "keyPrefix": "core.createProjectQuestion.llmService", - "default": "llm-service-azure-openai", - "staticOptions": [ - { - "id": "llm-service-azure-openai", - "label": "Azure OpenAI", - "detail": "Access powerful LLMs in OpenAI with Azure security and reliability", - "keyPrefix": "core.createProjectQuestion.llmServiceAzureOpenAIOption" - }, - { - "id": "llm-service-openai", - "label": "OpenAI", - "detail": "Access LLMs developed by OpenAI", - "keyPrefix": "core.createProjectQuestion.llmServiceOpenAIOption" - } - ] - }, - { - "name": "openAIKey", - "type": "text", - "title": "OpenAI Key", - "placeholder": "Input OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.openAIKey", - "condition": { "equals": { "llmService": "llm-service-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIKey", - "type": "text", - "title": "Azure OpenAI Key", - "placeholder": "Input Azure OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIEndpoint", - "type": "text", - "title": "Azure OpenAI Endpoint", - "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIDeploymentName", - "type": "text", - "title": "Azure OpenAI Deployment Name", - "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - } + { "use": "llm-service" } ] } \ No newline at end of file diff --git a/templates/v4/create/custom-copilot-rag-customize/questions.json b/templates/v4/create/custom-copilot-rag-customize/questions.json index d677230c79e..08b53bfdb17 100644 --- a/templates/v4/create/custom-copilot-rag-customize/questions.json +++ b/templates/v4/create/custom-copilot-rag-customize/questions.json @@ -1,63 +1,6 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ - { - "name": "llmService", - "type": "singleSelect", - "title": "Service for Large Language Model (LLM)", - "placeholder": "Select a service to access LLMs", - "keyPrefix": "core.createProjectQuestion.llmService", - "default": "llm-service-azure-openai", - "staticOptions": [ - { - "id": "llm-service-azure-openai", - "label": "Azure OpenAI", - "detail": "Access powerful LLMs in OpenAI with Azure security and reliability", - "keyPrefix": "core.createProjectQuestion.llmServiceAzureOpenAIOption" - }, - { - "id": "llm-service-openai", - "label": "OpenAI", - "detail": "Access LLMs developed by OpenAI", - "keyPrefix": "core.createProjectQuestion.llmServiceOpenAIOption" - } - ] - }, - { - "name": "openAIKey", - "type": "text", - "title": "OpenAI Key", - "placeholder": "Input OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.openAIKey", - "condition": { "equals": { "llmService": "llm-service-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIKey", - "type": "text", - "title": "Azure OpenAI Key", - "placeholder": "Input Azure OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIEndpoint", - "type": "text", - "title": "Azure OpenAI Endpoint", - "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIDeploymentName", - "type": "text", - "title": "Azure OpenAI Deployment Name", - "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - } + { "use": "llm-service" } ] } \ No newline at end of file diff --git a/templates/v4/create/da/api-plugin-from-existing-api/questions.json b/templates/v4/create/da/api-plugin-from-existing-api/questions.json index 007e659e7f7..27ea79c16e0 100644 --- a/templates/v4/create/da/api-plugin-from-existing-api/questions.json +++ b/templates/v4/create/da/api-plugin-from-existing-api/questions.json @@ -1,5 +1,5 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [ { "name": "openApiSpecType", @@ -10,17 +10,17 @@ "staticOptions": [ { "id": "enter-url", - "label": "Enter OpenAPI description document URL", + "label": "Enter OpenAPI Document URL", "keyPrefix": "core.createProjectQuestion.capability.selectOpenAPISpecFromUrl" }, { "id": "open-file", - "label": "Browse OpenAPI description document locally", + "label": "Browse to Open File", "keyPrefix": "core.createProjectQuestion.capability.selectOpenAPISpecFromFile" }, { "id": "search-api", - "label": "Search OpenAPI description document", + "label": "Search OpenAPI Document", "keyPrefix": "core.createProjectQuestion.capability.selectOpenAPISpecFromSearch" } ] @@ -28,9 +28,9 @@ { "name": "apiSpecLocation", "type": "text", - "title": "OpenAPI Description Document", - "placeholder": "https://example.com/openapi.yaml", - "prompt": "Enter an OpenAPI description document URL.", + "title": "OpenAPI Document", + "placeholder": "Enter OpenAPI Document URL", + "keyPrefix": "core.createProjectQuestion.apiSpec", "validation": "openapiUrl", "cliShortName": "a", "cliDescription": "OpenAPI description document URL.", @@ -39,9 +39,7 @@ { "name": "apiSpecLocation", "type": "singleFile", - "title": "OpenAPI Description Document", - "placeholder": "Select an OpenAPI description document.", - "prompt": "Select an OpenAPI description document from your local machine.", + "title": "OpenAPI Document", "filters": { "OpenAPI Description Document": ["json", "yml", "yaml"] }, @@ -73,7 +71,8 @@ { "name": "apiOperations", "type": "multiSelect", - "title": "API Operations", + "title": "Select Operation(s) Copilot Can Interact with", + "keyPrefix": "core.createProjectQuestion.apiSpec.copilotOperation", "optionsFrom": "openapi.operations", "optionsFromParams": { "apiSpecLocation": { "expr": "apiSpecLocation" } @@ -84,7 +83,8 @@ { "name": "apiOperations", "type": "multiSelect", - "title": "API Operations", + "title": "Select Operation(s) Copilot Can Interact with", + "keyPrefix": "core.createProjectQuestion.apiSpec.copilotOperation", "optionsFrom": "openapi.operations", "optionsFromParams": { "apiSpecLocation": { "expr": "selectOpenApiSpec" } diff --git a/templates/v4/create/da/api-plugin-from-scratch-bearer/questions.json b/templates/v4/create/da/api-plugin-from-scratch-bearer/questions.json index fcdb9458f8b..355fbf239e4 100644 --- a/templates/v4/create/da/api-plugin-from-scratch-bearer/questions.json +++ b/templates/v4/create/da/api-plugin-from-scratch-bearer/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [] } diff --git a/templates/v4/create/da/api-plugin-from-scratch-oauth/questions.json b/templates/v4/create/da/api-plugin-from-scratch-oauth/questions.json index fcdb9458f8b..355fbf239e4 100644 --- a/templates/v4/create/da/api-plugin-from-scratch-oauth/questions.json +++ b/templates/v4/create/da/api-plugin-from-scratch-oauth/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [] } diff --git a/templates/v4/create/da/api-plugin-from-scratch/questions.json b/templates/v4/create/da/api-plugin-from-scratch/questions.json index fcdb9458f8b..355fbf239e4 100644 --- a/templates/v4/create/da/api-plugin-from-scratch/questions.json +++ b/templates/v4/create/da/api-plugin-from-scratch/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [] } diff --git a/templates/v4/create/da/graph-connector/questions.json b/templates/v4/create/da/graph-connector/questions.json index 42086ec3e6e..451195609fd 100644 --- a/templates/v4/create/da/graph-connector/questions.json +++ b/templates/v4/create/da/graph-connector/questions.json @@ -3,16 +3,18 @@ { "name": "graphConnectorName", "type": "text", - "title": "Copilot connector name", - "placeholder": "GitHub Issues", + "title": "Add a connector name", + "placeholder": "Create a name for Copilot connector", + "keyPrefix": "core.GCNameQuestion", "cliDescription": "a name for Copilot connector", "validation": "graphConnectorName" }, { "name": "graphConnectorConnectionId", "type": "text", - "title": "Copilot connector connection id", - "placeholder": "githubissues", + "title": "Add a connection ID", + "placeholder": "Add a connection ID for Copilot connector", + "keyPrefix": "core.GCConnectionIdQuestion", "cliDescription": "a connection id for Copilot connector", "validation": "graphConnectorConnectionId" } diff --git a/templates/v4/create/da/mcp-server-static/questions.json b/templates/v4/create/da/mcp-server-static/questions.json index b421264ecc4..75197b064ba 100644 --- a/templates/v4/create/da/mcp-server-static/questions.json +++ b/templates/v4/create/da/mcp-server-static/questions.json @@ -1,18 +1,18 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [ { "name": "mcpServerUrl", "type": "text", "title": "MCP Server URL", - "keyPrefix": "template.daMcpServer.mcpServerUrl", + "keyPrefix": "core.createProjectQuestion.mcpForDa.ServerUrl", "validation": "uri" }, { "name": "mcpToolsFilePath", "type": "text", - "title": "MCP Tools File Path", - "prompt": "Enter the path to a JSON file containing MCP tool definitions.", + "title": "MCP Tools Definition File", + "keyPrefix": "core.MCPForDA.toolsFilePath", "cliDescription": "Path to a JSON file containing MCP tool definitions.", "optional": true, "condition": { "expr": "surface == 'cli' && nonInteractive != 'true' && mcpToolsJson == null" } @@ -20,7 +20,8 @@ { "name": "selectedMcpTools", "type": "multiSelect", - "title": "MCP Tools", + "title": "Select Operation(s) Copilot can interact with", + "keyPrefix": "core.createProjectQuestion.mcpForDa.PreFetchTools", "optionsFrom": "mcp.tools", "optionsFromParams": { "toolsJson": { "expr": "mcpToolsJson" }, diff --git a/templates/v4/create/da/mcp-server/questions.json b/templates/v4/create/da/mcp-server/questions.json index 9247ea09a77..4d526bd18e2 100644 --- a/templates/v4/create/da/mcp-server/questions.json +++ b/templates/v4/create/da/mcp-server/questions.json @@ -1,11 +1,11 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [ { "name": "mcpServerType", "type": "singleSelect", "title": "MCP Server Type", - "keyPrefix": "template.daMcpServer.mcpServerType", + "keyPrefix": "core.createProjectQuestion.mcpServerType", "optionsFrom": "mcp.serverTypes", "skipSingleOption": true, "default": "remote" @@ -14,23 +14,23 @@ "name": "mcpServerUrl", "type": "text", "title": "MCP Server URL", - "keyPrefix": "template.daMcpServer.mcpServerUrl", + "keyPrefix": "core.createProjectQuestion.mcpForDa.ServerUrl", "condition": { "expr": "mcpServerType == 'remote'" }, "validation": "uri" }, { "name": "selectedLocalServers", "type": "multiSelect", - "title": "Local MCP Servers", - "keyPrefix": "template.daMcpServer.localServers", + "title": "Local MCP Server", + "keyPrefix": "core.createProjectQuestion.mcpLocalServer", "optionsFrom": "mcp.localServers", "condition": { "expr": "mcpServerType == 'local'" } }, { "name": "authType", "type": "singleSelect", - "title": "Authentication", - "keyPrefix": "template.daMcpServer.authType", + "title": "Select Authentication Type", + "keyPrefix": "core.createProjectQuestion.mcpForDa.AuthType", "condition": { "expr": "mcpServerType == 'remote'" }, "staticOptions": [ { "id": "oauth", "label": "OAuth (with static registration)" }, @@ -45,29 +45,30 @@ "name": "oauthClientId", "type": "text", "title": "OAuth Client ID", - "keyPrefix": "template.daMcpServer.oauthClientId", + "keyPrefix": "core.createProjectQuestion.mcpForDa.ClientId", "condition": { "expr": "authType == 'oauth'" } }, { "name": "oauthClientSecret", "type": "text", + "password": true, "title": "OAuth Client Secret", - "keyPrefix": "template.daMcpServer.oauthClientSecret", + "keyPrefix": "core.createProjectQuestion.mcpForDa.ClientSecret", "condition": { "expr": "authType == 'oauth'" } }, { "name": "oauthScopes", "type": "text", - "title": "OAuth Scopes", - "keyPrefix": "template.daMcpServer.oauthScopes", + "title": "OAuth Scopes (optional)", + "keyPrefix": "core.createProjectQuestion.mcpForDa.Scopes", "condition": { "expr": "authType == 'oauth'" }, "optional": true }, { "name": "entraClientId", "type": "text", - "title": "Entra Client ID", - "keyPrefix": "template.daMcpServer.entraClientId", + "title": "Microsoft Entra Application (Client) ID", + "keyPrefix": "core.createProjectQuestion.mcpForDa.EntraClientId", "condition": { "expr": "authType == 'entra-sso'" } } ] diff --git a/templates/v4/create/da/no-action/questions.json b/templates/v4/create/da/no-action/questions.json index fcdb9458f8b..355fbf239e4 100644 --- a/templates/v4/create/da/no-action/questions.json +++ b/templates/v4/create/da/no-action/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [] } diff --git a/templates/v4/create/da/skill/questions.json b/templates/v4/create/da/skill/questions.json index fcdb9458f8b..355fbf239e4 100644 --- a/templates/v4/create/da/skill/questions.json +++ b/templates/v4/create/da/skill/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../../schema/question.schema.json", + "$schema": "../../../schema/questions.schema.json", "questions": [] } diff --git a/templates/v4/create/declarative-agent-meta-os-upgrade-project/questions.json b/templates/v4/create/declarative-agent-meta-os-upgrade-project/questions.json index cc05581a287..8f2246fd716 100644 --- a/templates/v4/create/declarative-agent-meta-os-upgrade-project/questions.json +++ b/templates/v4/create/declarative-agent-meta-os-upgrade-project/questions.json @@ -1,11 +1,12 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ { "name": "officeAddinFolder", "type": "text", - "title": "Office Add-in project folder", - "placeholder": "Path to an existing Office Add-in project" + "title": "The Existing Add-in Project Folder", + "placeholder": "The existing MetaOS Add-in project root folder", + "keyPrefix": "core.createProjectQuestion.DAMetaOS.capability.upgradeProject.projectFolder" } ] } \ No newline at end of file diff --git a/templates/v4/create/default-bot/questions.json b/templates/v4/create/default-bot/questions.json index b355f6356e8..0e0048ed7dc 100644 --- a/templates/v4/create/default-bot/questions.json +++ b/templates/v4/create/default-bot/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [] } \ No newline at end of file diff --git a/templates/v4/create/default-message-extension/questions.json b/templates/v4/create/default-message-extension/questions.json index b355f6356e8..0e0048ed7dc 100644 --- a/templates/v4/create/default-message-extension/questions.json +++ b/templates/v4/create/default-message-extension/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [] } \ No newline at end of file diff --git a/templates/v4/create/graph-connector/questions.json b/templates/v4/create/graph-connector/questions.json index 42086ec3e6e..451195609fd 100644 --- a/templates/v4/create/graph-connector/questions.json +++ b/templates/v4/create/graph-connector/questions.json @@ -3,16 +3,18 @@ { "name": "graphConnectorName", "type": "text", - "title": "Copilot connector name", - "placeholder": "GitHub Issues", + "title": "Add a connector name", + "placeholder": "Create a name for Copilot connector", + "keyPrefix": "core.GCNameQuestion", "cliDescription": "a name for Copilot connector", "validation": "graphConnectorName" }, { "name": "graphConnectorConnectionId", "type": "text", - "title": "Copilot connector connection id", - "placeholder": "githubissues", + "title": "Add a connection ID", + "placeholder": "Add a connection ID for Copilot connector", + "keyPrefix": "core.GCConnectionIdQuestion", "cliDescription": "a connection id for Copilot connector", "validation": "graphConnectorConnectionId" } diff --git a/templates/v4/create/non-sso-tab/questions.json b/templates/v4/create/non-sso-tab/questions.json index b355f6356e8..0e0048ed7dc 100644 --- a/templates/v4/create/non-sso-tab/questions.json +++ b/templates/v4/create/non-sso-tab/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [] } \ No newline at end of file diff --git a/templates/v4/create/office-addin-config/questions.json b/templates/v4/create/office-addin-config/questions.json index 3fdee7f971e..9c887a5957c 100644 --- a/templates/v4/create/office-addin-config/questions.json +++ b/templates/v4/create/office-addin-config/questions.json @@ -1,5 +1,5 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ { "name": "officeAddinFolder", diff --git a/templates/v4/create/office-addin-excel-cfshortcut/questions.json b/templates/v4/create/office-addin-excel-cfshortcut/questions.json index b355f6356e8..0e0048ed7dc 100644 --- a/templates/v4/create/office-addin-excel-cfshortcut/questions.json +++ b/templates/v4/create/office-addin-excel-cfshortcut/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [] } \ No newline at end of file diff --git a/templates/v4/create/office-addin-wxpo-taskpane/questions.json b/templates/v4/create/office-addin-wxpo-taskpane/questions.json index b355f6356e8..0e0048ed7dc 100644 --- a/templates/v4/create/office-addin-wxpo-taskpane/questions.json +++ b/templates/v4/create/office-addin-wxpo-taskpane/questions.json @@ -1,4 +1,4 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [] } \ No newline at end of file diff --git a/templates/v4/create/selector.json b/templates/v4/create/selector.json index 2fdadeb92ec..14b7a44b806 100644 --- a/templates/v4/create/selector.json +++ b/templates/v4/create/selector.json @@ -27,8 +27,8 @@ "iconPath": "teamsfx-graph-connector", "keyPrefix": "template.createProjectQuestion.createGraphConnector" }, { "id": "blank-app-type", - "label": "Blank App", - "detail": "Blank Microsoft 365 app with minimal structure", + "label": "Blank Copilot app/agent", + "detail": "A minimal app with just a manifest, icons, and environment files. Build any M365 capability from scratch.", "groupName": "Apps for Microsoft 365", "iconPath": "file", "keyPrefix": "template.createProjectQuestion.projectType.blankApp" }, @@ -56,7 +56,7 @@ { "name": "daTemplate", "type": "singleSelect", - "title": "Declarative Agent", + "title": "Create Declarative Agent", "placeholder": "Add an action to your declarative agent", "keyPrefix": "template.createProjectQuestion.declarativeCopilot", "condition": { "expr": "projectType == 'copilot-agent-type'" }, diff --git a/templates/v4/create/teams-collaborator-agent/questions.json b/templates/v4/create/teams-collaborator-agent/questions.json index 75f05ca7f44..8cadbcc8d6a 100644 --- a/templates/v4/create/teams-collaborator-agent/questions.json +++ b/templates/v4/create/teams-collaborator-agent/questions.json @@ -1,9 +1,10 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ { "name": "azureOpenAIKey", "type": "text", + "password": true, "title": "Azure OpenAI Key", "placeholder": "Input Azure OpenAI service key now or set it later in the project", "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", @@ -15,6 +16,7 @@ "title": "Azure OpenAI Endpoint", "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", + "condition": { "expr": "azureOpenAIKey != ''" }, "optional": true }, { @@ -23,7 +25,8 @@ "title": "Azure OpenAI Deployment Name", "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", + "condition": { "expr": "azureOpenAIKey != '' && azureOpenAIEndpoint != ''" }, "optional": true } ] -} \ No newline at end of file +} diff --git a/templates/v4/create/weather-agent/questions.json b/templates/v4/create/weather-agent/questions.json index d677230c79e..08b53bfdb17 100644 --- a/templates/v4/create/weather-agent/questions.json +++ b/templates/v4/create/weather-agent/questions.json @@ -1,63 +1,6 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ - { - "name": "llmService", - "type": "singleSelect", - "title": "Service for Large Language Model (LLM)", - "placeholder": "Select a service to access LLMs", - "keyPrefix": "core.createProjectQuestion.llmService", - "default": "llm-service-azure-openai", - "staticOptions": [ - { - "id": "llm-service-azure-openai", - "label": "Azure OpenAI", - "detail": "Access powerful LLMs in OpenAI with Azure security and reliability", - "keyPrefix": "core.createProjectQuestion.llmServiceAzureOpenAIOption" - }, - { - "id": "llm-service-openai", - "label": "OpenAI", - "detail": "Access LLMs developed by OpenAI", - "keyPrefix": "core.createProjectQuestion.llmServiceOpenAIOption" - } - ] - }, - { - "name": "openAIKey", - "type": "text", - "title": "OpenAI Key", - "placeholder": "Input OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.openAIKey", - "condition": { "equals": { "llmService": "llm-service-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIKey", - "type": "text", - "title": "Azure OpenAI Key", - "placeholder": "Input Azure OpenAI service key now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIKey", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIEndpoint", - "type": "text", - "title": "Azure OpenAI Endpoint", - "placeholder": "Input Azure OpenAI service endpoint now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIEndpoint", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - }, - { - "name": "azureOpenAIDeploymentName", - "type": "text", - "title": "Azure OpenAI Deployment Name", - "placeholder": "Input Azure OpenAI deployment name now or set it later in the project", - "keyPrefix": "core.createProjectQuestion.llmService.azureOpenAIDeploymentName", - "condition": { "equals": { "llmService": "llm-service-azure-openai" } }, - "optional": true - } + { "use": "llm-service" } ] } \ No newline at end of file diff --git a/templates/v4/modify/add-mcp-server/questions.json b/templates/v4/modify/add-mcp-server/questions.json index b6100b2b203..248b8a4fb99 100644 --- a/templates/v4/modify/add-mcp-server/questions.json +++ b/templates/v4/modify/add-mcp-server/questions.json @@ -1,5 +1,5 @@ { - "$schema": "../../schema/question.schema.json", + "$schema": "../../schema/questions.schema.json", "questions": [ { "name": "mcpServerUrl", diff --git a/templates/v4/schema/question.schema.json b/templates/v4/schema/questions.schema.json similarity index 91% rename from templates/v4/schema/question.schema.json rename to templates/v4/schema/questions.schema.json index 93ac49413a5..b35a834e46f 100644 --- a/templates/v4/schema/question.schema.json +++ b/templates/v4/schema/questions.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://aka.ms/m365atk/v4/question.schema.json", + "$id": "https://aka.ms/m365atk/v4/questions.schema.json", "title": "v4 questions.json", "description": "Authoring-time schema for a v4 template's Q2 questions (proposal §4). The field whitelist here is the single machine-readable source of truth; the §4 prose table is its human gloss.", "type": "object", @@ -10,10 +10,18 @@ "$schema": { "type": "string" }, "questions": { "type": "array", - "items": { "$ref": "#/$defs/question" } + "items": { + "oneOf": [{ "$ref": "#/$defs/question" }, { "$ref": "#/$defs/useRef" }] + } } }, "$defs": { + "useRef": { + "type": "object", + "additionalProperties": false, + "required": ["use"], + "properties": { "use": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]*$" } } + }, "question": { "type": "object", "additionalProperties": false, @@ -28,6 +36,7 @@ "placeholder": { "type": "string" }, "prompt": { "type": "string" }, "default": {}, + "password": { "type": "boolean" }, "filters": { "type": "object", "additionalProperties": { diff --git a/templates/v4/schema/selector.schema.json b/templates/v4/schema/selector.schema.json index 75ae495ef43..8d1cbbacf58 100644 --- a/templates/v4/schema/selector.schema.json +++ b/templates/v4/schema/selector.schema.json @@ -13,7 +13,7 @@ "items": { "comment": "A Q1 selector question is single-select only: it presents a closed staticOptions set and routes to exactly one template. The other input kinds (text / confirm / singleFile / folder / singleFileOrText / multiSelect) are Q2 (template) or general-input concerns, not routing dimensions, and the Q1 walk only renders staticOptions picks.", "allOf": [ - { "$ref": "./question.schema.json#/$defs/question" }, + { "$ref": "./questions.schema.json#/$defs/question" }, { "properties": { "type": { "const": "singleSelect" } } } ] }