Python: feat(foundry): add experimental to_prompt_agent converter#5959
Python: feat(foundry): add experimental to_prompt_agent converter#5959eavanvalkenburg wants to merge 8 commits into
Conversation
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>
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. |
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>
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
* 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>
Motivation and Context
Developers building agents with Agent Framework's
FoundryChatClienthave no way today to take that agent and publish it as a Foundry prompt agent without rewriting the definition by hand inazure-ai-projectsSDK types — model, instructions, tools, all restated. The same Python tool definitions also can't be reused between the Responses API path (FoundryChatClientlocal execution) and a prompt agent definition because the two surfaces have different shapes.This PR adds the missing converter so the same
Agentobject can either run locally viaagent.run(...)or be published as a hosted prompt agent viato_prompt_agent(agent), with the model lifted from the boundFoundryChatClient.Description
to_prompt_agent(agent) -> PromptAgentDefinitionin a newagent_framework_foundry._to_prompt_agentmodule and re-exports it from bothagent_framework_foundryand theagent_framework.foundrylazy-loading namespace (including the.pyistub).ExperimentalFeature.TO_PROMPT_AGENTtag (alphabetically slotted intoExperimentalFeature).agent.clientmust be aFoundryChatClient(or subclass) — otherwiseTypeError. Themodelis read from the bound client.FoundryChatClient.get_*_tool()or a literalazure.ai.projects.models.*Tool) are passed through unchanged.FunctionToolinstances (including@tool-decorated callables) become FoundryFunctionTooldeclarations. Prompt agents are server-side, so the deployed agent receives the schema but cannot execute the local Python — wiring server-side execution is the caller's responsibility, which the docstring and README call out.PromptAgentDefinition; the converter raisesValueErrorand points atFoundryChatClient.get_mcp_tool(...)for hosted MCP servers. The converter walks bothagent.default_options["tools"]andagent.mcp_toolsto catch local MCP thatnormalize_tools()split off.typediscriminator are rehydrated through the SDKToolmodel; missingtyperaisesValueError.samples/02-agents/providers/foundry/foundry_portable_agent.pythat drives the sameAgentthrough bothagent.run(...)andto_prompt_agent(agent).Contribution Checklist