Skip to content

Python: feat(foundry): add to_prompt_agent + deploy_as_prompt_agent (experimental)#5959

Open
eavanvalkenburg wants to merge 10 commits into
microsoft:mainfrom
eavanvalkenburg:feat/foundry-to-prompt-agent
Open

Python: feat(foundry): add to_prompt_agent + deploy_as_prompt_agent (experimental)#5959
eavanvalkenburg wants to merge 10 commits into
microsoft:mainfrom
eavanvalkenburg:feat/foundry-to-prompt-agent

Conversation

@eavanvalkenburg
Copy link
Copy Markdown
Member

@eavanvalkenburg eavanvalkenburg commented May 19, 2026

Motivation and Context

Developers building agents with Agent Framework's FoundryChatClient have no way today to take that agent and publish it as a Foundry prompt agent without rewriting the definition by hand in azure-ai-projects SDK types — model, instructions, tools, all restated. The same Python tool definitions also can't be reused between the Responses API path (FoundryChatClient local execution) and a prompt agent definition because the two surfaces have different shapes.

This PR adds two helpers so the same Agent object can either run locally via agent.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 the agent_framework.foundry lazy-loading namespace (__init__.py + .pyi stub):

  • to_prompt_agent(agent) -> PromptAgentDefinition — convert an Agent whose chat client is a FoundryChatClient into a Foundry PromptAgentDefinition you can pass to AIProjectClient.agents.create_version(...).
  • deploy_as_prompt_agent(agent, *, agent_name=None, metadata=None, description=None, **kwargs) -> AgentVersionDetails — convenience wrapper that reuses the bound FoundryChatClient's project client to call agents.create_version(...). Defaults agent_name / description from agent.name / agent.description so 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_AGENT tag.

Model resolution. The model is resolved the same way Agent.__init__ does at runtime (_agents.py:740): default_options["model"] first, then agent.client.model. So Agent(client=FoundryChatClient(model="legacy"), default_options={"model": "claude"}) publishes claude, matching what the agent actually runs with.

Tool conversion.

  • Foundry SDK tool instances (anything from FoundryChatClient.get_*_tool() or a literal azure.ai.projects.models.*Tool) are passed through unchanged.
  • AF FunctionTool instances (including @tool-decorated callables) become Foundry FunctionTool declarations — the deployed prompt agent receives the schema only. The matching using_prompt_agents.py sample shows how FoundryAgent runs the local Python by matching tool names when the deployed agent invokes the tool.
  • Local Agent Framework 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 so local MCP that normalize_tools() split off is still caught.
  • Dict-shaped tools with a type discriminator are rehydrated through the SDK Tool model; missing type raises ValueError.

Validation flow. agent.client must be a FoundryChatClient (or subclass); otherwise to_prompt_agent raises TypeError. deploy_as_prompt_agent defers that check (and the model/tool checks) to to_prompt_agent so 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 via deploy_as_prompt_agent (recommended one-liner), then republish via to_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 with FoundryAgent passing the same local @tool callable, 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_agent as the recommended path, shows the two-step to_prompt_agent variant, documents tool-conversion behaviour, and links to both runnable samples.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Copilot AI review requested due to automatic review settings May 19, 2026 18:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_agent with tool conversion rules (SDK tools pass-through, AF function tools -> declarations, local MCP rejected, dict tools rehydrated).
  • Re-exports to_prompt_agent via agent_framework_foundry and agent_framework.foundry (incl. .pyi) and registers ExperimentalFeature.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.

Comment thread python/packages/foundry/agent_framework_foundry/_to_prompt_agent.py Outdated
Comment thread python/packages/foundry/agent_framework_foundry/_to_prompt_agent.py Outdated
@moonbox3 moonbox3 added documentation Improvements or additions to documentation python labels May 19, 2026
@github-actions github-actions Bot changed the title feat(foundry): add experimental to_prompt_agent converter Python: feat(foundry): add experimental to_prompt_agent converter May 19, 2026
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread python/packages/foundry/agent_framework_foundry/_to_prompt_agent.py
Comment thread python/packages/foundry/tests/foundry/test_to_prompt_agent.py
eavanvalkenburg and others added 8 commits May 20, 2026 08:51
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>
@eavanvalkenburg eavanvalkenburg force-pushed the feat/foundry-to-prompt-agent branch from 203b9fc to a79474c Compare May 20, 2026 06:51
@moonbox3
Copy link
Copy Markdown
Contributor

moonbox3 commented May 20, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _feature_stage.py118694%100, 157, 168, 175, 208, 236
packages/foundry/agent_framework_foundry
   _to_prompt_agent.py580100% 
TOTAL35072407688% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
6978 30 💤 0 ❌ 0 🔥 1m 48s ⏱️

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>
@eavanvalkenburg eavanvalkenburg changed the title Python: feat(foundry): add experimental to_prompt_agent converter Python: feat(foundry): add to_prompt_agent + deploy_as_prompt_agent (experimental) May 20, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants