Python: feat(foundry): add to_prompt_agent + deploy_as_prompt_agent (experimental)#5959
Python: feat(foundry): add to_prompt_agent + deploy_as_prompt_agent (experimental)#5959eavanvalkenburg wants to merge 10 commits into
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
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>
203b9fc to
a79474c
Compare
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||||||||||||
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>
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 two helpers so the same
Agentobject can either run locally viaagent.run(...)or be published as a hosted prompt agent in one call.Description
Two new public APIs in
agent_framework_foundry, both re-exported from theagent_framework.foundrylazy-loading namespace (__init__.py+.pyistub):to_prompt_agent(agent) -> PromptAgentDefinition— convert anAgentwhose chat client is aFoundryChatClientinto a FoundryPromptAgentDefinitionyou can pass toAIProjectClient.agents.create_version(...).deploy_as_prompt_agent(agent, *, agent_name=None, metadata=None, description=None, **kwargs) -> AgentVersionDetails— convenience wrapper that reuses the boundFoundryChatClient's project client to callagents.create_version(...). Defaultsagent_name/descriptionfromagent.name/agent.descriptionso the Agent stays the single source of truth; extra kwargs fall through to the SDK call.Both helpers are marked experimental with the new
ExperimentalFeature.TO_PROMPT_AGENTtag.Model resolution. The model is resolved the same way
Agent.__init__does at runtime (_agents.py:740):default_options["model"]first, thenagent.client.model. SoAgent(client=FoundryChatClient(model="legacy"), default_options={"model": "claude"})publishesclaude, matching what the agent actually runs with.Tool conversion.
FoundryChatClient.get_*_tool()or a literalazure.ai.projects.models.*Tool) are passed through unchanged.FunctionToolinstances (including@tool-decorated callables) become FoundryFunctionTooldeclarations — the deployed prompt agent receives the schema only. The matchingusing_prompt_agents.pysample shows howFoundryAgentruns the local Python by matching tool names when the deployed agent invokes the tool.PromptAgentDefinition; the converter raisesValueErrorand points atFoundryChatClient.get_mcp_tool(...)for hosted MCP servers. The converter walks bothagent.default_options["tools"]andagent.mcp_toolsso local MCP thatnormalize_tools()split off is still caught.typediscriminator are rehydrated through the SDKToolmodel; missingtyperaisesValueError.Validation flow.
agent.clientmust be aFoundryChatClient(or subclass); otherwiseto_prompt_agentraisesTypeError.deploy_as_prompt_agentdefers that check (and the model/tool checks) toto_prompt_agentso error behaviour stays in one place.Samples
Two runnable samples under
samples/02-agents/providers/foundry/:creating_prompt_agents.py— build an Agent, run it locally, publish viadeploy_as_prompt_agent(recommended one-liner), then republish viato_prompt_agent+AIProjectClient.agents.create_version(...)to show the two-step alternative. Ends with a cleanup that deletes the agent and all its versions so re-runs stay idempotent.using_prompt_agents.py— the end-to-end loop: deploy the agent, connect back withFoundryAgentpassing the same local@toolcallable, run a query against the deployed prompt agent, then clean up.Documentation
README has a new "Publishing an agent as a Foundry prompt agent" section that introduces
deploy_as_prompt_agentas the recommended path, shows the two-stepto_prompt_agentvariant, documents tool-conversion behaviour, and links to both runnable samples.Contribution Checklist