Python: feat(foundry): add to_prompt_agent / deploy_as_prompt_agent (experimental)#5959
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds an experimental to_prompt_agent(agent) -> PromptAgentDefinition converter to make Agent Framework Foundry agents portable between local execution (agent.run) and publishing as a hosted Foundry prompt agent.
Changes:
- Introduces
agent_framework_foundry._to_prompt_agent.to_prompt_agentwith tool conversion rules (SDK tools pass-through, AF function tools -> declarations, local MCP rejected, dict tools rehydrated). - Re-exports
to_prompt_agentviaagent_framework_foundryandagent_framework.foundry(incl..pyi) and registersExperimentalFeature.TO_PROMPT_AGENT. - Adds unit tests, README guidance, and a portable-agent sample.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| python/samples/02-agents/providers/foundry/foundry_portable_agent.py | Adds an end-to-end sample running locally and publishing via to_prompt_agent. |
| python/packages/foundry/tests/foundry/test_to_prompt_agent.py | Adds coverage for client validation, model requirements, and tool conversion behaviors. |
| python/packages/foundry/agent_framework_foundry/_to_prompt_agent.py | Implements the converter and tool-shape conversion/validation logic. |
| python/packages/foundry/agent_framework_foundry/init.py | Re-exports to_prompt_agent from the package root. |
| python/packages/foundry/README.md | Documents how to publish an agent as a Foundry prompt agent (experimental). |
| python/packages/core/agent_framework/foundry/init.pyi | Exposes to_prompt_agent in the typed public surface. |
| python/packages/core/agent_framework/foundry/init.py | Adds lazy import mapping for to_prompt_agent. |
| python/packages/core/agent_framework/_feature_stage.py | Adds ExperimentalFeature.TO_PROMPT_AGENT. |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 85%
✓ Correctness
The converter logic is sound: it correctly accesses agent.default_options["tools"] for non-MCP tools, agent.mcp_tools for local MCP tools, and agent.client.model for the deployment name. The isinstance checks in _convert_tools are ordered correctly (ProjectsTool before FunctionTool before Mapping). FunctionTool.parameters() is a valid method returning a dict (confirmed at _tools.py:782). The only remaining concern (already flagged in the prior review) is the ProjectsTool(dict(tool_item)) positional-dict construction on line 179, which works with the Azure SDK's autorest-generated _model_base.Model but is non-obvious and may break if the SDK changes its internal base class. No new correctness issues found beyond what was already flaged.
✓ Security Reliability
The converter module is well-structured with proper validation: client type checks, model presence validation, tool type discrimination with clear error messages, and explicit rejection of local MCP tools. No new security or reliability issues found beyond those already flagged in the existing review thread (ProjectsTool positional construction, unreachable mcp_tools branch, sample cosmetics).
✓ Test Coverage
Test coverage is generally thorough, covering the main success paths, error conditions, and the experimental decorator. The primary gap is the absence of a test for an Agent created without instructions (a common real-world scenario where agents are purely tool-based). The converter explicitly calls
agent.default_options.get("instructions")which returns None in that case, and this path should be verified. Additionally, a test combining valid tools alongside a local MCP tool would strengthen coverage of the error path to ensure valid tools don't get lost before the MCP rejection fires.
✓ Design Approach
I found one design issue: the converter currently publishes the client’s base model instead of the agent’s effective model. In this repo, Agent(default_options={"model": ...}) is the authoritative override for local execution, so to_prompt_agent() can produce a prompt agent that runs on a different model than the same Agent uses locally.
Automated review by eavanvalkenburg's agents
203b9fc to
a79474c
Compare
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||||||||||||
Adds `to_prompt_agent(agent)`, an experimental converter (`ExperimentalFeature.TO_PROMPT_AGENT`) that turns an Agent Framework `Agent` into a Foundry `PromptAgentDefinition` ready to publish via `AIProjectClient.agents.create_version(...)`. Behaviour: * `agent.client` must be a `FoundryChatClient` (or subclass); otherwise `TypeError` is raised. The model deployment name is lifted from the bound client so the same Agent definition used for local runs can be published as a hosted prompt agent without restating the model. * Foundry SDK tool instances (from `FoundryChatClient.get_*_tool()`) are passed through unchanged. AF `FunctionTool`s (and `@tool`-decorated callables) are emitted as Foundry `FunctionTool` declarations. * Local AF MCP tools cannot be expressed in a `PromptAgentDefinition`; the converter raises `ValueError` and points at `FoundryChatClient.get_mcp_tool()` for hosted MCP servers. * The converter walks both `agent.default_options["tools"]` and `agent.mcp_tools` because `normalize_tools()` splits local MCP off into its own list. Re-exported through the `agent_framework.foundry` lazy-loading namespace (updates both `__init__.py` and the `__init__.pyi` type stub). Adds a portable-agent sample showing the same `Agent` driven through both `agent.run(...)` and `to_prompt_agent(agent)`, and a README section covering the new converter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ompt agents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Construct `PromptAgentDefinition` `Tool` from a dict via `**tool_item` unpacking rather than the positional Mapping constructor \u2014 cleaner and matches the typical Pydantic / Azure SDK pattern. * Drop the redundant `isinstance(mcp_tool, MCPTool)` guard in `_convert_tools`; the parameter is already typed `Iterable[MCPTool]` so the second `raise` was unreachable. The remaining single `raise` fires for every entry as intended. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Read the model from `agent.default_options.get("model")` first,
falling back to `agent.client.model`. This mirrors the order
`Agent.__init__` uses (`_agents.py:740`) when assembling
default_options, so the model the agent runs with is the same model
the converter publishes \u2014 e.g. when the caller passes
`default_options={"model": "..."}` to override the bound client.
* Updated the missing-model error message to point at both the client
and the default_options paths.
* Added tests:
* tool-only agent with no `instructions` produces a definition
where `instructions` is `None` and is omitted from the dict
payload (`Agent.__init__` strips None values from default_options
before storing them).
* `default_options['model']` wins over the bound client's model.
* Fallback to client.model when default_options has no model.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds `deploy_as_prompt_agent(agent)`, a convenience wrapper around `to_prompt_agent` that reuses the bound FoundryChatClient's project client to call `project_client.agents.create_version(...)`. Defaults `agent_name` / `description` from `agent.name` / `agent.description` so the Agent stays the single source of truth. * Exposed from `agent_framework_foundry` and the lazy-loading `agent_framework.foundry` namespace (including the .pyi stub). * Marked experimental with the existing `ExperimentalFeature.TO_PROMPT_AGENT` tag. * Tests cover the happy path, name/description defaulting, explicit override, no-name error, metadata + description forwarding, extra kwargs passthrough, and the experimental metadata. Samples: * Renamed the existing sample to `creating_prompt_agents.py`, drops 'portable' wording, presents `deploy_as_prompt_agent` first as the recommended path and `to_prompt_agent` + `AIProjectClient` as the two-step alternative, and adds a cleanup step that deletes the published agent so re-runs stay idempotent. * New `using_prompt_agents.py` shows the end-to-end loop: deploy the agent, connect to it with `FoundryAgent` passing the same local `@tool` callable, run a query against the deployed prompt agent, then clean up. README updated to introduce `deploy_as_prompt_agent` as the recommended path and link to both runnable samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The check was accidentally dropped while reworking docstrings in the previous commit. Test `test_to_prompt_agent_rejects_missing_model` exercises this path and was failing on CI as a result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renames the helper across the foundry package, core lazy-loader stubs, tests, README and samples. The new name better matches the action performed (a prompt-agent definition is created in Foundry) and is consistent with the surrounding ''create_*'' API surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…arams Remove the create_prompt_agent helper and consolidate on to_prompt_agent. Expose every PromptAgentDefinition parameter that has either an Agent Framework equivalent (sourced from default_options) or no equivalent (accepted as a keyword argument). * default_options-sourced (with kwarg overrides): temperature, top_p, string tool_choice * kwarg-only Foundry knobs: reasoning, text, structured_inputs, rai_config, ToolChoiceParam tool_choice Precedence is always: explicit keyword > default_options entry > unset. Tests cover every path (defaults, default_options, kwargs, kwarg override). Samples and README rewritten around the enriched to_prompt_agent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stop duplicating the generation-parameter surface between FoundryChatOptions and to_prompt_agent. Translate every field with an Agent Framework equivalent (temperature, top_p, tool_choice, reasoning, response_format/text/verbosity) from agent.default_options via a new RawFoundryChatClient helper _prepare_prompt_agent_options. Only Foundry-specific fields with no AF equivalent — structured_inputs and rai_config — remain as keyword arguments on to_prompt_agent. - tool_choice is dropped when there are no tools (mirrors _prepare_options semantics and avoids polluting tool-less prompt agents with Agent.__init__'s 'auto' default). - response_format Pydantic models route through openai.lib._parsing._responses.type_to_text_format_param; dict shapes go through the existing _prepare_response_and_text_format helper. - default_options is not mutated; text dict is defensively copied. Tests, README, and creating_prompt_agents.py sample updated to reflect the new single-source model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop creating_prompt_agents.py (the publish-only variant) and rename using_prompt_agents.py to foundry_prompt_agents.py so the single sample covers the full convert -> publish -> connect -> run loop. Update the README link list accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an agent.run() call against the local Agent before publishing, then run the deployed prompt agent on the same query. Expand the docstring with a compare-and-contrast covering runtime/latency, configurability, and persistence/sharing differences between the two execution paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…prompt_agent Exercises the ValueError path when a Pydantic response_format would overwrite an explicit text.format mapping with a different shape. Lifts _chat_client.py coverage from 89% to 90%. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b6ef7b8 to
ec9cf26
Compare
…_agent Lift the translation helper off RawFoundryChatClient and into the _to_prompt_agent module as a module-private function that takes the client as its first argument. The chat client no longer needs to carry a method whose only consumer is the prompt-agent converter, while still serving as the source of the request-path helper (_prepare_response_and_text_format) that the converter reuses for dict-shaped response_format values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add two pieces of guidance to python/AGENTS.md: * Terminology - reserve 'GA' for hosted services; use 'released' or 'stable' for Agent Framework code/features to match the feature-lifecycle stages. * Maintaining Documentation - review AGENTS.md and skills at the end of every run and update any guidance the conversation made stale; before adding a new principle, ask the user to confirm it should be captured. Also pulls in a docstring fix in foundry_prompt_agents.py that swaps the stray 'GA' for 'released', applying the new terminology rule. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ample cleanup safety - FunctionTool published as strict=True so the server-side schema validation matches what the local FoundryAgent(tools=[same_callable]) dispatcher enforces. AF FunctionTool has no 'strict' attribute, so the safer default is used uniformly instead of silently downgrading to a permissive contract. - _validate_mapping_tool now dispatches through ProjectsTool._deserialize so dict-shaped tools rehydrate to the concrete subclass (FunctionTool, WebSearchTool, ...) via the 'type' discriminator instead of returning a generic Tool. Added a test that asserts isinstance(WebSearchTool) and a new test for the function-typed dict path. - foundry_prompt_agents.py sample now wraps credential + project client in async with and the create_version / run flow in try/finally so a failure on connect or run still deletes the published prompt agent rather than leaving an orphaned, billable resource in the user's Foundry project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GitHub PR URLs use the singular segment /pull/N (compare to /issues/N for issues). The existing './pulls' ignore pattern never matched anything as a result, so legitimately stale PR links (e.g. PRs deleted from forks) surface as linkspector failures on unrelated PRs. This is the same convention the './issues' rule above already follows. Fixes the markdown-link-check failure on a dangling link in dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
Publishing an Agent Framework
Agentas a Foundry prompt agent required hand-rolling aPromptAgentDefinitionand callingAIProjectClient.agents.create_version(...)directly — duplicating instructions, tools, generation parameters, and the model deployment name that the localAgentalready carries.Description
Two experimental helpers in
agent-framework-foundryclose that gap. Both are gated byExperimentalFeature.TO_PROMPT_AGENT.to_prompt_agent(agent, *, structured_inputs=None, rai_config=None) -> PromptAgentDefinitionConverts an
Agentwhose chat client is aFoundryChatClientinto aPromptAgentDefinitionready forAIProjectClient.agents.create_version(...). TheAgentis the single source of truth: model, instructions, tools, and every generation parameter that has an Agent Framework equivalent are translated by a newRawFoundryChatClient._prepare_prompt_agent_optionshelper, reusing the samevalidate_tool_mode,_prepare_response_and_text_format, andopenai.lib._parsing._responses.type_to_text_format_parampaths as the regular request flow.default_optionskeyPromptAgentDefinitionfieldtemperaturetemperaturetop_ptop_ptool_choice(dropped when no tools)tool_choice(str/ToolChoiceFunction/ToolChoiceAllowed)reasoning(dict orReasoning)reasoningresponse_format(dict orBaseModel)text.formatverbositytext.verbositytexttext(defensively copied)Only Foundry-specific fields with no Agent Framework equivalent (
structured_inputs,rai_config) remain as keyword arguments.default_optionsis never mutated, andtool_choiceis dropped when the definition has no tools — mirroring_prepare_optionsand avoidingAgent.__init__'s default"auto"polluting tool-less prompt agents.deploy_as_prompt_agent(agent, *, structured_inputs=None, rai_config=None) -> AgentConvenience wrapper that combines
to_prompt_agentwithproject_client.agents.create_version(...). Reuses the agent's boundFoundryChatClient.project_clientand propagatesagent.name/agent.descriptionso callers don't restate them.Tooling
FoundryChatClientexposesget_web_search_tool(),get_code_interpreter_tool(), andget_mcp_tool()helpers so hosted Foundry tools can be passed straight throughAgent(tools=[...])and re-emitted unchanged in the published prompt agent. Local@toolcallables are emitted as FoundryFunctionTooldeclarations (schema only) — the runtime implementation is supplied client-side viaFoundryAgent(tools=[same_callable]). Local MCP tools cannot be published; the converter points callers atget_mcp_tool(...)instead.Sample
samples/02-agents/providers/foundry/foundry_prompt_agents.pywalks the full convert → publish → connect → run loop, runningagent.run(query)on the localAgentand on the deployed prompt agent with the same query, and documents the runtime / configurability / persistence trade-offs between the two execution paths.Contribution Checklist