-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Python: feat(foundry): add to_prompt_agent + deploy_as_prompt_agent (experimental) #5959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
eavanvalkenburg
wants to merge
10
commits into
microsoft:main
Choose a base branch
from
eavanvalkenburg:feat/foundry-to-prompt-agent
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b4e3c73
feat(foundry): add experimental to_prompt_agent converter
eavanvalkenburg ecef58d
chore(samples): remove snippet tags from portable agent sample
eavanvalkenburg c421dba
chore(samples): inline FoundryChatClient and enable prompt-agent publish
eavanvalkenburg 2772e3d
chore(samples): drop async credential context manager
eavanvalkenburg 15b7a3d
docs(foundry): trim README to_prompt_agent example to publish-only flow
eavanvalkenburg bfe4c50
docs(foundry): note FoundryAgent runs @tool callables for deployed pr…
eavanvalkenburg f115f4a
fix(foundry): address review comments on to_prompt_agent converter
eavanvalkenburg a79474c
fix(foundry): match Agent.__init__ model resolution in to_prompt_agent
eavanvalkenburg 1ca7d81
feat(foundry): add deploy_as_prompt_agent helper + samples
eavanvalkenburg ac0baba
fix(foundry): restore missing-model ValueError in to_prompt_agent
eavanvalkenburg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
220 changes: 220 additions & 0 deletions
220
python/packages/foundry/agent_framework_foundry/_to_prompt_agent.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| """Convert an Agent Framework agent into a Foundry ``PromptAgentDefinition``. | ||
|
|
||
| The converter accepts an :class:`agent_framework.Agent` whose chat client is a | ||
| :class:`agent_framework_foundry.FoundryChatClient` (or a subclass) and returns a | ||
| ``PromptAgentDefinition`` ready to publish via | ||
| ``AIProjectClient.agents.create_version(...)``. | ||
|
|
||
| The model is lifted from the bound ``FoundryChatClient`` so the same ``Agent`` | ||
| definition used for local execution can be published as a hosted prompt agent | ||
| without restating the model deployment name. | ||
|
|
||
| Function tools derived from local Python callables are translated to Foundry | ||
| ``FunctionTool`` *declarations* only. Prompt agents are server-side, so the | ||
| deployed agent will receive the schema for these tools but cannot execute the | ||
| underlying Python; wiring server-side execution is the caller's responsibility. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Iterable, Mapping | ||
| from typing import TYPE_CHECKING, Any, cast | ||
|
|
||
| from agent_framework import FunctionTool | ||
| from agent_framework._feature_stage import ExperimentalFeature, experimental | ||
| from agent_framework._mcp import MCPTool | ||
|
|
||
| from ._chat_client import RawFoundryChatClient | ||
|
|
||
| if TYPE_CHECKING: | ||
| from agent_framework import Agent | ||
| from azure.ai.projects.models import AgentVersionDetails, PromptAgentDefinition, Tool | ||
|
|
||
|
|
||
| @experimental(feature_id=ExperimentalFeature.TO_PROMPT_AGENT) | ||
| def to_prompt_agent(agent: Agent) -> PromptAgentDefinition: | ||
| """Convert an ``Agent`` into a Foundry ``PromptAgentDefinition``. | ||
|
|
||
| The agent's chat client must be a :class:`FoundryChatClient` (or any | ||
| subclass). The model deployment name is lifted from the bound client. | ||
|
|
||
| Args: | ||
| agent: An Agent Framework agent whose client is a ``FoundryChatClient``. | ||
|
|
||
| Returns: | ||
| A ``PromptAgentDefinition`` carrying the agent's model, instructions, | ||
| and tools. Pass it to ``AIProjectClient.agents.create_version(...)`` | ||
| to publish the agent as a prompt agent. | ||
| """ | ||
| if not isinstance(agent.client, RawFoundryChatClient): | ||
| raise TypeError( | ||
| "Creating a Foundry Prompt Agent requires an Agent whose client is a FoundryChatClient; " | ||
| f"got {type(agent.client).__name__!r}." | ||
| ) | ||
|
|
||
| # Match the resolution order Agent.__init__ uses when building default_options: | ||
| # an agent-level model override in default_options wins over the bound client's model. | ||
| model = agent.default_options.get("model") or agent.client.model | ||
| if not model: | ||
| raise ValueError( | ||
| "Agent has no model. Set 'model' on the FoundryChatClient (via the FOUNDRY_MODEL " | ||
| "environment variable or the model= argument), or pass default_options={'model': ...} " | ||
| "to the Agent before converting." | ||
| ) | ||
|
|
||
| instructions = agent.default_options.get("instructions") | ||
| tools = _convert_tools( | ||
| agent.default_options.get("tools", []), | ||
| getattr(agent, "mcp_tools", []), | ||
| ) | ||
|
|
||
| from azure.ai.projects.models import PromptAgentDefinition | ||
|
|
||
| return PromptAgentDefinition( | ||
| model=model, | ||
| instructions=instructions, | ||
| tools=tools or None, | ||
| ) | ||
|
|
||
|
|
||
| @experimental(feature_id=ExperimentalFeature.TO_PROMPT_AGENT) | ||
| async def deploy_as_prompt_agent( | ||
| agent: Agent, | ||
| *, | ||
| metadata: Mapping[str, str] | None = None, | ||
| agent_name: str | None = None, | ||
| description: str | None = None, | ||
| **kwargs: Any, | ||
| ) -> AgentVersionDetails: | ||
| """Publish an ``Agent`` to Foundry as a new prompt-agent version. | ||
|
|
||
| Convenience wrapper around :func:`to_prompt_agent` that uses the | ||
| :class:`FoundryChatClient` already bound to ``agent`` to call | ||
| ``project_client.agents.create_version(...)`` \u2014 so the caller does not | ||
| need to construct a separate :class:`AIProjectClient`. | ||
|
|
||
| Args: | ||
| agent: An Agent Framework agent whose client is a ``FoundryChatClient``. | ||
|
|
||
| Keyword Args: | ||
| metadata: Optional metadata dict (up to 16 key/value pairs) attached | ||
| to the version. | ||
| agent_name: The unique Foundry agent name. Must start and end with | ||
| alphanumeric characters, may contain hyphens in the middle, and | ||
| must not exceed 63 characters. Defaults to ``agent.name``, | ||
| this can be used to override the name set on the agent, in case it does | ||
| not adhere to the foundry naming restrictions. | ||
| description: Optional human-readable description for the version. | ||
| Defaults to ``agent.description``. | ||
| **kwargs: Forwarded to ``project_client.agents.create_version(...)``. | ||
|
|
||
| Returns: | ||
| The ``AgentVersionDetails`` returned by the Foundry service for the | ||
| newly created version. | ||
| """ | ||
| # to_prompt_agent enforces the FoundryChatClient requirement and model resolution. | ||
| definition = to_prompt_agent(agent) | ||
| client = cast("RawFoundryChatClient", agent.client) | ||
|
|
||
| resolved_name = agent_name or agent.name | ||
| if not resolved_name: | ||
| raise ValueError("Foundry agent_name is required. Pass agent_name= or set name= on the Agent.") | ||
|
|
||
| resolved_description = description if description is not None else agent.description | ||
|
|
||
| create_kwargs: dict[str, Any] = dict(kwargs) | ||
| if metadata is not None: | ||
| create_kwargs["metadata"] = dict(metadata) | ||
| if resolved_description is not None: | ||
| create_kwargs["description"] = resolved_description | ||
|
|
||
| return await client.project_client.agents.create_version( | ||
| agent_name=resolved_name, | ||
| definition=definition, | ||
| **create_kwargs, | ||
| ) | ||
|
|
||
|
|
||
| def _convert_tools( | ||
| tools: Iterable[Any] | None, | ||
| mcp_tools: Iterable[MCPTool] | None, | ||
| ) -> list[Tool]: | ||
| """Map AF agent tools to Foundry ``PromptAgentDefinition`` tool entries. | ||
|
|
||
| Tool sources walked, in order: | ||
|
|
||
| * ``agent.default_options["tools"]`` — function tools and hosted Foundry SDK | ||
| tool instances (returned by ``FoundryChatClient.get_*_tool()``). | ||
| * ``agent.mcp_tools`` — local Agent Framework MCP servers (split off from | ||
| the tools list by ``normalize_tools()``). These cannot be published as | ||
| prompt-agent tools; the caller must use the hosted MCP factory instead. | ||
|
|
||
| Hosted SDK tool instances are passed through unchanged. Mapping/dict tools | ||
| are passed through after light validation. Anything else raises | ||
| ``ValueError`` with a message that names the offending type. | ||
| """ | ||
| from azure.ai.projects.models import Tool as ProjectsTool | ||
|
|
||
| converted: list[Tool] = [] | ||
|
|
||
| for tool_item in tools or (): | ||
| if isinstance(tool_item, ProjectsTool): | ||
| converted.append(tool_item) | ||
| continue | ||
| if isinstance(tool_item, FunctionTool): | ||
| converted.append(_function_tool_to_foundry(tool_item)) | ||
| continue | ||
| if isinstance(tool_item, Mapping): | ||
| converted.append(_validate_mapping_tool(cast("Mapping[str, Any]", tool_item))) | ||
| continue | ||
| raise ValueError( | ||
| f"Unsupported tool type for PromptAgentDefinition: {type(tool_item).__name__}. " | ||
| "Use FoundryChatClient.get_*_tool() helpers, a callable / FunctionTool, " | ||
| "or a dict matching the Foundry tool schema." | ||
| ) | ||
|
|
||
| for mcp_tool in mcp_tools or (): | ||
| raise ValueError( | ||
| f"Local MCP tool {mcp_tool.name!r} cannot be published as a prompt-agent tool. " | ||
| "Use FoundryChatClient.get_mcp_tool(...) to register a hosted MCP server instead." | ||
| ) | ||
|
|
||
| return converted | ||
|
|
||
|
|
||
| def _function_tool_to_foundry(tool_item: FunctionTool) -> Tool: | ||
| """Build a Foundry ``FunctionTool`` declaration from an AF ``FunctionTool``. | ||
|
|
||
| The result carries only the schema (name, description, parameters). It is a | ||
| declaration of the tool the prompt agent may call; server-side execution | ||
| must be wired separately by the caller. | ||
| """ | ||
| try: | ||
| from azure.ai.projects.models import FunctionTool as ProjectsFunctionTool | ||
| except ImportError as exc: # pragma: no cover - sanity guard | ||
| raise ImportError( | ||
| "FunctionTool is not available in the installed azure-ai-projects. Upgrade azure-ai-projects." | ||
| ) from exc | ||
|
|
||
| return ProjectsFunctionTool( | ||
| name=tool_item.name, | ||
| description=tool_item.description or "", | ||
| parameters=tool_item.parameters(), | ||
| strict=False, | ||
| ) | ||
|
|
||
|
|
||
| def _validate_mapping_tool(tool_item: Mapping[str, Any]) -> Tool: | ||
| """Validate a dict-shaped tool and instantiate a Foundry ``Tool``. | ||
|
|
||
| The Foundry SDK can rehydrate a tool model from its raw JSON mapping via | ||
| the discriminator on ``type``. We require the ``type`` field so the | ||
| failure mode is obvious; everything else is left to the SDK. | ||
| """ | ||
| from azure.ai.projects.models import Tool as ProjectsTool | ||
|
|
||
| if "type" not in tool_item: | ||
| raise ValueError("Dict-shaped tools must include a 'type' field matching a Foundry tool discriminator.") | ||
| return ProjectsTool(**tool_item) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.