From 3d94030805cf794e2b5ff3999503416186e28776 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Thu, 5 Mar 2026 12:23:51 +0100 Subject: [PATCH 01/10] Add Copilot CLI skill definitions for azure-ai packages Add reusable skill files under sdk/ai/.skills/ to provide Copilot CLI agents with domain-specific guidance for common Azure SDK for Java workflows including TypeSpec codegen, test execution, release notes, duplicate class verification, and secret management. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/ai/.skills/codegen/SKILL.md | 61 ++++++ sdk/ai/.skills/dup-classes/SKILL.md | 61 ++++++ sdk/ai/.skills/github/SKILL.md | 156 ++++++++++++++ sdk/ai/.skills/release-notes/SKILL.md | 204 ++++++++++++++++++ sdk/ai/.skills/run-tests/SKILL.md | 66 ++++++ sdk/ai/.skills/search-m2/SKILL.md | 49 +++++ sdk/ai/.skills/test-proxy/SKILL.md | 36 ++++ sdk/ai/.skills/tsp-naming-collision/SKILL.md | 145 +++++++++++++ sdk/ai/.skills/tsp-type-override/SKILL.md | 207 +++++++++++++++++++ sdk/ai/.skills/wr-load/SKILL.md | 86 ++++++++ 10 files changed, 1071 insertions(+) create mode 100644 sdk/ai/.skills/codegen/SKILL.md create mode 100644 sdk/ai/.skills/dup-classes/SKILL.md create mode 100644 sdk/ai/.skills/github/SKILL.md create mode 100644 sdk/ai/.skills/release-notes/SKILL.md create mode 100644 sdk/ai/.skills/run-tests/SKILL.md create mode 100644 sdk/ai/.skills/search-m2/SKILL.md create mode 100644 sdk/ai/.skills/test-proxy/SKILL.md create mode 100644 sdk/ai/.skills/tsp-naming-collision/SKILL.md create mode 100644 sdk/ai/.skills/tsp-type-override/SKILL.md create mode 100644 sdk/ai/.skills/wr-load/SKILL.md diff --git a/sdk/ai/.skills/codegen/SKILL.md b/sdk/ai/.skills/codegen/SKILL.md new file mode 100644 index 000000000000..6e2b17892445 --- /dev/null +++ b/sdk/ai/.skills/codegen/SKILL.md @@ -0,0 +1,61 @@ +--- +name: codegen +description: Generate code from TypeSpec via tsp-client (update, sync, generate). Requires a tsp-location.yaml in the current working directory. Supports updating the commit hash before running. +--- + +# TypeSpec Code Generation (tsp-client) + +Use this skill to run `tsp-client` workflows for projects that include a `tsp-location.yaml` file. + +## Preconditions +- You must be in the directory that contains `tsp-location.yaml`. +- If the file is missing, warn the user and ask for the correct directory (do not run commands). + +## Commit hash update +If the user provides a commit hash, update the `commit:` field in `tsp-location.yaml` **before** running tsp-client. +- Read the file and locate the `commit:` line. +- Replace the value with the provided hash (keep the same key name and formatting). +- Example: + - Before: `commit: 6267b6...` + - After: `commit: ` + +## Commands + +### `tsp-client update` +Pull the latest codegen tooling or definitions (default action when the user is vague). +```bash +tsp-client update +``` + +### `tsp-client sync` +Fetch/sync TypeSpec inputs for the project. +```bash +tsp-client sync +``` + +### `tsp-client generate` +Generate code from TypeSpec inputs. +```bash +tsp-client generate +``` + +Keep the synced TypeSpec inputs: +```bash +tsp-client generate --save-inputs +``` + +## Steps +1. Verify `tsp-location.yaml` exists in the current directory. If not, stop and ask for the correct location. +2. If the user provided a commit hash, update the `commit:` value in `tsp-location.yaml`. +3. Determine the user intent: + - **Refresh/update/ingest changes from a commit**: run `tsp-client update`. + - **Fetch/sync spec from the current commit**: run `tsp-client sync`. + - **Generate from fetched spec**: run `tsp-client generate` (use `--save-inputs` only if the user asks to keep inputs). + - **Generate (no fetch requested)**: run `tsp-client generate`. +4. If the user doesn’t specify, default to `tsp-client update`. +5. If the project defines or creates a `TempTypeSpecFiles` folder and the user wants code generation, run `tsp-client generate` (with `--save-inputs` if requested). +6. If a tsp-client command fails, report the error output and suggest checking the TypeSpec repo/commit referenced in `tsp-location.yaml`. Build a GitHub URL from `repo:` and `directory:` (and include the `commit:` as the ref), e.g.: + - Repo: `Azure/azure-rest-api-specs` + - Commit: `6267b6...` + - Directory: `specification/cognitiveservices/OpenAI.Inference` + - URL: `https://github.com/Azure/azure-rest-api-specs/tree/6267b6.../specification/cognitiveservices/OpenAI.Inference` diff --git a/sdk/ai/.skills/dup-classes/SKILL.md b/sdk/ai/.skills/dup-classes/SKILL.md new file mode 100644 index 000000000000..3e3804509723 --- /dev/null +++ b/sdk/ai/.skills/dup-classes/SKILL.md @@ -0,0 +1,61 @@ +--- +name: dup-classes +description: Verify whether generated Java classes duplicate openai-java models by comparing fields/types (names may differ). Use when checking for duplicate model coverage. +--- + +# Duplicate Class Verification (Generated vs openai-java) + +Use this skill to compare generated Java models against the `openai-java` dependency. The goal is **field-by-field** comparison of model shapes, even when class or field names differ. + +## Inputs to confirm +- Generated source root (e.g., `src/main/java/...`) +- The relevant `pom.xml` (module) to resolve the `openai-java` dependency version +- Optional: package or class name hints to narrow the search + +## Steps +1. **Locate the pom.xml** in the current directory tree (`find . -name pom.xml`). If multiple, ask which module to use. +2. **Resolve openai-java**: + - Search the chosen pom for `openai-java` (or an explicit group/artifact provided by the user). + - Resolve the version (including properties) and locate the JAR in `~/.m2/repository`. +3. **List candidate classes**: + - Generated classes: scan the source root for `class` and `record` declarations. + - openai-java classes: `jar tf | rg '\.class$'` (filter by package hints if provided). +4. **Extract field signatures** (names may differ; compare shape): + - **Generated source**: + - For `record`, use the component list in the `record` declaration. + - For `class`, extract non-static field declarations (type + count) and note any `@JsonProperty` names. + - **openai-java JAR**: + - Use `javap -classpath -p ` to list fields (ignore `static`). + - If you need annotations, use `javap -classpath -p -verbose ` and look for `RuntimeVisibleAnnotations`. +5. **Compare shapes**: + - Compare **field count** and **field types** (order-independent). + - If `@JsonProperty` names exist in generated sources, compare those names with the openai-java field names. +6. **Report duplicates**: + - Provide a table: generated class → openai-java class, with matching field types and any name mismatches. + - Flag candidates with high similarity (same count and same types) even if names differ. + +## Useful commands + +### List generated class names +```bash +rg -n "^(public\s+)?(final\s+)?(class|record)\s+" +``` + +### Extract field lines from source (classes) +```bash +rg -n "^(\s*)(public|protected|private)\s+(static\s+)?(final\s+)?[A-Za-z0-9_<>,\[\].]+\s+[A-Za-z0-9_]+;" +``` + +### Extract record components +```bash +rg -n "record\s+[A-Za-z0-9_]+\s*\(([^)]*)\)" +``` + +### Inspect fields in a JAR class +```bash +javap -classpath -p +``` + +## Notes +- Use `search-m2` if you need help locating the dependency version or JAR path. +- If the user provides only a vague class hint, narrow candidates by package or field count first. diff --git a/sdk/ai/.skills/github/SKILL.md b/sdk/ai/.skills/github/SKILL.md new file mode 100644 index 000000000000..c8ee9fe1be6d --- /dev/null +++ b/sdk/ai/.skills/github/SKILL.md @@ -0,0 +1,156 @@ +--- +name: github +description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries." +--- + +# GitHub Skill + +Use the `gh` CLI to interact with GitHub. Always pass `--repo owner/repo` when not inside a cloned git directory. + +## Pull Requests + +List open PRs: +```bash +gh pr list --repo owner/repo +``` + +View a specific PR (summary, checks, comments): +```bash +gh pr view 55 --repo owner/repo +``` + +Check CI status on a PR: +```bash +gh pr checks 55 --repo owner/repo +``` + +## CI / Workflow Runs + +List recent runs: +```bash +gh run list --repo owner/repo --limit 10 +``` + +View a run summary (steps, status): +```bash +gh run view --repo owner/repo +``` + +View logs for failed steps only: +```bash +gh run view --repo owner/repo --log-failed +``` + +Re-run failed jobs: +```bash +gh run rerun --repo owner/repo --failed +``` + +## Issues + +List open issues (optionally filter by label): +```bash +gh issue list --repo owner/repo +gh issue list --repo owner/repo --label bug +``` + +View a specific issue: +```bash +gh issue view 42 --repo owner/repo +``` + +Create an issue: +```bash +gh issue create --repo owner/repo --title "Title" --body "Description" --label bug +``` + +## JSON Output & Filtering + +Most commands support `--json` with `--jq` for structured output: + +```bash +# List PR numbers and titles +gh pr list --repo owner/repo --json number,title --jq '.[] | "\(.number): \(.title)"' + +# List issues with assignees +gh issue list --repo owner/repo --json number,title,assignees \ + --jq '.[] | "\(.number): \(.title) → \(.assignees[].login // "unassigned")"' +``` + +## Advanced: `gh api` + +Use `gh api` for data or actions not covered by other subcommands. + +Fetch a PR with specific fields: +```bash +gh api repos/owner/repo/pulls/55 --jq '.title, .state, .user.login' +``` + +List check runs for a commit: +```bash +gh api repos/owner/repo/commits//check-runs \ + --jq '.check_runs[] | "\(.name): \(.conclusion)"' +``` + +Paginate results (e.g., all issues): +```bash +gh api --paginate repos/owner/repo/issues --jq '.[].title' +``` + +## Steps + +1. Check if `gh` is installed by running `gh --version`. + - If the command is **not found**, install it (see [Installation](#installation) below). +2. Check if `gh` is authenticated by running `gh auth status`. + - If not authenticated, run `gh auth login`. +3. If `owner/repo` is not provided, check if there is a `.git` directory and infer the remote via `gh repo view --json nameWithOwner`. Otherwise ask the user for the repo. +4. Choose the appropriate subcommand (`pr`, `issue`, `run`, `api`) based on the user's request. +5. Prefer structured subcommands (`gh pr`, `gh issue`, `gh run`) over raw `gh api` when they cover the use case. +6. Use `--json` + `--jq` when the user needs specific fields or wants to pipe output into further processing. +7. If a workflow run is failing, start with `gh pr checks` for a quick overview, then `gh run view --log-failed` for detailed output. +8. Report results clearly; if output is large, summarize and highlight the relevant parts. + +## Installation + +If `gh` is missing, install it using the recommended method for the current OS. +Detect the OS first, then run the matching command. + +### Windows +```powershell +winget install --id GitHub.cli +``` +> Note: open a **new terminal window** after installation for PATH changes to take effect. + +### macOS +```shell +brew install gh +``` + +### Linux (Debian / Ubuntu) +```bash +(type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \ + && sudo mkdir -p -m 755 /etc/apt/keyrings \ + && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && sudo mkdir -p -m 755 /etc/apt/sources.list.d \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y +``` + +### Linux (Fedora / RHEL / CentOS) +```bash +sudo dnf install 'dnf-command(config-manager)' +sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo +sudo dnf install gh --repo gh-cli +``` + +After installation, verify with `gh --version`, then authenticate with `gh auth login` if needed. + +## Notes + +- `gh` must be authenticated (`gh auth status`). If not, run `gh auth login` first. +- `--repo` accepts both `owner/repo` shorthand and full HTTPS URLs. +- For `gh api`, use `--method POST/PATCH/DELETE` for write operations and pass body fields with `-f field=value` or `-F field=`. +- `gh run list` defaults to the current branch when run inside a git repo; pass `--branch ` to target a specific branch. diff --git a/sdk/ai/.skills/release-notes/SKILL.md b/sdk/ai/.skills/release-notes/SKILL.md new file mode 100644 index 000000000000..e61eeed21659 --- /dev/null +++ b/sdk/ai/.skills/release-notes/SKILL.md @@ -0,0 +1,204 @@ +--- +name: release-notes +description: "Update CHANGELOG.md and README.md for an Azure SDK for Java package based on a GitHub PR. Use when the user wants to write or update release notes, changelogs, or readme docs from a PR reference." +--- + +# Release Notes Skill + +Update `CHANGELOG.md` and/or `README.md` for an Azure SDK for Java package using a GitHub PR as the source of truth. + +## Prerequisites + +- `gh` CLI authenticated (`gh auth status`). +- The current working directory must be the package root (where `CHANGELOG.md` and `README.md` live). + +## Inputs + +Ask the user for any missing inputs before proceeding: + +| Input | Required | Description | +|-------|----------|-------------| +| PR URL or number | **Yes** | GitHub PR to use as the source of changes. | +| Package directory | No | Defaults to `cwd`. Override if the user specifies a different package. | +| Scope | No | `changelog`, `readme`, or `both` (default: `both`). | + +## Step 1 — Gather PR information + +Use `gh` to collect the data you need. The diff may be too large for `gh pr diff`; fall back to the files API. + +```bash +# PR metadata +gh pr view --json title,body + +# File list with status (added/modified/removed/renamed) +gh api repos/{owner}/{repo}/pulls//files --paginate \ + --jq '.[] | .status + " " + .filename' + +# Renamed files (old → new) +gh api repos/{owner}/{repo}/pulls//files --paginate \ + --jq '.[] | select(.status == "renamed") | "\(.previous_filename) -> \(.filename)"' + +# Patch for a specific file (when you need detail) +gh api "repos/{owner}/{repo}/pulls//files?per_page=100" \ + --jq '.[] | select(.filename | test("")) | .patch' +``` + +Collect: +- Added, removed, and renamed model/enum classes. +- Changes to client classes (method renames, new methods, removed methods). +- Changes to `*ServiceVersion.java` (version string changes). +- Changes to `*ClientBuilder.java` (base URL, authentication, new builder methods). +- Changes to `module-info.java` (transitive exports, new requires). +- Changes to customization files. +- New or modified samples. + +## Step 2 — Check for existing entries + +Before writing anything, read the current `CHANGELOG.md` and `README.md` (if in scope) **in full** and compare their content against the changes you collected in Step 1. + +### 2a. Identify overlapping entries + +For each change you plan to document, check whether an entry already covers it: + +- **Exact match** — an existing bullet describes the same rename, addition, or removal using the same class/method names. +- **Topical overlap** — an existing bullet covers the same area (e.g., "tool renames" or "new sub-client") but with different detail, wording, or scope. + +### 2b. Report findings to the user + +If **any** overlap is found, **stop and consult the user before editing**. Present a summary like: + +> The following changes from PR #NNN already appear to be covered in the current files: +> +> **CHANGELOG.md** +> - _Features Added_ already mentions `FooClient` addition (line …). +> - _Breaking Changes_ already has a bullet about tool renames that partially overlaps the renames in this PR. +> +> **README.md** +> - The "Key concepts" section already lists the `BarClient` sub-client. +> +> Would you like me to: +> 1. Skip the entries that are already covered and only add the new ones? +> 2. Merge/update the overlapping entries (tell me how you'd like them worded)? +> 3. Proceed anyway and add everything as new entries? + +Wait for the user's response before continuing to Step 3 or Step 4. + +### 2c. No overlap + +If there is **no** overlap at all, inform the user briefly (e.g., "No existing entries overlap with this PR — proceeding to update.") and continue. + +## Step 3 — Update CHANGELOG.md + +### Format rules (CI-enforced) + +The CHANGELOG structure is **strict**. Every version section must contain exactly these headings in this order: + +```markdown +## (date or Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes +``` + +- **Do not** add, remove, rename, or reorder these headings. +- **Do not** delete or modify existing entries — all changes are **additive only**. Append new bullets below existing ones. +- If Step 2 identified a topical overlap and the user chose to merge/update an existing entry, that is the **only** case where you may edit an existing bullet — and only as the user directed. +- Only modify the target version section (usually the `(Unreleased)` one). +- Each entry is a markdown list item starting with `- `. + +### What goes where + +| Heading | Content | +|---------|---------| +| **Features Added** | New public classes, methods, enums, samples, tools, client capabilities. | +| **Breaking Changes** | Renamed/removed classes, renamed methods, changed enum values, changed method signatures, service version changes. | +| **Bugs Fixed** | Fixes to incorrect behavior (e.g., URL construction, serialization bugs). | +| **Other Changes** | Dependency updates, spec regeneration, module-info changes, internal refactors. | + +### Writing guidelines + +1. **Summarize, don't enumerate.** Group related changes under a single bullet when there is an overarching pattern (e.g., "Methods across sub-clients were renamed to include the resource name" with sub-bullets for each client). Don't list every file touched. +2. **Consumer perspective.** Only mention changes that matter to a user of the library. Internal implementation model renames that are not in the public API can be omitted. +3. **Use code formatting** for class names, method names, and enum values: `` `ClassName` ``. +4. **Show before → after** for renames: `` `OldName` → `NewName` `` or `` `OldName` renamed to `NewName` ``. +5. **Group repetitive renames** by pattern. For example, if 10 tool classes were renamed from `*AgentTool` to `*Tool`, write one bullet with representative examples rather than 10 bullets. +6. **Mention service version changes** (e.g., date-based to `v1`) in Breaking Changes. +7. **Don't over-list new models.** If the PR adds dozens of generated models, mention only the notable ones (new tool types, new feature-area models) and say "and related types" or similar. +8. **Omit trivial internal changes** like parameter reordering in generated `@HostParam`/`@QueryParam` annotations, checkstyle suppression updates, or whitespace. + +## Step 4 — Update README.md + +### Format rules (CI-enforced) + +The README structure is also checked by CI. Follow the existing heading hierarchy exactly: + +``` +# client library for Java +## Documentation +## Getting started +### Prerequisites +### Adding the package to your product +### Authentication +## Key concepts +### +## Examples +### +### Service API versions +#### Select a service API version +## Troubleshooting +## Next steps +## Contributing + +``` + +- **Do not** remove or reorder the top-level headings. +- **Do not** change the `[//]: #` version-update markers. +- **Do not** delete or rewrite existing prose or snippets unless they reference renamed APIs from this PR and Step 2 confirmed no conflict (or the user approved the change). +- You **may** add new `###` subsections under `## Key concepts` or `## Examples`. +- Keep existing code snippets intact unless they reference renamed APIs. + +### What to update + +1. **Package description** (opening paragraph): mention the REST API version if it changed (e.g., "targets the **v1** REST API"). +2. **Code snippets**: update any code that references renamed methods, classes, or builder patterns. Keep the `java com.azure...` snippet tags intact. +3. **Sub-client lists**: if new sub-clients were added, add them. Mark preview sub-clients with **(preview)**. +4. **Preview tools / features**: if the package defines `Tool` subclasses, document which are GA and which are preview (look for `Preview` in the class name or discriminator value). Use a table. +5. **Opt-in flags / experimental features**: if the package uses `FoundryFeaturesOptInKeys`, `AgentDefinitionFeatureKeys`, or `Foundry-Features` headers, document: + - Which sub-clients auto-set the header (check `*Impl.java` for hardcoded `foundryFeatures` strings). + - Which accept it as an optional parameter. + - List known flag values. +6. **OpenAI direct-usage snippets**: if the builder URL construction changed (e.g., `/openai` → `/openai/v1`), update the snippet and surrounding prose. Remove references to removed imports like `AzureUrlPathMode` or `AzureOpenAIServiceVersion` if they no longer apply. + +### Determining preview tools + +For the `azure-ai-agents` package, look at classes extending `com.azure.ai.agents.models.Tool`: + +```bash +# Read the Tool.java discriminator to find all subtypes +grep -A1 'equals(discriminatorValue)' src/main/java/com/azure/ai/agents/models/Tool.java +``` + +Tools whose discriminator value or class name contains `preview` are preview tools. All others are GA. + +### Determining preview operation groups + +Check which `*Impl.java` files hardcode a `foundryFeatures` value: + +```bash +grep -rl "final String foundryFeatures" src/main/java/*/implementation/*.java +``` + +Those operation groups are preview and auto-opt-in. Also check convenience client classes for `FoundryFeaturesOptInKeys` parameters — those are opt-in by caller. + +## Notes + +- **All edits are additive.** Never remove or rewrite existing content unless the user explicitly approves it after being consulted in Step 2. +- When the PR diff is too large for `gh pr diff` (HTTP 406), use `gh api .../pulls//files --paginate` instead. +- Paginate with `--paginate` and page with `?per_page=100&page=N` as needed. +- Always read the existing CHANGELOG and README **before** editing to avoid duplicating entries or breaking structure. +- If the PR title/body provides a summary, use it as a starting point but verify against the actual file changes. diff --git a/sdk/ai/.skills/run-tests/SKILL.md b/sdk/ai/.skills/run-tests/SKILL.md new file mode 100644 index 000000000000..e4d665dd5d1f --- /dev/null +++ b/sdk/ai/.skills/run-tests/SKILL.md @@ -0,0 +1,66 @@ +--- +name: run-tests +description: Run project tests using Maven (mvn). Use when the user asks to run tests. +--- + +# Run Tests (Maven) + +Use Maven (`mvn`) to run tests. Confirm you are in a directory with a `pom.xml` (project root or module root). + +## Common commands + +### All tests (default) +```bash +mvn test +``` + +### Specific module (multi-module) +```bash +mvn -pl -am test +``` + +### Specific test class or method (Surefire) +```bash +mvn -Dtest=MyTest test +mvn -Dtest=MyTest#myMethod test +``` + +## Test modes (AZURE_TEST_MODE) +If the user asks for live/record/playback, set the env var for the command: +```powershell +$env:AZURE_TEST_MODE = "LIVE" +mvn test +``` + +## Running tests with secrets + +Tests often require environment variables with connection strings, keys, or other secrets. + +- If `wr-load` is available, use it to load env vars from KeyVault before running tests: + ```bash + wr-load -Resource ; AZURE_TEST_MODE=LIVE mvn "-Dtest=" test + ``` + **Minimize `wr-load` calls** — it fetches secrets over the network. Run it once, capture the values, and reuse them. + +- If `wr-load` is **not** available, ask the user to set the required environment variables manually before running tests. Suggest they check the project's `test-resources.json` or README for the expected variable names. + +## Steps +1. Ensure you're in the correct Maven project directory (contains `pom.xml`). If not, ask for the correct path. +2. Start simple: `mvn "-Dtest=" test`. Only add flags if something fails. +3. If the user provides a test name, use `-Dtest=`. +4. If the user specifies a module, use `-pl -am test`. +5. If the user specifies a test mode (LIVE/RECORD/PLAYBACK), set the env var. +6. If tests require secrets, use `wr-load` if available; otherwise ask the user to set the required environment variables manually. +7. If the command fails, report the error output and ask how they want to proceed. + +## Troubleshooting (only add flags when needed) + +Apply these **only** if the simple `mvn test` command fails with the specific error described: + +- **Samples fail to compile on Java 8 base-testCompile**: add `-Dbuildhelper.addtestsource.skip=true -Dbuildhelper.addtestresource.skip=true` +- **JPMS/module-path errors** (e.g., `okio` module issues): add `-Dsurefire.useModulePath=false` +- **Build plugins block the run**: add skip flags as needed, e.g. `-Denforcer.skip=true -Dcodesnippet.skip=true -Dcheckstyle.skip=true` +- **Reactor blocking errors in async tests** (Netty thread): add `$env:AZURE_TEST_HTTP_CLIENTS = "okhttp"` +- **SSL handshake / PKIX path building failed** (`SSLHandshakeException`, `unable to find valid certification path to requested target`): the JVM's trust store is missing the corporate root CA. Add `-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT` to use the Windows certificate store instead. This is common on corporate networks with proxy/firewall TLS interception. + +Do NOT preemptively add all these flags. Start simple and escalate only on failure. diff --git a/sdk/ai/.skills/search-m2/SKILL.md b/sdk/ai/.skills/search-m2/SKILL.md new file mode 100644 index 000000000000..d3a09aa52afd --- /dev/null +++ b/sdk/ai/.skills/search-m2/SKILL.md @@ -0,0 +1,49 @@ +--- +name: search-m2 +description: Search for Java classes inside Maven dependencies in ~/.m2. Use when the user asks to locate classes or inspect JARs. Cross-reference pom.xml files in the current directory to resolve dependency names/versions. +--- + +# Search Maven Local Repository (search-m2) + +Use this skill to find which dependency JAR contains a class and to inspect JAR contents in `~/.m2/repository`. + +## Preconditions +- Work from the user’s current directory. +- Look for `pom.xml` files in the current directory tree and use them to resolve dependency names and versions. + +## Key paths +- Maven local repo: `~/.m2/repository` +- Dependency JAR path pattern: + `~/.m2/repository////-.jar` + +## Steps +1. **Discover pom files**: run `find . -name pom.xml` from the current directory. + - If multiple poms exist, prefer the closest one to the current directory or ask the user which project/module to use. +2. **Extract dependency coordinates**: + - If the user provides a group/artifact, search the poms for that dependency and read its version. + - If versions are defined via properties (e.g., `${foo.version}`), resolve the property from the same pom (or parent if obvious). +3. **Resolve the JAR path** using the groupId/artifactId/version mapping above. + - If the version is not found, list available versions in `~/.m2/repository///` and ask the user which to inspect. +4. **Search for classes**: + - List classes: `jar tf | rg '\.class$'` + - Find a specific class: `jar tf | rg '(\.class)?$'` +5. **Inspect class details** (if requested): + - `javap -classpath ` + +## Useful commands + +### Find a class across all jars (fallback) +```bash +rg -g '*.jar' --files ~/.m2/repository | while read -r jar; do + jar tf "$jar" | rg -q 'com/example/MyClass.class' && echo "$jar" +done +``` + +### Resolve versions for a dependency +```bash +ls ~/.m2/repository/// +``` + +## Notes +- If the user is vague, start by identifying the relevant pom.xml and extracting dependency coordinates. +- If the dependency is transitive and not in the pom, check `mvn -q -DskipTests dependency:tree` (only if needed and the user agrees). diff --git a/sdk/ai/.skills/test-proxy/SKILL.md b/sdk/ai/.skills/test-proxy/SKILL.md new file mode 100644 index 000000000000..18644e275288 --- /dev/null +++ b/sdk/ai/.skills/test-proxy/SKILL.md @@ -0,0 +1,36 @@ +--- +name: test-proxy +description: Push test-proxy recordings/assets using the test-proxy CLI (e.g., test-proxy push -a assets.json). Use when publishing recordings. +--- + +# Test Proxy Recordings + +Use this skill to publish recordings to the test-proxy assets repo. + +## Command +```bash +test-proxy push -a assets.json +``` + +## Steps +1. Check if `test-proxy` is installed by running `test-proxy --version`. + - If the command is **not found**, install it (see [Installation](#installation) below). +2. Confirm the assets file path (default: `assets.json` in the current directory). +3. If the file location is unclear, search for it or ask the user. +4. Run `test-proxy push -a `. +5. Report success or any errors. + +## Installation + +If `test-proxy` is missing, install it as a .NET global tool. + +### Prerequisites +.NET 8.0 (LTS) or later must be installed. Verify with `dotnet --version`. +If missing, ask the user to install the .NET SDK from https://dotnet.microsoft.com/download. + +### Install command +```powershell +dotnet tool update azure.sdk.tools.testproxy --global --prerelease --add-source https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json --ignore-failed-sources +``` + +After installation, verify with `test-proxy --version`. diff --git a/sdk/ai/.skills/tsp-naming-collision/SKILL.md b/sdk/ai/.skills/tsp-naming-collision/SKILL.md new file mode 100644 index 000000000000..de6c9afa3a78 --- /dev/null +++ b/sdk/ai/.skills/tsp-naming-collision/SKILL.md @@ -0,0 +1,145 @@ +--- +name: tsp-naming-collision +description: Fix Java codegen parameter names that end with a numeric suffix (e.g. createAgentRequest1) caused by TypeSpec model names colliding with synthetic body type names. Use when generated Java client methods have parameter names ending in '1'. +--- + +# Fix TypeSpec Naming Collisions in Java Codegen + +Fix parameter and implementation-model names that end with a numeric suffix (e.g. `createAgentRequest1`) in generated Java client code. This happens when a TypeSpec **named `model`** collides with the **synthetic body type** the Java codegen creates for an operation that spreads that model. + +## Root Cause + +When a TypeSpec route **spreads** a named model into an operation's parameters (via `...ModelName`), the Java codegen creates a synthetic body type to hold the body properties. It names this type `{OperationName}Request` (PascalCase of the operation name + `Request`). If a TypeSpec `model` already has that exact name, the codegen resolves the collision by appending `1`. + +Example collision: +- TypeSpec model: `CreateAgentRequest` +- Operation: `createAgent` spreads `...CreateAgentRequest` +- Codegen synthetic body: wants name `CreateAgentRequest` → collision → `CreateAgentRequest1` +- Result: parameter `createAgentRequest1`, implementation class `CreateAgentRequest1.java` + +> **Why aliases don't collide:** TypeSpec `alias` declarations don't occupy a name in the type namespace, so there is no collision. If the upstream spec used `alias CreateAgentRequest = { ... }` instead of `model CreateAgentRequest { ... }`, no fix would be needed. + +## Preconditions + +- You must be in the directory that contains `tsp-location.yaml`. +- The TypeSpec must already be synced locally into `TempTypeSpecFiles/`. If not, run `tsp-client sync` first. +- Identify the `client.tsp` file inside `TempTypeSpecFiles/` (usually under a subdirectory like `sdk-agents/`). This is the customization file where fixes are applied. + +## Important: TempTypeSpecFiles is volatile + +`TempTypeSpecFiles/` is regenerated on every `tsp-client sync` or `tsp-client update`. **Changes made only in `TempTypeSpecFiles/` will be lost.** Always apply the same edits to the corresponding `client.tsp` in a local checkout of `Azure/azure-rest-api-specs` (if available) so the changes can be committed to a PR. + +## Workflow + +### 1. Identify affected operations + +Search the generated Java client classes for parameter names ending with `1`: + +```bash +grep -n 'Request1[,)]' src/main/java/com/azure/ai/agents/*Client.java +``` + +Also check for implementation model classes with the `1` suffix: + +```bash +find . -name "*Request1.java" -path "*/implementation/models/*" +``` + +Collect the list of affected names (e.g. `createAgentRequest1`, `updateAgentRequest1`). + +### 2. Trace back to the TypeSpec models + +For each affected parameter, find the TypeSpec model that causes the collision. The model name matches the parameter name (PascalCase, without the `1`). + +Search the `.tsp` files: + +```bash +grep -rn "model CreateAgentRequest\|model UpdateAgentRequest" TempTypeSpecFiles/ --include="*.tsp" +``` + +Confirm the model is used via spread (`...ModelName`) in the route definitions: + +```bash +grep -rn "CreateAgentRequest\|UpdateAgentRequest" TempTypeSpecFiles/ --include="*.tsp" +``` + +Verify the model is a `model` (not an `alias`). Only named `model` types cause collisions. + +### 3. Add `@@clientName` overrides + +Edit the `client.tsp` customization file in `TempTypeSpecFiles/`. Add a `@@clientName` directive for each colliding model to give it a different client-side name. This frees the original name for the codegen's synthetic body type. + +Use a consistent naming convention. Recommended: rename `*Request` → `*Input`: + +```tsp +// Rename request models to avoid collision with synthetic body types generated +// by the Java codegen. The codegen names synthetic bodies as {OperationName}Request, +// which clashes with the identically-named TypeSpec models, causing a "1" suffix. +@@clientName(CreateAgentRequest, "CreateAgentInput"); +@@clientName(UpdateAgentRequest, "UpdateAgentInput"); +``` + +> **Note:** These models are typically not emitted as public Java classes — they only exist to be spread into operations. The rename is purely internal and does not affect the public API surface. + +### 4. Regenerate and verify + +Generate with `--save-inputs` to preserve the edited TypeSpec files: + +```bash +tsp-client generate --save-inputs +``` + +Verify the `1` suffix is gone: + +```bash +# Should return zero matches +grep -c "Request1" src/main/java/com/azure/ai/agents/*Client.java + +# Old *Request1.java files should no longer exist +find src -name "*Request1.java" -path "*/implementation/models/*" + +# New clean-named files should exist +find src -name "*Request.java" -path "*/implementation/models/*" +``` + +Compile to confirm no breakage: + +```bash +mvn compile -Denforcer.skip=true -Dcodesnippet.skip=true -Dcheckstyle.skip=true \ + -Dspotbugs.skip=true -Dspotless.skip=true -Drevapi.skip=true -Djacoco.skip=true \ + -Dmaven.javadoc.skip=true -Dshade.skip=true -Danimal.sniffer.skip=true +``` + +### 5. Apply changes to the local spec repo (if available) + +If the user has a local checkout of `Azure/azure-rest-api-specs`, apply the **same `client.tsp` edits** there. Derive the file path from `tsp-location.yaml`: + +- `directory` field gives the relative spec path (e.g. `specification/ai-foundry/data-plane/Foundry/src/sdk-agents`) +- The file to edit is `client.tsp` inside that directory + +For example, if the local repo is at ``: + +``` +/specification/ai-foundry/data-plane/Foundry/src/sdk-agents/client.tsp +``` + +Verify the file exists before editing. If it doesn't, warn the user and print the expected path. + +### 6. Full round-trip from the remote spec (optional) + +If the user wants to validate the fix end-to-end from the remote repo: + +1. Commit and push the `client.tsp` changes in the spec repo +2. Get the new commit hash +3. Update the `commit:` field in `tsp-location.yaml` with the new hash +4. Run `tsp-client update` to sync and regenerate from the remote +5. Verify the `1` suffix is gone and the build compiles + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `1` suffix persists after adding `@@clientName` | The `@@clientName` target doesn't match the TypeSpec model name exactly | Double-check the model name is the TypeSpec name (not the Java name); names are case-sensitive | +| New suffix appears (e.g. `2`) | Multiple models collide with the same synthetic name | Ensure every colliding model has a unique `@@clientName` | +| Build fails after regeneration | Handwritten code references the old `*1` names | Update any manual references in custom client code, tests, or samples | +| Changes lost after `tsp-client sync` | `TempTypeSpecFiles/` was overwritten | Apply changes to the spec repo `client.tsp` (see step 5) | diff --git a/sdk/ai/.skills/tsp-type-override/SKILL.md b/sdk/ai/.skills/tsp-type-override/SKILL.md new file mode 100644 index 000000000000..b7a45f1341ed --- /dev/null +++ b/sdk/ai/.skills/tsp-type-override/SKILL.md @@ -0,0 +1,207 @@ +--- +name: tsp-type-override +description: Override TypeSpec types with Java-native types (e.g. OffsetDateTime, DayOfWeek) using @@alternateType in a client.java.tsp file. Use when a TypeSpec model field has an incorrect or too-generic type that should map to a specific Java type. +--- + +# TypeSpec Type Override for Java + +Override types in generated Java code by adding `@@alternateType` decorators to the `client.java.tsp` file. + +## Preconditions +- You must be in the directory that contains `tsp-location.yaml`. +- The TypeSpec must already be synced locally into `TempTypeSpecFiles/`. If not, run `tsp-client sync` first. +- Identify the `client.java.tsp` file inside `TempTypeSpecFiles/`. This is the **only** file you should edit. + +## Important: TempTypeSpecFiles is volatile + +`TempTypeSpecFiles/` is a **transient working directory** managed by `tsp-client`. It is regenerated on every `tsp-client sync` or `tsp-client update` and is typically gitignored. **Any changes made only in `TempTypeSpecFiles/` will be lost** on the next sync/update cycle. + +If the user provides a **local checkout of `Azure/azure-rest-api-specs`**, always apply the same `client.java.tsp` edits there so the changes are preserved and can be committed to a PR. The path to the spec file inside that repo can be derived from `tsp-location.yaml`: +- `directory` field gives the relative path (e.g. `specification/ai-foundry/data-plane/Foundry`) +- The file to edit is `client.java.tsp` inside that directory + +## Workflow + +### 1. Locate the TypeSpec model and field + +Search the `.tsp` files under `TempTypeSpecFiles/` for the model and field the user wants to override: + +```bash +grep -rn "\|" TempTypeSpecFiles/ --include="*.tsp" +``` + +Read the model definition to confirm the current type of the target field (e.g. `string`, `int32`, a union, etc.). + +### 2. Determine the correct decorator form + +There are **two forms** of `@@alternateType`. Choose based on the target type: + +#### Form A — TypeSpec built-in type (preferred when possible) + +Use when there is a TypeSpec scalar that the Java emitter already maps to the desired Java type. + +| Desired Java type | TypeSpec alternate | +|-----------------------|------------------------| +| `OffsetDateTime` | `utcDateTime` | +| `Duration` | `duration` | +| `byte[]` | `bytes` | +| `long` / `Long` | `int64` | +| `double` / `Double` | `float64` | + +Syntax (applied to a **model property**, scoped to Java): + +```tsp +@@alternateType(ModelName.fieldName, utcDateTime, "java"); +``` + +This form is **fully supported** on model properties. + +#### Form B — External Java type via identity + +Use when no TypeSpec scalar maps to the desired Java type (e.g. `java.time.DayOfWeek`). + +Syntax (applied to the **type definition itself**, not a property): + +```tsp +@@alternateType(TypeName, { identity: "fully.qualified.ClassName" }, "java"); +``` + +> **Important constraints for external types:** +> - External types (`{ identity: ... }`) **cannot** be applied to model properties — they must target the type definition (Model, Enum, Union, Scalar). +> - A `scope` parameter (e.g. `"java"`) is **required** for external types. +> - **Known limitation (as of typespec-java 0.39.x):** The Java emitter does not fully support external types on Enum/Union definitions. It will still generate the class instead of referencing the JDK type. This is tracked as a bug. Only use Form B for Model types until the emitter is fixed. + +### 3. Apply the override + +Edit the `client.java.tsp` file inside `TempTypeSpecFiles/`. Add the decorator(s) under the type-replacement section (usually at the bottom of the file): + +```tsp +// EvaluatorVersion datetime fields are typed as string in the spec +@@alternateType(EvaluatorVersion.created_at, utcDateTime, "java"); +``` + +### 4. Generate and verify + +Always generate with `--save-inputs` so the edited TypeSpec files are preserved: + +```bash +tsp-client generate --save-inputs +``` + +After generation, verify the Java source uses the expected type: + +```bash +grep -n "OffsetDateTime\|DayOfWeek\|" src/main/java/com/azure/ai/projects/models/.java +``` + +Check that: +- The field type changed (e.g. `private OffsetDateTime createdAt;`) +- The getter return type changed (e.g. `public OffsetDateTime getCreatedAt()`) +- The JSON deserialization uses the correct parser (e.g. `CoreUtils.parseBestOffsetDateTime`) +- No spurious files were generated (e.g. under `src/main/java/java/`) + +### 5. Write unit tests for serialization/deserialization + +After generation, **always** write a unit test that verifies the generated model serializes and deserializes the overridden type to the **same wire-format values** defined in the original TypeSpec. This is critical because the emitter may generate serialization code that does not match the API wire format (e.g. `java.time.DayOfWeek.name()` produces `"MONDAY"` but the TypeSpec union defined `"Monday"`). + +Place the test class under `src/test/java/` in the model's package (e.g. `com.azure.ai.projects.models`). + +The test must cover three scenarios: + +1. **Serialization** — Construct the model with the Java type, serialize to JSON, and assert the JSON string values match the TSP-defined wire format (e.g. PascalCase `"Monday"`, not UPPER_CASE `"MONDAY"`). +2. **Deserialization** — Parse a JSON string using the TSP-defined wire-format values and assert the Java type is correctly populated. +3. **Round-trip** — Serialize → deserialize and assert the original values are preserved. + +Example test skeleton: + +```java +@Test +void serializationProducesWireFormatValues() throws IOException { + // Build model with the Java type + var schedule = new WeeklyRecurrenceSchedule(Arrays.asList(DayOfWeek.MONDAY, DayOfWeek.FRIDAY)); + String json = toJsonString(schedule); + // Assert the wire values match the TSP union/enum values, NOT the Java enum constant names + String expected = "{\"daysOfWeek\":[\"Monday\",\"Friday\"],\"type\":\"Weekly\"}"; + assertEquals(expected, json); +} + +@Test +void deserializationParsesWireFormatValues() throws IOException { + // Use TSP-defined wire-format values + String json = "{\"daysOfWeek\":[\"Monday\",\"Wednesday\"],\"type\":\"Weekly\"}"; + WeeklyRecurrenceSchedule schedule; + try (JsonReader reader = JsonProviders.createReader(json)) { + schedule = WeeklyRecurrenceSchedule.fromJson(reader); + } + assertEquals(Arrays.asList(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY), schedule.getDaysOfWeek()); +} + +@Test +void roundTripPreservesValues() throws IOException { + var original = new WeeklyRecurrenceSchedule(Arrays.asList(DayOfWeek.SUNDAY, DayOfWeek.SATURDAY)); + String json = toJsonString(original); + WeeklyRecurrenceSchedule deserialized; + try (JsonReader reader = JsonProviders.createReader(json)) { + deserialized = WeeklyRecurrenceSchedule.fromJson(reader); + } + assertEquals(original.getDaysOfWeek(), deserialized.getDaysOfWeek()); +} +``` + +#### If the tests fail: customize toJson/fromJson + +When the emitter generates incorrect serialization (e.g. `element.name()` instead of PascalCase), you must manually fix the `toJson` and `fromJson` methods in the generated model class: + +1. **Remove the `@Generated` annotation** from `toJson` and `fromJson`. This ensures your customizations survive future `tsp-client generate` / `tsp-client update` runs — the codegen will not overwrite methods that lack `@Generated`. +2. Fix the serialization logic to convert between the Java type and the TSP wire format. For example, for `java.time.DayOfWeek`: + - **`toJson`**: convert `DayOfWeek.MONDAY` → `"Monday"` (PascalCase) using a helper like: + ```java + private static String toPascalCase(DayOfWeek day) { + String name = day.name(); + return name.charAt(0) + name.substring(1).toLowerCase(Locale.ROOT); + } + ``` + - **`fromJson`**: convert `"Monday"` → `DayOfWeek.MONDAY` by uppercasing before `valueOf()`: + ```java + DayOfWeek.valueOf(reader.getString().toUpperCase(Locale.ROOT)) + ``` +3. Re-run the unit tests and confirm all three scenarios pass. + +### 6. Apply changes to the local spec repo (if provided) + +If the user supplied a local checkout path for `Azure/azure-rest-api-specs`, apply the **same edits** to the `client.java.tsp` there. Derive the file path from `tsp-location.yaml`: + +``` +//client.java.tsp +``` + +For example, if `directory: specification/ai-foundry/data-plane/Foundry` and the local repo is at `~/code/azure-rest-api-specs`: + +``` +~/code/azure-rest-api-specs/specification/ai-foundry/data-plane/Foundry/client.java.tsp +``` + +Verify the file exists before editing. If it doesn't, warn the user and print the expected path. + +### 7. Remind the user about the spec PR + +After confirming the generated code is correct, remind the user: + +> The `@@alternateType` changes in `client.java.tsp` are local overrides in `TempTypeSpecFiles/`. +> For these to persist across future code generations, the same changes must be contributed to the +> **Azure/azure-rest-api-specs** repository via a pull request targeting the corresponding +> `client.java.tsp` file under the `specification/` directory. +> +> Build the PR URL from `tsp-location.yaml`: +> - Repo: `repo` field (e.g. `Azure/azure-rest-api-specs`) +> - Directory: `directory` field (e.g. `specification/ai-foundry/data-plane/Foundry`) +> - File: `client.java.tsp` in that directory + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Generated class still uses `String` | Decorator not picked up | Verify the model/field names match exactly (case-sensitive, use the TypeSpec name, not the Java name) | +| File generated under `src/main/java/java/time/...` | External type identity used on an Enum/Union | Remove the decorator — this is the known emitter bug for Enum/Union external types | +| Compiler error on `@@alternateType` | Wrong target kind | External types must target type definitions, not properties. TypeSpec built-ins can target properties. | +| Warning: `external-type-on-model-property` | External type `{ identity: ... }` applied to a property | Move the decorator to the type definition instead | diff --git a/sdk/ai/.skills/wr-load/SKILL.md b/sdk/ai/.skills/wr-load/SKILL.md new file mode 100644 index 000000000000..7d940ee2b42e --- /dev/null +++ b/sdk/ai/.skills/wr-load/SKILL.md @@ -0,0 +1,86 @@ +--- +name: wr-load +description: Manage Azure KeyVault work resources via the work-resources CLI (wr-setup, wr-save, wr-update, wr-load, wr-list, wr-delete, wr-clear, wr-migrate). Use for provisioning, listing, loading, saving, updating, deleting, or clearing secrets. +--- + +# Work Resources (Azure KeyVault) + +Use the `wr-*` CLI commands from the **work-resources** project to manage secrets for test/dev resources. + +## Prerequisite +The `wr-*` commands must be installed and available on PATH. If they are missing, instruct the user to install them from the work-resources repo: +- Repo: https://github.com/jpalvarezl/work-resources +- Install: follow the repo README (install.sh / install.ps1) + +## Key rule: minimize wr-load calls + +`wr-load` fetches secrets from Azure KeyVault over the network. **Call it at most once per session.** After the first call, cache the values you received and reuse them directly in subsequent commands. Do NOT call `wr-load` again for the same resource. + +## Commands + +### `wr-load` +Load secrets into the current shell session. The script detects your shell automatically; use `-Export` to control the output format. + +```bash +wr-load -Resource +wr-load -Resource "res1,res2" +wr-load -SpawnShell +``` + +To combine with another command (env vars persist within the same shell process): +```bash +wr-load -Resource ; +``` + +### `wr-list` +List secrets in the vault (optionally by resource prefix). +```bash +wr-list +wr-list -Resource +``` + +### `wr-save` +Save a new secret. Always pass `-Value` to avoid interactive prompts that hang in pi. +```bash +wr-save -Resource -Name -EnvVarName -Value +``` + +### `wr-update` +Update an existing secret. Always pass `-Value` to avoid interactive prompts that hang in pi. +```bash +wr-update -Resource -Name -EnvVarName -Value +``` + +### `wr-delete` +Delete secrets from the vault. +```bash +wr-delete -Resource -Name -Force +wr-delete -Resource -All -Force +``` + +### `wr-clear` +Clear loaded secrets from the current session. +```bash +wr-clear +wr-clear -Resource +``` + +### `wr-setup` +Initial KeyVault setup (creates resource group/vault and assigns permissions). +```bash +wr-setup +``` + +### `wr-migrate` +Maintenance tool to add missing tags. +```bash +wr-migrate -DryRun +wr-migrate -Force +``` + +## Steps +1. Determine which `wr-*` command matches the user request. +2. If the request requires a resource name and it's missing, run `wr-list` and ask the user to pick one. +3. Execute the appropriate `wr-*` command. +4. For `wr-save` and `wr-update`, always include `-Value` to prevent interactive prompts. +5. After loading secrets with `wr-load`, cache the values. Do not call `wr-load` again for the same resource. From c07e66d9a5e1a883cc2a9f0fce64a6045acf05da Mon Sep 17 00:00:00 2001 From: Gerardo Lecaros <10088504+glecaros@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:14:12 -0800 Subject: [PATCH 02/10] cspell --- sdk/ai/.skills/cspell.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 sdk/ai/.skills/cspell.yaml diff --git a/sdk/ai/.skills/cspell.yaml b/sdk/ai/.skills/cspell.yaml new file mode 100644 index 000000000000..388327934b33 --- /dev/null +++ b/sdk/ai/.skills/cspell.yaml @@ -0,0 +1,14 @@ +import: + - ../../../vscode.json +overrides: + - filename: "**/sdk/ai/.skills/**/SKILL.md" + words: + - java + - FQCN + - dpkg + - Dcodesnippet + - Dcheckstyle + - Dspotless + - Drevapi + - Djacoco + - Danimal \ No newline at end of file From d6506a49ecc0084f1773d837559dfdc70b3362d2 Mon Sep 17 00:00:00 2001 From: Gerardo Lecaros <10088504+glecaros@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:31:33 -0800 Subject: [PATCH 03/10] path --- sdk/ai/.skills/cspell.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/ai/.skills/cspell.yaml b/sdk/ai/.skills/cspell.yaml index 388327934b33..7050d8e0aae2 100644 --- a/sdk/ai/.skills/cspell.yaml +++ b/sdk/ai/.skills/cspell.yaml @@ -1,14 +1,14 @@ import: - - ../../../vscode.json + - ../../../vscode/cspell.json overrides: - filename: "**/sdk/ai/.skills/**/SKILL.md" words: - - java - - FQCN - - dpkg - - Dcodesnippet + - Danimal - Dcheckstyle - - Dspotless - - Drevapi + - Dcodesnippet - Djacoco - - Danimal \ No newline at end of file + - dpkg + - Drevapi + - Dspotless + - FQCN + - java \ No newline at end of file From 3978870df920f4b629082430cb403b7b43773200 Mon Sep 17 00:00:00 2001 From: Gerardo Lecaros <10088504+glecaros@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:50:09 -0800 Subject: [PATCH 04/10] path --- sdk/ai/.skills/cspell.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/.skills/cspell.yaml b/sdk/ai/.skills/cspell.yaml index 7050d8e0aae2..611ada051697 100644 --- a/sdk/ai/.skills/cspell.yaml +++ b/sdk/ai/.skills/cspell.yaml @@ -1,5 +1,5 @@ import: - - ../../../vscode/cspell.json + - ../../../.vscode/cspell.json overrides: - filename: "**/sdk/ai/.skills/**/SKILL.md" words: From 3d0c9f7e31838e4eeb0120e376195f0fb59a4d44 Mon Sep 17 00:00:00 2001 From: Gerardo Lecaros <10088504+glecaros@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:10:14 +0000 Subject: [PATCH 05/10] fix --- sdk/ai/.skills/cspell.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/ai/.skills/cspell.yaml b/sdk/ai/.skills/cspell.yaml index 611ada051697..89de4646568f 100644 --- a/sdk/ai/.skills/cspell.yaml +++ b/sdk/ai/.skills/cspell.yaml @@ -1,7 +1,7 @@ import: - ../../../.vscode/cspell.json overrides: - - filename: "**/sdk/ai/.skills/**/SKILL.md" + - filename: "**/sdk/ai/.skills/*" words: - Danimal - Dcheckstyle @@ -9,6 +9,7 @@ overrides: - Djacoco - dpkg - Drevapi + - Dspotbugs - Dspotless - FQCN - - java \ No newline at end of file + - javap \ No newline at end of file From 9ab4072a583999543c1252c7003c6cfb1fb657bb Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Mon, 9 Mar 2026 14:31:53 +0100 Subject: [PATCH 06/10] Skill polish --- sdk/ai/.skills/run-tests/SKILL.md | 14 ++--- sdk/ai/.skills/wr-load/SKILL.md | 86 ------------------------------- 2 files changed, 3 insertions(+), 97 deletions(-) delete mode 100644 sdk/ai/.skills/wr-load/SKILL.md diff --git a/sdk/ai/.skills/run-tests/SKILL.md b/sdk/ai/.skills/run-tests/SKILL.md index e4d665dd5d1f..49c95632165c 100644 --- a/sdk/ai/.skills/run-tests/SKILL.md +++ b/sdk/ai/.skills/run-tests/SKILL.md @@ -34,15 +34,7 @@ mvn test ## Running tests with secrets -Tests often require environment variables with connection strings, keys, or other secrets. - -- If `wr-load` is available, use it to load env vars from KeyVault before running tests: - ```bash - wr-load -Resource ; AZURE_TEST_MODE=LIVE mvn "-Dtest=" test - ``` - **Minimize `wr-load` calls** — it fetches secrets over the network. Run it once, capture the values, and reuse them. - -- If `wr-load` is **not** available, ask the user to set the required environment variables manually before running tests. Suggest they check the project's `test-resources.json` or README for the expected variable names. +Tests often require environment variables with connection strings, keys, or other secrets. If another skill or tool for loading secrets or work resources is available, consult the user and use it. Otherwise, ask the user to provide the required environment variables manually. To discover which variables are needed, search for classes named `*ClientTestBase` in the test sources — these typically define the expected environment variable names. ## Steps 1. Ensure you're in the correct Maven project directory (contains `pom.xml`). If not, ask for the correct path. @@ -50,7 +42,7 @@ Tests often require environment variables with connection strings, keys, or othe 3. If the user provides a test name, use `-Dtest=`. 4. If the user specifies a module, use `-pl -am test`. 5. If the user specifies a test mode (LIVE/RECORD/PLAYBACK), set the env var. -6. If tests require secrets, use `wr-load` if available; otherwise ask the user to set the required environment variables manually. +6. If tests require secrets, use an available skill or tool for loading secrets if one exists (consult the user first); otherwise ask the user to provide the required environment variables manually. 7. If the command fails, report the error output and ask how they want to proceed. ## Troubleshooting (only add flags when needed) @@ -61,6 +53,6 @@ Apply these **only** if the simple `mvn test` command fails with the specific er - **JPMS/module-path errors** (e.g., `okio` module issues): add `-Dsurefire.useModulePath=false` - **Build plugins block the run**: add skip flags as needed, e.g. `-Denforcer.skip=true -Dcodesnippet.skip=true -Dcheckstyle.skip=true` - **Reactor blocking errors in async tests** (Netty thread): add `$env:AZURE_TEST_HTTP_CLIENTS = "okhttp"` -- **SSL handshake / PKIX path building failed** (`SSLHandshakeException`, `unable to find valid certification path to requested target`): the JVM's trust store is missing the corporate root CA. Add `-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT` to use the Windows certificate store instead. This is common on corporate networks with proxy/firewall TLS interception. +- **SSL handshake / PKIX path building failed** (`SSLHandshakeException`, `unable to find valid certification path to requested target`): the JVM's trust store is missing the corporate root CA. On **Windows**, add `-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT` to use the Windows certificate store. On **Linux/macOS**, the corporate root CA must be imported into the JVM trust store (or configure a custom one via `-Djavax.net.ssl.trustStore` / `-Djavax.net.ssl.trustStorePassword`). This is common on corporate networks with proxy/firewall TLS interception. Do NOT preemptively add all these flags. Start simple and escalate only on failure. diff --git a/sdk/ai/.skills/wr-load/SKILL.md b/sdk/ai/.skills/wr-load/SKILL.md deleted file mode 100644 index 7d940ee2b42e..000000000000 --- a/sdk/ai/.skills/wr-load/SKILL.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: wr-load -description: Manage Azure KeyVault work resources via the work-resources CLI (wr-setup, wr-save, wr-update, wr-load, wr-list, wr-delete, wr-clear, wr-migrate). Use for provisioning, listing, loading, saving, updating, deleting, or clearing secrets. ---- - -# Work Resources (Azure KeyVault) - -Use the `wr-*` CLI commands from the **work-resources** project to manage secrets for test/dev resources. - -## Prerequisite -The `wr-*` commands must be installed and available on PATH. If they are missing, instruct the user to install them from the work-resources repo: -- Repo: https://github.com/jpalvarezl/work-resources -- Install: follow the repo README (install.sh / install.ps1) - -## Key rule: minimize wr-load calls - -`wr-load` fetches secrets from Azure KeyVault over the network. **Call it at most once per session.** After the first call, cache the values you received and reuse them directly in subsequent commands. Do NOT call `wr-load` again for the same resource. - -## Commands - -### `wr-load` -Load secrets into the current shell session. The script detects your shell automatically; use `-Export` to control the output format. - -```bash -wr-load -Resource -wr-load -Resource "res1,res2" -wr-load -SpawnShell -``` - -To combine with another command (env vars persist within the same shell process): -```bash -wr-load -Resource ; -``` - -### `wr-list` -List secrets in the vault (optionally by resource prefix). -```bash -wr-list -wr-list -Resource -``` - -### `wr-save` -Save a new secret. Always pass `-Value` to avoid interactive prompts that hang in pi. -```bash -wr-save -Resource -Name -EnvVarName -Value -``` - -### `wr-update` -Update an existing secret. Always pass `-Value` to avoid interactive prompts that hang in pi. -```bash -wr-update -Resource -Name -EnvVarName -Value -``` - -### `wr-delete` -Delete secrets from the vault. -```bash -wr-delete -Resource -Name -Force -wr-delete -Resource -All -Force -``` - -### `wr-clear` -Clear loaded secrets from the current session. -```bash -wr-clear -wr-clear -Resource -``` - -### `wr-setup` -Initial KeyVault setup (creates resource group/vault and assigns permissions). -```bash -wr-setup -``` - -### `wr-migrate` -Maintenance tool to add missing tags. -```bash -wr-migrate -DryRun -wr-migrate -Force -``` - -## Steps -1. Determine which `wr-*` command matches the user request. -2. If the request requires a resource name and it's missing, run `wr-list` and ask the user to pick one. -3. Execute the appropriate `wr-*` command. -4. For `wr-save` and `wr-update`, always include `-Value` to prevent interactive prompts. -5. After loading secrets with `wr-load`, cache the values. Do not call `wr-load` again for the same resource. From 16a7e545cd1000ef6d5d494e76c0d34b1881017f Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 11 Mar 2026 14:18:07 +0100 Subject: [PATCH 07/10] Moved files to avoid using reserved word for name --- sdk/ai/.skills/dup-classes/SKILL.md | 61 ---- .../codegen/SKILL.md | 0 .../{.skills => .workflow_docs}/cspell.yaml | 28 +- sdk/ai/.workflow_docs/dedup-openai/SKILL.md | 252 +++++++++++++ sdk/ai/.workflow_docs/dup-classes/SKILL.md | 86 +++++ .../github/SKILL.md | 0 .../release-notes/SKILL.md | 0 .../run-tests/SKILL.md | 0 .../search-m2/SKILL.md | 0 .../test-proxy/SKILL.md | 0 .../tsp-naming-collision/SKILL.md | 0 .../tsp-type-override/SKILL.md | 39 +- .../union-type-wrappers/SKILL.md | 336 ++++++++++++++++++ 13 files changed, 724 insertions(+), 78 deletions(-) delete mode 100644 sdk/ai/.skills/dup-classes/SKILL.md rename sdk/ai/{.skills => .workflow_docs}/codegen/SKILL.md (100%) rename sdk/ai/{.skills => .workflow_docs}/cspell.yaml (80%) create mode 100644 sdk/ai/.workflow_docs/dedup-openai/SKILL.md create mode 100644 sdk/ai/.workflow_docs/dup-classes/SKILL.md rename sdk/ai/{.skills => .workflow_docs}/github/SKILL.md (100%) rename sdk/ai/{.skills => .workflow_docs}/release-notes/SKILL.md (100%) rename sdk/ai/{.skills => .workflow_docs}/run-tests/SKILL.md (100%) rename sdk/ai/{.skills => .workflow_docs}/search-m2/SKILL.md (100%) rename sdk/ai/{.skills => .workflow_docs}/test-proxy/SKILL.md (100%) rename sdk/ai/{.skills => .workflow_docs}/tsp-naming-collision/SKILL.md (100%) rename sdk/ai/{.skills => .workflow_docs}/tsp-type-override/SKILL.md (70%) create mode 100644 sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md diff --git a/sdk/ai/.skills/dup-classes/SKILL.md b/sdk/ai/.skills/dup-classes/SKILL.md deleted file mode 100644 index 3e3804509723..000000000000 --- a/sdk/ai/.skills/dup-classes/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: dup-classes -description: Verify whether generated Java classes duplicate openai-java models by comparing fields/types (names may differ). Use when checking for duplicate model coverage. ---- - -# Duplicate Class Verification (Generated vs openai-java) - -Use this skill to compare generated Java models against the `openai-java` dependency. The goal is **field-by-field** comparison of model shapes, even when class or field names differ. - -## Inputs to confirm -- Generated source root (e.g., `src/main/java/...`) -- The relevant `pom.xml` (module) to resolve the `openai-java` dependency version -- Optional: package or class name hints to narrow the search - -## Steps -1. **Locate the pom.xml** in the current directory tree (`find . -name pom.xml`). If multiple, ask which module to use. -2. **Resolve openai-java**: - - Search the chosen pom for `openai-java` (or an explicit group/artifact provided by the user). - - Resolve the version (including properties) and locate the JAR in `~/.m2/repository`. -3. **List candidate classes**: - - Generated classes: scan the source root for `class` and `record` declarations. - - openai-java classes: `jar tf | rg '\.class$'` (filter by package hints if provided). -4. **Extract field signatures** (names may differ; compare shape): - - **Generated source**: - - For `record`, use the component list in the `record` declaration. - - For `class`, extract non-static field declarations (type + count) and note any `@JsonProperty` names. - - **openai-java JAR**: - - Use `javap -classpath -p ` to list fields (ignore `static`). - - If you need annotations, use `javap -classpath -p -verbose ` and look for `RuntimeVisibleAnnotations`. -5. **Compare shapes**: - - Compare **field count** and **field types** (order-independent). - - If `@JsonProperty` names exist in generated sources, compare those names with the openai-java field names. -6. **Report duplicates**: - - Provide a table: generated class → openai-java class, with matching field types and any name mismatches. - - Flag candidates with high similarity (same count and same types) even if names differ. - -## Useful commands - -### List generated class names -```bash -rg -n "^(public\s+)?(final\s+)?(class|record)\s+" -``` - -### Extract field lines from source (classes) -```bash -rg -n "^(\s*)(public|protected|private)\s+(static\s+)?(final\s+)?[A-Za-z0-9_<>,\[\].]+\s+[A-Za-z0-9_]+;" -``` - -### Extract record components -```bash -rg -n "record\s+[A-Za-z0-9_]+\s*\(([^)]*)\)" -``` - -### Inspect fields in a JAR class -```bash -javap -classpath -p -``` - -## Notes -- Use `search-m2` if you need help locating the dependency version or JAR path. -- If the user provides only a vague class hint, narrow candidates by package or field count first. diff --git a/sdk/ai/.skills/codegen/SKILL.md b/sdk/ai/.workflow_docs/codegen/SKILL.md similarity index 100% rename from sdk/ai/.skills/codegen/SKILL.md rename to sdk/ai/.workflow_docs/codegen/SKILL.md diff --git a/sdk/ai/.skills/cspell.yaml b/sdk/ai/.workflow_docs/cspell.yaml similarity index 80% rename from sdk/ai/.skills/cspell.yaml rename to sdk/ai/.workflow_docs/cspell.yaml index 89de4646568f..3f7a39458ecd 100644 --- a/sdk/ai/.skills/cspell.yaml +++ b/sdk/ai/.workflow_docs/cspell.yaml @@ -1,15 +1,15 @@ -import: - - ../../../.vscode/cspell.json -overrides: - - filename: "**/sdk/ai/.skills/*" - words: - - Danimal - - Dcheckstyle - - Dcodesnippet - - Djacoco - - dpkg - - Drevapi - - Dspotbugs - - Dspotless - - FQCN +import: + - ../../../.vscode/cspell.json +overrides: + - filename: "**/sdk/ai/.workflow_docs/*" + words: + - Danimal + - Dcheckstyle + - Dcodesnippet + - Djacoco + - dpkg + - Drevapi + - Dspotbugs + - Dspotless + - FQCN - javap \ No newline at end of file diff --git a/sdk/ai/.workflow_docs/dedup-openai/SKILL.md b/sdk/ai/.workflow_docs/dedup-openai/SKILL.md new file mode 100644 index 000000000000..965d9bc032ac --- /dev/null +++ b/sdk/ai/.workflow_docs/dedup-openai/SKILL.md @@ -0,0 +1,252 @@ +--- +name: dedup-openai +description: Suppress generated Java classes that duplicate openai-java models, using @@alternateType in TypeSpec and manual serialization bridges. Use after dup-classes has identified actionable duplicates. +--- + +# De-duplicate Generated Classes Against openai-java + +Use this skill **after** the `dup-classes` skill has identified actionable duplicates. This skill suppresses the generated classes and bridges to the openai-java equivalents. + +## Preconditions +- A `tsp-location.yaml` must exist in the current directory. +- TypeSpec must be synced (`tsp-client sync`) so `TempTypeSpecFiles/` exists. +- The `openai-java` dependency must be in the project's `pom.xml`. +- You must know which classes to suppress (use `dup-classes` skill first). + +## Key concepts + +### What can be suppressed +Only **standalone models** that don't participate in a discriminator hierarchy. A model is standalone if: +- It does NOT extend `Tool`, `TextResponseFormatConfiguration`, or another base class with a `fromJson` discriminator +- It is NOT a subtype dispatched by a parent's `fromJson` method + +### What cannot be suppressed +**Structural equivalents** — classes that extend a base type in a discriminator hierarchy (e.g., `FunctionTool extends Tool`). The SDK's polymorphic serialization requires these. They produce identical JSON but are not actionable. Do NOT attempt to suppress them. + +### Two suppression mechanisms + +| Mechanism | When to use | Effect | +|-----------|-------------|--------| +| `@@alternateType(OpenAI.X, { identity: "com.openai.models.X" }, "java")` | The model is referenced as a field type or union member in other models | Codegen replaces the type with the openai-java class. The generated class is NOT emitted at all. | +| `@@access(OpenAI.X, Access.internal, "java")` | The model is NOT referenced by any public model | Codegen moves the class to `implementation.models`. | + +**Prefer `@@alternateType`** — it fully prevents emission and is the cleanest approach. Use `@@access(internal)` only as a supplement when a model isn't reachable through the type graph but is still emitted. + +### Why `@@access(internal)` alone may not work +If a model is referenced by a union or property in a public model, `@@access(internal)` will NOT move it. The codegen keeps it public because removing it would break the type graph. Example: `ComparisonFilter` is a member of the `Filters` union used by `FileSearchTool.filters` — `@@access(internal)` has no effect, but `@@alternateType` prevents emission entirely. + +## Steps + +### 1. Edit the TypeSpec client file + +Locate the `client.tsp` (or `client.java.tsp`) in `TempTypeSpecFiles/sdk-*/`: + +```bash +find TempTypeSpecFiles -name "client*.tsp" -path "*/sdk-*" +``` + +Add `@@alternateType` directives for each actionable duplicate: + +```tsp +// De-dup: map to openai-java equivalents +@@alternateType(OpenAI.ComparisonFilter, { identity: "com.openai.models.ComparisonFilter" }, "java"); +@@alternateType(OpenAI.Reasoning, { identity: "com.openai.models.Reasoning" }, "java"); +``` + +**Finding the correct model name:** The TypeSpec models are in the `OpenAI` namespace. Search the openai-typespec package: + +```bash +grep -rn "^model " TempTypeSpecFiles/node_modules/@azure-tools/openai-typespec/src/ --include="*.tsp" +``` + +### 2. Regenerate and verify + +```bash +tsp-client generate +``` + +After generation, verify the suppressed classes are gone: + +```bash +# Should NOT exist: +ls src/main/java/com/azure/ai/agents/models/.java +# Should NOT exist in implementation either (with @@alternateType): +ls src/main/java/com/azure/ai/agents/implementation/models/.java +``` + +### 3. Fix serialization in parent models + +When a model's property type changes from a generated `JsonSerializable` class to an openai-java class, the `toJson`/`fromJson` methods in parent models will break because the openai-java type doesn't implement `JsonSerializable`. + +**Pattern for `toJson`** — use `OpenAIJsonHelper.toBinaryData()`: +```java +// Before (generated, won't compile): +jsonWriter.writeJsonField("reasoning", this.reasoning); + +// After: +if (this.reasoning != null) { + jsonWriter.writeFieldName("reasoning"); + OpenAIJsonHelper.toBinaryData(this.reasoning).writeTo(jsonWriter); +} +``` + +**Pattern for `fromJson`** — read as BinaryData, convert with `OpenAIJsonHelper.fromBinaryData()`: +```java +// Before (generated, won't compile): +reasoning = Reasoning.fromJson(reader); + +// After: +BinaryData reasoningData + = reader.getNullable(nonNullReader -> BinaryData.fromObject(nonNullReader.readUntyped())); +reasoning = OpenAIJsonHelper.fromBinaryData(reasoningData, com.openai.models.Reasoning.class); +``` + +**Pattern for getter/setter** — use the openai-java type directly, with javadoc above and marker comment inside the body: +```java +// Field stores the openai-java type directly (no BinaryData indirection) +private com.openai.models.Reasoning reasoning; // AI Tooling: openai-java de-dup + +/** + * Gets the reasoning configuration. + * @return the reasoning, or null if not set. + */ +public com.openai.models.Reasoning getReasoning() { + // AI Tooling: openai-java de-dup + return this.reasoning; +} + +/** + * Sets the reasoning configuration. + * @param reasoning the reasoning to set. + * @return this object. + */ +public PromptAgentDefinition setReasoning(com.openai.models.Reasoning reasoning) { + // AI Tooling: openai-java de-dup + this.reasoning = reasoning; + return this; +} +``` + +Remove `@Generated` from any method you modify so the codegen preserves your changes on re-generation. See [Codegen survival rules](#codegen-survival-rules) for comment/javadoc placement. + +### 4. Add typed convenience setters (for BinaryData fields) + +When a property is already `BinaryData` (e.g., because it's a union type), add **distinctly named** setter methods for the openai-java types. Do NOT overload `setX` with different parameter types — this causes null-ambiguity. Use descriptive names instead: + +```java +/** + * Sets the filters using an openai-java ComparisonFilter. + * @param filter the filter to apply, or null to clear. + * @return this object. + */ +public FileSearchTool setComparisonFilter(com.openai.models.ComparisonFilter filter) { + // AI Tooling: openai-java de-dup + this.filters = OpenAIJsonHelper.toBinaryData(filter); + return this; +} + +/** + * Sets the filters using an openai-java CompoundFilter. + * @param filter the filter to apply, or null to clear. + * @return this object. + */ +public FileSearchTool setCompoundFilter(com.openai.models.CompoundFilter filter) { + // AI Tooling: openai-java de-dup + this.filters = OpenAIJsonHelper.toBinaryData(filter); + return this; +} +``` + +### 5. Add `OpenAIJsonHelper` methods if needed + +The `OpenAIJsonHelper` class in `com.azure.ai.agents.implementation` may need two bridge methods: + +```java +// Serialize openai-java object → BinaryData (writes as JSON object, not quoted string) +public static BinaryData toBinaryData(Object openAIObject) + +// Deserialize BinaryData → openai-java type +public static T fromBinaryData(BinaryData data, Class type) +``` + +These use the openai-java `ObjectMappers.jsonMapper()` (which handles Kotlin internals correctly). Do NOT use `BinaryData.fromObject()` or `BinaryData.toObject()` with openai-java types — the default Jackson ObjectMapper cannot serialize Kotlin `SynchronizedLazyImpl` fields. + +### 6. Write serialization tests + +Write round-trip tests verifying the JSON shape is preserved. Test pattern: + +```java +@Test +public void testRoundTrip() throws IOException { + // Build with openai-java type + Reasoning reasoning = Reasoning.builder().effort(ReasoningEffort.HIGH).build(); + PromptAgentDefinition original = new PromptAgentDefinition("gpt-4o").setReasoning(reasoning); + + // Serialize + String json = serialize(original); + assertTrue(json.contains("\"effort\":\"high\"")); + + // Deserialize + PromptAgentDefinition deserialized = deserialize(json); + assertEquals(ReasoningEffort.HIGH, deserialized.getReasoning().effort().get()); + + // Re-serialize and compare + assertEquals(json, serialize(deserialized)); +} +``` + +Cover: all enum values, null/absent fields, combined with other fields, polymorphic deserialization via parent `fromJson`. + +### 7. Apply changes to the spec repo + +If a local checkout of `Azure/azure-rest-api-specs` is available, apply the same `client.tsp` edits there. Derive the path from `tsp-location.yaml`: + +``` +//client.tsp +``` + +## Codegen survival rules + +The TypeSpec Java codegen (`tsp-client update` / `tsp-client generate`) will re-generate files on every run. Methods **without** `@Generated` are preserved (body intact), but everything **above** the method signature (javadoc, comments) is regenerated. Follow these rules so your manual edits survive: + +1. **Remove `@Generated`** from any method you modify. The codegen will not overwrite the method body. +2. **Place marker comments inside the method body**, not above the signature. The codegen rewrites the javadoc block above the signature but does not touch the body. +3. **Place javadoc above the method** normally. Since the method lacks `@Generated`, the codegen preserves the javadoc you wrote. +4. **For field declarations**, place marker comments on the same line (trailing), not on the line above. The codegen regenerates the comment block above the field. + +```java +// ✅ SURVIVES codegen: javadoc above, marker inside body +/** + * Gets the reasoning configuration. + * @return the reasoning, or null if not set. + */ +public com.openai.models.Reasoning getReasoning() { + // AI Tooling: openai-java de-dup ← inside body, survives + return this.reasoning; +} + +// ❌ WIPED by codegen: marker above signature +// AI Tooling: openai-java de-dup ← above signature, gets wiped +public com.openai.models.Reasoning getReasoning() { + return this.reasoning; +} + +// ✅ SURVIVES codegen: field marker on same line +private com.openai.models.Reasoning reasoning; // AI Tooling: openai-java de-dup + +// ❌ WIPED by codegen: field marker on line above +// AI Tooling: openai-java de-dup +private com.openai.models.Reasoning reasoning; +``` + +## Common pitfalls + +| Problem | Cause | Fix | +|---------|-------|-----| +| Class stays public despite `@@access(internal)` | Referenced by a union or property in a public model | Use `@@alternateType` instead | +| `BinaryData.fromObject(openAIObj)` throws `SynchronizedLazyImpl` error | Default Jackson can't serialize Kotlin internals | Use `OpenAIJsonHelper.toBinaryData()` which uses `ObjectMappers.jsonMapper()` | +| `BinaryData.fromString(json).writeTo(writer)` writes quoted string | `fromString` creates text content, not JSON | Use `BinaryData.fromObject(reader.readUntyped())` to store as a JSON object | +| Getter/setter bridge through BinaryData on every call | Unnecessary indirection | Store the openai-java type directly in the field; bridge only in `toJson`/`fromJson` | +| Tried to suppress a `Tool` subclass | Structural equivalent, not an actionable duplicate | Don't suppress — it's needed for polymorphic deserialization | +| Javadoc/comments above method wiped after codegen | Codegen rewrites everything above non-`@Generated` method signatures | Place marker comments inside the method body; javadoc survives if `@Generated` is removed (see [Codegen survival rules](#codegen-survival-rules)) | +| Overloaded setters cause null ambiguity | `setFilters(null)` matches `BinaryData`, `ComparisonFilter`, and `CompoundFilter` | Use distinct method names: `setComparisonFilter()`, `setCompoundFilter()` | diff --git a/sdk/ai/.workflow_docs/dup-classes/SKILL.md b/sdk/ai/.workflow_docs/dup-classes/SKILL.md new file mode 100644 index 000000000000..71ab3b16d6b8 --- /dev/null +++ b/sdk/ai/.workflow_docs/dup-classes/SKILL.md @@ -0,0 +1,86 @@ +--- +name: dup-classes +description: Verify whether generated Java classes duplicate openai-java models by comparing fields/types (names may differ). Use when checking for duplicate model coverage. +--- + +# Duplicate Class Verification (Generated vs openai-java) + +Use this skill to compare generated Java models against the `openai-java` dependency. The goal is **field-by-field** comparison of model shapes, even when class or field names differ. + +## Inputs to confirm +- Generated source root (e.g., `src/main/java/...`) +- The relevant `pom.xml` (module) to resolve the `openai-java` dependency version +- Optional: package or class name hints to narrow the search + +## Steps +1. **Locate the pom.xml** in the current directory tree (`find . -name pom.xml`). If multiple, ask which module to use. +2. **Resolve openai-java**: + - Search the chosen pom for `openai-java` (or an explicit group/artifact provided by the user). + - Resolve the version (including properties) and locate the JAR in `~/.m2/repository`. + - The model classes live in `openai-java-core`, not the top-level `openai-java` artifact. +3. **List candidate classes**: + - Generated classes: scan the source root for `class` and `record` declarations. + - openai-java classes: `jar tf | grep '\.class$'` (filter by package hints if provided). +4. **Extract field signatures** (names may differ; compare shape): + - **Generated source**: + - For `record`, use the component list in the `record` declaration. + - For `class`, extract non-static field declarations (type + count) and note any `@JsonProperty` names. + - Check `toJson`/`fromJson` methods for the actual JSON keys used in serialization. + - **openai-java JAR**: + - Use `javap -classpath -p ` to list fields (ignore `static`, `validated`, `hashCode$delegate`, `additionalProperties`). + - Extract `@JsonProperty` keys from the sources JAR (`*-sources.jar`) for JSON key comparison. +5. **Compare shapes**: + - Compare **field count** and **field types** (order-independent). + - Compare **JSON keys** from `@JsonProperty` (openai-java) vs `toJson`/`fromJson` string literals (generated). + - Compare **enum/union values** when field types are enums or string unions. + - Follow type hierarchy: `BinaryData` ↔ `JsonValue` (both represent untyped JSON), `Map` ↔ `Map`, Java enum ↔ Kotlin string enum. +6. **Categorize results** (do NOT treat all matches the same): + + **Actionable duplicates** — standalone models not in any type hierarchy. These can potentially be suppressed and replaced with the openai-java equivalent. Examples: `ComparisonFilter`, `Reasoning`. + + **Structural equivalents** — classes that produce identical JSON but participate in a discriminator hierarchy (e.g., `extends Tool`, `extends TextResponseFormatConfiguration`). The SDK's polymorphic serialization (`Tool.fromJson()` dispatches to `FunctionTool.fromJson()`, etc.) requires these to exist. They are NOT actionable duplicates. Examples: `FunctionTool`, `FileSearchTool`, `ComputerUsePreviewTool`. + + **Partial matches** — classes with most fields matching but extra Azure-specific fields. Note the extra fields. Examples: `CodeInterpreterTool` (extra `container`), `WebSearchTool` (extra `custom_search_configuration`). + +7. **Report**: + - Provide a table with: generated class → openai-java class, field count, matching fields, category. + - Clearly separate actionable duplicates from structural equivalents. + - For actionable duplicates, note whether `@@alternateType` or `@@access(internal)` would be the right suppression mechanism (see `dedup-openai` skill). + +## Useful commands + +### List generated class names +```bash +rg -n "^(public\s+)?(final\s+)?(class|record)\s+" +``` + +### Extract field lines from source (classes) +```bash +grep -E '^\s+private\s+' | grep -v 'static\s' +``` + +### Extract JSON keys from generated toJson/fromJson +```bash +grep -E 'jsonWriter\.write|"[a-z_]+"' | grep -v '//' +``` + +### Inspect fields in a JAR class +```bash +javap -classpath -p +``` + +### Extract @JsonProperty from openai-java sources JAR +```bash +jar xf main/com/openai/models/.kt +grep '@JsonProperty' main/com/openai/models/.kt +``` + +### Check if a class participates in a hierarchy +```bash +grep 'extends\s' # If it extends Tool, TextResponseFormatConfiguration, etc. → structural +``` + +## Notes +- Use `search-m2` if you need help locating the dependency version or JAR path. +- If the user provides only a vague class hint, narrow candidates by package or field count first. +- The openai-java classes are Kotlin and use Jackson; generated classes use azure-json (`JsonSerializable`). Compare at the JSON wire level, not at the Java API level. diff --git a/sdk/ai/.skills/github/SKILL.md b/sdk/ai/.workflow_docs/github/SKILL.md similarity index 100% rename from sdk/ai/.skills/github/SKILL.md rename to sdk/ai/.workflow_docs/github/SKILL.md diff --git a/sdk/ai/.skills/release-notes/SKILL.md b/sdk/ai/.workflow_docs/release-notes/SKILL.md similarity index 100% rename from sdk/ai/.skills/release-notes/SKILL.md rename to sdk/ai/.workflow_docs/release-notes/SKILL.md diff --git a/sdk/ai/.skills/run-tests/SKILL.md b/sdk/ai/.workflow_docs/run-tests/SKILL.md similarity index 100% rename from sdk/ai/.skills/run-tests/SKILL.md rename to sdk/ai/.workflow_docs/run-tests/SKILL.md diff --git a/sdk/ai/.skills/search-m2/SKILL.md b/sdk/ai/.workflow_docs/search-m2/SKILL.md similarity index 100% rename from sdk/ai/.skills/search-m2/SKILL.md rename to sdk/ai/.workflow_docs/search-m2/SKILL.md diff --git a/sdk/ai/.skills/test-proxy/SKILL.md b/sdk/ai/.workflow_docs/test-proxy/SKILL.md similarity index 100% rename from sdk/ai/.skills/test-proxy/SKILL.md rename to sdk/ai/.workflow_docs/test-proxy/SKILL.md diff --git a/sdk/ai/.skills/tsp-naming-collision/SKILL.md b/sdk/ai/.workflow_docs/tsp-naming-collision/SKILL.md similarity index 100% rename from sdk/ai/.skills/tsp-naming-collision/SKILL.md rename to sdk/ai/.workflow_docs/tsp-naming-collision/SKILL.md diff --git a/sdk/ai/.skills/tsp-type-override/SKILL.md b/sdk/ai/.workflow_docs/tsp-type-override/SKILL.md similarity index 70% rename from sdk/ai/.skills/tsp-type-override/SKILL.md rename to sdk/ai/.workflow_docs/tsp-type-override/SKILL.md index b7a45f1341ed..b18f9c17b0b0 100644 --- a/sdk/ai/.skills/tsp-type-override/SKILL.md +++ b/sdk/ai/.workflow_docs/tsp-type-override/SKILL.md @@ -56,9 +56,9 @@ Syntax (applied to a **model property**, scoped to Java): This form is **fully supported** on model properties. -#### Form B — External Java type via identity +#### Form B — External Java type via identity (on a type definition) -Use when no TypeSpec scalar maps to the desired Java type (e.g. `java.time.DayOfWeek`). +Use when no TypeSpec scalar maps to the desired Java type (e.g. `java.time.DayOfWeek`), or when you want to **prevent emission of a model entirely** by mapping it to an existing external class (e.g. an openai-java type). Syntax (applied to the **type definition itself**, not a property): @@ -71,6 +71,32 @@ Syntax (applied to the **type definition itself**, not a property): > - A `scope` parameter (e.g. `"java"`) is **required** for external types. > - **Known limitation (as of typespec-java 0.39.x):** The Java emitter does not fully support external types on Enum/Union definitions. It will still generate the class instead of referencing the JDK type. This is tracked as a bug. Only use Form B for Model types until the emitter is fixed. +> **De-duplication use case:** Form B can suppress emission of generated models that duplicate an external dependency. For example, `@@alternateType(OpenAI.Reasoning, { identity: "com.openai.models.Reasoning" }, "java")` prevents the codegen from emitting its own `Reasoning` class — any property typed as `OpenAI.Reasoning` will use `com.openai.models.Reasoning` directly. This works for Model types that are members of unions too (e.g. `ComparisonFilter` inside a `Filters` union). See the `dedup-openai` skill for the full workflow including serialization fixes. + +#### Form C — External Java type on a single property (model indirection) + +Use when you need to override a **single property** to an external Java type, but Form B cannot be used because it would change the type globally, and `{ identity: ... }` cannot be applied directly to properties. + +The workaround is a **two-step indirection**: define a dummy model annotated with the external identity, then use `@@alternateType` on the property pointing to that model. + +```tsp +// Step 1: Define a dummy model with the external Java type identity +@alternateType({ identity: "java.util.TimeZone" }, "java") +model TimeZoneType {} + +// Step 2: Override the property type to use the dummy model +@@alternateType(OpenAI.ApproximateLocation.timezone, TimeZoneType, "java"); +``` + +**Why this works:** `@alternateType({identity: ...})` is supported on Model definitions (Form B). The `@@alternateType` on a property accepts any TypeSpec type as the alternate (Form A). By combining both, the property override resolves through the model to the external Java class. + +**What does NOT work (and why this form exists):** +- `{ identity: ... }` directly on a property → compiler error / silently ignored. +- `@alternateType({identity: ...})` on a `scalar` → the Java emitter ignores the identity and falls back to `BinaryData` (as of typespec-java 0.40.x). +- `@alternateType({identity: ...})` on a `scalar extends string` → the emitter ignores the identity entirely and uses `String`. + +**Important:** The Java emitter generates `writeJsonField` / `TypeName.fromJson(reader)` calls for the overridden property, which will **not compile** because the external Java type (e.g. `java.util.TimeZone`) does not implement `JsonSerializable`. You **must** fix `toJson`/`fromJson` manually — see step 5 below. + ### 3. Apply the override Edit the `client.java.tsp` file inside `TempTypeSpecFiles/`. Add the decorator(s) under the type-replacement section (usually at the bottom of the file): @@ -153,7 +179,8 @@ void roundTripPreservesValues() throws IOException { When the emitter generates incorrect serialization (e.g. `element.name()` instead of PascalCase), you must manually fix the `toJson` and `fromJson` methods in the generated model class: 1. **Remove the `@Generated` annotation** from `toJson` and `fromJson`. This ensures your customizations survive future `tsp-client generate` / `tsp-client update` runs — the codegen will not overwrite methods that lack `@Generated`. -2. Fix the serialization logic to convert between the Java type and the TSP wire format. For example, for `java.time.DayOfWeek`: +2. **Place any marker comments inside the method body**, not above the signature. The codegen rewrites everything above the method signature (including javadoc and comments) even for non-`@Generated` methods. Javadoc you write above a non-`@Generated` method will survive, but standalone comments above the signature will be wiped. Place markers like `// AI Tooling: ...` on the first line inside the method body. For fields, place marker comments on the same line (trailing), not on the line above. +3. Fix the serialization logic to convert between the Java type and the TSP wire format. For example, for `java.time.DayOfWeek`: - **`toJson`**: convert `DayOfWeek.MONDAY` → `"Monday"` (PascalCase) using a helper like: ```java private static String toPascalCase(DayOfWeek day) { @@ -167,6 +194,10 @@ When the emitter generates incorrect serialization (e.g. `element.name()` instea ``` 3. Re-run the unit tests and confirm all three scenarios pass. +**Form C serialization fixes:** When using the model indirection (Form C), the emitter generates `writeJsonField("field", this.field)` and `ExternalType.fromJson(reader)` — both will fail to compile because the external Java type does not implement `JsonSerializable`. Fix by: + - **`toJson`**: replace `writeJsonField` with the correct writer method (e.g. `writeStringField("timezone", this.timezone != null ? this.timezone.getID() : null)`) + - **`fromJson`**: replace `ExternalType.fromJson(reader)` with the correct factory (e.g. `TimeZone.getTimeZone(reader.getString())`) + ### 6. Apply changes to the local spec repo (if provided) If the user supplied a local checkout path for `Azure/azure-rest-api-specs`, apply the **same edits** to the `client.java.tsp` there. Derive the file path from `tsp-location.yaml`: @@ -205,3 +236,5 @@ After confirming the generated code is correct, remind the user: | File generated under `src/main/java/java/time/...` | External type identity used on an Enum/Union | Remove the decorator — this is the known emitter bug for Enum/Union external types | | Compiler error on `@@alternateType` | Wrong target kind | External types must target type definitions, not properties. TypeSpec built-ins can target properties. | | Warning: `external-type-on-model-property` | External type `{ identity: ... }` applied to a property | Move the decorator to the type definition instead | +| Property becomes `BinaryData` instead of external type | `@alternateType({identity: ...})` used on a `scalar` | Scalars don't support external identity resolution. Use Form C (model indirection) instead. | +| `writeJsonField` / `fromJson` compile errors after Form C | Emitter treats external type as `JsonSerializable` | Remove `@Generated` from `toJson`/`fromJson` and fix serialization manually (see step 5). | diff --git a/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md b/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md new file mode 100644 index 000000000000..6c7250cb9c08 --- /dev/null +++ b/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md @@ -0,0 +1,336 @@ +--- +name: union-type-wrappers +description: Add typed getters and setters over BinaryData properties that represent TypeSpec union types in generated Java models. Use when generated classes expose BinaryData for union-typed fields and you need ergonomic, type-safe accessors instead. +--- + +# Union Type Wrappers for Generated Java Models + +When the Java codegen encounters a TypeSpec union type (e.g. `string | SomeModel`), it emits the property as `BinaryData`. This skill replaces public `BinaryData` accessors with typed setters and getters for each union variant, following the same pattern established for `PromptAgentDefinition.toolChoice`. + +## Preconditions +- You must be in a Java SDK module directory containing `tsp-location.yaml` and `pom.xml`. +- TypeSpec sources must be available in `TempTypeSpecFiles/`. If missing, run `tsp-client sync` first. +- The project must compile before starting. + +## Important: BinaryData.writeTo(JsonWriter) semantics + +Understanding how `BinaryData` writes to JSON is critical for choosing the correct factory method: + +| Factory method | Content type | `writeTo(JsonWriter)` calls | JSON output | +|---|---|---|---| +| `BinaryData.fromString("auto")` | `StringContent` | `jsonWriter.writeString("auto")` | `"auto"` (quoted) | +| `BinaryData.fromObject("auto")` | `SerializableContent` | `jsonWriter.writeRawValue(...)` | `"auto"` (quoted via Jackson) | +| `BinaryData.fromObject(42.0)` | `SerializableContent` | `jsonWriter.writeRawValue(...)` | `42.0` (raw) | +| `BinaryData.fromObject(true)` | `SerializableContent` | `jsonWriter.writeRawValue(...)` | `true` (raw) | +| `BinaryData.fromObject(jsonSerializable)` | `SerializableContent` | `jsonWriter.writeRawValue(...)` | `{...}` (JSON object via JacksonAdapter) | + +Key: `JacksonAdapter` has special handling for `JsonSerializable` types — `BinaryData.fromObject()` and `BinaryData.toObject()` both work correctly with Azure `JsonSerializable` models. + +### Setter factory method rules + +| Union variant | Factory method | Reason | +|---|---|---| +| String value (ID, enum token) | `BinaryData.fromString(value)` | Writes as JSON string via `writeString()` | +| Numeric / boolean primitive | `BinaryData.fromObject(value)` | Writes as raw JSON value | +| Azure `JsonSerializable` model | `BinaryData.fromObject(value)` | JacksonAdapter handles serialization | +| Stainless (openai-java) type | `BinaryData.fromObject(value)` | Jackson handles serialization | + +### Getter deserialization rules + +| Union variant | Deserialization | Notes | +|---|---|---| +| String | `this.field.toString()` | Works for both `StringContent` and `SerializableContent` | +| Primitive (Number, Boolean) | `this.field.toObject(Double.class)` etc. | Jackson deserializes raw JSON values | +| Azure `JsonSerializable` model | `this.field.toObject(ModelClass.class)` | JacksonAdapter calls `fromJson()` | +| Stainless type | `this.field.toObject(StainlessType.class)` | Jackson deserializes natively | + +## Workflow + +### 1. Scan for BinaryData properties + +Find all `BinaryData` fields in generated model classes: + +```bash +grep -rn "private\s\+\(final\s\+\)\?BinaryData\s\+" src/main/java/ --include="*.java" +``` + +Exclude `List` and `Map<..., BinaryData>` from this pass — those are collection-of-union patterns that require separate handling. + +### 2. Cross-reference against TypeSpec + +For each `BinaryData` property found, determine whether it comes from a **union type** or an **`unknown` type**: + +```bash +# Search for the property name (use the wire name, e.g. tool_choice, not toolChoice) +grep -rn "" TempTypeSpecFiles/ --include="*.tsp" +``` + +- **Union type** (`type_a | type_b`): Proceed with this skill. +- **`unknown` type**: Leave as `BinaryData` — this is the correct representation. + +Also check `client.tsp` for any `@@changePropertyType` overrides that may have already flattened the union to `string` (like `tool_choice`). + +### 3. Identify the union variants + +For each union type, determine what types the property can hold. Sources: + +1. **Local TSP files** — look at the type definition in `TempTypeSpecFiles/`. +2. **Stainless SDK JAR** — if the union comes from `OpenAI.*`, inspect the Stainless types: + ```bash + jar tf ~/.m2/repository/com/openai/openai-java-core//openai-java-core-.jar | grep "" + javap -cp "com.openai.models.responses.\$" + ``` +3. **Generated Azure models** — check if the Azure SDK already generates the variant model classes (e.g. `AutoCodeInterpreterToolParam`, `McpToolFilter`). + +### 4. Apply changes to the model class + +For each union-typed `BinaryData` property, apply the following pattern: + +#### 4a. Leave the property field as-is + +Do **not** modify the property declaration. Keep `@Generated`, the block comment, and the visibility exactly as the codegen produced them. The field is already `private` — there is nothing to change. + +```java +/* + * Original generated block comment. + */ +@Generated +private BinaryData myField; +``` + +#### 4b. Make the existing getter and setter private + +- Remove `@Generated`. +- Change visibility to `private`. +- **Keep the original method name** — do NOT rename to `*Internal`. +- **Keep the original javadoc intact.** +- Add `// AI Tooling: union type` as the **first line inside the method body**. + +```java +/** + * Get the myField property: original description. + * + * @return the myField value. + */ +private BinaryData getMyField() { + // AI Tooling: union type + return this.myField; +} + +/** + * Set the myField property: original description. + * + * @param myField the myField value to set. + * @return the MyClass object itself. + */ +private MyClass setMyField(BinaryData myField) { + // AI Tooling: union type + this.myField = myField; + return this; +} +``` + +**For `@Immutable` classes (value is a constructor param):** + +- Change the `BinaryData` constructor visibility to package-private (keep for `fromJson` deserialization). +- Add public constructor overloads for each variant. +- Make the `BinaryData` getter private. + +```java +/** + * Creates an instance of MyFilter class. + * + * @param type the type value to set. + * @param key the key value to set. + * @param value the value value to set. + */ +MyFilter(MyFilterType type, String key, BinaryData value) { + // AI Tooling: union type + this.type = type; + this.key = key; + this.value = value; +} + +public MyFilter(MyFilterType type, String key, String value) { + this.type = type; + this.key = key; + this.value = BinaryData.fromObject(value); +} +``` + +#### 4c. Add typed setters (one per union variant) + +Naming convention: **`set( value)`** — use method overloading. + +Copy the javadoc from the original generated setter, adapting the `@param` description to the specific variant type. Add `// AI Tooling: union type` as the first line inside the method body. + +```java +/** + * Set the myField property: original description. + * + * @param myField the string value to set. + * @return the MyClass object itself. + */ +public MyClass setMyField(String myField) { + // AI Tooling: union type + this.myField = BinaryData.fromString(myField); + return this; +} + +/** + * Set the myField property: original description. + * + * @param myField the SomeModel value to set. + * @return the MyClass object itself. + */ +public MyClass setMyField(SomeModel myField) { + // AI Tooling: union type + this.myField = BinaryData.fromObject(myField); + return this; +} +``` + +When overloading isn't possible (e.g. two different `String` meanings), disambiguate with parameter names and javadoc. + +For `List` variants: +```java +/** + * Set the allowedTools property: original description. + * + * @param allowedTools the list of tool name strings to set. + * @return the McpTool object itself. + */ +public McpTool setAllowedTools(List allowedTools) { + // AI Tooling: union type + this.allowedTools = BinaryData.fromObject(allowedTools); + return this; +} +``` + +#### 4d. Add typed getters (one per union variant) + +Naming convention: **`getAs()`** + +Copy the javadoc from the original generated getter, adapting the `@return` description. Add `// AI Tooling: union type` as the first line inside the method body. + +```java +/** + * Get the myField property as a String: original description. + * + * @return the myField value as a String. + */ +public String getMyFieldAsString() { + // AI Tooling: union type + if (this.myField == null) { + return null; + } + return this.myField.toString(); +} + +/** + * Get the myField property as a {@link SomeModel}: original description. + * + * @return the myField value as a SomeModel. + */ +public SomeModel getMyFieldAsSomeModel() { + // AI Tooling: union type + if (this.myField == null) { + return null; + } + return this.myField.toObject(SomeModel.class); +} +``` + +For `List` variants: +```java +/** + * Get the allowedTools property as a list of tool name strings: original description. + * + * @return the allowedTools value as a list of Strings. + */ +@SuppressWarnings("unchecked") +public List getAllowedToolsAsStringList() { + // AI Tooling: union type + if (this.allowedTools == null) { + return null; + } + return this.allowedTools.toObject(List.class); +} +``` + +### 5. Update callers + +Search for existing code that uses the old `BinaryData` API: + +```bash +grep -rn "\.setMyField(BinaryData\|\.getMyField()" src/ --include="*.java" +``` + +Update samples, tests, and internal code to use the new typed API. Remove unused `BinaryData` imports where applicable. + +### 6. Write unit tests + +Create a test class per model under `src/test/java/.../models/SerializationTests.java`. + +Each test class must include: + +1. **Serialization tests** — one per union variant. Construct the model with the typed setter, serialize to JSON, assert the JSON contains the expected field and value. +2. **Deserialization tests** — one per union variant. Parse a JSON string, assert the typed getter returns the correct value. +3. **Null/absent tests** — verify getters return `null` when the field is not set or absent from JSON. +4. **Round-trip tests** — serialize → deserialize → assert values match. + +Use these helpers: + +```java +private String serializeToJson(MyModel model) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + model.toJson(jsonWriter); + } + return outputStream.toString("UTF-8"); +} + +private MyModel deserializeFromJson(String json) throws IOException { + try (JsonReader jsonReader = JsonProviders.createReader(json)) { + return MyModel.fromJson(jsonReader); + } +} +``` + +### 7. Compile and run tests + +```bash +mvn compile -Dbuildhelper.addtestsource.skip=true -Dbuildhelper.addtestresource.skip=true \ + -Dcodesnippet.skip=true -Dcheckstyle.skip=true -Dspotless.check.skip=true + +mvn "-Dtest=*SerializationTests" test \ + -Dcodesnippet.skip=true -Dcheckstyle.skip=true -Dspotless.check.skip=true +``` + +All tests must pass before finishing. + +## Checklist + +Before reporting completion, verify: + +- [ ] Every `BinaryData` property was classified as **union** or **unknown** +- [ ] Property fields left exactly as generated (no modifications) +- [ ] Original `BinaryData` getter/setter made private, name kept, `@Generated` removed, javadoc preserved +- [ ] `// AI Tooling: union type` placed inside the body of every modified or added getter/setter +- [ ] Typed setters added for each union variant with javadoc copied from original +- [ ] Typed getters added for each union variant (`get*As*()`) with javadoc copied from original +- [ ] All callers (samples, tests, internal code) updated to use new API +- [ ] Unused `BinaryData` imports removed from callers +- [ ] Unit tests written and passing for serialization, deserialization, null, and round-trip +- [ ] Full compilation succeeds (main + test + samples) + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `BinaryData.fromObject(jsonSerializable)` produces wrong JSON | JacksonAdapter not on classpath | Verify `azure-core` dependency includes `JacksonAdapter` | +| `toObject(AzureModel.class)` fails | JacksonAdapter doesn't find `fromJson` | Use `BinaryData.toObject()` which delegates to `JacksonAdapter.deserialize()` — confirm azure-core ≥ 1.51 | +| Setter creates `StringContent` but expects raw JSON | Wrong factory method | Use `fromString()` only for string tokens; use `fromObject()` for primitives and objects | +| Test fails on deserialized value comparison | Asymmetry between `fromString`/`fromObject` for string values | Deserialization always uses `fromObject(readUntyped())`, producing `SerializableContent`. Getters must handle both content types. | +| Compilation error: cannot find `List` | Missing import after adding `List` setter | Add `import java.util.List;` | +| `@SuppressWarnings` needed | Unchecked cast on `toObject(List.class)` | Add `@SuppressWarnings("unchecked")` to the method | From 141010739f2cbc40c2d3041d634b3ab4c060a64f Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 11 Mar 2026 14:34:48 +0100 Subject: [PATCH 08/10] Skill update --- sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md b/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md index 6c7250cb9c08..e2a6bee66d80 100644 --- a/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md +++ b/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md @@ -39,7 +39,7 @@ Key: `JacksonAdapter` has special handling for `JsonSerializable` types — `Bin | Union variant | Deserialization | Notes | |---|---|---| -| String | `this.field.toString()` | Works for both `StringContent` and `SerializableContent` | +| String | `this.field.toObject(String.class)` | Consistent regardless of how BinaryData was created (`fromString` vs `fromObject` during deserialization) | | Primitive (Number, Boolean) | `this.field.toObject(Double.class)` etc. | Jackson deserializes raw JSON values | | Azure `JsonSerializable` model | `this.field.toObject(ModelClass.class)` | JacksonAdapter calls `fromJson()` | | Stainless type | `this.field.toObject(StainlessType.class)` | Jackson deserializes natively | @@ -224,7 +224,7 @@ public String getMyFieldAsString() { if (this.myField == null) { return null; } - return this.myField.toString(); + return this.myField.toObject(String.class); } /** @@ -331,6 +331,6 @@ Before reporting completion, verify: | `BinaryData.fromObject(jsonSerializable)` produces wrong JSON | JacksonAdapter not on classpath | Verify `azure-core` dependency includes `JacksonAdapter` | | `toObject(AzureModel.class)` fails | JacksonAdapter doesn't find `fromJson` | Use `BinaryData.toObject()` which delegates to `JacksonAdapter.deserialize()` — confirm azure-core ≥ 1.51 | | Setter creates `StringContent` but expects raw JSON | Wrong factory method | Use `fromString()` only for string tokens; use `fromObject()` for primitives and objects | -| Test fails on deserialized value comparison | Asymmetry between `fromString`/`fromObject` for string values | Deserialization always uses `fromObject(readUntyped())`, producing `SerializableContent`. Getters must handle both content types. | +| Test fails on deserialized value comparison | Asymmetry between `fromString`/`fromObject` for string values | Deserialization always uses `fromObject(readUntyped())`, producing `SerializableContent`. Use `toObject(String.class)` in string getters — never `toString()` — to normalize both paths. | | Compilation error: cannot find `List` | Missing import after adding `List` setter | Add `import java.util.List;` | | `@SuppressWarnings` needed | Unchecked cast on `toObject(List.class)` | Add `@SuppressWarnings("unchecked")` to the method | From 8f55b92be1e9106cccdf29aaab3f01bf2fa68586 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Wed, 11 Mar 2026 14:53:50 +0100 Subject: [PATCH 09/10] More word exceptions --- sdk/ai/.workflow_docs/cspell.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/ai/.workflow_docs/cspell.yaml b/sdk/ai/.workflow_docs/cspell.yaml index 3f7a39458ecd..4c4b978db014 100644 --- a/sdk/ai/.workflow_docs/cspell.yaml +++ b/sdk/ai/.workflow_docs/cspell.yaml @@ -4,6 +4,7 @@ overrides: - filename: "**/sdk/ai/.workflow_docs/*" words: - Danimal + - Dbuildhelper - Dcheckstyle - Dcodesnippet - Djacoco @@ -11,5 +12,7 @@ overrides: - Drevapi - Dspotbugs - Dspotless + - Dtest + - dedup - FQCN - javap \ No newline at end of file From ad456c9977a614c5dc697103e9371b7e93b2ed66 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Thu, 12 Mar 2026 10:28:00 +0100 Subject: [PATCH 10/10] Polish union-type-wrappers: change BinaryData getter/setter to package-private --- sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md b/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md index e2a6bee66d80..6389dbc15ce1 100644 --- a/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md +++ b/sdk/ai/.workflow_docs/union-type-wrappers/SKILL.md @@ -98,10 +98,10 @@ Do **not** modify the property declaration. Keep `@Generated`, the block comment private BinaryData myField; ``` -#### 4b. Make the existing getter and setter private +#### 4b. Make the existing getter and setter package-private - Remove `@Generated`. -- Change visibility to `private`. +- Change visibility to **package-private** (no access modifier). This keeps them hidden from SDK consumers but accessible to unit tests in the same package. - **Keep the original method name** — do NOT rename to `*Internal`. - **Keep the original javadoc intact.** - Add `// AI Tooling: union type` as the **first line inside the method body**. @@ -112,7 +112,7 @@ private BinaryData myField; * * @return the myField value. */ -private BinaryData getMyField() { +BinaryData getMyField() { // AI Tooling: union type return this.myField; } @@ -123,7 +123,7 @@ private BinaryData getMyField() { * @param myField the myField value to set. * @return the MyClass object itself. */ -private MyClass setMyField(BinaryData myField) { +MyClass setMyField(BinaryData myField) { // AI Tooling: union type this.myField = myField; return this; @@ -315,7 +315,7 @@ Before reporting completion, verify: - [ ] Every `BinaryData` property was classified as **union** or **unknown** - [ ] Property fields left exactly as generated (no modifications) -- [ ] Original `BinaryData` getter/setter made private, name kept, `@Generated` removed, javadoc preserved +- [ ] Original `BinaryData` getter/setter made package-private, name kept, `@Generated` removed, javadoc preserved - [ ] `// AI Tooling: union type` placed inside the body of every modified or added getter/setter - [ ] Typed setters added for each union variant with javadoc copied from original - [ ] Typed getters added for each union variant (`get*As*()`) with javadoc copied from original