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
@@ -124,10 +124,20 @@ class DurableAgentExecutor(ABC, Generic[TaskT]):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_new_session(self, agent_name: str, **kwargs: Any) -> DurableAgentSession:
|
||||
def get_new_session(
|
||||
self,
|
||||
agent_name: str,
|
||||
*,
|
||||
session_id: str | None = None,
|
||||
service_session_id: str | None = None,
|
||||
) -> DurableAgentSession:
|
||||
"""Create a new DurableAgentSession with random session ID."""
|
||||
session_id = self._create_session_id(agent_name)
|
||||
return DurableAgentSession.from_session_id(session_id, **kwargs)
|
||||
durable_session_id = self._create_session_id(agent_name)
|
||||
return DurableAgentSession(
|
||||
durable_session_id=durable_session_id,
|
||||
session_id=session_id,
|
||||
service_session_id=service_session_id,
|
||||
)
|
||||
|
||||
def _create_session_id(
|
||||
self,
|
||||
|
||||
@@ -284,46 +284,48 @@ class DurableAgentSession(AgentSession):
|
||||
durable_session_id: AgentSessionId | None = None,
|
||||
session_id: str | None = None,
|
||||
service_session_id: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(session_id=session_id, service_session_id=service_session_id, **kwargs)
|
||||
self._session_id_value: AgentSessionId | None = durable_session_id
|
||||
super().__init__(session_id=session_id, service_session_id=service_session_id)
|
||||
self.durable_session_id: AgentSessionId | None = durable_session_id
|
||||
|
||||
@property
|
||||
def durable_session_id(self) -> AgentSessionId | None:
|
||||
return self._session_id_value
|
||||
|
||||
@durable_session_id.setter
|
||||
def durable_session_id(self, value: AgentSessionId | None) -> None:
|
||||
self._session_id_value = value
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
state = super().to_dict()
|
||||
if self.durable_session_id is not None:
|
||||
state[self._SERIALIZED_SESSION_ID_KEY] = str(self.durable_session_id)
|
||||
return state
|
||||
|
||||
@classmethod
|
||||
def from_session_id(
|
||||
cls,
|
||||
session_id: AgentSessionId,
|
||||
**kwargs: Any,
|
||||
durable_session_id: AgentSessionId,
|
||||
*,
|
||||
session_id: str | None = None,
|
||||
service_session_id: str | None = None,
|
||||
) -> DurableAgentSession:
|
||||
return cls(durable_session_id=session_id, **kwargs)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
state = super().to_dict()
|
||||
if self._session_id_value is not None:
|
||||
state[self._SERIALIZED_SESSION_ID_KEY] = str(self._session_id_value)
|
||||
return state
|
||||
"""Create a DurableAgentSession from an AgentSessionId."""
|
||||
return cls(
|
||||
durable_session_id=durable_session_id,
|
||||
session_id=session_id,
|
||||
service_session_id=service_session_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> DurableAgentSession:
|
||||
state_payload = dict(data)
|
||||
session_id_value = state_payload.pop(cls._SERIALIZED_SESSION_ID_KEY, None)
|
||||
session = super().from_dict(state_payload)
|
||||
"""Create a DurableAgentSession from a state dict."""
|
||||
data = dict(data) # defensive copy — avoid mutating caller's dict
|
||||
session_id_value = data.pop(cls._SERIALIZED_SESSION_ID_KEY, None)
|
||||
session = super().from_dict(data)
|
||||
durable_session_id: AgentSessionId | None = None
|
||||
# We need to create a DurableAgentSession from the base AgentSession
|
||||
if session_id_value is not None:
|
||||
if not isinstance(session_id_value, str):
|
||||
raise ValueError("durable_session_id must be a string when present in serialized state")
|
||||
durable_session_id = AgentSessionId.parse(session_id_value)
|
||||
|
||||
durable_session = cls(
|
||||
durable_session_id=durable_session_id,
|
||||
session_id=session.session_id,
|
||||
service_session_id=session.service_session_id,
|
||||
)
|
||||
durable_session.state.update(session.state)
|
||||
if session_id_value is not None:
|
||||
if not isinstance(session_id_value, str):
|
||||
raise ValueError("durable_session_id must be a string when present in serialized state")
|
||||
durable_session._session_id_value = AgentSessionId.parse(session_id_value)
|
||||
return durable_session
|
||||
|
||||
@@ -133,16 +133,13 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
def create_session(self, **kwargs: Any) -> DurableAgentSession:
|
||||
def create_session(self, *, session_id: str | None = None) -> DurableAgentSession:
|
||||
"""Create a new agent session via the provider."""
|
||||
return self._executor.get_new_session(self.name, **kwargs)
|
||||
return self._executor.get_new_session(self.name)
|
||||
|
||||
def get_session(self, **kwargs: Any) -> AgentSession:
|
||||
"""Retrieve an existing session via the provider.
|
||||
|
||||
For durable agents, sessions do not use `service_session_id` so this is not used.
|
||||
"""
|
||||
return self._executor.get_new_session(self.name, **kwargs)
|
||||
def get_session(self, service_session_id: str, *, session_id: str | None = None) -> AgentSession:
|
||||
"""Retrieve an existing session via the provider."""
|
||||
return self._executor.get_new_session(self.name, service_session_id=service_session_id, session_id=session_id)
|
||||
|
||||
def _normalize_messages(self, messages: AgentRunInputs | None) -> str:
|
||||
"""Convert supported message inputs to a single string.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Unit tests for AgentSessionId and DurableAgentSession."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentSession
|
||||
|
||||
@@ -153,7 +155,7 @@ class TestDurableAgentSession:
|
||||
def test_from_session_id(self) -> None:
|
||||
"""Test creating DurableAgentSession from session ID."""
|
||||
session_id = AgentSessionId(name="TestAgent", key="test-key")
|
||||
session = DurableAgentSession.from_session_id(session_id)
|
||||
session = DurableAgentSession(durable_session_id=session_id)
|
||||
|
||||
assert isinstance(session, DurableAgentSession)
|
||||
assert session.durable_session_id is not None
|
||||
@@ -161,10 +163,10 @@ class TestDurableAgentSession:
|
||||
assert session.durable_session_id.name == "TestAgent"
|
||||
assert session.durable_session_id.key == "test-key"
|
||||
|
||||
def test_from_session_id_with_service_session_id(self) -> None:
|
||||
"""Test creating DurableAgentSession with service session ID."""
|
||||
def test_init_with_service_session_id(self) -> None:
|
||||
"""Test creating DurableAgentSession with explicit service session ID."""
|
||||
session_id = AgentSessionId(name="TestAgent", key="test-key")
|
||||
session = DurableAgentSession.from_session_id(session_id, service_session_id="service-123")
|
||||
session = DurableAgentSession(durable_session_id=session_id, service_session_id="service-123")
|
||||
|
||||
assert session.durable_session_id is not None
|
||||
assert session.durable_session_id == session_id
|
||||
@@ -192,7 +194,7 @@ class TestDurableAgentSession:
|
||||
|
||||
def test_from_dict_with_durable_session_id(self) -> None:
|
||||
"""Test deserialization restores durable session ID."""
|
||||
serialized = {
|
||||
serialized: dict[str, Any] = {
|
||||
"type": "session",
|
||||
"session_id": "session-123",
|
||||
"service_session_id": "service-123",
|
||||
@@ -210,7 +212,7 @@ class TestDurableAgentSession:
|
||||
|
||||
def test_from_dict_without_durable_session_id(self) -> None:
|
||||
"""Test deserialization without durable session ID."""
|
||||
serialized = {
|
||||
serialized: dict[str, Any] = {
|
||||
"type": "session",
|
||||
"session_id": "session-456",
|
||||
"service_session_id": "service-456",
|
||||
|
||||
@@ -88,15 +88,6 @@ class TestDurableAIAgentClientIntegration:
|
||||
|
||||
assert isinstance(session, DurableAgentSession)
|
||||
|
||||
def test_client_agent_session_with_parameters(self, agent_client: DurableAIAgentClient) -> None:
|
||||
"""Verify agent can create sessions with custom parameters."""
|
||||
agent = agent_client.get_agent("assistant")
|
||||
|
||||
session = agent.create_session(service_session_id="client-session-123")
|
||||
|
||||
assert isinstance(session, DurableAgentSession)
|
||||
assert session.service_session_id == "client-session-123"
|
||||
|
||||
|
||||
class TestDurableAIAgentClientPollingConfiguration:
|
||||
"""Test polling configuration parameters for DurableAIAgentClient."""
|
||||
|
||||
@@ -82,17 +82,6 @@ class TestDurableAIAgentOrchestrationContextIntegration:
|
||||
|
||||
assert isinstance(session, DurableAgentSession)
|
||||
|
||||
def test_orchestration_agent_session_with_parameters(
|
||||
self, agent_context: DurableAIAgentOrchestrationContext
|
||||
) -> None:
|
||||
"""Verify agent can create sessions with custom parameters."""
|
||||
agent = agent_context.get_agent("assistant")
|
||||
|
||||
session = agent.create_session(service_session_id="orch-session-456")
|
||||
|
||||
assert isinstance(session, DurableAgentSession)
|
||||
assert session.service_session_id == "orch-session-456"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "--tb=short"])
|
||||
|
||||
@@ -184,16 +184,31 @@ class TestDurableAIAgentSessionManagement:
|
||||
mock_executor.get_new_session.assert_called_once_with("test_agent")
|
||||
assert session == mock_session
|
||||
|
||||
def test_create_session_forwards_kwargs(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
|
||||
"""Verify create_session forwards kwargs to executor."""
|
||||
mock_session = DurableAgentSession(service_session_id="session-123")
|
||||
def test_get_session_forwards_service_session_id(
|
||||
self, test_agent: DurableAIAgent[Any], mock_executor: Mock
|
||||
) -> None:
|
||||
"""Verify get_session forwards service_session_id and session_id to executor."""
|
||||
mock_session = DurableAgentSession(service_session_id="svc-123")
|
||||
mock_executor.get_new_session.return_value = mock_session
|
||||
|
||||
test_agent.create_session(service_session_id="session-123")
|
||||
session = test_agent.get_session("svc-123", session_id="local-456")
|
||||
|
||||
mock_executor.get_new_session.assert_called_once()
|
||||
_, kwargs = mock_executor.get_new_session.call_args
|
||||
assert kwargs["service_session_id"] == "session-123"
|
||||
mock_executor.get_new_session.assert_called_once_with(
|
||||
"test_agent", service_session_id="svc-123", session_id="local-456"
|
||||
)
|
||||
assert session.service_session_id == "svc-123"
|
||||
|
||||
def test_get_session_without_session_id(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
|
||||
"""Verify get_session works with only service_session_id (session_id defaults to None)."""
|
||||
mock_session = DurableAgentSession(service_session_id="svc-789")
|
||||
mock_executor.get_new_session.return_value = mock_session
|
||||
|
||||
session = test_agent.get_session("svc-789")
|
||||
|
||||
mock_executor.get_new_session.assert_called_once_with(
|
||||
"test_agent", service_session_id="svc-789", session_id=None
|
||||
)
|
||||
assert session.service_session_id == "svc-789"
|
||||
|
||||
|
||||
class TestDurableAgentProviderInterface:
|
||||
|
||||
Reference in New Issue
Block a user