diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 000000000..a3885b1ec --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,31 @@ +name: release-please + +# Opens/maintains one "Release PR" per runtime and, when a Release PR is merged, +# tags the runtime (e.g. python/2.0.0). Existing per-runtime publish workflows +# (prompty-python.yml, prompty-rust.yml, prompty-ts-release.yml, prompty-csharp.yml) +# trigger on those tags and do the actual package publishing. +# +# IMPORTANT: `token` must be a GitHub App installation token or a PAT — NOT the +# default GITHUB_TOKEN. Tags created with GITHUB_TOKEN do not trigger the +# tag-listening publish workflows (GitHub's recursive-workflow guard). Provide a +# token via the RELEASE_PLEASE_TOKEN secret so the publish workflows fire. + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..fde407404 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,9 @@ +{ + "runtime/python/prompty": "2.0.0-beta.3", + "runtime/rust": "2.0.0-beta.4", + "runtime/typescript/packages/core": "2.0.0-beta.5", + "runtime/csharp": "2.0.0-beta.4", + "runtime/java": "2.0.0-beta.4", + "runtime/go/prompty": "2.0.0-beta.4", + "runtime/swift/prompty": "2.0.0-beta.4" +} diff --git a/RELEASING.md b/RELEASING.md index 7bd7b4c28..6af2e73d1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,6 +12,81 @@ via OIDC (no secrets needed). | Python | [PyPI](https://pypi.org/project/prompty/) | `python/{version}` | `prompty-python.yml` | | TypeScript | [npm](https://www.npmjs.com/package/@prompty/core) | `typescript/{version}` | `prompty-ts-release.yml` | +Those tags are now produced automatically by **release-please** (see below). The +manual tag-and-push steps later in this document remain as a break-glass path. + +## Automated releases (release-please) + +Prompty ships **seven runtimes independently** via +[release-please](https://github.com/googleapis/release-please). There is no shared +version number — `python/2.1.0` and `rust/2.0.3` can coexist. Config lives in +`release-please-config.json` + `.release-please-manifest.json` at the repo root, and +the driver is `.github/workflows/release-please.yml`. + +> **VS Code extension** (`vscode/`) is intentionally **not** managed by +> release-please — it uses the Marketplace pre-release channel (`vscode/2.0.0-pre.N`) +> and keeps its own pipeline. + +### Flow + +``` +conventional commit on main + │ + ▼ +release-please.yml ── opens/updates one "Release PR" per runtime with changes + │ (title: "chore(): release ") + ▼ +merge the Release PR ── bumps version files, writes CHANGELOG, pushes a tag + │ like `python/2.1.0` + ▼ +prompty-.yml ── existing publish workflow triggers on the tag and publishes +``` + +release-please **only opens PRs and cuts tags**; nothing publishes until a human +merges a Release PR. Commits route to a runtime by the **paths they touch**; add a +scope when ambiguous (`feat(python):`, `fix(rust):`). Only `feat`, `fix`, `perf`, +`revert`, `deps` trigger releases; `feat!:` / `BREAKING CHANGE:` bumps major. + +### Components + +| Component | Path | release-type | Version source | Publish | +| ---------- | ---------------------------------- | ------------ | ---------------------------------------------------- | ------- | +| `python` | `runtime/python/prompty` | `python` | `prompty/_version.py` (`# x-release-please-version`) | ✅ PyPI | +| `rust` | `runtime/rust` | `rust` | all workspace `Cargo.toml` (native) | ✅ crates| +| `typescript` | `runtime/typescript/packages/core` | `node` | all `packages/*/package.json` (`node-workspace`) | ✅ npm | +| `csharp` | `runtime/csharp` | `simple` | 4 `*.csproj` `` (annotated) | ✅ NuGet | +| `java` | `runtime/java` | `simple` | `build.gradle.kts` `version` (annotated) | check only | +| `go` | `runtime/go/prompty` | `go` | git tag only | check only | +| `swift` | `runtime/swift/prompty` | `simple` | git tag only | check only | + +Tag format (`separator:"/"`, `include-component-in-tag:true`, `include-v-in-tag:false`) +reproduces the existing `python/2.1.0` tags exactly — **no publish workflow changed**. + +### Graduating beta → 2.0.0 + +All runtimes were on `2.0.0-beta.N`. The manifest is seeded at each runtime's last +beta with **no prerelease mode**, so the first Release PR graduates each runtime to a +clean **`2.0.0`** stable release; normal `2.0.1` / `2.1.0` / `3.0.0` bumps follow. + +### One-time setup before enabling on `main` + +1. **`RELEASE_PLEASE_TOKEN` secret (required for auto-publish).** The workflow falls + back to `GITHUB_TOKEN`, but tags cut with `GITHUB_TOKEN` **do not trigger** the + tag-listening publish workflows. Provide a **GitHub App installation token** (or + fine-grained PAT) with `contents:write` + `pull_requests:write` as + `RELEASE_PLEASE_TOKEN`. +2. **`bootstrap-sha`** in the config is pinned to the adoption commit so the first run + doesn't replay old history. + +### Verify on the first Release PR (before merging) + +- **typescript** — confirm all four `packages/*/package.json` versions **and** their + `^@prompty/core` ranges bump (`node-workspace`); if a sibling is missed, register it + as its own package in the config. +- **rust** — confirm all workspace members + `path`/`version` deps bump together. +- **csharp / java** — confirm the annotated `` / `version =` lines rewrite. +- **python** — Release PR sets `_version.py` to `2.0.0`; the built wheel is `prompty-2.0.0`. + ## Pre-flight checklist (MUST DO before every release) Run these **exact** steps locally before pushing tags. They mirror CI. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 000000000..7d6c4339b --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "separator": "/", + "include-component-in-tag": true, + "include-v-in-tag": false, + "bootstrap-sha": "3c1c0cc27ee429983ee362aa60ac510678da4af9", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "deps", "section": "Dependencies" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "chore", "section": "Miscellaneous", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "refactor", "section": "Code Refactoring", "hidden": true } + ], + "packages": { + "runtime/python/prompty": { + "component": "python", + "release-type": "python", + "package-name": "prompty", + "extra-files": [ + { "type": "generic", "path": "prompty/_version.py" } + ] + }, + "runtime/rust": { + "component": "rust", + "release-type": "rust" + }, + "runtime/typescript/packages/core": { + "component": "typescript", + "release-type": "node", + "package-name": "@prompty/core" + }, + "runtime/csharp": { + "component": "csharp", + "release-type": "simple", + "extra-files": [ + { "type": "generic", "path": "Prompty.Core/Prompty.Core.csproj" }, + { "type": "generic", "path": "Prompty.OpenAI/Prompty.OpenAI.csproj" }, + { "type": "generic", "path": "Prompty.Foundry/Prompty.Foundry.csproj" }, + { "type": "generic", "path": "Prompty.Anthropic/Prompty.Anthropic.csproj" } + ] + }, + "runtime/java": { + "component": "java", + "release-type": "simple", + "extra-files": [ + { "type": "generic", "path": "build.gradle.kts" } + ] + }, + "runtime/go/prompty": { + "component": "go", + "release-type": "go" + }, + "runtime/swift/prompty": { + "component": "swift", + "release-type": "simple" + } + }, + "plugins": [ + "node-workspace" + ] +} diff --git a/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj b/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj index ed7820b85..14387d110 100644 --- a/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj +++ b/runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj @@ -4,7 +4,7 @@ net9.0 enable enable - 2.0.0-beta.4 + 2.0.0-beta.4 Prompty.Anthropic Microsoft Anthropic provider for Prompty — executor and processor for Claude models via the Anthropic Messages API. diff --git a/runtime/csharp/Prompty.Core/Prompty.Core.csproj b/runtime/csharp/Prompty.Core/Prompty.Core.csproj index fb4e4b0e1..19c633812 100644 --- a/runtime/csharp/Prompty.Core/Prompty.Core.csproj +++ b/runtime/csharp/Prompty.Core/Prompty.Core.csproj @@ -4,7 +4,7 @@ net9.0 enable enable - 2.0.0-beta.4 + 2.0.0-beta.4 Prompty.Core Microsoft Prompty is an asset class and format for LLM prompts. Core library with loader, pipeline, renderers, parsers, and tracing. diff --git a/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj b/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj index ec3d0ba7f..22abf7713 100644 --- a/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj +++ b/runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj @@ -6,7 +6,7 @@ enable OPENAI001;CS1591 true - 2.0.0-beta.4 + 2.0.0-beta.4 Prompty.Foundry Microsoft Microsoft Foundry (Azure OpenAI) provider for Prompty — executor and processor for Azure-hosted models. diff --git a/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj b/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj index 6c8c5d436..306dba14e 100644 --- a/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj +++ b/runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj @@ -6,7 +6,7 @@ enable OPENAI001;CS1591 true - 2.0.0-beta.4 + 2.0.0-beta.4 Prompty.OpenAI Microsoft OpenAI provider for Prompty — executor and processor for OpenAI chat, embedding, image, and agent APIs. diff --git a/runtime/java/build.gradle.kts b/runtime/java/build.gradle.kts index 78fef8933..8135a6694 100644 --- a/runtime/java/build.gradle.kts +++ b/runtime/java/build.gradle.kts @@ -4,7 +4,7 @@ plugins { allprojects { group = "com.microsoft.prompty" - version = "2.0.0-beta.4" + version = "2.0.0-beta.4" // x-release-please-version } subprojects { diff --git a/runtime/python/prompty/.env.example b/runtime/python/prompty/.env.example index 782293591..58f932046 100644 --- a/runtime/python/prompty/.env.example +++ b/runtime/python/prompty/.env.example @@ -9,7 +9,7 @@ OPENAI_API_KEY= OPENAI_BASE_URL= OPENAI_MODEL=gpt-4o-mini OPENAI_EMBEDDING_MODEL=text-embedding-3-small -OPENAI_IMAGE_MODEL=dall-e-2 +OPENAI_IMAGE_MODEL=gpt-image-1 # Direct OpenAI (api.openai.com — no proxy/compat layer) DIRECT_OPENAI_API_KEY= diff --git a/runtime/python/prompty/prompty/__init__.py b/runtime/python/prompty/prompty/__init__.py index 9d147c09e..f06551fcd 100644 --- a/runtime/python/prompty/prompty/__init__.py +++ b/runtime/python/prompty/prompty/__init__.py @@ -139,8 +139,6 @@ "bind_tools", "tool", # Backward-compat aliases - "AzureExecutor", - "AzureProcessor", "PromptAgent", "AgentDefinition", # Model discovery @@ -280,7 +278,5 @@ ) # Backward-compat aliases (will be removed in a future version) -AzureExecutor = FoundryExecutor -AzureProcessor = FoundryProcessor PromptAgent = Agent AgentDefinition = Agent diff --git a/runtime/python/prompty/prompty/_version.py b/runtime/python/prompty/prompty/_version.py index 930a3a4dd..2af2e6a97 100644 --- a/runtime/python/prompty/prompty/_version.py +++ b/runtime/python/prompty/prompty/_version.py @@ -1 +1 @@ -VERSION = "2.0.0b3" +VERSION = "2.0.0-beta.3" # x-release-please-version diff --git a/runtime/python/prompty/prompty/core/discovery.py b/runtime/python/prompty/prompty/core/discovery.py index fb7f2733c..926bca919 100644 --- a/runtime/python/prompty/prompty/core/discovery.py +++ b/runtime/python/prompty/prompty/core/discovery.py @@ -172,11 +172,11 @@ def _discover(group: str, key: str) -> Any: return _cache[cache_key] eps = importlib.metadata.entry_points(group=group, name=key) - ep_list = list(eps) if not isinstance(eps, list) else eps - if not ep_list: + first = next(iter(eps), None) + if first is None: raise InvokerError(group, key) - loaded = _instantiate(ep_list[0].load()) + loaded = _instantiate(first.load()) with _lock: _cache[cache_key] = loaded return loaded diff --git a/runtime/python/prompty/prompty/providers/azure/__init__.py b/runtime/python/prompty/prompty/providers/azure/__init__.py deleted file mode 100644 index e768779ef..000000000 --- a/runtime/python/prompty/prompty/providers/azure/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Backward-compatibility aliases — use ``prompty.providers.foundry`` instead.""" - -from __future__ import annotations - -from ..foundry.executor import FoundryExecutor as AzureExecutor -from ..foundry.models import list_models, list_models_async -from ..foundry.processor import FoundryProcessor as AzureProcessor - -__all__ = ["AzureExecutor", "AzureProcessor", "list_models", "list_models_async"] diff --git a/runtime/python/prompty/prompty/providers/azure/executor.py b/runtime/python/prompty/prompty/providers/azure/executor.py deleted file mode 100644 index f64b7c253..000000000 --- a/runtime/python/prompty/prompty/providers/azure/executor.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Backward-compatibility alias — use ``prompty.providers.foundry.executor`` instead.""" - -from __future__ import annotations - -from ..foundry.executor import FoundryExecutor as AzureExecutor - -__all__ = ["AzureExecutor"] diff --git a/runtime/python/prompty/prompty/providers/azure/processor.py b/runtime/python/prompty/prompty/providers/azure/processor.py deleted file mode 100644 index d56f84aac..000000000 --- a/runtime/python/prompty/prompty/providers/azure/processor.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Backward-compatibility alias — use ``prompty.providers.foundry.processor`` instead.""" - -from __future__ import annotations - -from ..foundry.processor import FoundryProcessor as AzureProcessor - -__all__ = ["AzureProcessor"] diff --git a/runtime/python/prompty/pyproject.toml b/runtime/python/prompty/pyproject.toml index f62e4e307..5f9d160f5 100644 --- a/runtime/python/prompty/pyproject.toml +++ b/runtime/python/prompty/pyproject.toml @@ -96,7 +96,7 @@ ignore = ["E501"] [tool.pytest.ini_options] asyncio_mode = "strict" markers = [ - "integration: tests that hit real OpenAI / Azure OpenAI endpoints (deselected by default)", + "integration: tests that hit real OpenAI / Azure OpenAI (Foundry) endpoints (deselected by default)", ] addopts = "-m 'not integration'" diff --git a/runtime/python/prompty/tests/integration/conftest.py b/runtime/python/prompty/tests/integration/conftest.py index 38ad5e71e..d64907941 100644 --- a/runtime/python/prompty/tests/integration/conftest.py +++ b/runtime/python/prompty/tests/integration/conftest.py @@ -17,7 +17,10 @@ from __future__ import annotations +import json import os +from functools import cache +from pathlib import Path from typing import Any import pytest @@ -46,7 +49,7 @@ def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: _OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "") # optional: proxy via Azure _OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini") # override default chat model _OPENAI_EMBEDDING_MODEL = os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small") -_OPENAI_IMAGE_MODEL = os.environ.get("OPENAI_IMAGE_MODEL", "dall-e-2") +_OPENAI_IMAGE_MODEL = os.environ.get("OPENAI_IMAGE_MODEL", "gpt-image-1") _AZURE_KEY = os.environ.get("AZURE_OPENAI_API_KEY", "") _AZURE_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT", "") _AZURE_CHAT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT", "") @@ -405,3 +408,60 @@ def make_entra_agent( if metadata is not None: data["metadata"] = metadata return Agent.load(data) + + +# --------------------------------------------------------------------------- +# Conformance vector access +# --------------------------------------------------------------------------- +# +# Live agent-loop tests are anchored to the SAME canonical @vector corpus the +# deterministic model-conformance suite replays (schema/tsp-output/ +# .typra-generated/vectors.json, emitted by Typra from the TypeSpec sources). +# The live test reuses a vector's scenario — its messages, tools, and derived +# tool-dispatch order — but swaps the scripted model double for a real provider, +# so one Typra-governed definition validates both the deterministic engine and +# the live provider path. + +# repo root: tests/integration -> tests -> prompty -> python -> runtime -> root +_VECTORS_JSON = Path(__file__).resolve().parents[5] / "schema" / "tsp-output" / ".typra-generated" / "vectors.json" + + +@cache +def _load_vectors_corpus() -> tuple[dict[str, Any], ...]: + if not _VECTORS_JSON.exists(): + raise FileNotFoundError( + f"Conformance vector corpus not found at {_VECTORS_JSON}. " + "Regenerate it via the Typra emitter (schema build)." + ) + with open(_VECTORS_JSON, encoding="utf-8") as handle: + return tuple(json.load(handle).get("vectors", [])) + + +def load_turn_vector(name: str) -> dict[str, Any]: + """Return the ``TurnConformance.run`` vector named ``name`` from the corpus. + + Raises ``KeyError`` if no such vector exists so a renamed or removed vector + fails loudly rather than silently skipping live coverage. + """ + for entry in _load_vectors_corpus(): + if ( + entry.get("contract") == "TurnConformance" + and entry.get("operation") == "run" + and isinstance(entry.get("vector"), dict) + and entry["vector"].get("name") == name + ): + return entry["vector"] + raise KeyError(f"TurnConformance.run vector '{name}' not found in {_VECTORS_JSON}") + + +def turn_vector_tool_order(vector: dict[str, Any]) -> list[str]: + """Derive the expected ordered tool-dispatch sequence from a run vector. + + Reads each scripted turn's ``expected_tool_calls`` in order — this is the + dependency chain the live provider must reproduce. + """ + order: list[str] = [] + for step in vector.get("sequence", []): + for call in step.get("expected_tool_calls") or []: + order.append(call["name"]) + return order diff --git a/runtime/python/prompty/tests/integration/test_agent_vector.py b/runtime/python/prompty/tests/integration/test_agent_vector.py new file mode 100644 index 000000000..bbc812b85 --- /dev/null +++ b/runtime/python/prompty/tests/integration/test_agent_vector.py @@ -0,0 +1,137 @@ +"""Live multi-iteration agent-loop tests anchored to a conformance vector. + +The deterministic model-conformance suite replays the ``TurnConformance.run`` +vector ``multi_turn_tool_calls`` through a scripted model double. Here we drive +the SAME canonical vector against a real provider: the vector supplies the +scenario (messages + tools) and the expected tool-dispatch order, while the +runtime's agent loop calls a live LLM that must reproduce the multi-turn +dependency chain get_weather -> convert_temperature -> final grounded answer. + +One Typra-governed vector therefore validates both the deterministic engine and +the live provider path. Unlike the value-path integration tests these do not +fall back to mocks: a mocked model cannot demonstrate a real model choosing a +two-step tool chain, so they skip when live credentials are absent. +""" + +from __future__ import annotations + +import pytest + +from prompty.core.pipeline import turn, turn_async + +from .conftest import ( + has_foundry, + has_openai, + load_turn_vector, + make_foundry_agent, + make_openai_agent, + turn_vector_tool_order, +) + +_VECTOR = load_turn_vector("multi_turn_tool_calls") +_EXPECTED_TOOL_ORDER = turn_vector_tool_order(_VECTOR) # ["get_weather", "convert_temperature"] +_VECTOR_TOOLS = _VECTOR["input"]["tools"] + +# Operational directive appended to the vector's system message so a live model +# reliably delegates the conversion to the tool instead of doing the arithmetic +# itself. The vector still owns the scenario, tools, and expected tool order; +# this only forces the runtime through the multi-iteration path under test. +_TOOL_USE_DIRECTIVE = ( + " Always call the convert_temperature tool for any unit conversion; never convert temperatures yourself." +) + + +def _instructions_from_vector() -> str: + """Render the vector's messages as a prompty role-marker instruction block.""" + parts: list[str] = [] + for message in _VECTOR["input"]["messages"]: + role = message["role"] + content = message["content"] + if role == "system": + content = content + _TOOL_USE_DIRECTIVE + parts.append(f"{role}:\n{content}") + return "\n".join(parts) + + +class _ToolRecorder: + """Real tool implementations that record the live dispatch order and args.""" + + def __init__(self) -> None: + self.calls: list[tuple[str, dict]] = [] + + def get_weather(self, city: str) -> str: + self.calls.append(("get_weather", {"city": city})) + # Fahrenheit, matching the vector's turn-1 tool result. + return "72°F sunny" + + def convert_temperature(self, value: float, from_unit: str, to_unit: str) -> str: + self.calls.append(("convert_temperature", {"value": value, "from_unit": from_unit, "to_unit": to_unit})) + celsius = (float(value) - 32.0) * 5.0 / 9.0 + return f"{celsius:.1f}°C" + + def as_tools(self) -> dict: + return { + "get_weather": self.get_weather, + "convert_temperature": self.convert_temperature, + } + + @property + def order(self) -> list[str]: + return [name for name, _ in self.calls] + + +def _assert_multi_turn_chain(recorder: _ToolRecorder, result: str) -> None: + """Assert the live loop reproduced the vector's multi-iteration chain.""" + # 1. Exact ordered dispatch sequence the vector prescribes. + assert recorder.order == _EXPECTED_TOOL_ORDER, ( + f"expected tool order {_EXPECTED_TOOL_ORDER}, observed {recorder.order}" + ) + # 2. The convert step consumed the weather step's output (72F) — this is what + # makes it a genuine multi-ITERATION chain, not two independent calls. + convert_args = next(args for name, args in recorder.calls if name == "convert_temperature") + assert abs(float(convert_args["value"]) - 72.0) < 0.5, ( + f"convert_temperature should receive ~72 from get_weather, got {convert_args['value']}" + ) + # 3. Final answer is grounded in the tool outputs (converted Celsius value). + assert isinstance(result, str) and result + assert "22" in result, f"final answer should contain the converted value, got: {result!r}" + + +@pytest.mark.skipif(not has_openai, reason="OPENAI_API_KEY not set (live agent-loop vector)") +class TestOpenAIAgentVector: + def test_multi_turn_tool_chain(self): + recorder = _ToolRecorder() + agent = make_openai_agent( + api_type="chat", + options={"temperature": 0, "maxOutputTokens": 300}, + tools=_VECTOR_TOOLS, + ) + agent.instructions = _instructions_from_vector() + result = turn(agent, tools=recorder.as_tools()) + _assert_multi_turn_chain(recorder, result) + + @pytest.mark.asyncio + async def test_multi_turn_tool_chain_async(self): + recorder = _ToolRecorder() + agent = make_openai_agent( + api_type="chat", + options={"temperature": 0, "maxOutputTokens": 300}, + tools=_VECTOR_TOOLS, + ) + agent.instructions = _instructions_from_vector() + result = await turn_async(agent, tools=recorder.as_tools()) + _assert_multi_turn_chain(recorder, result) + + +@pytest.mark.skipif(not has_foundry, reason="Azure/Foundry env vars not set (live agent-loop vector)") +class TestFoundryAgentVector: + def test_multi_turn_tool_chain(self): + recorder = _ToolRecorder() + agent = make_foundry_agent( + api_type="chat", + options={"temperature": 0, "maxOutputTokens": 300}, + tools=_VECTOR_TOOLS, + ) + agent.instructions = _instructions_from_vector() + result = turn(agent, tools=recorder.as_tools()) + _assert_multi_turn_chain(recorder, result) diff --git a/runtime/python/prompty/tests/model/conftest.py b/runtime/python/prompty/tests/model/conftest.py index 86c734ad0..eed19841c 100644 --- a/runtime/python/prompty/tests/model/conftest.py +++ b/runtime/python/prompty/tests/model/conftest.py @@ -91,13 +91,13 @@ def processor_provider(): resolve to a total seam rather than a missing attachment. """ from prompty.model._processor_resolver import new_processor_provider - from prompty.providers.azure.processor import AzureProcessor + from prompty.providers.foundry.processor import FoundryProcessor from prompty.providers.openai.processor import OpenAIProcessor return new_processor_provider( { "openai": OpenAIProcessor(), - "azure": AzureProcessor(), + "azure": FoundryProcessor(), "custom": _PassthroughProcessor(), } ) diff --git a/runtime/python/prompty/tests/test_executor.py b/runtime/python/prompty/tests/test_executor.py index 55331375c..6ef10c053 100644 --- a/runtime/python/prompty/tests/test_executor.py +++ b/runtime/python/prompty/tests/test_executor.py @@ -534,12 +534,6 @@ def test_resolve_client_reference_connection(self): finally: clear_connections() - def test_azure_executor_backward_compat(self): - """AzureExecutor import still works and is the same class as FoundryExecutor.""" - from prompty.providers.azure.executor import AzureExecutor - - assert AzureExecutor is FoundryExecutor - # --------------------------------------------------------------------------- # Structured Output (outputs → response_format) diff --git a/runtime/python/prompty/tests/test_processor.py b/runtime/python/prompty/tests/test_processor.py index e483b24d4..fbd29b1c8 100644 --- a/runtime/python/prompty/tests/test_processor.py +++ b/runtime/python/prompty/tests/test_processor.py @@ -162,12 +162,6 @@ def test_tool_calls(self): assert isinstance(result, list) assert result[0].name == "search" - def test_azure_processor_backward_compat(self): - """AzureProcessor import still works and is the same class as FoundryProcessor.""" - from prompty.providers.azure.processor import AzureProcessor - - assert AzureProcessor is FoundryProcessor - # --------------------------------------------------------------------------- # ToolCall dataclass