mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: clean up kwargs across agents, chat clients, tools, and sessions (#4581)
* Python: clean up kwargs across agents, chat clients, tools, and sessions (#3642) Audit and refactor public **kwargs usage across core agents, chat clients, tools, sessions, and provider packages per the migration strategy codified in CODING_STANDARD.md. Key changes: - Add explicit runtime buckets: function_invocation_kwargs and client_kwargs on RawAgent.run() and chat client get_response() layers. - Refactor FunctionTool to prefer explicit ctx: FunctionInvocationContext injection; legacy **kwargs tools still work via _forward_runtime_kwargs. - Refactor Agent.as_tool() to use direct JSON schema, always-streaming wrapper, approval_mode parameter, and UserInputRequiredException propagation (integrates PR #4568 behavior). - Remove implicit session bleeding into FunctionInvocationContext; tools that need a session must receive it via function_invocation_kwargs. - Lower chat-client layers after FunctionInvocationLayer accept only compatibility **kwargs (client_kwargs flattened, function_invocation_kwargs ignored). - Add layered docstring composition from Raw... implementations via _docstrings.py helper. - Clean up provider constructors to use explicit additional_properties. - Deprecation warnings on legacy direct kwargs paths. - Update samples, tests, and typing across all 23 packages. Resolves #3642 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * clarified docstring * feedback fixes * Add unit tests for _docstrings.py build/apply helpers Tests cover: no docstring source, no extra kwargs, appending to existing Keyword Args section, inserting after Args, inserting in plain docstrings, multiline descriptions, ordering, and apply_layered_docstring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add test for propagate_session TypeError on non-AgentSession values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add tests for multi-content and empty UserInputRequiredException propagation Cover the branching logic in _try_execute_function_calls for: - Multiple user_input_request items in a single exception (extra_user_input_contents path) - Empty contents list (fallback function_result path) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add tests for DurableAIAgent.get_session forwarding service_session_id Verifies get_session correctly forwards service_session_id and session_id to the executor's get_new_session, replacing the removed kwargs test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify ag-ui test stub to read session from client_kwargs only Remove dual-mode detection (client_kwargs vs raw kwargs fallback) from the test mock. Session is now read exclusively from client_kwargs, matching the settled public calling convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated create and get sessions in durable * fixed docstrings * fix test * updated session handling * updated from main * updated tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
b7990908fe
commit
a4b9539b62
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from agent_framework import AgentContext, AgentSession
|
||||
from agent_framework import AgentContext, AgentSession, FunctionInvocationContext, tool
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@@ -18,9 +18,6 @@ sub-agent invoked as a tool using ``propagate_session=True``.
|
||||
When session propagation is enabled, both agents share the same session object,
|
||||
including session_id and the mutable state dict. This allows correlated
|
||||
conversation tracking and shared state across the agent hierarchy.
|
||||
|
||||
The middleware functions below are purely for observability — they are NOT
|
||||
required for session propagation to work.
|
||||
"""
|
||||
|
||||
|
||||
@@ -28,65 +25,83 @@ async def log_session(
|
||||
context: AgentContext,
|
||||
call_next: Callable[[], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Agent middleware that logs the session received by each agent.
|
||||
|
||||
NOT required for session propagation — only used to observe the flow.
|
||||
If propagation is working, both agents will show the same session_id.
|
||||
"""
|
||||
"""Agent middleware that logs the session received by each agent."""
|
||||
session: AgentSession | None = context.session
|
||||
if not session:
|
||||
print("No session found.")
|
||||
await call_next()
|
||||
return
|
||||
agent_name = context.agent.name or "unknown"
|
||||
session_id = session.session_id if session else None
|
||||
state = dict(session.state) if session else {}
|
||||
print(f" [{agent_name}] session_id={session_id}, state={state}")
|
||||
print(
|
||||
f" [{agent_name}] session_id={session.session_id}, "
|
||||
f"service_session_id={session.service_session_id} state={session.state}"
|
||||
)
|
||||
await call_next()
|
||||
|
||||
|
||||
@tool(description="Use this tool to store the findings so that other agents can reason over them.")
|
||||
def store_findings(findings: str, ctx: FunctionInvocationContext) -> None:
|
||||
if ctx.session is None:
|
||||
return
|
||||
current_findings = ctx.session.state.get("findings")
|
||||
if current_findings is None:
|
||||
ctx.session.state["findings"] = findings
|
||||
else:
|
||||
ctx.session.state["findings"] = f"{current_findings}\n{findings}"
|
||||
|
||||
|
||||
@tool(description="Use this tool to gather the current findings from other agents.")
|
||||
def recall_findings(ctx: FunctionInvocationContext) -> str:
|
||||
if ctx.session is None:
|
||||
return "No session available"
|
||||
current_findings = ctx.session.state.get("findings")
|
||||
if current_findings is None:
|
||||
return "Nothing yet"
|
||||
return current_findings
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Agent-as-Tool: Session Propagation ===\n")
|
||||
|
||||
client = OpenAIResponsesClient()
|
||||
|
||||
# --- Sub-agent: a research specialist ---
|
||||
# The sub-agent has the same log_session middleware to prove it receives the session.
|
||||
research_agent = client.as_agent(
|
||||
name="ResearchAgent",
|
||||
instructions="You are a research assistant. Provide concise answers.",
|
||||
instructions="You are a research assistant. Provide concise answers and store your findings.",
|
||||
middleware=[log_session],
|
||||
tools=[store_findings, recall_findings],
|
||||
)
|
||||
|
||||
# propagate_session=True: the coordinator's session will be forwarded
|
||||
research_tool = research_agent.as_tool(
|
||||
name="research",
|
||||
description="Research a topic and return findings",
|
||||
description="Research a topic and store your findings.",
|
||||
arg_name="query",
|
||||
arg_description="The research query",
|
||||
propagate_session=True,
|
||||
)
|
||||
|
||||
# --- Coordinator agent ---
|
||||
coordinator = client.as_agent(
|
||||
name="CoordinatorAgent",
|
||||
instructions="You coordinate research. Use the 'research' tool to look up information.",
|
||||
tools=[research_tool],
|
||||
instructions=(
|
||||
"You coordinate research. Use the 'research' tool to start research "
|
||||
"and then use the recall findings tool to gather up everything."
|
||||
),
|
||||
tools=[research_tool, store_findings, recall_findings],
|
||||
middleware=[log_session],
|
||||
)
|
||||
|
||||
# Create a shared session and put some state in it
|
||||
session = coordinator.create_session()
|
||||
session.state["request_source"] = "demo"
|
||||
session.state["findings"] = None
|
||||
print(f"Session ID: {session.session_id}")
|
||||
print(f"Session state before run: {session.state}\n")
|
||||
|
||||
query = "What are the latest developments in quantum computing?"
|
||||
query = "What are the latest developments in quantum computing and in AI?"
|
||||
print(f"User: {query}\n")
|
||||
|
||||
result = await coordinator.run(query, session=session)
|
||||
|
||||
print(f"\nCoordinator: {result}\n")
|
||||
print(f"Session state after run: {session.state}")
|
||||
print(
|
||||
"\nIf both agents show the same session_id above, session propagation is working."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import tool
|
||||
from agent_framework import FunctionInvocationContext, tool
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field
|
||||
@@ -14,27 +14,27 @@ load_dotenv()
|
||||
"""
|
||||
AI Function with kwargs Example
|
||||
|
||||
This example demonstrates how to inject custom keyword arguments (kwargs) into an AI function
|
||||
from the agent's run method, without exposing them to the AI model.
|
||||
This example demonstrates how to inject runtime context into an AI function
|
||||
from the agent's run method, without exposing it to the AI model.
|
||||
|
||||
This is useful for passing runtime information like access tokens, user IDs, or
|
||||
request-specific context that the tool needs but the model shouldn't know about
|
||||
or provide.
|
||||
or provide. The injected context parameter can be typed as
|
||||
``FunctionInvocationContext`` as shown here, or left untyped as ``ctx`` when you
|
||||
prefer a lighter-weight sample setup.
|
||||
"""
|
||||
|
||||
|
||||
# Define the function tool with **kwargs to accept injected arguments
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
|
||||
# see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
# Define the function tool with explicit invocation context.
|
||||
# The context parameter can also be declared as an untyped ``ctx`` parameter.
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
**kwargs: Any,
|
||||
ctx: FunctionInvocationContext,
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
# Extract the injected argument from kwargs
|
||||
user_id = kwargs.get("user_id", "unknown")
|
||||
# Extract the injected argument from the explicit context
|
||||
user_id = ctx.kwargs.get("user_id", "unknown")
|
||||
|
||||
# Simulate using the user_id for logging or personalization
|
||||
print(f"Getting weather for user: {user_id}")
|
||||
@@ -49,9 +49,11 @@ async def main() -> None:
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
# Pass the injected argument when running the agent
|
||||
# The 'user_id' kwarg will be passed down to the tool execution via **kwargs
|
||||
response = await agent.run("What is the weather like in Amsterdam?", user_id="user_123")
|
||||
# Pass the runtime context explicitly when running the agent.
|
||||
response = await agent.run(
|
||||
"What is the weather like in Amsterdam?",
|
||||
function_invocation_kwargs={"user_id": "user_123"},
|
||||
)
|
||||
|
||||
print(f"Agent: {response.text}")
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import AgentSession, tool
|
||||
from agent_framework import AgentSession, FunctionInvocationContext, tool
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field
|
||||
@@ -14,23 +14,21 @@ load_dotenv()
|
||||
"""
|
||||
AI Function with Session Injection Example
|
||||
|
||||
This example demonstrates the behavior when passing 'session' to agent.run()
|
||||
and accessing that session in AI function.
|
||||
This example demonstrates accessing the agent session inside a tool function
|
||||
via ``FunctionInvocationContext.session``. The session is automatically
|
||||
available when the agent is invoked with a session.
|
||||
"""
|
||||
|
||||
|
||||
# Define the function tool with **kwargs
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
|
||||
# see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
# Define the function tool with explicit invocation context.
|
||||
# The context parameter can also be declared as an untyped parameter with the name: ``ctx``.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
**kwargs: Any,
|
||||
ctx: FunctionInvocationContext,
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
# Get session object from kwargs
|
||||
session = kwargs.get("session")
|
||||
session = ctx.session
|
||||
if session and isinstance(session, AgentSession) and session.service_session_id:
|
||||
print(f"Session ID: {session.service_session_id}.")
|
||||
|
||||
@@ -42,17 +40,19 @@ async def main() -> None:
|
||||
name="WeatherAgent",
|
||||
instructions="You are a helpful weather assistant.",
|
||||
tools=[get_weather],
|
||||
options={"store": True},
|
||||
default_options={"store": True},
|
||||
)
|
||||
|
||||
# Create a session
|
||||
session = agent.create_session()
|
||||
|
||||
# Run the agent with the session
|
||||
# Pass session via additional_function_arguments so tools can access it via **kwargs
|
||||
opts = {"additional_function_arguments": {"session": session}}
|
||||
print(f"Agent: {await agent.run('What is the weather in London?', session=session, options=opts)}")
|
||||
print(f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session, options=opts)}")
|
||||
# Run the agent with the session; tools receive it via ctx.session.
|
||||
print(
|
||||
f"Agent: {await agent.run('What is the weather in London?', session=session)}"
|
||||
)
|
||||
print(
|
||||
f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session)}"
|
||||
)
|
||||
print(f"Agent: {await agent.run('What cities did I ask about?', session=session)}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user