mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: updated docstrings of all _files (#1107)
* updated docstrings of all _files * fix mypy * fixed codeblocks in workflows and some other files --------- Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
d6c40edbc6
commit
8b80a03fbb
@@ -7,11 +7,11 @@ import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent_framework import __version__ as agent_framework_version
|
||||
from py2docfx.__main__ import main as py2docfx_main
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def generate_af_docs(root_path: Path):
|
||||
"""Generate documentation for the Agent Framework using py2docfx.
|
||||
|
||||
@@ -21,9 +21,10 @@ async def generate_af_docs(root_path: Path):
|
||||
"packages": [
|
||||
{
|
||||
"package_info": {
|
||||
"name": "agent-framework",
|
||||
"version": agent_framework_version,
|
||||
"name": "agent-framework-core",
|
||||
"version": "1.0.0b251001",
|
||||
"install_type": "pypi",
|
||||
"extras": ["all"]
|
||||
},
|
||||
"sphinx_extensions": [
|
||||
"sphinxcontrib.autodoc_pydantic",
|
||||
@@ -75,6 +76,7 @@ async def generate_af_docs(root_path: Path):
|
||||
str((root_path / "docs" / "build").absolute()),
|
||||
"-j",
|
||||
json.dumps(package),
|
||||
"--verbose"
|
||||
]
|
||||
try:
|
||||
await py2docfx_main(args)
|
||||
|
||||
@@ -29,7 +29,7 @@ from ._types import (
|
||||
Role,
|
||||
ToolMode,
|
||||
)
|
||||
from .exceptions import AgentExecutionException
|
||||
from .exceptions import AgentExecutionException, AgentInitializationError
|
||||
from .observability import use_agent_observability
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
@@ -53,7 +53,70 @@ __all__ = ["AgentProtocol", "BaseAgent", "ChatAgent"]
|
||||
|
||||
@runtime_checkable
|
||||
class AgentProtocol(Protocol):
|
||||
"""A protocol for an agent that can be invoked."""
|
||||
"""A protocol for an agent that can be invoked.
|
||||
|
||||
This protocol defines the interface that all agents must implement,
|
||||
including properties for identification and methods for execution.
|
||||
|
||||
Note:
|
||||
Protocols use structural subtyping (duck typing). Classes don't need
|
||||
to explicitly inherit from this protocol to be considered compatible.
|
||||
This allows you to create completely custom agents without using
|
||||
any Agent Framework base classes.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import AgentProtocol
|
||||
|
||||
|
||||
# Any class implementing the required methods is compatible
|
||||
# No need to inherit from AgentProtocol or use any framework classes
|
||||
class CustomAgent:
|
||||
def __init__(self):
|
||||
self._id = "custom-agent-001"
|
||||
self._name = "Custom Agent"
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return self.name or self.id
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
return "A fully custom agent implementation"
|
||||
|
||||
async def run(self, messages=None, *, thread=None, **kwargs):
|
||||
# Your custom implementation
|
||||
from agent_framework import AgentRunResponse
|
||||
|
||||
return AgentRunResponse(messages=[], response_id="custom-response")
|
||||
|
||||
def run_stream(self, messages=None, *, thread=None, **kwargs):
|
||||
# Your custom streaming implementation
|
||||
async def _stream():
|
||||
from agent_framework import AgentRunResponseUpdate
|
||||
|
||||
yield AgentRunResponseUpdate()
|
||||
|
||||
return _stream()
|
||||
|
||||
def get_new_thread(self, **kwargs):
|
||||
# Return your own thread implementation
|
||||
return {"id": "custom-thread", "messages": []}
|
||||
|
||||
|
||||
# Verify the instance satisfies the protocol
|
||||
instance = CustomAgent()
|
||||
assert isinstance(instance, AgentProtocol)
|
||||
"""
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
@@ -137,7 +200,51 @@ class AgentProtocol(Protocol):
|
||||
|
||||
|
||||
class BaseAgent(SerializationMixin):
|
||||
"""Base class for all Agent Framework agents."""
|
||||
"""Base class for all Agent Framework agents.
|
||||
|
||||
This class provides core functionality for agent implementations, including
|
||||
context providers, middleware support, and thread management.
|
||||
|
||||
Note:
|
||||
BaseAgent cannot be instantiated directly as it doesn't implement the
|
||||
``run()``, ``run_stream()``, and other methods required by AgentProtocol.
|
||||
Use a concrete implementation like ChatAgent or create a subclass.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import BaseAgent, AgentThread, AgentRunResponse
|
||||
|
||||
|
||||
# Create a concrete subclass that implements the protocol
|
||||
class SimpleAgent(BaseAgent):
|
||||
async def run(self, messages=None, *, thread=None, **kwargs):
|
||||
# Custom implementation
|
||||
return AgentRunResponse(messages=[], response_id="simple-response")
|
||||
|
||||
def run_stream(self, messages=None, *, thread=None, **kwargs):
|
||||
async def _stream():
|
||||
# Custom streaming implementation
|
||||
yield AgentRunResponseUpdate()
|
||||
|
||||
return _stream()
|
||||
|
||||
|
||||
# Now instantiate the concrete subclass
|
||||
agent = SimpleAgent(name="my-agent", description="A simple agent implementation")
|
||||
|
||||
# Create with specific ID and additional properties
|
||||
agent = SimpleAgent(
|
||||
id="custom-id-123",
|
||||
name="configured-agent",
|
||||
description="An agent with custom configuration",
|
||||
additional_properties={"version": "1.0", "environment": "production"},
|
||||
)
|
||||
|
||||
# Access agent properties
|
||||
print(agent.id) # Custom or auto-generated UUID
|
||||
print(agent.display_name) # Returns name or id
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"}
|
||||
|
||||
@@ -152,14 +259,13 @@ class BaseAgent(SerializationMixin):
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Base class for all Agent Framework agents.
|
||||
"""Initialize a BaseAgent instance.
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the agent If no id is provided,
|
||||
id: The unique identifier of the agent. If no id is provided,
|
||||
a new UUID will be generated.
|
||||
name: The name of the agent, can be None.
|
||||
description: The description of the agent.
|
||||
display_name: The display name of the agent, which is either the name or id.
|
||||
context_providers: The collection of multiple context providers to include during agent invocation.
|
||||
middleware: List of middleware to intercept agent and function invocations.
|
||||
additional_properties: Additional properties set on the agent.
|
||||
@@ -189,6 +295,11 @@ class BaseAgent(SerializationMixin):
|
||||
"""Notify the thread of new messages.
|
||||
|
||||
This also calls the invoked method of a potential context provider on the thread.
|
||||
|
||||
Args:
|
||||
thread: The thread to notify of new messages.
|
||||
input_messages: The input messages to notify about.
|
||||
response_messages: The response messages to notify about.
|
||||
"""
|
||||
if isinstance(input_messages, ChatMessage) or len(input_messages) > 0:
|
||||
await thread.on_new_messages(input_messages)
|
||||
@@ -206,11 +317,26 @@ class BaseAgent(SerializationMixin):
|
||||
return self.name or self.id
|
||||
|
||||
def get_new_thread(self, **kwargs: Any) -> AgentThread:
|
||||
"""Returns AgentThread instance that is compatible with the agent."""
|
||||
"""Return a new AgentThread instance that is compatible with the agent.
|
||||
|
||||
Args:
|
||||
kwargs: Additional keyword arguments passed to AgentThread.
|
||||
|
||||
Returns:
|
||||
A new AgentThread instance configured with the agent's context provider.
|
||||
"""
|
||||
return AgentThread(**kwargs, context_provider=self.context_provider)
|
||||
|
||||
async def deserialize_thread(self, serialized_thread: Any, **kwargs: Any) -> AgentThread:
|
||||
"""Deserializes the thread."""
|
||||
"""Deserialize a thread from its serialized state.
|
||||
|
||||
Args:
|
||||
serialized_thread: The serialized thread data.
|
||||
kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
A new AgentThread instance restored from the serialized state.
|
||||
"""
|
||||
thread: AgentThread = self.get_new_thread()
|
||||
await thread.update_from_thread_state(serialized_thread, **kwargs)
|
||||
return thread
|
||||
@@ -233,11 +359,29 @@ class BaseAgent(SerializationMixin):
|
||||
description: The description for the tool. If None, uses the agent's description or empty string.
|
||||
arg_name: The name of the function argument (default: "task").
|
||||
arg_description: The description for the function argument.
|
||||
If None, defaults to "Input for {self.display_name}".
|
||||
If None, defaults to "Task for {tool_name}".
|
||||
stream_callback: Optional callback for streaming responses. If provided, uses run_stream.
|
||||
|
||||
Returns:
|
||||
An AIFunction that can be used as a tool by other agents.
|
||||
|
||||
Raises:
|
||||
TypeError: If the agent does not implement AgentProtocol.
|
||||
ValueError: If the agent tool name cannot be determined.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
|
||||
# Create an agent
|
||||
agent = ChatAgent(chat_client=client, name="research-agent", description="Performs research tasks")
|
||||
|
||||
# Convert the agent to a tool
|
||||
research_tool = agent.as_tool()
|
||||
|
||||
# Use the tool with another agent
|
||||
coordinator = ChatAgent(chat_client=client, name="coordinator", tools=research_tool)
|
||||
"""
|
||||
# Verify that self implements AgentProtocol
|
||||
if not isinstance(self, AgentProtocol):
|
||||
@@ -318,7 +462,50 @@ class BaseAgent(SerializationMixin):
|
||||
@use_agent_middleware
|
||||
@use_agent_observability
|
||||
class ChatAgent(BaseAgent):
|
||||
"""A Chat Client Agent."""
|
||||
"""A Chat Client Agent.
|
||||
|
||||
This is the primary agent implementation that uses a chat client to interact
|
||||
with language models. It supports tools, context providers, middleware, and
|
||||
both streaming and non-streaming responses.
|
||||
|
||||
Examples:
|
||||
Basic usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.clients import OpenAIChatClient
|
||||
|
||||
# Create a basic chat agent
|
||||
client = OpenAIChatClient(model="gpt-4")
|
||||
agent = ChatAgent(chat_client=client, name="assistant", description="A helpful assistant")
|
||||
|
||||
# Run the agent with a simple message
|
||||
response = await agent.run("Hello, how are you?")
|
||||
print(response.text)
|
||||
|
||||
With tools and streaming:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Create an agent with tools and instructions
|
||||
def get_weather(location: str) -> str:
|
||||
return f"The weather in {location} is sunny."
|
||||
|
||||
|
||||
agent = ChatAgent(
|
||||
chat_client=client,
|
||||
name="weather-agent",
|
||||
instructions="You are a weather assistant.",
|
||||
tools=get_weather,
|
||||
temperature=0.7,
|
||||
max_tokens=500,
|
||||
)
|
||||
|
||||
# Use streaming responses
|
||||
async for update in agent.run_stream("What's the weather in Paris?"):
|
||||
print(update.text, end="")
|
||||
"""
|
||||
|
||||
AGENT_SYSTEM_NAME: ClassVar[str] = "microsoft.agent_framework"
|
||||
|
||||
@@ -331,6 +518,7 @@ class ChatAgent(BaseAgent):
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
conversation_id: str | None = None,
|
||||
context_providers: ContextProvider | list[ContextProvider] | AggregateContextProvider | None = None,
|
||||
middleware: Middleware | list[Middleware] | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
@@ -355,43 +543,54 @@ class ChatAgent(BaseAgent):
|
||||
request_kwargs: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Create a ChatAgent.
|
||||
"""Initialize a ChatAgent instance.
|
||||
|
||||
Remarks:
|
||||
The set of attributes from frequency_penalty to additional_properties are used to
|
||||
call the chat client, they can also be passed to both run methods.
|
||||
Note:
|
||||
The set of parameters from frequency_penalty to request_kwargs are used to
|
||||
call the chat client. They can also be passed to both run methods.
|
||||
When both are set, the ones passed to the run methods take precedence.
|
||||
|
||||
Args:
|
||||
chat_client: The chat client to use for the agent.
|
||||
instructions: Optional instructions for the agent.
|
||||
These will be put into the messages sent to the chat client service as a system message.
|
||||
id: The unique identifier for the agent, will be created automatically if not provided.
|
||||
These will be put into the messages sent to the chat client service as a system message.
|
||||
id: The unique identifier for the agent. Will be created automatically if not provided.
|
||||
name: The name of the agent.
|
||||
description: A brief description of the agent's purpose.
|
||||
chat_message_store_factory: factory function to create an instance of ChatMessageStoreProtocol.
|
||||
chat_message_store_factory: Factory function to create an instance of ChatMessageStoreProtocol.
|
||||
If not provided, the default in-memory store will be used.
|
||||
conversation_id: The conversation ID for service-managed threads.
|
||||
Cannot be used together with chat_message_store_factory.
|
||||
context_providers: The collection of multiple context providers to include during agent invocation.
|
||||
middleware: List of middleware to intercept agent and function invocations.
|
||||
frequency_penalty: the frequency penalty to use.
|
||||
logit_bias: the logit bias to use.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: additional metadata to include in the request.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: the presence penalty to use.
|
||||
response_format: the format of the response.
|
||||
seed: the random seed to use.
|
||||
stop: the stop sequence(s) for the request.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
request_kwargs: a dictionary of other values that will be passed through
|
||||
to the chat_client `get_response` and `get_streaming_response` methods.
|
||||
kwargs: any additional keyword arguments. Will be stored as `additional_properties`
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
request_kwargs: A dictionary of other values that will be passed through
|
||||
to the chat_client ``get_response`` and ``get_streaming_response`` methods.
|
||||
kwargs: Any additional keyword arguments. Will be stored as ``additional_properties``.
|
||||
|
||||
Raises:
|
||||
AgentInitializationError: If both conversation_id and chat_message_store_factory are provided.
|
||||
"""
|
||||
if conversation_id is not None and chat_message_store_factory is not None:
|
||||
raise AgentInitializationError(
|
||||
"Cannot specify both conversation_id and chat_message_store_factory. "
|
||||
"Use conversation_id for service-managed threads or chat_message_store_factory for local storage."
|
||||
)
|
||||
|
||||
if not hasattr(chat_client, FUNCTION_INVOKING_CHAT_CLIENT_MARKER) and isinstance(chat_client, BaseChatClient):
|
||||
logger.warning(
|
||||
"The provided chat client does not support function invoking, this might limit agent capabilities."
|
||||
@@ -417,6 +616,7 @@ class ChatAgent(BaseAgent):
|
||||
agent_tools = [tool for tool in normalized_tools if not isinstance(tool, MCPTool)]
|
||||
self.chat_options = ChatOptions(
|
||||
model_id=model,
|
||||
conversation_id=conversation_id,
|
||||
frequency_penalty=frequency_penalty,
|
||||
instructions=instructions,
|
||||
logit_bias=logit_bias,
|
||||
@@ -438,12 +638,16 @@ class ChatAgent(BaseAgent):
|
||||
self._update_agent_name()
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
"""Async context manager entry.
|
||||
"""Enter the async context manager.
|
||||
|
||||
If any of the chat_client, local_mcp_tools, or context_providers are context managers,
|
||||
If any of the chat_client or local_mcp_tools are context managers,
|
||||
they will be entered into the async exit stack to ensure proper cleanup.
|
||||
|
||||
This list might be extended in the future.
|
||||
Note:
|
||||
This list might be extended in the future.
|
||||
|
||||
Returns:
|
||||
The ChatAgent instance.
|
||||
"""
|
||||
for context_manager in chain([self.chat_client], self._local_mcp_tools):
|
||||
if isinstance(context_manager, AbstractAsyncContextManager):
|
||||
@@ -456,17 +660,22 @@ class ChatAgent(BaseAgent):
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: Any,
|
||||
) -> None:
|
||||
"""Async context manager exit.
|
||||
"""Exit the async context manager.
|
||||
|
||||
Close the async exit stack to ensure all context managers are exited properly.
|
||||
|
||||
Args:
|
||||
exc_type: The exception type if an exception was raised, None otherwise.
|
||||
exc_val: The exception value if an exception was raised, None otherwise.
|
||||
exc_tb: The exception traceback if an exception was raised, None otherwise.
|
||||
"""
|
||||
await self._async_exit_stack.aclose()
|
||||
|
||||
def _update_agent_name(self) -> None:
|
||||
"""Update the agent name in a chat client.
|
||||
"""Update the agent name in the chat client.
|
||||
|
||||
Checks if there is a agent name, the implementation
|
||||
should check if there is already a agent name defined, and if not
|
||||
Checks if the chat client supports agent name updates. The implementation
|
||||
should check if there is already an agent name defined, and if not
|
||||
set it to this value.
|
||||
"""
|
||||
if hasattr(self.chat_client, "_update_agent_name") and callable(self.chat_client._update_agent_name): # type: ignore[reportAttributeAccessIssue, attr-defined]
|
||||
@@ -501,33 +710,36 @@ class ChatAgent(BaseAgent):
|
||||
) -> AgentRunResponse:
|
||||
"""Run the agent with the given messages and options.
|
||||
|
||||
Remarks:
|
||||
Since you won't always call the agent.run directly, but it get's called
|
||||
through orchestration, it is advised to set your default values for
|
||||
Note:
|
||||
Since you won't always call ``agent.run()`` directly (it gets called
|
||||
through workflows), it is advised to set your default values for
|
||||
all the chat client parameters in the agent constructor.
|
||||
If both parameters are used, the ones passed to the run methods take precedence.
|
||||
|
||||
Args:
|
||||
messages: The messages to process.
|
||||
thread: The thread to use for the agent.
|
||||
frequency_penalty: the frequency penalty to use.
|
||||
logit_bias: the logit bias to use.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: additional metadata to include in the request.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: the presence penalty to use.
|
||||
response_format: the format of the response.
|
||||
seed: the random seed to use.
|
||||
stop: the stop sequence(s) for the request.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
additional_properties: additional properties to include in the request.
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
additional_properties: Additional properties to include in the request.
|
||||
kwargs: Additional keyword arguments for the agent.
|
||||
will only be passed to functions that are called.
|
||||
Will only be passed to functions that are called.
|
||||
|
||||
Returns:
|
||||
An AgentRunResponse containing the agent's response.
|
||||
"""
|
||||
input_messages = self._normalize_messages(messages)
|
||||
thread, run_chat_options, thread_messages = await self._prepare_thread_and_messages(
|
||||
@@ -627,34 +839,36 @@ class ChatAgent(BaseAgent):
|
||||
) -> AsyncIterable[AgentRunResponseUpdate]:
|
||||
"""Stream the agent with the given messages and options.
|
||||
|
||||
Remarks:
|
||||
Since you won't always call the agent.run_stream directly, but it get's called
|
||||
through orchestration, it is advised to set your default values for
|
||||
Note:
|
||||
Since you won't always call ``agent.run_stream()`` directly (it gets called
|
||||
through orchestration), it is advised to set your default values for
|
||||
all the chat client parameters in the agent constructor.
|
||||
If both parameters are used, the ones passed to the run methods take precedence.
|
||||
|
||||
Args:
|
||||
messages: The messages to process.
|
||||
thread: The thread to use for the agent.
|
||||
frequency_penalty: the frequency penalty to use.
|
||||
logit_bias: the logit bias to use.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: additional metadata to include in the request.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: the presence penalty to use.
|
||||
response_format: the format of the response.
|
||||
seed: the random seed to use.
|
||||
stop: the stop sequence(s) for the request.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
additional_properties: additional properties to include in the request.
|
||||
kwargs: any additional keyword arguments.
|
||||
will only be passed to functions that are called.
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
additional_properties: Additional properties to include in the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
Will only be passed to functions that are called.
|
||||
|
||||
Yields:
|
||||
AgentRunResponseUpdate objects containing chunks of the agent's response.
|
||||
"""
|
||||
input_messages = self._normalize_messages(messages)
|
||||
thread, run_chat_options, thread_messages = await self._prepare_thread_and_messages(
|
||||
@@ -737,24 +951,35 @@ class ChatAgent(BaseAgent):
|
||||
|
||||
If you supply a service_thread_id, the thread will be marked as service managed.
|
||||
|
||||
If you don't supply a service_thread_id but have a conversation_id configured on the agent,
|
||||
that conversation_id will be used to create a service-managed thread.
|
||||
|
||||
If you don't supply a service_thread_id but have a chat_message_store_factory configured on the agent,
|
||||
that factory will be used to create a message store for the thread and the thread will be
|
||||
managed locally.
|
||||
|
||||
When neither is present, the thread will be created without a service ID or message store,
|
||||
this will be updated based on usage, when you run the agent with this thread.
|
||||
If you run with store=True, the response will respond with a thread_id and that will be set.
|
||||
Otherwise a messages store is created from the default factory.
|
||||
When neither is present, the thread will be created without a service ID or message store.
|
||||
This will be updated based on usage when you run the agent with this thread.
|
||||
If you run with ``store=True``, the response will include a thread_id and that will be set.
|
||||
Otherwise a message store is created from the default factory.
|
||||
|
||||
Args:
|
||||
service_thread_id: Optional service managed thread ID.
|
||||
kwargs: not used at present.
|
||||
kwargs: Not used at present.
|
||||
|
||||
Returns:
|
||||
A new AgentThread instance.
|
||||
"""
|
||||
if service_thread_id is not None:
|
||||
return AgentThread(
|
||||
service_thread_id=service_thread_id,
|
||||
context_provider=self.context_provider,
|
||||
)
|
||||
if self.chat_options.conversation_id is not None:
|
||||
return AgentThread(
|
||||
service_thread_id=self.chat_options.conversation_id,
|
||||
context_provider=self.context_provider,
|
||||
)
|
||||
if self.chat_message_store_factory is not None:
|
||||
return AgentThread(
|
||||
message_store=self.chat_message_store_factory(),
|
||||
@@ -800,19 +1025,23 @@ class ChatAgent(BaseAgent):
|
||||
thread: AgentThread | None,
|
||||
input_messages: list[ChatMessage] | None = None,
|
||||
) -> tuple[AgentThread, ChatOptions, list[ChatMessage]]:
|
||||
"""Prepare the messages for agent execution.
|
||||
"""Prepare the thread and messages for agent execution.
|
||||
|
||||
Also updates the chat_options of the agent, with
|
||||
This method prepares the conversation thread, merges context provider data,
|
||||
and assembles the final message list for the chat client.
|
||||
|
||||
Args:
|
||||
thread: The conversation thread.
|
||||
input_messages: Messages to process.
|
||||
|
||||
Returns:
|
||||
The validated thread and normalized messages.
|
||||
A tuple containing:
|
||||
- The validated or created thread
|
||||
- The merged chat options
|
||||
- The complete list of messages for the chat client
|
||||
|
||||
Raises:
|
||||
AgentExecutionException: If the thread is not of the expected type.
|
||||
AgentExecutionException: If the conversation IDs on the thread and agent don't match.
|
||||
"""
|
||||
chat_options = copy(self.chat_options) if self.chat_options else ChatOptions()
|
||||
thread = thread or self.get_new_thread()
|
||||
@@ -852,4 +1081,9 @@ class ChatAgent(BaseAgent):
|
||||
return thread, chat_options, thread_messages
|
||||
|
||||
def _get_agent_name(self) -> str:
|
||||
"""Get the agent name for message attribution.
|
||||
|
||||
Returns:
|
||||
The agent's name, or 'UnnamedAgent' if no name is set.
|
||||
"""
|
||||
return self.name or "UnnamedAgent"
|
||||
|
||||
@@ -49,7 +49,44 @@ __all__ = [
|
||||
|
||||
@runtime_checkable
|
||||
class ChatClientProtocol(Protocol):
|
||||
"""A protocol for a chat client that can generate responses."""
|
||||
"""A protocol for a chat client that can generate responses.
|
||||
|
||||
This protocol defines the interface that all chat clients must implement,
|
||||
including methods for generating both streaming and non-streaming responses.
|
||||
|
||||
Note:
|
||||
Protocols use structural subtyping (duck typing). Classes don't need
|
||||
to explicitly inherit from this protocol to be considered compatible.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatClientProtocol, ChatResponse, ChatMessage
|
||||
|
||||
|
||||
# Any class implementing the required methods is compatible
|
||||
class CustomChatClient:
|
||||
@property
|
||||
def additional_properties(self) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
async def get_response(self, messages, **kwargs):
|
||||
# Your custom implementation
|
||||
return ChatResponse(messages=[], response_id="custom")
|
||||
|
||||
def get_streaming_response(self, messages, **kwargs):
|
||||
async def _stream():
|
||||
from agent_framework import ChatResponseUpdate
|
||||
|
||||
yield ChatResponseUpdate()
|
||||
|
||||
return _stream()
|
||||
|
||||
|
||||
# Verify the instance satisfies the protocol
|
||||
client = CustomChatClient()
|
||||
assert isinstance(client, ChatClientProtocol)
|
||||
"""
|
||||
|
||||
@property
|
||||
def additional_properties(self) -> dict[str, Any]:
|
||||
@@ -75,41 +112,41 @@ class ChatClientProtocol(Protocol):
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Sends input and returns the response.
|
||||
"""Send input and return the response.
|
||||
|
||||
Args:
|
||||
messages: The sequence of input messages to send.
|
||||
response_format: the format of the response.
|
||||
frequency_penalty: the frequency penalty to use.
|
||||
logit_bias: the logit bias to use.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: additional metadata to include in the request.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: the presence penalty to use.
|
||||
seed: the random seed to use.
|
||||
stop: the stop sequence(s) for the request.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
additional_properties: additional properties to include in the request
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
additional_properties: Additional properties to include in the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
Will only be passed to functions that are called.
|
||||
|
||||
Returns:
|
||||
The response messages generated by the client.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input message sequence is `None`.
|
||||
ValueError: If the input message sequence is ``None``.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -132,42 +169,42 @@ class ChatClientProtocol(Protocol):
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Sends input messages and streams the response.
|
||||
"""Send input messages and stream the response.
|
||||
|
||||
Args:
|
||||
messages: The sequence of input messages to send.
|
||||
frequency_penalty: the frequency penalty to use.
|
||||
logit_bias: the logit bias to use.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: additional metadata to include in the request.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: the presence penalty to use.
|
||||
response_format: the format of the response.
|
||||
seed: the random seed to use.
|
||||
stop: the stop sequence(s) for the request.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
additional_properties: additional properties to include in the request
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
additional_properties: Additional properties to include in the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
Will only be passed to functions that are called.
|
||||
|
||||
Yields:
|
||||
An async iterable of chat response updates containing the content of the response messages
|
||||
generated by the client.
|
||||
ChatResponseUpdate: An async iterable of chat response updates containing
|
||||
the content of the response messages generated by the client.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input message sequence is `None`.
|
||||
ValueError: If the input message sequence is ``None``.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -176,7 +213,14 @@ class ChatClientProtocol(Protocol):
|
||||
|
||||
|
||||
def prepare_messages(messages: str | ChatMessage | list[str] | list[ChatMessage]) -> list[ChatMessage]:
|
||||
"""Turn the allowed input into a list of chat messages."""
|
||||
"""Convert various message input formats into a list of ChatMessage objects.
|
||||
|
||||
Args:
|
||||
messages: The input messages in various supported formats.
|
||||
|
||||
Returns:
|
||||
A list of ChatMessage objects.
|
||||
"""
|
||||
if isinstance(messages, str):
|
||||
return [ChatMessage(role="user", text=messages)]
|
||||
if isinstance(messages, ChatMessage):
|
||||
@@ -189,8 +233,150 @@ def prepare_messages(messages: str | ChatMessage | list[str] | list[ChatMessage]
|
||||
return return_messages
|
||||
|
||||
|
||||
def merge_chat_options(
|
||||
*,
|
||||
base_chat_options: ChatOptions | Any | None,
|
||||
model: str | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
logit_bias: dict[str | int, float] | None = None,
|
||||
max_tokens: int | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
presence_penalty: float | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
seed: int | None = None,
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: list[ToolProtocol | dict[str, Any] | Callable[..., Any]] | None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
) -> ChatOptions:
|
||||
"""Merge base chat options with direct parameters to create a new ChatOptions instance.
|
||||
|
||||
When both base_chat_options and individual parameters are provided, the individual
|
||||
parameters take precedence and override the corresponding values in base_chat_options.
|
||||
Tools from both sources are combined into a single list.
|
||||
|
||||
Args:
|
||||
base_chat_options: Optional base ChatOptions to merge with direct parameters.
|
||||
model: The model to use for the agent.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: Additional metadata to include in the request.
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The normalized tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
additional_properties: Additional properties to include in the request.
|
||||
|
||||
Returns:
|
||||
A new ChatOptions instance with merged values.
|
||||
|
||||
Raises:
|
||||
TypeError: If base_chat_options is not None and not an instance of ChatOptions.
|
||||
"""
|
||||
# Validate base_chat_options type if provided
|
||||
if base_chat_options is not None and not isinstance(base_chat_options, ChatOptions):
|
||||
raise TypeError("chat_options must be an instance of ChatOptions")
|
||||
|
||||
if base_chat_options is not None:
|
||||
# Combine tools from both sources
|
||||
base_tools = base_chat_options.tools or []
|
||||
combined_tools = [*base_tools, *(tools or [])] if tools else base_tools
|
||||
|
||||
# Create new chat_options, using direct parameters when provided, otherwise fall back to base
|
||||
return ChatOptions(
|
||||
model_id=model if model is not None else base_chat_options.model_id,
|
||||
frequency_penalty=(
|
||||
frequency_penalty if frequency_penalty is not None else base_chat_options.frequency_penalty
|
||||
),
|
||||
logit_bias=logit_bias if logit_bias is not None else base_chat_options.logit_bias,
|
||||
max_tokens=max_tokens if max_tokens is not None else base_chat_options.max_tokens,
|
||||
metadata=metadata if metadata is not None else base_chat_options.metadata,
|
||||
presence_penalty=(presence_penalty if presence_penalty is not None else base_chat_options.presence_penalty),
|
||||
response_format=(response_format if response_format is not None else base_chat_options.response_format),
|
||||
seed=seed if seed is not None else base_chat_options.seed,
|
||||
stop=stop if stop is not None else base_chat_options.stop,
|
||||
store=store if store is not None else base_chat_options.store,
|
||||
temperature=temperature if temperature is not None else base_chat_options.temperature,
|
||||
top_p=top_p if top_p is not None else base_chat_options.top_p,
|
||||
tool_choice=(
|
||||
tool_choice if (tool_choice is not None and tool_choice != "auto") else base_chat_options.tool_choice # type: ignore[arg-type]
|
||||
),
|
||||
tools=combined_tools or None,
|
||||
user=user if user is not None else base_chat_options.user,
|
||||
additional_properties=(
|
||||
additional_properties if additional_properties is not None else base_chat_options.additional_properties
|
||||
),
|
||||
conversation_id=base_chat_options.conversation_id,
|
||||
)
|
||||
# No base options, create from direct parameters only
|
||||
return ChatOptions(
|
||||
model_id=model,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
max_tokens=max_tokens,
|
||||
metadata=metadata,
|
||||
presence_penalty=presence_penalty,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
stop=stop,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools,
|
||||
user=user,
|
||||
additional_properties=additional_properties or {},
|
||||
)
|
||||
|
||||
|
||||
class BaseChatClient(SerializationMixin, ABC):
|
||||
"""Base class for chat clients."""
|
||||
"""Base class for chat clients.
|
||||
|
||||
This abstract base class provides core functionality for chat client implementations,
|
||||
including middleware support, message preparation, and tool normalization.
|
||||
|
||||
Note:
|
||||
BaseChatClient cannot be instantiated directly as it's an abstract base class.
|
||||
Subclasses must implement ``_inner_get_response()`` and ``_inner_get_streaming_response()``.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import BaseChatClient, ChatResponse, ChatMessage
|
||||
from collections.abc import AsyncIterable
|
||||
|
||||
|
||||
class CustomChatClient(BaseChatClient):
|
||||
async def _inner_get_response(self, *, messages, chat_options, **kwargs):
|
||||
# Your custom implementation
|
||||
return ChatResponse(
|
||||
messages=[ChatMessage(role="assistant", text="Hello!")], response_id="custom-response"
|
||||
)
|
||||
|
||||
async def _inner_get_streaming_response(self, *, messages, chat_options, **kwargs):
|
||||
# Your custom streaming implementation
|
||||
from agent_framework import ChatResponseUpdate
|
||||
|
||||
yield ChatResponseUpdate(role="assistant", contents=[{"type": "text", "text": "Hello!"}])
|
||||
|
||||
|
||||
# Create an instance of your custom client
|
||||
client = CustomChatClient()
|
||||
|
||||
# Use the client to get responses
|
||||
response = await client.get_response("Hello, how are you?")
|
||||
"""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "unknown"
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"}
|
||||
@@ -210,12 +396,12 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize BaseChatClient.
|
||||
"""Initialize a BaseChatClient instance.
|
||||
|
||||
Args:
|
||||
additional_properties: Additional properties for the client.
|
||||
middleware: Middleware for the client.
|
||||
**kwargs: Additional keyword arguments (merged into additional_properties).
|
||||
additional_properties: Additional properties for the client.
|
||||
kwargs: Additional keyword arguments (merged into additional_properties).
|
||||
"""
|
||||
# Merge kwargs into additional_properties
|
||||
self.additional_properties = additional_properties or {}
|
||||
@@ -247,7 +433,17 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
def prepare_messages(
|
||||
self, messages: str | ChatMessage | list[str] | list[ChatMessage], chat_options: ChatOptions
|
||||
) -> MutableSequence[ChatMessage]:
|
||||
"""Turn the allowed input into a list of chat messages."""
|
||||
"""Convert various message input formats into a list of ChatMessage objects.
|
||||
|
||||
Prepends system instructions if present in chat_options.
|
||||
|
||||
Args:
|
||||
messages: The input messages in various supported formats.
|
||||
chat_options: The chat options containing instructions and other settings.
|
||||
|
||||
Returns:
|
||||
A mutable sequence of ChatMessage objects.
|
||||
"""
|
||||
if chat_options.instructions:
|
||||
system_msg = ChatMessage(role="system", text=chat_options.instructions)
|
||||
return [system_msg, *prepare_messages(messages)]
|
||||
@@ -265,19 +461,38 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
return {k: v for k, v in kwargs.items() if not k.startswith("_")}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_tools(
|
||||
async def _normalize_tools(
|
||||
tools: ToolProtocol
|
||||
| MutableMapping[str, Any]
|
||||
| Callable[..., Any]
|
||||
| list[ToolProtocol | MutableMapping[str, Any] | Callable[..., Any]]
|
||||
| Sequence[ToolProtocol | MutableMapping[str, Any] | Callable[..., Any]]
|
||||
| None = None,
|
||||
) -> list[ToolProtocol | dict[str, Any] | Callable[..., Any]]:
|
||||
"""Normalize the tools input to a list of tools."""
|
||||
"""Normalize tools input to a consistent list format.
|
||||
|
||||
Expands MCP tools to their constituent functions, connecting them if needed.
|
||||
|
||||
Args:
|
||||
tools: The tools in various supported formats.
|
||||
|
||||
Returns:
|
||||
A normalized list of tools.
|
||||
"""
|
||||
from typing import cast
|
||||
|
||||
final_tools: list[ToolProtocol | dict[str, Any] | Callable[..., Any]] = []
|
||||
if not tools:
|
||||
return final_tools
|
||||
for tool in tools if isinstance(tools, list) else [tools]: # type: ignore[reportUnknownType]
|
||||
# Use cast when a sequence is passed (likely already a list)
|
||||
tools_list = (
|
||||
cast(list[ToolProtocol | MutableMapping[str, Any] | Callable[..., Any]], tools)
|
||||
if isinstance(tools, Sequence) and not isinstance(tools, (str, bytes))
|
||||
else [tools]
|
||||
)
|
||||
for tool in tools_list: # type: ignore[reportUnknownType]
|
||||
if isinstance(tool, MCPTool):
|
||||
if not tool.is_connected:
|
||||
await tool.connect()
|
||||
final_tools.extend(tool.functions) # type: ignore
|
||||
continue
|
||||
final_tools.append(tool) # type: ignore
|
||||
@@ -352,7 +567,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
@@ -361,54 +576,64 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
) -> ChatResponse:
|
||||
"""Get a response from a chat client.
|
||||
|
||||
When both ``chat_options`` (in kwargs) and individual parameters are provided,
|
||||
the individual parameters take precedence and override the corresponding values
|
||||
in ``chat_options``. Tools from both sources are combined into a single list.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
frequency_penalty: the frequency penalty to use.
|
||||
logit_bias: the logit bias to use.
|
||||
messages: The message or messages to send to the model.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: additional metadata to include in the request.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: the presence penalty to use.
|
||||
response_format: the format of the response.
|
||||
seed: the random seed to use.
|
||||
stop: the stop sequence(s) for the request.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
additional_properties: additional properties to include in the request.
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
additional_properties: Additional properties to include in the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
May include ``chat_options`` which provides base values that can be overridden by direct parameters.
|
||||
|
||||
Returns:
|
||||
A chat response from the model.
|
||||
"""
|
||||
# Should we merge chat options instead of ignoring the input params?
|
||||
if "chat_options" in kwargs:
|
||||
chat_options = kwargs.pop("chat_options")
|
||||
if not isinstance(chat_options, ChatOptions):
|
||||
raise TypeError("chat_options must be an instance of ChatOptions")
|
||||
else:
|
||||
chat_options = ChatOptions(
|
||||
model_id=model,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
max_tokens=max_tokens,
|
||||
metadata=metadata,
|
||||
presence_penalty=presence_penalty,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
stop=stop,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
tool_choice=tool_choice,
|
||||
tools=self._normalize_tools(tools), # type: ignore
|
||||
user=user,
|
||||
additional_properties=additional_properties or {},
|
||||
# Normalize tools and merge with base chat_options
|
||||
normalized_tools = await self._normalize_tools(tools)
|
||||
chat_options = merge_chat_options(
|
||||
base_chat_options=kwargs.pop("chat_options", None),
|
||||
model=model,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
max_tokens=max_tokens,
|
||||
metadata=metadata,
|
||||
presence_penalty=presence_penalty,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
stop=stop,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
tool_choice=tool_choice,
|
||||
tools=normalized_tools,
|
||||
top_p=top_p,
|
||||
user=user,
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
|
||||
# Validate that store is True when conversation_id is set
|
||||
if chat_options.conversation_id is not None and chat_options.store is not True:
|
||||
logger.warning(
|
||||
"When conversation_id is set, store must be True for service-managed threads. "
|
||||
"Automatically setting store=True."
|
||||
)
|
||||
chat_options.store = True
|
||||
|
||||
prepped_messages = self.prepare_messages(messages, chat_options)
|
||||
self._prepare_tool_choice(chat_options=chat_options)
|
||||
|
||||
@@ -434,7 +659,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
@@ -443,53 +668,64 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Get a streaming response from a chat client.
|
||||
|
||||
When both ``chat_options`` (in kwargs) and individual parameters are provided,
|
||||
the individual parameters take precedence and override the corresponding values
|
||||
in ``chat_options``. Tools from both sources are combined into a single list.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
frequency_penalty: the frequency penalty to use
|
||||
logit_bias: the logit bias to use
|
||||
messages: The message or messages to send to the model.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: additional metadata to include in the request.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: the presence penalty to use.
|
||||
response_format: the format of the response.
|
||||
seed: the random seed to use.
|
||||
stop: the stop sequence(s) for the request.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
additional_properties: additional properties to include in the request
|
||||
kwargs: any additional keyword arguments
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
additional_properties: Additional properties to include in the request.
|
||||
kwargs: Any additional keyword arguments.
|
||||
May include ``chat_options`` which provides base values that can be overridden by direct parameters.
|
||||
|
||||
Yields:
|
||||
A stream representing the response(s) from the LLM.
|
||||
ChatResponseUpdate: A stream representing the response(s) from the LLM.
|
||||
"""
|
||||
# Should we merge chat options instead of ignoring the input params?
|
||||
if "chat_options" in kwargs:
|
||||
chat_options = kwargs.pop("chat_options")
|
||||
if not isinstance(chat_options, ChatOptions):
|
||||
raise TypeError("chat_options must be an instance of ChatOptions")
|
||||
else:
|
||||
chat_options = ChatOptions(
|
||||
model_id=model,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
max_tokens=max_tokens,
|
||||
metadata=metadata,
|
||||
presence_penalty=presence_penalty,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
stop=stop,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
tool_choice=tool_choice,
|
||||
tools=self._normalize_tools(tools),
|
||||
user=user,
|
||||
additional_properties=additional_properties or {},
|
||||
# Normalize tools and merge with base chat_options
|
||||
normalized_tools = await self._normalize_tools(tools)
|
||||
chat_options = merge_chat_options(
|
||||
base_chat_options=kwargs.pop("chat_options", None),
|
||||
model=model,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
max_tokens=max_tokens,
|
||||
metadata=metadata,
|
||||
presence_penalty=presence_penalty,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
stop=stop,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
tool_choice=tool_choice,
|
||||
tools=normalized_tools,
|
||||
top_p=top_p,
|
||||
user=user,
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
|
||||
# Validate that store is True when conversation_id is set
|
||||
if chat_options.conversation_id is not None and chat_options.store is not True:
|
||||
logger.warning(
|
||||
"When conversation_id is set, store must be True for service-managed threads. "
|
||||
"Automatically setting store=True."
|
||||
)
|
||||
chat_options.store = True
|
||||
|
||||
prepped_messages = self.prepare_messages(messages, chat_options)
|
||||
self._prepare_tool_choice(chat_options=chat_options)
|
||||
|
||||
@@ -502,8 +738,11 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
def _prepare_tool_choice(self, chat_options: ChatOptions) -> None:
|
||||
"""Prepare the tools and tool choice for the chat options.
|
||||
|
||||
This function should be overridden by subclasses to customize tool handling.
|
||||
Because it currently parses only AIFunctions.
|
||||
This function should be overridden by subclasses to customize tool handling,
|
||||
as it currently parses only AIFunctions.
|
||||
|
||||
Args:
|
||||
chat_options: The chat options to prepare.
|
||||
"""
|
||||
chat_tool_mode = chat_options.tool_choice
|
||||
if chat_tool_mode is None or chat_tool_mode == ToolMode.NONE or chat_tool_mode == "none":
|
||||
@@ -520,49 +759,123 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
|
||||
Override this in the subclass to return the proper URL.
|
||||
If the service does not have a URL, return None.
|
||||
|
||||
Returns:
|
||||
The service URL or 'Unknown' if not implemented.
|
||||
"""
|
||||
return "Unknown"
|
||||
|
||||
def create_agent(
|
||||
self,
|
||||
*,
|
||||
id: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
instructions: str | None = None,
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
context_providers: ContextProvider | list[ContextProvider] | AggregateContextProvider | None = None,
|
||||
middleware: Middleware | list[Middleware] | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
logit_bias: dict[str | int, float] | None = None,
|
||||
max_tokens: int | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
model: str | None = None,
|
||||
presence_penalty: float | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
seed: int | None = None,
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
request_kwargs: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "ChatAgent":
|
||||
"""Create an agent with the given name and instructions.
|
||||
"""Create a ChatAgent with this client.
|
||||
|
||||
This is a convenience method that creates a ChatAgent instance with this
|
||||
chat client already configured.
|
||||
|
||||
Args:
|
||||
id: The unique identifier for the agent. Will be created automatically if not provided.
|
||||
name: The name of the agent.
|
||||
instructions: The instructions for the agent.
|
||||
tools: Optional list of tools to associate with the agent.
|
||||
description: A brief description of the agent's purpose.
|
||||
instructions: Optional instructions for the agent.
|
||||
These will be put into the messages sent to the chat client service as a system message.
|
||||
chat_message_store_factory: Factory function to create an instance of ChatMessageStoreProtocol.
|
||||
If not provided, the default in-memory store will be used.
|
||||
context_providers: Context providers to include during agent invocation.
|
||||
middleware: List of middleware to intercept agent and function invocations.
|
||||
**kwargs: Additional keyword arguments to pass to the agent.
|
||||
See ChatAgent for all the available options.
|
||||
frequency_penalty: The frequency penalty to use.
|
||||
logit_bias: The logit bias to use.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
metadata: Additional metadata to include in the request.
|
||||
model: The model to use for the agent.
|
||||
presence_penalty: The presence penalty to use.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use.
|
||||
stop: The stop sequence(s) for the request.
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
request_kwargs: A dictionary of other values that will be passed through
|
||||
to the chat_client ``get_response`` and ``get_streaming_response`` methods.
|
||||
kwargs: Any additional keyword arguments. Will be stored as ``additional_properties``.
|
||||
|
||||
Returns:
|
||||
An instance of ChatAgent.
|
||||
A ChatAgent instance configured with this chat client.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.clients import OpenAIChatClient
|
||||
|
||||
# Create a client
|
||||
client = OpenAIChatClient(model="gpt-4")
|
||||
|
||||
# Create an agent using the convenience method
|
||||
agent = client.create_agent(
|
||||
name="assistant", instructions="You are a helpful assistant.", temperature=0.7
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
response = await agent.run("Hello!")
|
||||
"""
|
||||
from ._agents import ChatAgent
|
||||
|
||||
return ChatAgent(
|
||||
chat_client=self,
|
||||
id=id,
|
||||
name=name,
|
||||
description=description,
|
||||
instructions=instructions,
|
||||
tools=tools,
|
||||
chat_message_store_factory=chat_message_store_factory,
|
||||
context_providers=context_providers,
|
||||
middleware=middleware,
|
||||
frequency_penalty=frequency_penalty,
|
||||
logit_bias=logit_bias,
|
||||
max_tokens=max_tokens,
|
||||
metadata=metadata,
|
||||
model=model,
|
||||
presence_penalty=presence_penalty,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
stop=stop,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools,
|
||||
top_p=top_p,
|
||||
user=user,
|
||||
request_kwargs=request_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -243,7 +243,22 @@ def _normalize_mcp_name(name: str) -> str:
|
||||
|
||||
|
||||
class MCPTool:
|
||||
"""Main MCP class, to initialize use one of the subclasses."""
|
||||
"""Main MCP class for connecting to Model Context Protocol servers.
|
||||
|
||||
This is the base class for MCP tool implementations. It handles connection management,
|
||||
tool and prompt loading, and communication with MCP servers.
|
||||
|
||||
Note:
|
||||
MCPTool cannot be instantiated directly. Use one of the subclasses:
|
||||
MCPStdioTool, MCPStreamableHTTPTool, or MCPWebsocketTool.
|
||||
|
||||
Examples:
|
||||
See the subclass documentation for usage examples:
|
||||
|
||||
- :class:`MCPStdioTool` for stdio-based MCP servers
|
||||
- :class:`MCPStreamableHTTPTool` for HTTP-based MCP servers
|
||||
- :class:`MCPWebsocketTool` for WebSocket-based MCP servers
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -256,7 +271,18 @@ class MCPTool:
|
||||
request_timeout: int | None = None,
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
) -> None:
|
||||
"""Initialize the MCP Plugin Base."""
|
||||
"""Initialize the MCP Tool base.
|
||||
|
||||
Args:
|
||||
name: The name of the MCP tool.
|
||||
description: The description of the tool.
|
||||
additional_properties: Additional properties for the tool.
|
||||
load_tools: Whether to automatically load tools from the MCP server.
|
||||
load_prompts: Whether to automatically load prompts from the MCP server.
|
||||
session: Pre-existing session to use for the MCP connection.
|
||||
request_timeout: The default timeout in seconds for all requests.
|
||||
chat_client: The chat client to use for sampling callbacks.
|
||||
"""
|
||||
self.name = name
|
||||
self.description = description or ""
|
||||
self.additional_properties = additional_properties
|
||||
@@ -273,7 +299,14 @@ class MCPTool:
|
||||
return f"MCPTool(name={self.name}, description={self.description})"
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Connect to the MCP server."""
|
||||
"""Connect to the MCP server.
|
||||
|
||||
Establishes a connection to the MCP server, initializes the session,
|
||||
and loads tools and prompts if configured to do so.
|
||||
|
||||
Raises:
|
||||
ToolException: If connection or session initialization fails.
|
||||
"""
|
||||
if not self.session:
|
||||
try:
|
||||
transport = await self._exit_stack.enter_async_context(self.get_mcp_client())
|
||||
@@ -324,9 +357,19 @@ class MCPTool:
|
||||
"""Callback function for sampling.
|
||||
|
||||
This function is called when the MCP server needs to get a message completed.
|
||||
It uses the configured chat client to generate responses.
|
||||
|
||||
This is a simple version of this function, it can be overridden to allow more complex sampling.
|
||||
It get's added to the session at initialization time, so overriding it is the best way to do this.
|
||||
Note:
|
||||
This is a simple version of this function. It can be overridden to allow
|
||||
more complex sampling. It gets added to the session at initialization time,
|
||||
so overriding it is the best way to customize this behavior.
|
||||
|
||||
Args:
|
||||
context: The request context from the MCP server.
|
||||
params: The message creation request parameters.
|
||||
|
||||
Returns:
|
||||
Either a CreateMessageResult with the generated message or ErrorData if generation fails.
|
||||
"""
|
||||
if not self.chat_client:
|
||||
return types.ErrorData(
|
||||
@@ -377,7 +420,11 @@ class MCPTool:
|
||||
This function is called when the MCP Server sends a log message.
|
||||
By default it will log the message to the logger with the level set in the params.
|
||||
|
||||
Please subclass the MCP*Plugin and override this function if you want to adapt the behavior.
|
||||
Note:
|
||||
Subclass MCPTool and override this function if you want to adapt the behavior.
|
||||
|
||||
Args:
|
||||
params: The logging message notification parameters from the MCP server.
|
||||
"""
|
||||
logger.log(LOG_LEVEL_MAPPING[params.level], params.data)
|
||||
|
||||
@@ -387,12 +434,17 @@ class MCPTool:
|
||||
) -> None:
|
||||
"""Handle messages from the MCP server.
|
||||
|
||||
By default this function will handle exceptions on the server, by logging those.
|
||||
By default this function will handle exceptions on the server by logging them,
|
||||
and it will trigger a reload of the tools and prompts when the list changed
|
||||
notification is received.
|
||||
|
||||
And it will trigger a reload of the tools and prompts when the list changed notification is received.
|
||||
Note:
|
||||
If you want to extend this behavior, you can subclass MCPTool and override
|
||||
this function. If you want to keep the default behavior, make sure to call
|
||||
``super().message_handler(message)``.
|
||||
|
||||
If you want to extend this behavior you can subclass the MCPPlugin and override this function,
|
||||
if you want to keep the default behavior, make sure to call `super().message_handler(message)`.
|
||||
Args:
|
||||
message: The message from the MCP server (request responder, notification, or exception).
|
||||
"""
|
||||
if isinstance(message, Exception):
|
||||
logger.error("Error from MCP server: %s", message, exc_info=message)
|
||||
@@ -407,7 +459,14 @@ class MCPTool:
|
||||
logger.debug("Unhandled notification: %s", message.root.method)
|
||||
|
||||
async def load_prompts(self) -> None:
|
||||
"""Load prompts from the MCP server."""
|
||||
"""Load prompts from the MCP server.
|
||||
|
||||
Retrieves available prompts from the connected MCP server and converts
|
||||
them into AIFunction instances.
|
||||
|
||||
Raises:
|
||||
ToolExecutionException: If the MCP server is not connected.
|
||||
"""
|
||||
if not self.session:
|
||||
raise ToolExecutionException("MCP server not connected, please call connect() before using this method.")
|
||||
try:
|
||||
@@ -430,7 +489,14 @@ class MCPTool:
|
||||
self.functions.append(func)
|
||||
|
||||
async def load_tools(self) -> None:
|
||||
"""Load tools from the MCP server."""
|
||||
"""Load tools from the MCP server.
|
||||
|
||||
Retrieves available tools from the connected MCP server and converts
|
||||
them into AIFunction instances.
|
||||
|
||||
Raises:
|
||||
ToolExecutionException: If the MCP server is not connected.
|
||||
"""
|
||||
if not self.session:
|
||||
raise ToolExecutionException("MCP server not connected, please call connect() before using this method.")
|
||||
try:
|
||||
@@ -454,18 +520,37 @@ class MCPTool:
|
||||
self.functions.append(func)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Disconnect from the MCP server."""
|
||||
"""Disconnect from the MCP server.
|
||||
|
||||
Closes the connection and cleans up resources.
|
||||
"""
|
||||
await self._exit_stack.aclose()
|
||||
self.session = None
|
||||
self.is_connected = False
|
||||
|
||||
@abstractmethod
|
||||
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
||||
"""Get an MCP client."""
|
||||
"""Get an MCP client.
|
||||
|
||||
Returns:
|
||||
An async context manager for the MCP client transport.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def call_tool(self, tool_name: str, **kwargs: Any) -> list[Contents]:
|
||||
"""Call a tool with the given arguments."""
|
||||
"""Call a tool with the given arguments.
|
||||
|
||||
Args:
|
||||
tool_name: The name of the tool to call.
|
||||
kwargs: Arguments to pass to the tool.
|
||||
|
||||
Returns:
|
||||
A list of content items returned by the tool.
|
||||
|
||||
Raises:
|
||||
ToolExecutionException: If the MCP server is not connected, tools are not loaded,
|
||||
or the tool call fails.
|
||||
"""
|
||||
if not self.session:
|
||||
raise ToolExecutionException("MCP server not connected, please call connect() before using this method.")
|
||||
if not self.load_tools_flag:
|
||||
@@ -480,7 +565,19 @@ class MCPTool:
|
||||
raise ToolExecutionException(f"Failed to call tool '{tool_name}'.", inner_exception=ex) from ex
|
||||
|
||||
async def get_prompt(self, prompt_name: str, **kwargs: Any) -> list[ChatMessage]:
|
||||
"""Call a prompt with the given arguments."""
|
||||
"""Call a prompt with the given arguments.
|
||||
|
||||
Args:
|
||||
prompt_name: The name of the prompt to retrieve.
|
||||
kwargs: Arguments to pass to the prompt.
|
||||
|
||||
Returns:
|
||||
A list of chat messages returned by the prompt.
|
||||
|
||||
Raises:
|
||||
ToolExecutionException: If the MCP server is not connected, prompts are not loaded,
|
||||
or the prompt call fails.
|
||||
"""
|
||||
if not self.session:
|
||||
raise ToolExecutionException("MCP server not connected, please call connect() before using this method.")
|
||||
if not self.load_prompts_flag:
|
||||
@@ -496,7 +593,17 @@ class MCPTool:
|
||||
raise ToolExecutionException(f"Failed to call prompt '{prompt_name}'.", inner_exception=ex) from ex
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Enter the context manager."""
|
||||
"""Enter the async context manager.
|
||||
|
||||
Connects to the MCP server automatically.
|
||||
|
||||
Returns:
|
||||
The MCPTool instance.
|
||||
|
||||
Raises:
|
||||
ToolException: If connection fails.
|
||||
ToolExecutionException: If context manager setup fails.
|
||||
"""
|
||||
try:
|
||||
await self.connect()
|
||||
return self
|
||||
@@ -509,7 +616,15 @@ class MCPTool:
|
||||
async def __aexit__(
|
||||
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any
|
||||
) -> None:
|
||||
"""Exit the context manager."""
|
||||
"""Exit the async context manager.
|
||||
|
||||
Closes the connection and cleans up resources.
|
||||
|
||||
Args:
|
||||
exc_type: The exception type if an exception was raised, None otherwise.
|
||||
exc_value: The exception value if an exception was raised, None otherwise.
|
||||
traceback: The exception traceback if an exception was raised, None otherwise.
|
||||
"""
|
||||
await self.close()
|
||||
|
||||
|
||||
@@ -517,7 +632,29 @@ class MCPTool:
|
||||
|
||||
|
||||
class MCPStdioTool(MCPTool):
|
||||
"""MCP stdio server configuration."""
|
||||
"""MCP tool for connecting to stdio-based MCP servers.
|
||||
|
||||
This class connects to MCP servers that communicate via standard input/output,
|
||||
typically used for local processes.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import MCPStdioTool, ChatAgent
|
||||
|
||||
# Create an MCP stdio tool
|
||||
mcp_tool = MCPStdioTool(
|
||||
name="filesystem",
|
||||
command="npx",
|
||||
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
||||
description="File system operations",
|
||||
)
|
||||
|
||||
# Use with a chat agent
|
||||
async with mcp_tool:
|
||||
agent = ChatAgent(chat_client=client, name="assistant", tools=mcp_tool)
|
||||
response = await agent.run("List files in the directory")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -536,28 +673,27 @@ class MCPStdioTool(MCPTool):
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the MCP stdio plugin.
|
||||
"""Initialize the MCP stdio tool.
|
||||
|
||||
The arguments are used to create a StdioServerParameters object.
|
||||
Which is then used to create a stdio client.
|
||||
see mcp.client.stdio.stdio_client and mcp.client.stdio.stdio_server_parameters
|
||||
for more details.
|
||||
Note:
|
||||
The arguments are used to create a StdioServerParameters object,
|
||||
which is then used to create a stdio client. See ``mcp.client.stdio.stdio_client``
|
||||
and ``mcp.client.stdio.stdio_server_parameters`` for more details.
|
||||
|
||||
Args:
|
||||
name: The name of the plugin.
|
||||
name: The name of the tool.
|
||||
command: The command to run the MCP server.
|
||||
load_tools: Whether to load tools from the MCP server.
|
||||
load_prompts: Whether to load prompts from the MCP server.
|
||||
request_timeout: The default timeout used for all requests.
|
||||
request_timeout: The default timeout in seconds for all requests.
|
||||
session: The session to use for the MCP connection.
|
||||
description: The description of the plugin.
|
||||
description: The description of the tool.
|
||||
additional_properties: Additional properties.
|
||||
args: The arguments to pass to the command.
|
||||
env: The environment variables to set for the command.
|
||||
encoding: The encoding to use for the command output.
|
||||
chat_client: The chat client to use for sampling.
|
||||
kwargs: Any extra arguments to pass to the stdio client.
|
||||
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
@@ -576,7 +712,11 @@ class MCPStdioTool(MCPTool):
|
||||
self._client_kwargs = kwargs
|
||||
|
||||
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
||||
"""Get an MCP stdio client."""
|
||||
"""Get an MCP stdio client.
|
||||
|
||||
Returns:
|
||||
An async context manager for the stdio client transport.
|
||||
"""
|
||||
args: dict[str, Any] = {
|
||||
"command": self.command,
|
||||
"args": self.args,
|
||||
@@ -590,7 +730,28 @@ class MCPStdioTool(MCPTool):
|
||||
|
||||
|
||||
class MCPStreamableHTTPTool(MCPTool):
|
||||
"""MCP streamable http server configuration."""
|
||||
"""MCP tool for connecting to HTTP-based MCP servers.
|
||||
|
||||
This class connects to MCP servers that communicate via streamable HTTP/SSE.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import MCPStreamableHTTPTool, ChatAgent
|
||||
|
||||
# Create an MCP HTTP tool
|
||||
mcp_tool = MCPStreamableHTTPTool(
|
||||
name="web-api",
|
||||
url="https://api.example.com/mcp",
|
||||
headers={"Authorization": "Bearer token"},
|
||||
description="Web API operations",
|
||||
)
|
||||
|
||||
# Use with a chat agent
|
||||
async with mcp_tool:
|
||||
agent = ChatAgent(chat_client=client, name="assistant", tools=mcp_tool)
|
||||
response = await agent.run("Fetch data from the API")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -610,29 +771,29 @@ class MCPStreamableHTTPTool(MCPTool):
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the MCP streamable http plugin.
|
||||
"""Initialize the MCP streamable HTTP tool.
|
||||
|
||||
The arguments are used to create a streamable http client.
|
||||
see mcp.client.streamable_http.streamablehttp_client for more details.
|
||||
|
||||
Any extra arguments passed to the constructor will be passed to the
|
||||
streamable http client constructor.
|
||||
Note:
|
||||
The arguments are used to create a streamable HTTP client.
|
||||
See ``mcp.client.streamable_http.streamablehttp_client`` for more details.
|
||||
Any extra arguments passed to the constructor will be passed to the
|
||||
streamable HTTP client constructor.
|
||||
|
||||
Args:
|
||||
name: The name of the plugin.
|
||||
name: The name of the tool.
|
||||
url: The URL of the MCP server.
|
||||
load_tools: Whether to load tools from the MCP server.
|
||||
load_prompts: Whether to load prompts from the MCP server.
|
||||
request_timeout: The default timeout used for all requests.
|
||||
request_timeout: The default timeout in seconds for all requests.
|
||||
session: The session to use for the MCP connection.
|
||||
description: The description of the plugin.
|
||||
description: The description of the tool.
|
||||
additional_properties: Additional properties.
|
||||
headers: The headers to send with the request.
|
||||
timeout: The timeout for the request.
|
||||
sse_read_timeout: The timeout for reading from the SSE stream.
|
||||
terminate_on_close: Close the transport when the MCP client is terminated.
|
||||
chat_client: The chat client to use for sampling.
|
||||
kwargs: Any extra arguments to pass to the sse client.
|
||||
kwargs: Any extra arguments to pass to the SSE client.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
@@ -652,7 +813,11 @@ class MCPStreamableHTTPTool(MCPTool):
|
||||
self._client_kwargs = kwargs
|
||||
|
||||
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
||||
"""Get an MCP streamable http client."""
|
||||
"""Get an MCP streamable HTTP client.
|
||||
|
||||
Returns:
|
||||
An async context manager for the streamable HTTP client transport.
|
||||
"""
|
||||
args: dict[str, Any] = {
|
||||
"url": self.url,
|
||||
}
|
||||
@@ -670,7 +835,25 @@ class MCPStreamableHTTPTool(MCPTool):
|
||||
|
||||
|
||||
class MCPWebsocketTool(MCPTool):
|
||||
"""MCP websocket server configuration."""
|
||||
"""MCP tool for connecting to WebSocket-based MCP servers.
|
||||
|
||||
This class connects to MCP servers that communicate via WebSocket.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import MCPWebsocketTool, ChatAgent
|
||||
|
||||
# Create an MCP WebSocket tool
|
||||
mcp_tool = MCPWebsocketTool(
|
||||
name="realtime-service", url="wss://service.example.com/mcp", description="Real-time service operations"
|
||||
)
|
||||
|
||||
# Use with a chat agent
|
||||
async with mcp_tool:
|
||||
agent = ChatAgent(chat_client=client, name="assistant", tools=mcp_tool)
|
||||
response = await agent.run("Connect to the real-time service")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -686,26 +869,25 @@ class MCPWebsocketTool(MCPTool):
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the MCP websocket plugin.
|
||||
"""Initialize the MCP WebSocket tool.
|
||||
|
||||
The arguments are used to create a websocket client.
|
||||
see mcp.client.websocket.websocket_client for more details.
|
||||
|
||||
Any extra arguments passed to the constructor will be passed to the
|
||||
websocket client constructor.
|
||||
Note:
|
||||
The arguments are used to create a WebSocket client.
|
||||
See ``mcp.client.websocket.websocket_client`` for more details.
|
||||
Any extra arguments passed to the constructor will be passed to the
|
||||
WebSocket client constructor.
|
||||
|
||||
Args:
|
||||
name: The name of the plugin.
|
||||
name: The name of the tool.
|
||||
url: The URL of the MCP server.
|
||||
load_tools: Whether to load tools from the MCP server.
|
||||
load_prompts: Whether to load prompts from the MCP server.
|
||||
request_timeout: The default timeout used for all requests.
|
||||
request_timeout: The default timeout in seconds for all requests.
|
||||
session: The session to use for the MCP connection.
|
||||
description: The description of the plugin.
|
||||
description: The description of the tool.
|
||||
additional_properties: Additional properties.
|
||||
chat_client: The chat client to use for sampling.
|
||||
kwargs: Any extra arguments to pass to the websocket client.
|
||||
|
||||
kwargs: Any extra arguments to pass to the WebSocket client.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
@@ -721,7 +903,11 @@ class MCPWebsocketTool(MCPTool):
|
||||
self._client_kwargs = kwargs
|
||||
|
||||
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
||||
"""Get an MCP websocket client."""
|
||||
"""Get an MCP WebSocket client.
|
||||
|
||||
Returns:
|
||||
An async context manager for the WebSocket client transport.
|
||||
"""
|
||||
args: dict[str, Any] = {
|
||||
"url": self.url,
|
||||
}
|
||||
|
||||
@@ -26,12 +26,28 @@ __all__ = ["AggregateContextProvider", "Context", "ContextProvider"]
|
||||
|
||||
|
||||
class Context:
|
||||
"""A class containing any context that should be provided to the AI model as supplied by an ContextProvider.
|
||||
"""A class containing any context that should be provided to the AI model as supplied by a ContextProvider.
|
||||
|
||||
Each ContextProvider has the ability to provide its own context for each invocation.
|
||||
The Context class contains the additional context supplied by the ContextProvider.
|
||||
This context will be combined with context supplied by other providers before being passed to the AI model.
|
||||
This context is per invocation, and will not be stored as part of the chat history.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import Context, ChatMessage
|
||||
|
||||
# Create context with instructions
|
||||
context = Context(
|
||||
instructions="Use a professional tone when responding.",
|
||||
messages=[ChatMessage(content="Previous context", role="user")],
|
||||
tools=[my_tool],
|
||||
)
|
||||
|
||||
# Access context properties
|
||||
print(context.instructions)
|
||||
print(len(context.messages))
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -43,9 +59,9 @@ class Context:
|
||||
"""Create a new Context object.
|
||||
|
||||
Args:
|
||||
instructions: Instructions to provide to the AI model.
|
||||
messages: a list of messages.
|
||||
tools: a list of tools to provide to this run.
|
||||
instructions: The instructions to provide to the AI model.
|
||||
messages: The list of messages to include in the context.
|
||||
tools: The list of tools to provide to this run.
|
||||
"""
|
||||
self.instructions = instructions
|
||||
self.messages: Sequence[ChatMessage] = messages or []
|
||||
@@ -62,7 +78,27 @@ class ContextProvider(ABC):
|
||||
It can listen to changes in the conversation and provide additional context to the AI model
|
||||
just before invocation.
|
||||
|
||||
It also has a default memory prompt that can be used by all providers.
|
||||
Note:
|
||||
ContextProvider is an abstract base class. You must subclass it and implement
|
||||
the ``invoking()`` method to create a custom context provider. Ideally, you should
|
||||
also implement the ``invoked()`` and ``thread_created()`` methods to track conversation
|
||||
state, but these are optional.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ContextProvider, Context, ChatMessage
|
||||
|
||||
|
||||
class CustomContextProvider(ContextProvider):
|
||||
async def invoking(self, messages, **kwargs):
|
||||
# Add custom instructions before each invocation
|
||||
return Context(instructions="Always be concise and helpful.", messages=[], tools=[])
|
||||
|
||||
|
||||
# Use with a chat agent
|
||||
async with CustomContextProvider() as provider:
|
||||
agent = ChatAgent(chat_client=client, name="assistant", context_providers=provider)
|
||||
"""
|
||||
|
||||
# Default prompt to be used by all context providers when assembling memories/instructions
|
||||
@@ -71,9 +107,9 @@ class ContextProvider(ABC):
|
||||
async def thread_created(self, thread_id: str | None) -> None:
|
||||
"""Called just after a new thread is created.
|
||||
|
||||
Implementers can use this method to do any operations required at the creation of a new thread.
|
||||
For example, checking long term storage for any data that is relevant
|
||||
to the current session based on the input text.
|
||||
Implementers can use this method to perform any operations required at the creation
|
||||
of a new thread. For example, checking long-term storage for any data that is relevant
|
||||
to the current session.
|
||||
|
||||
Args:
|
||||
thread_id: The ID of the new thread.
|
||||
@@ -89,36 +125,39 @@ class ContextProvider(ABC):
|
||||
) -> None:
|
||||
"""Called after the agent has received a response from the underlying inference service.
|
||||
|
||||
You can inspect the request and response messages, and update the state of the context provider
|
||||
You can inspect the request and response messages, and update the state of the context provider.
|
||||
|
||||
Args:
|
||||
request_messages: messages that were sent to the model/agent
|
||||
response_messages: messages that were returned by the model/agent
|
||||
invoke_exception: exception that was thrown, if any.
|
||||
kwargs: not used at present.
|
||||
request_messages: The messages that were sent to the model/agent.
|
||||
response_messages: The messages that were returned by the model/agent.
|
||||
invoke_exception: The exception that was thrown, if any.
|
||||
kwargs: Additional keyword arguments (not used at present).
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
|
||||
"""Called just before the Model/Agent/etc. is invoked.
|
||||
"""Called just before the model/agent is invoked.
|
||||
|
||||
Implementers can load any additional context required at this time,
|
||||
and they should return any context that should be passed to the agent.
|
||||
|
||||
Args:
|
||||
messages: The most recent messages that the agent is being invoked with.
|
||||
kwargs: not used at present.
|
||||
kwargs: Additional keyword arguments (not used at present).
|
||||
|
||||
Returns:
|
||||
A Context object containing instructions, messages, and tools to include.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
"""Async context manager entry.
|
||||
"""Enter the async context manager.
|
||||
|
||||
Override this method to perform any setup operations when the context provider is entered.
|
||||
|
||||
Returns:
|
||||
Self for chaining.
|
||||
The ContextProvider instance for chaining.
|
||||
"""
|
||||
return self
|
||||
|
||||
@@ -128,14 +167,14 @@ class ContextProvider(ABC):
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
"""Async context manager exit.
|
||||
"""Exit the async context manager.
|
||||
|
||||
Override this method to perform any cleanup operations when the context provider is exited.
|
||||
|
||||
Args:
|
||||
exc_type: Exception type if an exception occurred, None otherwise.
|
||||
exc_val: Exception value if an exception occurred, None otherwise.
|
||||
exc_tb: Exception traceback if an exception occurred, None otherwise.
|
||||
exc_type: The exception type if an exception occurred, None otherwise.
|
||||
exc_val: The exception value if an exception occurred, None otherwise.
|
||||
exc_tb: The exception traceback if an exception occurred, None otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -146,14 +185,40 @@ class ContextProvider(ABC):
|
||||
class AggregateContextProvider(ContextProvider):
|
||||
"""A ContextProvider that contains multiple context providers.
|
||||
|
||||
It delegates events to multiple context providers and aggregates responses from those events before returning.
|
||||
It delegates events to multiple context providers and aggregates responses from those
|
||||
events before returning. This allows you to combine multiple context providers into a
|
||||
single provider.
|
||||
|
||||
Note:
|
||||
An AggregateContextProvider is created automatically when you pass a single context
|
||||
provider or a sequence of context providers to the agent constructor.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import AggregateContextProvider, ChatAgent
|
||||
|
||||
# Create multiple context providers
|
||||
provider1 = CustomContextProvider1()
|
||||
provider2 = CustomContextProvider2()
|
||||
provider3 = CustomContextProvider3()
|
||||
|
||||
# Pass them to the agent - AggregateContextProvider is created automatically
|
||||
agent = ChatAgent(chat_client=client, name="assistant", context_providers=[provider1, provider2, provider3])
|
||||
|
||||
# Verify that an AggregateContextProvider was created
|
||||
assert isinstance(agent.context_providers, AggregateContextProvider)
|
||||
|
||||
# Add additional providers to the agent
|
||||
provider4 = CustomContextProvider4()
|
||||
agent.context_providers.add(provider4)
|
||||
"""
|
||||
|
||||
def __init__(self, context_providers: ContextProvider | Sequence[ContextProvider] | None = None) -> None:
|
||||
"""Initialize the AggregateContextProvider with context providers.
|
||||
|
||||
Args:
|
||||
context_providers: Context providers to add.
|
||||
context_providers: The context provider(s) to add.
|
||||
"""
|
||||
if isinstance(context_providers, ContextProvider):
|
||||
self.providers = [context_providers]
|
||||
@@ -162,10 +227,10 @@ class AggregateContextProvider(ContextProvider):
|
||||
self._exit_stack: AsyncExitStack | None = None
|
||||
|
||||
def add(self, context_provider: ContextProvider) -> None:
|
||||
"""Adds new context provider.
|
||||
"""Add a new context provider.
|
||||
|
||||
Args:
|
||||
context_provider: Context provider to add.
|
||||
context_provider: The context provider to add.
|
||||
"""
|
||||
self.providers.append(context_provider)
|
||||
|
||||
@@ -208,10 +273,10 @@ class AggregateContextProvider(ContextProvider):
|
||||
|
||||
@override
|
||||
async def __aenter__(self) -> "Self":
|
||||
"""Enter async context manager and set up all providers.
|
||||
"""Enter the async context manager and set up all providers.
|
||||
|
||||
Returns:
|
||||
Self for chaining.
|
||||
The AggregateContextProvider instance for chaining.
|
||||
"""
|
||||
self._exit_stack = AsyncExitStack()
|
||||
await self._exit_stack.__aenter__()
|
||||
@@ -229,12 +294,12 @@ class AggregateContextProvider(ContextProvider):
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
"""Exit async context manager and clean up all providers.
|
||||
"""Exit the async context manager and clean up all providers.
|
||||
|
||||
Args:
|
||||
exc_type: Exception type if an exception occurred, None otherwise.
|
||||
exc_val: Exception value if an exception occurred, None otherwise.
|
||||
exc_tb: Exception traceback if an exception occurred, None otherwise.
|
||||
exc_type: The exception type if an exception occurred, None otherwise.
|
||||
exc_val: The exception value if an exception occurred, None otherwise.
|
||||
exc_tb: The exception traceback if an exception occurred, None otherwise.
|
||||
"""
|
||||
if self._exit_stack is not None:
|
||||
await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
|
||||
import inspect
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable, MutableSequence
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeAlias, TypeVar
|
||||
|
||||
from ._serialization import SerializationMixin
|
||||
from ._types import AgentRunResponse, AgentRunResponseUpdate, ChatMessage
|
||||
from .exceptions import MiddlewareException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterable, MutableSequence
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ._agents import AgentProtocol
|
||||
@@ -20,18 +18,6 @@ if TYPE_CHECKING:
|
||||
from ._tools import AIFunction
|
||||
from ._types import ChatOptions, ChatResponse, ChatResponseUpdate
|
||||
|
||||
TAgent = TypeVar("TAgent", bound="AgentProtocol")
|
||||
TChatClient = TypeVar("TChatClient", bound="ChatClientProtocol")
|
||||
TContext = TypeVar("TContext")
|
||||
|
||||
|
||||
class MiddlewareType(Enum):
|
||||
"""Enum representing the type of middleware."""
|
||||
|
||||
AGENT = "agent"
|
||||
FUNCTION = "function"
|
||||
CHAT = "chat"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentMiddleware",
|
||||
@@ -49,88 +35,271 @@ __all__ = [
|
||||
"use_chat_middleware",
|
||||
]
|
||||
|
||||
TAgent = TypeVar("TAgent", bound="AgentProtocol")
|
||||
TChatClient = TypeVar("TChatClient", bound="ChatClientProtocol")
|
||||
TContext = TypeVar("TContext")
|
||||
|
||||
@dataclass
|
||||
class AgentRunContext:
|
||||
|
||||
class MiddlewareType(str, Enum):
|
||||
"""Enum representing the type of middleware.
|
||||
|
||||
Used internally to identify and categorize middleware types.
|
||||
"""
|
||||
|
||||
AGENT = "agent"
|
||||
FUNCTION = "function"
|
||||
CHAT = "chat"
|
||||
|
||||
|
||||
class AgentRunContext(SerializationMixin):
|
||||
"""Context object for agent middleware invocations.
|
||||
|
||||
This context is passed through the agent middleware pipeline and contains all information
|
||||
about the agent invocation.
|
||||
|
||||
Attributes:
|
||||
agent: The agent being invoked.
|
||||
messages: The messages being sent to the agent.
|
||||
is_streaming: Whether this is a streaming invocation.
|
||||
metadata: Metadata dictionary for sharing data between agent middleware.
|
||||
result: Agent execution result. Can be observed after calling next()
|
||||
result: Agent execution result. Can be observed after calling ``next()``
|
||||
to see the actual execution result or can be set to override the execution result.
|
||||
For non-streaming: should be AgentRunResponse
|
||||
For streaming: should be AsyncIterable[AgentRunResponseUpdate]
|
||||
For non-streaming: should be AgentRunResponse.
|
||||
For streaming: should be AsyncIterable[AgentRunResponseUpdate].
|
||||
terminate: A flag indicating whether to terminate execution after current middleware.
|
||||
When set to True, execution will stop as soon as control returns to framework.
|
||||
kwargs: Additional keyword arguments passed to the agent run method.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import AgentMiddleware, AgentRunContext
|
||||
|
||||
|
||||
class LoggingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentRunContext, next):
|
||||
print(f"Agent: {context.agent.name}")
|
||||
print(f"Messages: {len(context.messages)}")
|
||||
print(f"Streaming: {context.is_streaming}")
|
||||
|
||||
# Store metadata
|
||||
context.metadata["start_time"] = time.time()
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
|
||||
# Access result after execution
|
||||
print(f"Result: {context.result}")
|
||||
"""
|
||||
|
||||
agent: "AgentProtocol"
|
||||
messages: list[ChatMessage]
|
||||
is_streaming: bool = False
|
||||
metadata: dict[str, Any] = field(default_factory=dict) # type: ignore
|
||||
result: AgentRunResponse | AsyncIterable[AgentRunResponseUpdate] | None = None
|
||||
terminate: bool = False
|
||||
kwargs: dict[str, Any] = field(default_factory=dict) # type: ignore
|
||||
INJECTABLE: ClassVar[set[str]] = {"agent", "result"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent: "AgentProtocol",
|
||||
messages: list[ChatMessage],
|
||||
is_streaming: bool = False,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
result: AgentRunResponse | AsyncIterable[AgentRunResponseUpdate] | None = None,
|
||||
terminate: bool = False,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the AgentRunContext.
|
||||
|
||||
Args:
|
||||
agent: The agent being invoked.
|
||||
messages: The messages being sent to the agent.
|
||||
is_streaming: Whether this is a streaming invocation.
|
||||
metadata: Metadata dictionary for sharing data between agent middleware.
|
||||
result: Agent execution result.
|
||||
terminate: A flag indicating whether to terminate execution after current middleware.
|
||||
kwargs: Additional keyword arguments passed to the agent run method.
|
||||
"""
|
||||
self.agent = agent
|
||||
self.messages = messages
|
||||
self.is_streaming = is_streaming
|
||||
self.metadata = metadata if metadata is not None else {}
|
||||
self.result = result
|
||||
self.terminate = terminate
|
||||
self.kwargs = kwargs if kwargs is not None else {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionInvocationContext:
|
||||
class FunctionInvocationContext(SerializationMixin):
|
||||
"""Context object for function middleware invocations.
|
||||
|
||||
This context is passed through the function middleware pipeline and contains all information
|
||||
about the function invocation.
|
||||
|
||||
Attributes:
|
||||
function: The function being invoked.
|
||||
arguments: The validated arguments for the function.
|
||||
metadata: Metadata dictionary for sharing data between function middleware.
|
||||
result: Function execution result. Can be observed after calling next()
|
||||
result: Function execution result. Can be observed after calling ``next()``
|
||||
to see the actual execution result or can be set to override the execution result.
|
||||
terminate: A flag indicating whether to terminate execution after current middleware.
|
||||
When set to True, execution will stop as soon as control returns to framework.
|
||||
kwargs: Additional keyword arguments passed to the chat method that invoked this function.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import FunctionMiddleware, FunctionInvocationContext
|
||||
|
||||
|
||||
class ValidationMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
print(f"Function: {context.function.name}")
|
||||
print(f"Arguments: {context.arguments}")
|
||||
|
||||
# Validate arguments
|
||||
if not self.validate(context.arguments):
|
||||
context.result = {"error": "Validation failed"}
|
||||
context.terminate = True
|
||||
return
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
"""
|
||||
|
||||
function: "AIFunction[Any, Any]"
|
||||
arguments: "BaseModel"
|
||||
metadata: dict[str, Any] = field(default_factory=dict) # type: ignore
|
||||
result: Any = None
|
||||
terminate: bool = False
|
||||
kwargs: dict[str, Any] = field(default_factory=dict) # type: ignore
|
||||
INJECTABLE: ClassVar[set[str]] = {"function", "arguments", "result"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
function: "AIFunction[Any, Any]",
|
||||
arguments: "BaseModel",
|
||||
metadata: dict[str, Any] | None = None,
|
||||
result: Any = None,
|
||||
terminate: bool = False,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the FunctionInvocationContext.
|
||||
|
||||
Args:
|
||||
function: The function being invoked.
|
||||
arguments: The validated arguments for the function.
|
||||
metadata: Metadata dictionary for sharing data between function middleware.
|
||||
result: Function execution result.
|
||||
terminate: A flag indicating whether to terminate execution after current middleware.
|
||||
kwargs: Additional keyword arguments passed to the chat method that invoked this function.
|
||||
"""
|
||||
self.function = function
|
||||
self.arguments = arguments
|
||||
self.metadata = metadata if metadata is not None else {}
|
||||
self.result = result
|
||||
self.terminate = terminate
|
||||
self.kwargs = kwargs if kwargs is not None else {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatContext:
|
||||
class ChatContext(SerializationMixin):
|
||||
"""Context object for chat middleware invocations.
|
||||
|
||||
This context is passed through the chat middleware pipeline and contains all information
|
||||
about the chat request.
|
||||
|
||||
Attributes:
|
||||
chat_client: The chat client being invoked.
|
||||
messages: The messages being sent to the chat client.
|
||||
chat_options: The options for the chat request.
|
||||
is_streaming: Whether this is a streaming invocation.
|
||||
metadata: Metadata dictionary.
|
||||
result: Chat execution result. Can be observed after calling next()
|
||||
metadata: Metadata dictionary for sharing data between chat middleware.
|
||||
result: Chat execution result. Can be observed after calling ``next()``
|
||||
to see the actual execution result or can be set to override the execution result.
|
||||
For non-streaming: should be ChatResponse
|
||||
For streaming: should be AsyncIterable[ChatResponseUpdate]
|
||||
For non-streaming: should be ChatResponse.
|
||||
For streaming: should be AsyncIterable[ChatResponseUpdate].
|
||||
terminate: A flag indicating whether to terminate execution after current middleware.
|
||||
When set to True, execution will stop as soon as control returns to framework.
|
||||
kwargs: Additional keyword arguments passed to the chat client.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatMiddleware, ChatContext
|
||||
|
||||
|
||||
class TokenCounterMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next):
|
||||
print(f"Chat client: {context.chat_client.__class__.__name__}")
|
||||
print(f"Messages: {len(context.messages)}")
|
||||
print(f"Model: {context.chat_options.model}")
|
||||
|
||||
# Store metadata
|
||||
context.metadata["input_tokens"] = self.count_tokens(context.messages)
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
|
||||
# Access result and count output tokens
|
||||
if context.result:
|
||||
context.metadata["output_tokens"] = self.count_tokens(context.result)
|
||||
"""
|
||||
|
||||
chat_client: "ChatClientProtocol"
|
||||
messages: "MutableSequence[ChatMessage]"
|
||||
chat_options: "ChatOptions"
|
||||
is_streaming: bool = False
|
||||
metadata: dict[str, Any] = field(default_factory=dict) # type: ignore
|
||||
result: "ChatResponse | AsyncIterable[ChatResponseUpdate] | None" = None
|
||||
terminate: bool = False
|
||||
kwargs: dict[str, Any] = field(default_factory=dict) # type: ignore
|
||||
INJECTABLE: ClassVar[set[str]] = {"chat_client", "result"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chat_client: "ChatClientProtocol",
|
||||
messages: "MutableSequence[ChatMessage]",
|
||||
chat_options: "ChatOptions",
|
||||
is_streaming: bool = False,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
result: "ChatResponse | AsyncIterable[ChatResponseUpdate] | None" = None,
|
||||
terminate: bool = False,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the ChatContext.
|
||||
|
||||
Args:
|
||||
chat_client: The chat client being invoked.
|
||||
messages: The messages being sent to the chat client.
|
||||
chat_options: The options for the chat request.
|
||||
is_streaming: Whether this is a streaming invocation.
|
||||
metadata: Metadata dictionary for sharing data between chat middleware.
|
||||
result: Chat execution result.
|
||||
terminate: A flag indicating whether to terminate execution after current middleware.
|
||||
kwargs: Additional keyword arguments passed to the chat client.
|
||||
"""
|
||||
self.chat_client = chat_client
|
||||
self.messages = messages
|
||||
self.chat_options = chat_options
|
||||
self.is_streaming = is_streaming
|
||||
self.metadata = metadata if metadata is not None else {}
|
||||
self.result = result
|
||||
self.terminate = terminate
|
||||
self.kwargs = kwargs if kwargs is not None else {}
|
||||
|
||||
|
||||
class AgentMiddleware(ABC):
|
||||
"""Abstract base class for agent middleware that can intercept agent invocations."""
|
||||
"""Abstract base class for agent middleware that can intercept agent invocations.
|
||||
|
||||
Agent middleware allows you to intercept and modify agent invocations before and after
|
||||
execution. You can inspect messages, modify context, override results, or terminate
|
||||
execution early.
|
||||
|
||||
Note:
|
||||
AgentMiddleware is an abstract base class. You must subclass it and implement
|
||||
the ``process()`` method to create custom agent middleware.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import AgentMiddleware, AgentRunContext, ChatAgent
|
||||
|
||||
|
||||
class RetryMiddleware(AgentMiddleware):
|
||||
def __init__(self, max_retries: int = 3):
|
||||
self.max_retries = max_retries
|
||||
|
||||
async def process(self, context: AgentRunContext, next):
|
||||
for attempt in range(self.max_retries):
|
||||
await next(context)
|
||||
if context.result and not context.result.is_error:
|
||||
break
|
||||
print(f"Retry {attempt + 1}/{self.max_retries}")
|
||||
|
||||
|
||||
# Use with an agent
|
||||
agent = ChatAgent(chat_client=client, name="assistant", middleware=RetryMiddleware())
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def process(
|
||||
@@ -159,7 +328,46 @@ class AgentMiddleware(ABC):
|
||||
|
||||
|
||||
class FunctionMiddleware(ABC):
|
||||
"""Abstract base class for function middleware that can intercept function invocations."""
|
||||
"""Abstract base class for function middleware that can intercept function invocations.
|
||||
|
||||
Function middleware allows you to intercept and modify function/tool invocations before
|
||||
and after execution. You can validate arguments, cache results, log invocations, or
|
||||
override function execution.
|
||||
|
||||
Note:
|
||||
FunctionMiddleware is an abstract base class. You must subclass it and implement
|
||||
the ``process()`` method to create custom function middleware.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import FunctionMiddleware, FunctionInvocationContext, ChatAgent
|
||||
|
||||
|
||||
class CachingMiddleware(FunctionMiddleware):
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
cache_key = f"{context.function.name}:{context.arguments}"
|
||||
|
||||
# Check cache
|
||||
if cache_key in self.cache:
|
||||
context.result = self.cache[cache_key]
|
||||
context.terminate = True
|
||||
return
|
||||
|
||||
# Execute function
|
||||
await next(context)
|
||||
|
||||
# Cache result
|
||||
if context.result:
|
||||
self.cache[cache_key] = context.result
|
||||
|
||||
|
||||
# Use with an agent
|
||||
agent = ChatAgent(chat_client=client, name="assistant", middleware=CachingMiddleware())
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def process(
|
||||
@@ -185,7 +393,41 @@ class FunctionMiddleware(ABC):
|
||||
|
||||
|
||||
class ChatMiddleware(ABC):
|
||||
"""Abstract base class for chat middleware that can intercept chat client requests."""
|
||||
"""Abstract base class for chat middleware that can intercept chat client requests.
|
||||
|
||||
Chat middleware allows you to intercept and modify chat client requests before and after
|
||||
execution. You can modify messages, add system prompts, log requests, or override
|
||||
chat responses.
|
||||
|
||||
Note:
|
||||
ChatMiddleware is an abstract base class. You must subclass it and implement
|
||||
the ``process()`` method to create custom chat middleware.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatMiddleware, ChatContext, ChatAgent
|
||||
|
||||
|
||||
class SystemPromptMiddleware(ChatMiddleware):
|
||||
def __init__(self, system_prompt: str):
|
||||
self.system_prompt = system_prompt
|
||||
|
||||
async def process(self, context: ChatContext, next):
|
||||
# Add system prompt to messages
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
context.messages.insert(0, ChatMessage(role="system", content=self.system_prompt))
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
|
||||
|
||||
# Use with an agent
|
||||
agent = ChatAgent(
|
||||
chat_client=client, name="assistant", middleware=SystemPromptMiddleware("You are a helpful assistant.")
|
||||
)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def process(
|
||||
@@ -233,8 +475,9 @@ Middleware: TypeAlias = (
|
||||
)
|
||||
AgentMiddlewares: TypeAlias = AgentMiddleware | AgentMiddlewareCallable
|
||||
|
||||
# region Middleware type markers for decorators
|
||||
|
||||
|
||||
# Middleware type markers for decorators
|
||||
def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable:
|
||||
"""Decorator to mark a function as agent middleware.
|
||||
|
||||
@@ -247,11 +490,21 @@ def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable:
|
||||
Returns:
|
||||
The same function with agent middleware marker.
|
||||
|
||||
Example:
|
||||
@agent_middleware
|
||||
async def my_middleware(context: AgentRunContext, next):
|
||||
# Process agent invocation
|
||||
await next(context)
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import agent_middleware, AgentRunContext, ChatAgent
|
||||
|
||||
|
||||
@agent_middleware
|
||||
async def logging_middleware(context: AgentRunContext, next):
|
||||
print(f"Before: {context.agent.name}")
|
||||
await next(context)
|
||||
print(f"After: {context.result}")
|
||||
|
||||
|
||||
# Use with an agent
|
||||
agent = ChatAgent(chat_client=client, name="assistant", middleware=logging_middleware)
|
||||
"""
|
||||
# Add marker attribute to identify this as agent middleware
|
||||
func._middleware_type: MiddlewareType = MiddlewareType.AGENT # type: ignore
|
||||
@@ -270,11 +523,21 @@ def function_middleware(func: FunctionMiddlewareCallable) -> FunctionMiddlewareC
|
||||
Returns:
|
||||
The same function with function middleware marker.
|
||||
|
||||
Example:
|
||||
@function_middleware
|
||||
async def my_middleware(context: FunctionInvocationContext, next):
|
||||
# Process function invocation
|
||||
await next(context)
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import function_middleware, FunctionInvocationContext, ChatAgent
|
||||
|
||||
|
||||
@function_middleware
|
||||
async def logging_middleware(context: FunctionInvocationContext, next):
|
||||
print(f"Calling: {context.function.name}")
|
||||
await next(context)
|
||||
print(f"Result: {context.result}")
|
||||
|
||||
|
||||
# Use with an agent
|
||||
agent = ChatAgent(chat_client=client, name="assistant", middleware=logging_middleware)
|
||||
"""
|
||||
# Add marker attribute to identify this as function middleware
|
||||
func._middleware_type: MiddlewareType = MiddlewareType.FUNCTION # type: ignore
|
||||
@@ -293,11 +556,21 @@ def chat_middleware(func: ChatMiddlewareCallable) -> ChatMiddlewareCallable:
|
||||
Returns:
|
||||
The same function with chat middleware marker.
|
||||
|
||||
Example:
|
||||
@chat_middleware
|
||||
async def my_middleware(context: ChatContext, next):
|
||||
# Process chat invocation
|
||||
await next(context)
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import chat_middleware, ChatContext, ChatAgent
|
||||
|
||||
|
||||
@chat_middleware
|
||||
async def logging_middleware(context: ChatContext, next):
|
||||
print(f"Messages: {len(context.messages)}")
|
||||
await next(context)
|
||||
print(f"Response: {context.result}")
|
||||
|
||||
|
||||
# Use with an agent
|
||||
agent = ChatAgent(chat_client=client, name="assistant", middleware=logging_middleware)
|
||||
"""
|
||||
# Add marker attribute to identify this as chat middleware
|
||||
func._middleware_type: MiddlewareType = MiddlewareType.CHAT # type: ignore
|
||||
@@ -307,6 +580,9 @@ def chat_middleware(func: ChatMiddlewareCallable) -> ChatMiddlewareCallable:
|
||||
class MiddlewareWrapper(Generic[TContext]):
|
||||
"""Generic wrapper to convert pure functions into middleware protocol objects.
|
||||
|
||||
This wrapper allows function-based middleware to be used alongside class-based middleware
|
||||
by providing a unified interface.
|
||||
|
||||
Type Parameters:
|
||||
TContext: The type of context object this middleware operates on.
|
||||
"""
|
||||
@@ -319,7 +595,10 @@ class MiddlewareWrapper(Generic[TContext]):
|
||||
|
||||
|
||||
class BaseMiddlewarePipeline(ABC):
|
||||
"""Base class for middleware pipeline execution."""
|
||||
"""Base class for middleware pipeline execution.
|
||||
|
||||
Provides common functionality for building and executing middleware chains.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the base middleware pipeline."""
|
||||
@@ -327,12 +606,22 @@ class BaseMiddlewarePipeline(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def _register_middleware(self, middleware: Any) -> None:
|
||||
"""Register a middleware item. Must be implemented by subclasses."""
|
||||
"""Register a middleware item.
|
||||
|
||||
Must be implemented by subclasses.
|
||||
|
||||
Args:
|
||||
middleware: The middleware to register.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def has_middlewares(self) -> bool:
|
||||
"""Check if there are any middlewares registered."""
|
||||
"""Check if there are any middlewares registered.
|
||||
|
||||
Returns:
|
||||
True if middlewares are registered, False otherwise.
|
||||
"""
|
||||
return bool(self._middlewares)
|
||||
|
||||
def _register_middleware_with_wrapper(
|
||||
@@ -342,6 +631,8 @@ class BaseMiddlewarePipeline(ABC):
|
||||
) -> None:
|
||||
"""Generic middleware registration with automatic wrapping.
|
||||
|
||||
Wraps callable middleware in a MiddlewareWrapper if needed.
|
||||
|
||||
Args:
|
||||
middleware: The middleware instance or callable to register.
|
||||
expected_type: The expected middleware base class type.
|
||||
@@ -360,12 +651,12 @@ class BaseMiddlewarePipeline(ABC):
|
||||
"""Create a chain of middleware handlers.
|
||||
|
||||
Args:
|
||||
final_handler: The final handler to execute
|
||||
result_container: Container to store the result
|
||||
result_key: Key to use in the result container
|
||||
final_handler: The final handler to execute.
|
||||
result_container: Container to store the result.
|
||||
result_key: Key to use in the result container.
|
||||
|
||||
Returns:
|
||||
The first handler in the chain
|
||||
The first handler in the chain.
|
||||
"""
|
||||
|
||||
def create_next_handler(index: int) -> Callable[[Any], Awaitable[None]]:
|
||||
@@ -398,12 +689,12 @@ class BaseMiddlewarePipeline(ABC):
|
||||
"""Create a chain of middleware handlers for streaming operations.
|
||||
|
||||
Args:
|
||||
final_handler: The final handler to execute
|
||||
result_container: Container to store the result
|
||||
result_key: Key to use in the result container
|
||||
final_handler: The final handler to execute.
|
||||
result_container: Container to store the result.
|
||||
result_key: Key to use in the result container.
|
||||
|
||||
Returns:
|
||||
The first handler in the chain
|
||||
The first handler in the chain.
|
||||
"""
|
||||
|
||||
def create_next_handler(index: int) -> Callable[[Any], Awaitable[None]]:
|
||||
@@ -441,13 +732,17 @@ class BaseMiddlewarePipeline(ABC):
|
||||
|
||||
|
||||
class AgentMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
"""Executes agent middleware in a chain."""
|
||||
"""Executes agent middleware in a chain.
|
||||
|
||||
Manages the execution of multiple agent middleware in sequence, allowing each middleware
|
||||
to process the agent invocation and pass control to the next middleware in the chain.
|
||||
"""
|
||||
|
||||
def __init__(self, middlewares: list[AgentMiddleware | AgentMiddlewareCallable] | None = None):
|
||||
"""Initialize the agent middleware pipeline.
|
||||
|
||||
Args:
|
||||
middlewares: List of agent middleware to include in the pipeline.
|
||||
middlewares: The list of agent middleware to include in the pipeline.
|
||||
"""
|
||||
super().__init__()
|
||||
self._middlewares: list[AgentMiddleware] = []
|
||||
@@ -457,7 +752,11 @@ class AgentMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
self._register_middleware(middleware)
|
||||
|
||||
def _register_middleware(self, middleware: AgentMiddleware | AgentMiddlewareCallable) -> None:
|
||||
"""Register an agent middleware item."""
|
||||
"""Register an agent middleware item.
|
||||
|
||||
Args:
|
||||
middleware: The agent middleware to register.
|
||||
"""
|
||||
self._register_middleware_with_wrapper(middleware, AgentMiddleware)
|
||||
|
||||
async def execute(
|
||||
@@ -562,13 +861,17 @@ class AgentMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
|
||||
|
||||
class FunctionMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
"""Executes function middleware in a chain."""
|
||||
"""Executes function middleware in a chain.
|
||||
|
||||
Manages the execution of multiple function middleware in sequence, allowing each middleware
|
||||
to process the function invocation and pass control to the next middleware in the chain.
|
||||
"""
|
||||
|
||||
def __init__(self, middlewares: list[FunctionMiddleware | FunctionMiddlewareCallable] | None = None):
|
||||
"""Initialize the function middleware pipeline.
|
||||
|
||||
Args:
|
||||
middlewares: List of function middleware to include in the pipeline.
|
||||
middlewares: The list of function middleware to include in the pipeline.
|
||||
"""
|
||||
super().__init__()
|
||||
self._middlewares: list[FunctionMiddleware] = []
|
||||
@@ -578,7 +881,11 @@ class FunctionMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
self._register_middleware(middleware)
|
||||
|
||||
def _register_middleware(self, middleware: FunctionMiddleware | FunctionMiddlewareCallable) -> None:
|
||||
"""Register a function middleware item."""
|
||||
"""Register a function middleware item.
|
||||
|
||||
Args:
|
||||
middleware: The function middleware to register.
|
||||
"""
|
||||
self._register_middleware_with_wrapper(middleware, FunctionMiddleware)
|
||||
|
||||
async def execute(
|
||||
@@ -627,13 +934,17 @@ class FunctionMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
|
||||
|
||||
class ChatMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
"""Executes chat middleware in a chain."""
|
||||
"""Executes chat middleware in a chain.
|
||||
|
||||
Manages the execution of multiple chat middleware in sequence, allowing each middleware
|
||||
to process the chat request and pass control to the next middleware in the chain.
|
||||
"""
|
||||
|
||||
def __init__(self, middlewares: list[ChatMiddleware | ChatMiddlewareCallable] | None = None):
|
||||
"""Initialize the chat middleware pipeline.
|
||||
|
||||
Args:
|
||||
middlewares: List of chat middleware to include in the pipeline.
|
||||
middlewares: The list of chat middleware to include in the pipeline.
|
||||
"""
|
||||
super().__init__()
|
||||
self._middlewares: list[ChatMiddleware] = []
|
||||
@@ -643,7 +954,11 @@ class ChatMiddlewarePipeline(BaseMiddlewarePipeline):
|
||||
self._register_middleware(middleware)
|
||||
|
||||
def _register_middleware(self, middleware: ChatMiddleware | ChatMiddlewareCallable) -> None:
|
||||
"""Register a chat middleware item."""
|
||||
"""Register a chat middleware item.
|
||||
|
||||
Args:
|
||||
middleware: The chat middleware to register.
|
||||
"""
|
||||
self._register_middleware_with_wrapper(middleware, ChatMiddleware)
|
||||
|
||||
async def execute(
|
||||
@@ -823,17 +1138,37 @@ def use_agent_middleware(agent_class: type[TAgent]) -> type[TAgent]:
|
||||
"""Class decorator that adds middleware support to an agent class.
|
||||
|
||||
This decorator adds middleware functionality to any agent class.
|
||||
It wraps the run() and run_stream() methods to provide middleware execution.
|
||||
It wraps the ``run()`` and ``run_stream()`` methods to provide middleware execution.
|
||||
|
||||
The middleware execution can be terminated at any point by setting the
|
||||
context.terminate property to True. Once set, the pipeline will stop executing
|
||||
``context.terminate`` property to True. Once set, the pipeline will stop executing
|
||||
further middleware as soon as control returns to the pipeline.
|
||||
|
||||
Note:
|
||||
This decorator is already applied to built-in agent classes. You only need to use
|
||||
it if you're creating custom agent implementations.
|
||||
|
||||
Args:
|
||||
agent_class: The agent class to add middleware support to.
|
||||
|
||||
Returns:
|
||||
The modified agent class with middleware support.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import use_agent_middleware
|
||||
|
||||
|
||||
@use_agent_middleware
|
||||
class CustomAgent:
|
||||
async def run(self, messages, **kwargs):
|
||||
# Agent implementation
|
||||
pass
|
||||
|
||||
async def run_stream(self, messages, **kwargs):
|
||||
# Streaming implementation
|
||||
pass
|
||||
"""
|
||||
# Store original methods
|
||||
original_run = agent_class.run # type: ignore[attr-defined]
|
||||
@@ -965,13 +1300,33 @@ def use_chat_middleware(chat_client_class: type[TChatClient]) -> type[TChatClien
|
||||
"""Class decorator that adds middleware support to a chat client class.
|
||||
|
||||
This decorator adds middleware functionality to any chat client class.
|
||||
It wraps the get_response() and get_streaming_response() methods to provide middleware execution.
|
||||
It wraps the ``get_response()`` and ``get_streaming_response()`` methods to provide middleware execution.
|
||||
|
||||
Note:
|
||||
This decorator is already applied to built-in chat client classes. You only need to use
|
||||
it if you're creating custom chat client implementations.
|
||||
|
||||
Args:
|
||||
chat_client_class: The chat client class to add middleware support to.
|
||||
|
||||
Returns:
|
||||
The modified chat client class with middleware support.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import use_chat_middleware
|
||||
|
||||
|
||||
@use_chat_middleware
|
||||
class CustomChatClient:
|
||||
async def get_response(self, messages, **kwargs):
|
||||
# Chat client implementation
|
||||
pass
|
||||
|
||||
async def get_streaming_response(self, messages, **kwargs):
|
||||
# Streaming implementation
|
||||
pass
|
||||
"""
|
||||
# Store original methods
|
||||
original_get_response = chat_client_class.get_response
|
||||
@@ -1140,7 +1495,14 @@ def categorize_middleware(
|
||||
def create_function_middleware_pipeline(
|
||||
*middleware_sources: list[Middleware] | None,
|
||||
) -> FunctionMiddlewarePipeline | None:
|
||||
"""Create a function middleware pipeline from multiple middleware sources."""
|
||||
"""Create a function middleware pipeline from multiple middleware sources.
|
||||
|
||||
Args:
|
||||
*middleware_sources: Variable number of middleware sources.
|
||||
|
||||
Returns:
|
||||
A FunctionMiddlewarePipeline if function middleware is found, None otherwise.
|
||||
"""
|
||||
middleware = categorize_middleware(*middleware_sources)
|
||||
function_middlewares = middleware["function"]
|
||||
return FunctionMiddlewarePipeline(function_middlewares) if function_middlewares else None # type: ignore[arg-type]
|
||||
|
||||
@@ -18,20 +18,66 @@ _CAMEL_TO_SNAKE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
|
||||
|
||||
@runtime_checkable
|
||||
class SerializationProtocol(Protocol):
|
||||
"""Protocol for objects that support serialization and deserialization."""
|
||||
"""Protocol for objects that support serialization and deserialization.
|
||||
|
||||
This protocol defines the interface for objects that can be converted to and from dictionaries.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import SerializationProtocol
|
||||
|
||||
|
||||
class MySerializable:
|
||||
def __init__(self, value: str):
|
||||
self.value = value
|
||||
|
||||
def to_dict(self, **kwargs):
|
||||
return {"value": self.value}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value, **kwargs):
|
||||
return cls(value["value"])
|
||||
|
||||
|
||||
# Verify it implements the protocol
|
||||
assert isinstance(MySerializable("test"), SerializationProtocol)
|
||||
"""
|
||||
|
||||
def to_dict(self, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Convert the instance to a dictionary."""
|
||||
"""Convert the instance to a dictionary.
|
||||
|
||||
Args:
|
||||
kwargs: Additional keyword arguments for serialization.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the instance.
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[TProtocol], value: MutableMapping[str, Any], /, **kwargs: Any) -> TProtocol:
|
||||
"""Create an instance from a dictionary."""
|
||||
"""Create an instance from a dictionary.
|
||||
|
||||
Args:
|
||||
value: Dictionary containing the instance data (positional-only).
|
||||
kwargs: Additional keyword arguments for deserialization.
|
||||
|
||||
Returns:
|
||||
New instance of the class.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
def is_serializable(value: Any) -> bool:
|
||||
"""Check if a value is JSON serializable."""
|
||||
"""Check if a value is JSON serializable.
|
||||
|
||||
Args:
|
||||
value: The value to check.
|
||||
|
||||
Returns:
|
||||
True if the value is JSON serializable, False otherwise.
|
||||
"""
|
||||
return isinstance(value, (str, int, float, bool, type(None), list, dict))
|
||||
|
||||
|
||||
@@ -47,8 +93,7 @@ class SerializationMixin:
|
||||
Because we setup the __init__ method to handle MutableMapping,
|
||||
we can pass in a dict to the second class and it will convert it to an instance of the first class.
|
||||
|
||||
Example:
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
class SerializableMixinType(SerializationMixin):
|
||||
@@ -72,11 +117,10 @@ class SerializationMixin:
|
||||
|
||||
instance = MyClass.from_dict({"regular_param": "value", "param": {"param1": "value1", "param2": 42}})
|
||||
|
||||
A more complex use case involves a injectable dependency that is not serialized.
|
||||
A more complex use case involves an injectable dependency that is not serialized.
|
||||
In this case, the dependency is passed in via the dependencies parameter to from_dict/from_json.
|
||||
|
||||
Example:
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from libary import Client
|
||||
@@ -85,16 +129,6 @@ class SerializationMixin:
|
||||
class MyClass(SerializationMixin):
|
||||
INJECTABLE = {"client"}
|
||||
|
||||
def __init__(self, regular_param: str, client: Client) -> None:
|
||||
self.client = client
|
||||
self.regular_param = regular_param
|
||||
|
||||
|
||||
json_of_class = MyClass(regular_param="value", client=Client()).to_json()
|
||||
# this looks like: {"type": "my_class", "regular_param": "value"}
|
||||
|
||||
instance = MyClass.from_dict(json_of_class, dependencies={"my_class.client": Client()})
|
||||
|
||||
During serialization, the field listed as INJECTABLE (and also DEFAULT_EXCLUDE) will be excluded from the output.
|
||||
Then in deserialization,
|
||||
the dependencies dict is checked for any keys matching the formats:
|
||||
@@ -114,7 +148,7 @@ class SerializationMixin:
|
||||
"""Convert the instance and any nested objects to a dictionary.
|
||||
|
||||
Args:
|
||||
exclude: Set of field names to exclude from serialization.
|
||||
exclude: The set of field names to exclude from serialization.
|
||||
exclude_none: Whether to exclude None values from the output. Defaults to True.
|
||||
|
||||
Returns:
|
||||
@@ -180,7 +214,7 @@ class SerializationMixin:
|
||||
"""Convert the instance to a JSON string.
|
||||
|
||||
Args:
|
||||
exclude: Set of field names to exclude from serialization.
|
||||
exclude: The set of field names to exclude from serialization.
|
||||
exclude_none: Whether to exclude None values from the output. Defaults to True.
|
||||
|
||||
Returns:
|
||||
@@ -195,9 +229,9 @@ class SerializationMixin:
|
||||
"""Create an instance from a dictionary.
|
||||
|
||||
Args:
|
||||
value: Dictionary containing the instance data (positional-only).
|
||||
dependencies: Dictionary mapping dependency keys to values.
|
||||
Keys should be in format "<type>.<parameter>" or "<type>.<dict-parameter>.<key>".
|
||||
value: The dictionary containing the instance data (positional-only).
|
||||
dependencies: The dictionary mapping dependency keys to values.
|
||||
Keys should be in format ``"<type>.<parameter>"`` or ``"<type>.<dict-parameter>.<key>"``.
|
||||
|
||||
Returns:
|
||||
New instance of the class.
|
||||
@@ -248,9 +282,9 @@ class SerializationMixin:
|
||||
"""Create an instance from a JSON string.
|
||||
|
||||
Args:
|
||||
value: JSON string containing the instance data (positional-only).
|
||||
dependencies: Dictionary mapping dependency keys to values.
|
||||
Keys should be in format "<type>.<parameter>" or "<type>.<dict-parameter>.<key>".
|
||||
value: The JSON string containing the instance data (positional-only).
|
||||
dependencies: The dictionary mapping dependency keys to values.
|
||||
Keys should be in format ``"<type>.<parameter>"`` or ``"<type>.<dict-parameter>.<key>"``.
|
||||
|
||||
Returns:
|
||||
New instance of the class.
|
||||
@@ -262,7 +296,7 @@ class SerializationMixin:
|
||||
def _get_type_identifier(cls) -> str:
|
||||
"""Get the type identifier for this class.
|
||||
|
||||
Returns the value of the 'type' class variable if present,
|
||||
Returns the value of the ``type`` class variable if present,
|
||||
otherwise returns a snake_cased version of the class name.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -35,9 +35,9 @@ AGENT_FRAMEWORK_USER_AGENT = f"{HTTP_USER_AGENT}/{version_info}" # type: ignore
|
||||
def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Prepend "agent-framework" to the User-Agent in the headers.
|
||||
|
||||
When user agent telemetry is disabled, through the AZURE_TELEMETRY_DISABLED environment variable,
|
||||
the User-Agent header will not include the agent-framework information, it will be sent back as is,
|
||||
or as a empty dict when None is passed.
|
||||
When user agent telemetry is disabled through the ``AGENT_FRAMEWORK_USER_AGENT_DISABLED``
|
||||
environment variable, the User-Agent header will not include the agent-framework information.
|
||||
It will be sent back as is, or as an empty dict when None is passed.
|
||||
|
||||
Args:
|
||||
headers: The existing headers dictionary.
|
||||
@@ -45,6 +45,20 @@ def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None)
|
||||
Returns:
|
||||
A new dict with "User-Agent" set to "agent-framework-python/{version}" if headers is None.
|
||||
The modified headers dictionary with "agent-framework-python/{version}" prepended to the User-Agent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import prepend_agent_framework_to_user_agent
|
||||
|
||||
# Add agent-framework to new headers
|
||||
headers = prepend_agent_framework_to_user_agent()
|
||||
print(headers["User-Agent"]) # "agent-framework-python/0.1.0"
|
||||
|
||||
# Prepend to existing headers
|
||||
existing = {"User-Agent": "my-app/1.0"}
|
||||
headers = prepend_agent_framework_to_user_agent(existing)
|
||||
print(headers["User-Agent"]) # "agent-framework-python/0.1.0 my-app/1.0"
|
||||
"""
|
||||
if not IS_TELEMETRY_ENABLED:
|
||||
return headers or {}
|
||||
|
||||
@@ -17,6 +17,38 @@ class ChatMessageStoreProtocol(Protocol):
|
||||
|
||||
Implementations of this protocol are responsible for managing the storage of chat messages,
|
||||
including handling large volumes of data by truncating or summarizing messages as necessary.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
|
||||
class MyMessageStore:
|
||||
def __init__(self):
|
||||
self._messages = []
|
||||
|
||||
async def list_messages(self) -> list[ChatMessage]:
|
||||
return self._messages
|
||||
|
||||
async def add_messages(self, messages: Sequence[ChatMessage]) -> None:
|
||||
self._messages.extend(messages)
|
||||
|
||||
@classmethod
|
||||
async def deserialize(cls, serialized_store_state, **kwargs):
|
||||
store = cls()
|
||||
store._messages = serialized_store_state.get("messages", [])
|
||||
return store
|
||||
|
||||
async def update_from_state(self, serialized_store_state, **kwargs) -> None:
|
||||
self._messages = serialized_store_state.get("messages", [])
|
||||
|
||||
async def serialize(self, **kwargs):
|
||||
return {"messages": self._messages}
|
||||
|
||||
|
||||
# Use the custom store
|
||||
store = MyMessageStore()
|
||||
"""
|
||||
|
||||
async def list_messages(self) -> list[ChatMessage]:
|
||||
@@ -27,21 +59,32 @@ class ChatMessageStoreProtocol(Protocol):
|
||||
If the messages stored in the store become very large, it is up to the store to
|
||||
truncate, summarize or otherwise limit the number of messages returned.
|
||||
|
||||
When using implementations of ChatMessageStoreProtocol, a new one should be created for each thread
|
||||
When using implementations of ``ChatMessageStoreProtocol``, a new one should be created for each thread
|
||||
since they may contain state that is specific to a thread.
|
||||
"""
|
||||
...
|
||||
|
||||
async def add_messages(self, messages: Sequence[ChatMessage]) -> None:
|
||||
"""Adds messages to the store."""
|
||||
"""Adds messages to the store.
|
||||
|
||||
Args:
|
||||
messages: The sequence of ChatMessage objects to add to the store.
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
async def deserialize(cls, serialized_store_state: Any, **kwargs: Any) -> "ChatMessageStoreProtocol":
|
||||
"""Creates a new instance of the store from previously serialized state.
|
||||
|
||||
This method, together with serialize_state can be used to save and load messages from a persistent store
|
||||
This method, together with ``serialize()`` can be used to save and load messages from a persistent store
|
||||
if this store only has messages in memory.
|
||||
|
||||
Args:
|
||||
serialized_store_state: The previously serialized state data containing messages.
|
||||
**kwargs: Additional arguments for deserialization.
|
||||
|
||||
Returns:
|
||||
A new instance of the store populated with messages from the serialized state.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -57,8 +100,14 @@ class ChatMessageStoreProtocol(Protocol):
|
||||
async def serialize(self, **kwargs: Any) -> Any:
|
||||
"""Serializes the current object's state.
|
||||
|
||||
This method, together with deserialize can be used to save and load messages from a persistent store
|
||||
This method, together with ``deserialize()`` can be used to save and load messages from a persistent store
|
||||
if this store only has messages in memory.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional arguments for serialization.
|
||||
|
||||
Returns:
|
||||
The serialized state data that can be used with ``deserialize()``.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -107,13 +156,34 @@ class ChatMessageStore:
|
||||
|
||||
This implementation provides a simple, list-based storage for chat messages
|
||||
with support for serialization and deserialization. It implements all the
|
||||
required methods of the ChatMessageStoreProtocol protocol.
|
||||
required methods of the ``ChatMessageStoreProtocol`` protocol.
|
||||
|
||||
The store maintains messages in memory and provides methods to serialize
|
||||
and deserialize the state for persistence purposes.
|
||||
|
||||
Args:
|
||||
messages: Optional initial list of ChatMessage objects to populate the store.
|
||||
messages: The optional initial list of ChatMessage objects to populate the store.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatMessageStore, ChatMessage
|
||||
|
||||
# Create an empty store
|
||||
store = ChatMessageStore()
|
||||
|
||||
# Add messages
|
||||
message = ChatMessage(role="user", content="Hello")
|
||||
await store.add_messages([message])
|
||||
|
||||
# Retrieve messages
|
||||
messages = await store.list_messages()
|
||||
|
||||
# Serialize for persistence
|
||||
state = await store.serialize()
|
||||
|
||||
# Deserialize from saved state
|
||||
restored_store = await ChatMessageStore.deserialize(state)
|
||||
"""
|
||||
|
||||
def __init__(self, messages: Sequence[ChatMessage] | None = None):
|
||||
@@ -188,7 +258,36 @@ TAgentThread = TypeVar("TAgentThread", bound="AgentThread")
|
||||
|
||||
|
||||
class AgentThread:
|
||||
"""The Agent thread class, this can represent both a locally managed thread or a thread managed by the service."""
|
||||
"""The Agent thread class, this can represent both a locally managed thread or a thread managed by the service.
|
||||
|
||||
An ``AgentThread`` maintains the conversation state and message history for an agent interaction.
|
||||
It can either use a service-managed thread (via ``service_thread_id``) or a local message store
|
||||
(via ``message_store``), but not both.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatAgent, ChatMessageStore
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
client = OpenAIChatClient(model="gpt-4o")
|
||||
|
||||
# Create agent with service-managed threads using a service_thread_id
|
||||
service_agent = ChatAgent(name="assistant", client=client)
|
||||
service_thread = await service_agent.get_new_thread(service_thread_id="thread_abc123")
|
||||
|
||||
# Create agent with service-managed threads using conversation_id
|
||||
conversation_agent = ChatAgent(name="assistant", client=client, conversation_id="thread_abc123")
|
||||
conversation_thread = await conversation_agent.get_new_thread()
|
||||
|
||||
# Create agent with custom message store factory
|
||||
local_agent = ChatAgent(name="assistant", client=client, chat_message_store_factory=ChatMessageStore)
|
||||
local_thread = await local_agent.get_new_thread()
|
||||
|
||||
# Serialize and restore thread state
|
||||
state = await local_thread.serialize()
|
||||
restored_thread = await local_agent.deserialize_thread(state)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -197,15 +296,15 @@ class AgentThread:
|
||||
message_store: ChatMessageStoreProtocol | None = None,
|
||||
context_provider: AggregateContextProvider | None = None,
|
||||
) -> None:
|
||||
"""Initialize an AgentThread, do not use this method manually, always use: agent.get_new_thread().
|
||||
"""Initialize an AgentThread, do not use this method manually, always use: ``agent.get_new_thread()``.
|
||||
|
||||
Args:
|
||||
service_thread_id: Optional ID of the thread managed by the agent service.
|
||||
message_store: Optional ChatMessageStore implementation for managing chat messages.
|
||||
context_provider: Optional ContextProvider for the thread.
|
||||
service_thread_id: The optional ID of the thread managed by the agent service.
|
||||
message_store: The optional ChatMessageStore implementation for managing chat messages.
|
||||
context_provider: The optional ContextProvider for the thread.
|
||||
|
||||
Note:
|
||||
Either service_thread_id or message_store may be set, but not both.
|
||||
Either ``service_thread_id`` or ``message_store`` may be set, but not both.
|
||||
"""
|
||||
if service_thread_id is not None and message_store is not None:
|
||||
raise AgentThreadException("Only the service_thread_id or message_store may be set, but not both.")
|
||||
@@ -218,7 +317,7 @@ class AgentThread:
|
||||
def is_initialized(self) -> bool:
|
||||
"""Indicates if the thread is initialized.
|
||||
|
||||
This means either the service_thread_id or the message_store is set.
|
||||
This means either the ``service_thread_id`` or the ``message_store`` is set.
|
||||
"""
|
||||
return self._service_thread_id is not None or self._message_store is not None
|
||||
|
||||
@@ -231,7 +330,8 @@ class AgentThread:
|
||||
def service_thread_id(self, service_thread_id: str | None) -> None:
|
||||
"""Sets the ID of the current thread to support cases where the thread is owned by the agent service.
|
||||
|
||||
Note that either service_thread_id or message_store may be set, but not both.
|
||||
Note:
|
||||
Either ``service_thread_id`` or ``message_store`` may be set, but not both.
|
||||
"""
|
||||
if service_thread_id is None:
|
||||
return
|
||||
@@ -245,14 +345,15 @@ class AgentThread:
|
||||
|
||||
@property
|
||||
def message_store(self) -> ChatMessageStoreProtocol | None:
|
||||
"""Gets the ChatMessageStoreProtocol used by this thread."""
|
||||
"""Gets the ``ChatMessageStoreProtocol`` used by this thread."""
|
||||
return self._message_store
|
||||
|
||||
@message_store.setter
|
||||
def message_store(self, message_store: ChatMessageStoreProtocol | None) -> None:
|
||||
"""Sets the ChatMessageStoreProtocol used by this thread.
|
||||
"""Sets the ``ChatMessageStoreProtocol`` used by this thread.
|
||||
|
||||
Note that either service_thread_id or message_store may be set, but not both.
|
||||
Note:
|
||||
Either ``service_thread_id`` or ``message_store`` may be set, but not both.
|
||||
"""
|
||||
if message_store is None:
|
||||
return
|
||||
@@ -266,7 +367,11 @@ class AgentThread:
|
||||
self._message_store = message_store
|
||||
|
||||
async def on_new_messages(self, new_messages: ChatMessage | Sequence[ChatMessage]) -> None:
|
||||
"""Invoked when a new message has been contributed to the chat by any participant."""
|
||||
"""Invoked when a new message has been contributed to the chat by any participant.
|
||||
|
||||
Args:
|
||||
new_messages: The new ChatMessage or sequence of ChatMessage objects to add to the thread.
|
||||
"""
|
||||
if self._service_thread_id is not None:
|
||||
# If the thread messages are stored in the service there is nothing to do here,
|
||||
# since invoking the service should already update the thread.
|
||||
|
||||
@@ -89,7 +89,18 @@ _NOOP_HISTOGRAM = _NoOpHistogram()
|
||||
def _parse_inputs(
|
||||
inputs: "Contents | dict[str, Any] | str | list[Contents | dict[str, Any] | str] | None",
|
||||
) -> list["Contents"]:
|
||||
"""Parse the inputs for a tool, ensuring they are of type Contents."""
|
||||
"""Parse the inputs for a tool, ensuring they are of type Contents.
|
||||
|
||||
Args:
|
||||
inputs: The inputs to parse. Can be a single item or list of Contents, dicts, or strings.
|
||||
|
||||
Returns:
|
||||
A list of Contents objects.
|
||||
|
||||
Raises:
|
||||
ValueError: If an unsupported input type is encountered.
|
||||
TypeError: If the input type is not supported.
|
||||
"""
|
||||
if inputs is None:
|
||||
return []
|
||||
|
||||
@@ -133,10 +144,32 @@ def _parse_inputs(
|
||||
class ToolProtocol(Protocol):
|
||||
"""Represents a generic tool that can be specified to an AI service.
|
||||
|
||||
Parameters:
|
||||
This protocol defines the interface that all tools must implement to be compatible
|
||||
with the agent framework.
|
||||
|
||||
Attributes:
|
||||
name: The name of the tool.
|
||||
description: A description of the tool.
|
||||
description: A description of the tool, suitable for use in describing the purpose to a model.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ToolProtocol
|
||||
|
||||
|
||||
class CustomTool:
|
||||
def __init__(self, name: str, description: str) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.additional_properties = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"CustomTool(name={self.name})"
|
||||
|
||||
|
||||
# Tool now implements ToolProtocol
|
||||
tool: ToolProtocol = CustomTool("my_tool", "Does something useful")
|
||||
"""
|
||||
|
||||
name: str
|
||||
@@ -154,10 +187,27 @@ class ToolProtocol(Protocol):
|
||||
class BaseTool(SerializationMixin):
|
||||
"""Base class for AI tools, providing common attributes and methods.
|
||||
|
||||
This class provides the foundation for creating custom tools with serialization support.
|
||||
|
||||
Args:
|
||||
name: The name of the tool.
|
||||
description: A description of the tool.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import BaseTool
|
||||
|
||||
|
||||
class MyCustomTool(BaseTool):
|
||||
def __init__(self, name: str, custom_param: str) -> None:
|
||||
super().__init__(name=name, description="My custom tool")
|
||||
self.custom_param = custom_param
|
||||
|
||||
|
||||
tool = MyCustomTool(name="custom", custom_param="value")
|
||||
print(tool) # MyCustomTool(name=custom, description=My custom tool)
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"}
|
||||
@@ -196,6 +246,17 @@ class HostedCodeInterpreterTool(BaseTool):
|
||||
|
||||
This tool does not implement code interpretation itself. It serves as a marker to inform a service
|
||||
that it is allowed to execute generated code if the service is capable of doing so.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedCodeInterpreterTool
|
||||
|
||||
# Create a code interpreter tool
|
||||
code_tool = HostedCodeInterpreterTool()
|
||||
|
||||
# With file inputs
|
||||
code_tool_with_files = HostedCodeInterpreterTool(inputs=[{"file_id": "file-123"}, {"file_id": "file-456"}])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -235,7 +296,22 @@ class HostedCodeInterpreterTool(BaseTool):
|
||||
|
||||
|
||||
class HostedWebSearchTool(BaseTool):
|
||||
"""Represents a web search tool that can be specified to an AI service to enable it to perform web searches."""
|
||||
"""Represents a web search tool that can be specified to an AI service to enable it to perform web searches.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedWebSearchTool
|
||||
|
||||
# Create a basic web search tool
|
||||
search_tool = HostedWebSearchTool()
|
||||
|
||||
# With location context
|
||||
search_tool_with_location = HostedWebSearchTool(
|
||||
description="Search the web for information",
|
||||
additional_properties={"user_location": {"city": "Seattle", "country": "US"}},
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -265,13 +341,14 @@ class HostedWebSearchTool(BaseTool):
|
||||
|
||||
|
||||
class HostedMCPSpecificApproval(TypedDict, total=False):
|
||||
"""Represents the `specific` mode for a hosted tool.
|
||||
"""Represents the specific mode for a hosted tool.
|
||||
|
||||
When using this mode, the user must specify which tools always or never require approval.
|
||||
This is represented as a dictionary with two optional keys:
|
||||
- `always_require_approval`: A sequence of tool names that always require approval.
|
||||
- `never_require_approval`: A sequence of tool names that never require approval.
|
||||
|
||||
Attributes:
|
||||
always_require_approval: A sequence of tool names that always require approval.
|
||||
never_require_approval: A sequence of tool names that never require approval.
|
||||
"""
|
||||
|
||||
always_require_approval: Collection[str] | None
|
||||
@@ -279,7 +356,39 @@ class HostedMCPSpecificApproval(TypedDict, total=False):
|
||||
|
||||
|
||||
class HostedMCPTool(BaseTool):
|
||||
"""Represents a MCP tool that is managed and executed by the service."""
|
||||
"""Represents a MCP tool that is managed and executed by the service.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedMCPTool
|
||||
|
||||
# Create a basic MCP tool
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="my_mcp_tool",
|
||||
url="https://example.com/mcp",
|
||||
)
|
||||
|
||||
# With approval mode and allowed tools
|
||||
mcp_tool_with_approval = HostedMCPTool(
|
||||
name="my_mcp_tool",
|
||||
description="My MCP tool",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode="always_require",
|
||||
allowed_tools=["tool1", "tool2"],
|
||||
headers={"Authorization": "Bearer token"},
|
||||
)
|
||||
|
||||
# With specific approval mode
|
||||
mcp_tool_specific = HostedMCPTool(
|
||||
name="my_mcp_tool",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode={
|
||||
"always_require_approval": ["dangerous_tool"],
|
||||
"never_require_approval": ["safe_tool"],
|
||||
},
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -346,7 +455,23 @@ class HostedMCPTool(BaseTool):
|
||||
|
||||
|
||||
class HostedFileSearchTool(BaseTool):
|
||||
"""Represents a file search tool that can be specified to an AI service to enable it to perform file searches."""
|
||||
"""Represents a file search tool that can be specified to an AI service to enable it to perform file searches.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedFileSearchTool
|
||||
|
||||
# Create a basic file search tool
|
||||
file_search = HostedFileSearchTool()
|
||||
|
||||
# With vector store inputs and max results
|
||||
file_search_with_inputs = HostedFileSearchTool(
|
||||
inputs=[{"vector_store_id": "vs_123"}],
|
||||
max_results=10,
|
||||
description="Search files in vector store",
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -387,7 +512,12 @@ class HostedFileSearchTool(BaseTool):
|
||||
|
||||
|
||||
def _default_histogram() -> Histogram:
|
||||
"""Get the default histogram for function invocation duration."""
|
||||
"""Get the default histogram for function invocation duration.
|
||||
|
||||
Returns:
|
||||
A Histogram instance for recording function invocation duration,
|
||||
or a no-op histogram if observability is disabled.
|
||||
"""
|
||||
from .observability import OBSERVABILITY_SETTINGS # local import to avoid circulars
|
||||
|
||||
if not OBSERVABILITY_SETTINGS.ENABLED: # type: ignore[name-defined]
|
||||
@@ -411,12 +541,49 @@ def _default_histogram() -> Histogram:
|
||||
class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
"""A AITool that is callable as code.
|
||||
|
||||
This class wraps a Python function to make it callable by AI models with automatic
|
||||
parameter validation and JSON schema generation.
|
||||
|
||||
Args:
|
||||
name: The name of the function.
|
||||
description: A description of the function.
|
||||
additional_properties: Additional properties to set on the function.
|
||||
func: The function to wrap. If None, returns a decorator.
|
||||
func: The function to wrap.
|
||||
input_model: The Pydantic model that defines the input parameters for the function.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel
|
||||
from agent_framework import AIFunction, ai_function
|
||||
|
||||
|
||||
# Using the decorator with string annotations
|
||||
@ai_function
|
||||
def get_weather(
|
||||
location: Annotated[str, "The city name"],
|
||||
unit: Annotated[str, "Temperature unit"] = "celsius",
|
||||
) -> str:
|
||||
'''Get the weather for a location.'''
|
||||
return f"Weather in {location}: 22°{unit[0].upper()}"
|
||||
|
||||
|
||||
# Using direct instantiation with Field
|
||||
class WeatherArgs(BaseModel):
|
||||
location: Annotated[str, Field(description="The city name")]
|
||||
unit: Annotated[str, Field(description="Temperature unit")] = "celsius"
|
||||
|
||||
|
||||
weather_func = AIFunction(
|
||||
name="get_weather",
|
||||
description="Get the weather for a location",
|
||||
func=lambda location, unit="celsius": f"Weather in {location}: 22°{unit[0].upper()}",
|
||||
input_model=WeatherArgs,
|
||||
)
|
||||
|
||||
# Invoke the function
|
||||
result = await weather_func.invoke(arguments=WeatherArgs(location="Seattle"))
|
||||
"""
|
||||
|
||||
INJECTABLE: ClassVar[set[str]] = {"func"}
|
||||
@@ -466,7 +633,13 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
|
||||
Args:
|
||||
arguments: A Pydantic model instance containing the arguments for the function.
|
||||
kwargs: keyword arguments to pass to the function, will not be used if `arguments` is provided.
|
||||
kwargs: Keyword arguments to pass to the function, will not be used if ``arguments`` is provided.
|
||||
|
||||
Returns:
|
||||
The result of the function execution.
|
||||
|
||||
Raises:
|
||||
TypeError: If arguments is not an instance of the expected input model.
|
||||
"""
|
||||
global OBSERVABILITY_SETTINGS
|
||||
from .observability import OBSERVABILITY_SETTINGS
|
||||
@@ -530,11 +703,19 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
logger.info("Function duration: %fs", duration)
|
||||
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
"""Create the json schema of the parameters."""
|
||||
"""Create the JSON schema of the parameters.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the JSON schema for the function's parameters.
|
||||
"""
|
||||
return self.input_model.model_json_schema()
|
||||
|
||||
def to_json_schema_spec(self) -> dict[str, Any]:
|
||||
"""Convert a AIFunction to the JSON Schema function specification format."""
|
||||
"""Convert a AIFunction to the JSON Schema function specification format.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the function specification in JSON Schema format.
|
||||
"""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
@@ -554,7 +735,14 @@ def _tools_to_dict(
|
||||
| None
|
||||
),
|
||||
) -> list[str | dict[str, Any]] | None:
|
||||
"""Parse the tools to a dict."""
|
||||
"""Parse the tools to a dict.
|
||||
|
||||
Args:
|
||||
tools: The tools to parse. Can be a single tool or a sequence of tools.
|
||||
|
||||
Returns:
|
||||
A list of tool specifications as dictionaries, or None if no tools provided.
|
||||
"""
|
||||
if not tools:
|
||||
return None
|
||||
if not isinstance(tools, list):
|
||||
@@ -592,8 +780,14 @@ def _tools_to_dict(
|
||||
def _parse_annotation(annotation: Any) -> Any:
|
||||
"""Parse a type annotation and return the corresponding type.
|
||||
|
||||
If the second annotation (after the type) is a string, then we convert that to a pydantic Field description.
|
||||
If the second annotation (after the type) is a string, then we convert that to a Pydantic Field description.
|
||||
The rest are returned as-is, allowing for multiple annotations.
|
||||
|
||||
Args:
|
||||
annotation: The type annotation to parse.
|
||||
|
||||
Returns:
|
||||
The parsed annotation, potentially wrapped in Annotated with a Field.
|
||||
"""
|
||||
origin = get_origin(annotation)
|
||||
if origin is not None:
|
||||
@@ -617,26 +811,13 @@ def ai_function(
|
||||
) -> AIFunction[Any, ReturnT]:
|
||||
"""Decorate a function to turn it into a AIFunction that can be passed to models and executed automatically.
|
||||
|
||||
This function will create a Pydantic model from the function's signature,
|
||||
which will be used to validate the arguments passed to the function.
|
||||
And will be used to generate the JSON schema for the function's parameters.
|
||||
In order to add descriptions to parameters, in your function signature,
|
||||
use the `Annotated` type from `typing` and the `Field` class from `pydantic`:
|
||||
This decorator creates a Pydantic model from the function's signature,
|
||||
which will be used to validate the arguments passed to the function
|
||||
and to generate the JSON schema for the function's parameters.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
def ai_function_example(
|
||||
arg1: Annotated[str, Field(description="The first argument")],
|
||||
arg2: Annotated[int, Field(description="The second argument")],
|
||||
) -> str:
|
||||
# An example function that takes two arguments and returns a string.
|
||||
return f"arg1: {arg1}, arg2: {arg2}"
|
||||
To add descriptions to parameters, use the ``Annotated`` type from ``typing``
|
||||
with a string description as the second argument. You can also use Pydantic's
|
||||
``Field`` class for more advanced configuration.
|
||||
|
||||
Args:
|
||||
func: The function to wrap. If None, returns a decorator.
|
||||
@@ -644,6 +825,38 @@ def ai_function(
|
||||
description: A description of the tool. Defaults to the function's docstring.
|
||||
additional_properties: Additional properties to set on the tool.
|
||||
|
||||
Returns:
|
||||
An AIFunction instance that wraps the decorated function.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Annotated
|
||||
from agent_framework import ai_function
|
||||
|
||||
|
||||
# Using string annotations (recommended)
|
||||
@ai_function
|
||||
def get_weather(
|
||||
location: Annotated[str, "The city name"],
|
||||
unit: Annotated[str, "Temperature unit"] = "celsius",
|
||||
) -> str:
|
||||
'''Get the weather for a location.'''
|
||||
return f"Weather in {location}: 22°{unit[0].upper()}"
|
||||
|
||||
|
||||
# With custom name and description
|
||||
@ai_function(name="custom_weather", description="Custom weather function")
|
||||
def another_weather_func(location: str) -> str:
|
||||
return f"Weather in {location}"
|
||||
|
||||
|
||||
# Async functions are also supported
|
||||
@ai_function
|
||||
async def async_get_weather(location: str) -> str:
|
||||
'''Get weather asynchronously.'''
|
||||
# Simulate async operation
|
||||
return f"Weather in {location}"
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., ReturnT | Awaitable[ReturnT]]) -> AIFunction[Any, ReturnT]:
|
||||
@@ -689,7 +902,22 @@ async def _auto_invoke_function(
|
||||
request_index: int | None = None,
|
||||
middleware_pipeline: Any = None, # Optional MiddlewarePipeline
|
||||
) -> "Contents":
|
||||
"""Invoke a function call requested by the agent, applying filters that are defined in the agent."""
|
||||
"""Invoke a function call requested by the agent, applying middleware that is defined.
|
||||
|
||||
Args:
|
||||
function_call_content: The function call content from the model.
|
||||
custom_args: Additional custom arguments to merge with parsed arguments.
|
||||
tool_map: A mapping of tool names to AIFunction instances.
|
||||
sequence_index: The index of the function call in the sequence.
|
||||
request_index: The index of the request iteration.
|
||||
middleware_pipeline: Optional middleware pipeline to apply during execution.
|
||||
|
||||
Returns:
|
||||
A FunctionResultContent containing the result or exception.
|
||||
|
||||
Raises:
|
||||
KeyError: If the requested function is not found in the tool map.
|
||||
"""
|
||||
from ._types import FunctionResultContent
|
||||
|
||||
tool: AIFunction[BaseModel, Any] | None = tool_map.get(function_call_content.name)
|
||||
@@ -775,6 +1003,18 @@ async def execute_function_calls(
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]",
|
||||
middleware_pipeline: Any = None, # Optional MiddlewarePipeline to avoid circular imports
|
||||
) -> list["Contents"]:
|
||||
"""Execute multiple function calls concurrently.
|
||||
|
||||
Args:
|
||||
custom_args: Custom arguments to pass to each function.
|
||||
attempt_idx: The index of the current attempt iteration.
|
||||
function_calls: A sequence of FunctionCallContent to execute.
|
||||
tools: The tools available for execution.
|
||||
middleware_pipeline: Optional middleware pipeline to apply during execution.
|
||||
|
||||
Returns:
|
||||
A list of Contents containing the results of each function call.
|
||||
"""
|
||||
tool_map = _get_tool_map(tools)
|
||||
# Run all function calls concurrently
|
||||
return await asyncio.gather(*[
|
||||
@@ -790,8 +1030,13 @@ async def execute_function_calls(
|
||||
])
|
||||
|
||||
|
||||
def update_conversation_id(kwargs: dict[str, Any], conversation_id: str | None) -> None:
|
||||
"""Update kwargs with conversation id."""
|
||||
def _update_conversation_id(kwargs: dict[str, Any], conversation_id: str | None) -> None:
|
||||
"""Update kwargs with conversation id.
|
||||
|
||||
Args:
|
||||
kwargs: The keyword arguments dictionary to update.
|
||||
conversation_id: The conversation ID to set, or None to skip.
|
||||
"""
|
||||
if conversation_id is None:
|
||||
return
|
||||
if "chat_options" in kwargs:
|
||||
@@ -802,15 +1047,14 @@ def update_conversation_id(kwargs: dict[str, Any], conversation_id: str | None)
|
||||
|
||||
def _handle_function_calls_response(
|
||||
func: Callable[..., Awaitable["ChatResponse"]],
|
||||
*,
|
||||
max_iterations: int = 10,
|
||||
) -> Callable[..., Awaitable["ChatResponse"]]:
|
||||
"""Decorate the get_response method to enable function calls.
|
||||
|
||||
Args:
|
||||
func: The get_response method to decorate.
|
||||
max_iterations: The maximum number of function call iterations to perform.
|
||||
|
||||
Returns:
|
||||
A decorated function that handles function calls automatically.
|
||||
"""
|
||||
|
||||
def decorator(
|
||||
@@ -835,10 +1079,17 @@ def _handle_function_calls_response(
|
||||
# because the underlying function may not preserve it in kwargs
|
||||
stored_middleware_pipeline = kwargs.get("_function_middleware_pipeline")
|
||||
|
||||
# Get max_iterations from instance additional_properties or class attribute
|
||||
instance_max_iterations: int = DEFAULT_MAX_ITERATIONS
|
||||
if hasattr(self, "additional_properties") and self.additional_properties:
|
||||
instance_max_iterations = self.additional_properties.get("max_iterations", DEFAULT_MAX_ITERATIONS)
|
||||
elif hasattr(self.__class__, "MAX_ITERATIONS"):
|
||||
instance_max_iterations = getattr(self.__class__, "MAX_ITERATIONS", DEFAULT_MAX_ITERATIONS)
|
||||
|
||||
prepped_messages = prepare_messages(messages)
|
||||
response: "ChatResponse | None" = None
|
||||
fcc_messages: "list[ChatMessage]" = []
|
||||
for attempt_idx in range(max_iterations):
|
||||
for attempt_idx in range(instance_max_iterations):
|
||||
response = await func(self, messages=prepped_messages, **kwargs)
|
||||
# if there are function calls, we will handle them first
|
||||
function_results = {
|
||||
@@ -851,7 +1102,7 @@ def _handle_function_calls_response(
|
||||
]
|
||||
|
||||
if response.conversation_id is not None:
|
||||
update_conversation_id(kwargs, response.conversation_id)
|
||||
_update_conversation_id(kwargs, response.conversation_id)
|
||||
prepped_messages = []
|
||||
|
||||
tools = kwargs.get("tools")
|
||||
@@ -909,15 +1160,14 @@ def _handle_function_calls_response(
|
||||
|
||||
def _handle_function_calls_streaming_response(
|
||||
func: Callable[..., AsyncIterable["ChatResponseUpdate"]],
|
||||
*,
|
||||
max_iterations: int = 10,
|
||||
) -> Callable[..., AsyncIterable["ChatResponseUpdate"]]:
|
||||
"""Decorate the get_streaming_response method to handle function calls.
|
||||
|
||||
Args:
|
||||
func: The get_streaming_response method to decorate.
|
||||
max_iterations: The maximum number of function call iterations to perform.
|
||||
|
||||
Returns:
|
||||
A decorated function that handles function calls in streaming mode.
|
||||
"""
|
||||
|
||||
def decorator(
|
||||
@@ -943,8 +1193,15 @@ def _handle_function_calls_streaming_response(
|
||||
# because the underlying function may not preserve it in kwargs
|
||||
stored_middleware_pipeline = kwargs.get("_function_middleware_pipeline")
|
||||
|
||||
# Get max_iterations from instance additional_properties or class attribute
|
||||
instance_max_iterations: int = DEFAULT_MAX_ITERATIONS
|
||||
if hasattr(self, "additional_properties") and self.additional_properties:
|
||||
instance_max_iterations = self.additional_properties.get("max_iterations", DEFAULT_MAX_ITERATIONS)
|
||||
elif hasattr(self.__class__, "MAX_ITERATIONS"):
|
||||
instance_max_iterations = getattr(self.__class__, "MAX_ITERATIONS", DEFAULT_MAX_ITERATIONS)
|
||||
|
||||
prepped_messages = prepare_messages(messages)
|
||||
for attempt_idx in range(max_iterations):
|
||||
for attempt_idx in range(instance_max_iterations):
|
||||
all_updates: list["ChatResponseUpdate"] = []
|
||||
async for update in func(self, messages=prepped_messages, **kwargs):
|
||||
all_updates.append(update)
|
||||
@@ -971,7 +1228,7 @@ def _handle_function_calls_streaming_response(
|
||||
# When conversation id is present, it means that messages are hosted on the server.
|
||||
# In this case, we need to update kwargs with conversation id and also clear messages
|
||||
if response.conversation_id is not None:
|
||||
update_conversation_id(kwargs, response.conversation_id)
|
||||
_update_conversation_id(kwargs, response.conversation_id)
|
||||
prepped_messages = []
|
||||
|
||||
tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = kwargs.get("tools")
|
||||
@@ -1008,16 +1265,51 @@ def _handle_function_calls_streaming_response(
|
||||
def use_function_invocation(
|
||||
chat_client: type[TChatClient],
|
||||
) -> type[TChatClient]:
|
||||
"""Class decorator that enables tool calling for a chat client."""
|
||||
"""Class decorator that enables tool calling for a chat client.
|
||||
|
||||
This decorator wraps the ``get_response`` and ``get_streaming_response`` methods
|
||||
to automatically handle function calls from the model, execute them, and return
|
||||
the results back to the model for further processing.
|
||||
|
||||
Args:
|
||||
chat_client: The chat client class to decorate.
|
||||
|
||||
Returns:
|
||||
The decorated chat client class with function invocation enabled.
|
||||
|
||||
Raises:
|
||||
ChatClientInitializationError: If the chat client does not have the required methods.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import use_function_invocation, BaseChatClient
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
class MyCustomClient(BaseChatClient):
|
||||
async def get_response(self, messages, **kwargs):
|
||||
# Implementation here
|
||||
pass
|
||||
|
||||
async def get_streaming_response(self, messages, **kwargs):
|
||||
# Implementation here
|
||||
pass
|
||||
|
||||
|
||||
# The client now automatically handles function calls
|
||||
client = MyCustomClient()
|
||||
"""
|
||||
if getattr(chat_client, FUNCTION_INVOKING_CHAT_CLIENT_MARKER, False):
|
||||
return chat_client
|
||||
|
||||
max_iterations = DEFAULT_MAX_ITERATIONS
|
||||
# Set MAX_ITERATIONS as a class variable if not already set
|
||||
if not hasattr(chat_client, "MAX_ITERATIONS"):
|
||||
chat_client.MAX_ITERATIONS = DEFAULT_MAX_ITERATIONS # type: ignore
|
||||
|
||||
try:
|
||||
chat_client.get_response = _handle_function_calls_response( # type: ignore
|
||||
func=chat_client.get_response, # type: ignore
|
||||
max_iterations=max_iterations,
|
||||
)
|
||||
except AttributeError as ex:
|
||||
raise ChatClientInitializationError(
|
||||
@@ -1026,7 +1318,6 @@ def use_function_invocation(
|
||||
try:
|
||||
chat_client.get_streaming_response = _handle_function_calls_streaming_response( # type: ignore
|
||||
func=chat_client.get_streaming_response,
|
||||
max_iterations=max_iterations,
|
||||
)
|
||||
except AttributeError as ex:
|
||||
raise ChatClientInitializationError(
|
||||
|
||||
@@ -200,6 +200,34 @@ class UsageDetails(SerializationMixin):
|
||||
output_token_count: The number of tokens in the output.
|
||||
total_token_count: The total number of tokens used to produce the response.
|
||||
additional_counts: A dictionary of additional token counts, can be set by passing kwargs.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import UsageDetails
|
||||
|
||||
# Create usage details
|
||||
usage = UsageDetails(
|
||||
input_token_count=100,
|
||||
output_token_count=50,
|
||||
total_token_count=150,
|
||||
)
|
||||
print(usage.total_token_count) # 150
|
||||
|
||||
# With additional counts
|
||||
usage = UsageDetails(
|
||||
input_token_count=100,
|
||||
output_token_count=50,
|
||||
total_token_count=150,
|
||||
reasoning_tokens=25,
|
||||
)
|
||||
print(usage.additional_counts["reasoning_tokens"]) # 25
|
||||
|
||||
# Combine usage details
|
||||
usage1 = UsageDetails(input_token_count=100, output_token_count=50)
|
||||
usage2 = UsageDetails(input_token_count=200, output_token_count=100)
|
||||
combined = usage1 + usage2
|
||||
print(combined.input_token_count) # 300
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"_extra_counts"}
|
||||
@@ -332,7 +360,17 @@ class UsageDetails(SerializationMixin):
|
||||
|
||||
|
||||
class TextSpanRegion(SerializationMixin):
|
||||
"""Represents a region of text that has been annotated."""
|
||||
"""Represents a region of text that has been annotated.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import TextSpanRegion
|
||||
|
||||
# Create a text span region
|
||||
region = TextSpanRegion(start_index=0, end_index=10)
|
||||
print(region.type) # "text_span"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -444,6 +482,20 @@ class CitationAnnotation(BaseAnnotation):
|
||||
annotated_regions: A list of regions that have been annotated with this citation.
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content from an underlying implementation.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import CitationAnnotation, TextSpanRegion
|
||||
|
||||
# Create a citation annotation
|
||||
citation = CitationAnnotation(
|
||||
title="Agent Framework Documentation",
|
||||
url="https://example.com/docs",
|
||||
snippet="This is a relevant excerpt...",
|
||||
annotated_regions=[TextSpanRegion(start_index=0, end_index=25)],
|
||||
)
|
||||
print(citation.title) # "Agent Framework Documentation"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -576,6 +628,21 @@ class TextContent(BaseContent):
|
||||
annotations: Optional annotations associated with the content.
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import TextContent
|
||||
|
||||
# Create basic text content
|
||||
text = TextContent(text="Hello, world!")
|
||||
print(text.text) # "Hello, world!"
|
||||
|
||||
# Concatenate text content
|
||||
text1 = TextContent(text="Hello, ")
|
||||
text2 = TextContent(text="world!")
|
||||
combined = text1 + text2
|
||||
print(combined.text) # "Hello, world!"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -701,6 +768,21 @@ class TextReasoningContent(BaseContent):
|
||||
annotations: Optional annotations associated with the content.
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import TextReasoningContent
|
||||
|
||||
# Create reasoning content
|
||||
reasoning = TextReasoningContent(text="Let me think step by step...")
|
||||
print(reasoning.text) # "Let me think step by step..."
|
||||
|
||||
# Concatenate reasoning content
|
||||
reasoning1 = TextReasoningContent(text="First, ")
|
||||
reasoning2 = TextReasoningContent(text="second, ")
|
||||
combined = reasoning1 + reasoning2
|
||||
print(combined.text) # "First, second, "
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -823,6 +905,22 @@ class DataContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import DataContent
|
||||
|
||||
# Create from binary data
|
||||
image_data = b"raw image bytes"
|
||||
data_content = DataContent(data=image_data, media_type="image/png")
|
||||
|
||||
# Create from data URI
|
||||
data_uri = "data:image/png;base64,iVBORw0KGgoAAAANS..."
|
||||
data_content = DataContent(uri=data_uri)
|
||||
|
||||
# Check media type
|
||||
if data_content.has_top_level_media_type("image"):
|
||||
print("This is an image")
|
||||
"""
|
||||
|
||||
@overload
|
||||
@@ -960,6 +1058,26 @@ class UriContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import UriContent
|
||||
|
||||
# Create URI content for an image
|
||||
image_uri = UriContent(
|
||||
uri="https://example.com/image.png",
|
||||
media_type="image/png",
|
||||
)
|
||||
|
||||
# Create URI content for a document
|
||||
doc_uri = UriContent(
|
||||
uri="https://example.com/document.pdf",
|
||||
media_type="application/pdf",
|
||||
)
|
||||
|
||||
# Check if it's an image
|
||||
if image_uri.has_top_level_media_type("image"):
|
||||
print("This is an image URI")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -1028,7 +1146,22 @@ class ErrorContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ErrorContent
|
||||
|
||||
# Create an error content
|
||||
error = ErrorContent(
|
||||
message="Failed to process request",
|
||||
error_code="PROCESSING_ERROR",
|
||||
details="The input format was invalid",
|
||||
)
|
||||
print(str(error)) # "Error PROCESSING_ERROR: Failed to process request"
|
||||
|
||||
# Error without code
|
||||
simple_error = ErrorContent(message="Something went wrong")
|
||||
print(str(simple_error)) # "Something went wrong"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -1082,6 +1215,28 @@ class FunctionCallContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import FunctionCallContent
|
||||
|
||||
# Create a function call
|
||||
func_call = FunctionCallContent(
|
||||
call_id="call_123",
|
||||
name="get_weather",
|
||||
arguments={"location": "Seattle", "unit": "celsius"},
|
||||
)
|
||||
|
||||
# Parse arguments
|
||||
args = func_call.parse_arguments()
|
||||
print(args) # {"location": "Seattle", "unit": "celsius"}
|
||||
|
||||
# Create with string arguments (gradual completion)
|
||||
func_call_partial = FunctionCallContent(
|
||||
call_id="call_124",
|
||||
name="search",
|
||||
arguments='{"query": "weather"}',
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -1170,6 +1325,23 @@ class FunctionResultContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import FunctionResultContent
|
||||
|
||||
# Create a successful function result
|
||||
result = FunctionResultContent(
|
||||
call_id="call_123",
|
||||
result={"temperature": 22, "condition": "sunny"},
|
||||
)
|
||||
|
||||
# Create a failed function result
|
||||
failed_result = FunctionResultContent(
|
||||
call_id="call_124",
|
||||
result="Function execution failed",
|
||||
exception=ValueError("Invalid location"),
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -1216,6 +1388,20 @@ class UsageContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import UsageContent, UsageDetails
|
||||
|
||||
# Create usage content
|
||||
usage = UsageContent(
|
||||
details=UsageDetails(
|
||||
input_token_count=100,
|
||||
output_token_count=50,
|
||||
total_token_count=150,
|
||||
),
|
||||
)
|
||||
print(usage.details.total_token_count) # 150
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -1250,6 +1436,14 @@ class HostedFileContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedFileContent
|
||||
|
||||
# Create hosted file content
|
||||
file_content = HostedFileContent(file_id="file-abc123")
|
||||
print(file_content.file_id) # "file-abc123"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -1279,6 +1473,14 @@ class HostedVectorStoreContent(BaseContent):
|
||||
additional_properties: Optional additional properties associated with the content.
|
||||
raw_representation: Optional raw representation of the content.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedVectorStoreContent
|
||||
|
||||
# Create hosted vector store content
|
||||
vs_content = HostedVectorStoreContent(vector_store_id="vs-xyz789")
|
||||
print(vs_content.vector_store_id) # "vs-xyz789"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -1333,7 +1535,26 @@ class BaseUserInputRequest(BaseContent):
|
||||
|
||||
|
||||
class FunctionApprovalResponseContent(BaseContent):
|
||||
"""Represents a response for user approval of a function call."""
|
||||
"""Represents a response for user approval of a function call.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import FunctionApprovalResponseContent, FunctionCallContent
|
||||
|
||||
# Create a function approval response
|
||||
func_call = FunctionCallContent(
|
||||
call_id="call_123",
|
||||
name="send_email",
|
||||
arguments={"to": "user@example.com"},
|
||||
)
|
||||
response = FunctionApprovalResponseContent(
|
||||
approved=False,
|
||||
id="approval_001",
|
||||
function_call=func_call,
|
||||
)
|
||||
print(response.approved) # False
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1375,7 +1596,28 @@ class FunctionApprovalResponseContent(BaseContent):
|
||||
|
||||
|
||||
class FunctionApprovalRequestContent(BaseContent):
|
||||
"""Represents a request for user approval of a function call."""
|
||||
"""Represents a request for user approval of a function call.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import FunctionApprovalRequestContent, FunctionCallContent
|
||||
|
||||
# Create a function approval request
|
||||
func_call = FunctionCallContent(
|
||||
call_id="call_123",
|
||||
name="send_email",
|
||||
arguments={"to": "user@example.com", "subject": "Hello"},
|
||||
)
|
||||
approval_request = FunctionApprovalRequestContent(
|
||||
id="approval_001",
|
||||
function_call=func_call,
|
||||
)
|
||||
|
||||
# Create response
|
||||
approval_response = approval_request.create_response(approved=True)
|
||||
print(approval_response.approved) # True
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1453,6 +1695,24 @@ class Role(SerializationMixin, metaclass=EnumLike):
|
||||
USER: The role that provides user input for chat interactions.
|
||||
ASSISTANT: The role that provides responses to system-instructed, user-prompted input.
|
||||
TOOL: The role that provides additional information and references in response to tool use requests.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import Role
|
||||
|
||||
# Use predefined role constants
|
||||
system_role = Role.SYSTEM
|
||||
user_role = Role.USER
|
||||
assistant_role = Role.ASSISTANT
|
||||
tool_role = Role.TOOL
|
||||
|
||||
# Create custom role
|
||||
custom_role = Role(value="custom")
|
||||
|
||||
# Compare roles
|
||||
print(system_role == Role.SYSTEM) # True
|
||||
print(system_role.value) # "system"
|
||||
"""
|
||||
|
||||
# Constants configuration for EnumLike metaclass
|
||||
@@ -1501,6 +1761,21 @@ class FinishReason(SerializationMixin, metaclass=EnumLike):
|
||||
|
||||
Attributes:
|
||||
value: The string representation of the finish reason.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import FinishReason
|
||||
|
||||
# Use predefined finish reason constants
|
||||
stop_reason = FinishReason.STOP # Normal completion
|
||||
length_reason = FinishReason.LENGTH # Max tokens reached
|
||||
tool_calls_reason = FinishReason.TOOL_CALLS # Tool calls triggered
|
||||
filter_reason = FinishReason.CONTENT_FILTER # Content filter triggered
|
||||
|
||||
# Check finish reason
|
||||
if stop_reason == FinishReason.STOP:
|
||||
print("Response completed normally")
|
||||
"""
|
||||
|
||||
# Constants configuration for EnumLike metaclass
|
||||
@@ -1548,7 +1823,7 @@ class FinishReason(SerializationMixin, metaclass=EnumLike):
|
||||
|
||||
|
||||
class ChatMessage(SerializationMixin):
|
||||
"""Represents a chat message.
|
||||
r"""Represents a chat message.
|
||||
|
||||
Attributes:
|
||||
role: The role of the author of the message.
|
||||
@@ -1558,6 +1833,38 @@ class ChatMessage(SerializationMixin):
|
||||
additional_properties: Any additional properties associated with the chat message.
|
||||
raw_representation: The raw representation of the chat message from an underlying implementation.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
|
||||
# Create a message with text
|
||||
user_msg = ChatMessage(role="user", text="What's the weather?")
|
||||
print(user_msg.text) # "What's the weather?"
|
||||
|
||||
# Create a message with role string
|
||||
system_msg = ChatMessage(role="system", text="You are a helpful assistant.")
|
||||
|
||||
# Create a message with contents
|
||||
assistant_msg = ChatMessage(
|
||||
role="assistant",
|
||||
contents=[TextContent(text="The weather is sunny!")],
|
||||
)
|
||||
print(assistant_msg.text) # "The weather is sunny!"
|
||||
|
||||
# Serialization - to_dict and from_dict
|
||||
msg_dict = user_msg.to_dict()
|
||||
# {'type': 'chat_message', 'role': {'type': 'role', 'value': 'user'},
|
||||
# 'contents': [{'type': 'text', 'text': "What's the weather?"}], 'additional_properties': {}}
|
||||
restored_msg = ChatMessage.from_dict(msg_dict)
|
||||
print(restored_msg.text) # "What's the weather?"
|
||||
|
||||
# Serialization - to_json and from_json
|
||||
msg_json = user_msg.to_json()
|
||||
# '{"type": "chat_message", "role": {"type": "role", "value": "user"}, "contents": [...], ...}'
|
||||
restored_from_json = ChatMessage.from_json(msg_json)
|
||||
print(restored_from_json.role.value) # "user"
|
||||
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
||||
@@ -1790,6 +2097,40 @@ class ChatResponse(SerializationMixin):
|
||||
structured_output: The structured output of the chat response, if applicable.
|
||||
additional_properties: Any additional properties associated with the chat response.
|
||||
raw_representation: The raw representation of the chat response from an underlying implementation.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatResponse, ChatMessage
|
||||
|
||||
# Create a simple text response
|
||||
response = ChatResponse(text="Hello, how can I help you?")
|
||||
print(response.text) # "Hello, how can I help you?"
|
||||
|
||||
# Create a response with messages
|
||||
msg = ChatMessage(role="assistant", text="The weather is sunny.")
|
||||
response = ChatResponse(
|
||||
messages=[msg],
|
||||
finish_reason="stop",
|
||||
model_id="gpt-4",
|
||||
)
|
||||
|
||||
# Combine streaming updates
|
||||
updates = [...] # List of ChatResponseUpdate objects
|
||||
response = ChatResponse.from_chat_response_updates(updates)
|
||||
|
||||
# Serialization - to_dict and from_dict
|
||||
response_dict = response.to_dict()
|
||||
# {'type': 'chat_response', 'messages': [...], 'model_id': 'gpt-4',
|
||||
# 'finish_reason': {'type': 'finish_reason', 'value': 'stop'}}
|
||||
restored_response = ChatResponse.from_dict(response_dict)
|
||||
print(restored_response.model_id) # "gpt-4"
|
||||
|
||||
# Serialization - to_json and from_json
|
||||
response_json = response.to_json()
|
||||
# '{"type": "chat_response", "messages": [...], "model_id": "gpt-4", ...}'
|
||||
restored_from_json = ChatResponse.from_json(response_json)
|
||||
print(restored_from_json.text) # "The weather is sunny."
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation", "additional_properties"}
|
||||
@@ -1993,6 +2334,35 @@ class ChatResponseUpdate(SerializationMixin):
|
||||
additional_properties: Any additional properties associated with the chat response update.
|
||||
raw_representation: The raw representation of the chat response update from an underlying implementation.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatResponseUpdate, TextContent
|
||||
|
||||
# Create a response update
|
||||
update = ChatResponseUpdate(
|
||||
contents=[TextContent(text="Hello")],
|
||||
role="assistant",
|
||||
message_id="msg_123",
|
||||
)
|
||||
print(update.text) # "Hello"
|
||||
|
||||
# Create update with text shorthand
|
||||
update = ChatResponseUpdate(text="World!", role="assistant")
|
||||
|
||||
# Serialization - to_dict and from_dict
|
||||
update_dict = update.to_dict()
|
||||
# {'type': 'chat_response_update', 'contents': [{'type': 'text', 'text': 'Hello'}],
|
||||
# 'role': {'type': 'role', 'value': 'assistant'}, 'message_id': 'msg_123'}
|
||||
restored_update = ChatResponseUpdate.from_dict(update_dict)
|
||||
print(restored_update.text) # "Hello"
|
||||
|
||||
# Serialization - to_json and from_json
|
||||
update_json = update.to_json()
|
||||
# '{"type": "chat_response_update", "contents": [{"type": "text", "text": "Hello"}], ...}'
|
||||
restored_from_json = ChatResponseUpdate.from_json(update_json)
|
||||
print(restored_from_json.message_id) # "msg_123"
|
||||
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
||||
@@ -2095,6 +2465,37 @@ class AgentRunResponse(SerializationMixin):
|
||||
Provides one or more response messages and metadata about the response.
|
||||
A typical response will contain a single message, but may contain multiple
|
||||
messages in scenarios involving function calls, RAG retrievals, or complex logic.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import AgentRunResponse, ChatMessage
|
||||
|
||||
# Create agent response
|
||||
msg = ChatMessage(role="assistant", text="Task completed successfully.")
|
||||
response = AgentRunResponse(messages=[msg], response_id="run_123")
|
||||
print(response.text) # "Task completed successfully."
|
||||
|
||||
# Access user input requests
|
||||
user_requests = response.user_input_requests
|
||||
print(len(user_requests)) # 0
|
||||
|
||||
# Combine streaming updates
|
||||
updates = [...] # List of AgentRunResponseUpdate objects
|
||||
response = AgentRunResponse.from_agent_run_response_updates(updates)
|
||||
|
||||
# Serialization - to_dict and from_dict
|
||||
response_dict = response.to_dict()
|
||||
# {'type': 'agent_run_response', 'messages': [...], 'response_id': 'run_123',
|
||||
# 'additional_properties': {}}
|
||||
restored_response = AgentRunResponse.from_dict(response_dict)
|
||||
print(restored_response.response_id) # "run_123"
|
||||
|
||||
# Serialization - to_json and from_json
|
||||
response_json = response.to_json()
|
||||
# '{"type": "agent_run_response", "messages": [...], "response_id": "run_123", ...}'
|
||||
restored_from_json = AgentRunResponse.from_json(response_json)
|
||||
print(restored_from_json.text) # "Task completed successfully."
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
||||
@@ -2217,7 +2618,37 @@ class AgentRunResponse(SerializationMixin):
|
||||
|
||||
|
||||
class AgentRunResponseUpdate(SerializationMixin):
|
||||
"""Represents a single streaming response chunk from an Agent."""
|
||||
"""Represents a single streaming response chunk from an Agent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import AgentRunResponseUpdate, TextContent
|
||||
|
||||
# Create an agent run update
|
||||
update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Processing...")],
|
||||
role="assistant",
|
||||
response_id="run_123",
|
||||
)
|
||||
print(update.text) # "Processing..."
|
||||
|
||||
# Check for user input requests
|
||||
user_requests = update.user_input_requests
|
||||
|
||||
# Serialization - to_dict and from_dict
|
||||
update_dict = update.to_dict()
|
||||
# {'type': 'agent_run_response_update', 'contents': [{'type': 'text', 'text': 'Processing...'}],
|
||||
# 'role': {'type': 'role', 'value': 'assistant'}, 'response_id': 'run_123'}
|
||||
restored_update = AgentRunResponseUpdate.from_dict(update_dict)
|
||||
print(restored_update.response_id) # "run_123"
|
||||
|
||||
# Serialization - to_json and from_json
|
||||
update_json = update.to_json()
|
||||
# '{"type": "agent_run_response_update", "contents": [{"type": "text", "text": "Processing..."}], ...}'
|
||||
restored_from_json = AgentRunResponseUpdate.from_json(update_json)
|
||||
print(restored_from_json.text) # "Processing..."
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"raw_representation"}
|
||||
|
||||
@@ -2294,7 +2725,25 @@ class AgentRunResponseUpdate(SerializationMixin):
|
||||
|
||||
|
||||
class ToolMode(SerializationMixin, metaclass=EnumLike):
|
||||
"""Defines if and how tools are used in a chat request."""
|
||||
"""Defines if and how tools are used in a chat request.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ToolMode
|
||||
|
||||
# Use predefined tool modes
|
||||
auto_mode = ToolMode.AUTO # Model decides when to use tools
|
||||
required_mode = ToolMode.REQUIRED_ANY # Model must use a tool
|
||||
none_mode = ToolMode.NONE # No tools allowed
|
||||
|
||||
# Require a specific function
|
||||
specific_mode = ToolMode.REQUIRED(function_name="get_weather")
|
||||
print(specific_mode.required_function_name) # "get_weather"
|
||||
|
||||
# Compare modes
|
||||
print(auto_mode == "auto") # True
|
||||
"""
|
||||
|
||||
# Constants configuration for EnumLike metaclass
|
||||
_constants: ClassVar[dict[str, tuple[str, ...]]] = {
|
||||
@@ -2355,7 +2804,46 @@ class ToolMode(SerializationMixin, metaclass=EnumLike):
|
||||
|
||||
|
||||
class ChatOptions(SerializationMixin):
|
||||
"""Common request settings for AI services."""
|
||||
"""Common request settings for AI services.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ChatOptions, ai_function
|
||||
|
||||
# Create basic chat options
|
||||
options = ChatOptions(
|
||||
model_id="gpt-4",
|
||||
temperature=0.7,
|
||||
max_tokens=1000,
|
||||
)
|
||||
|
||||
|
||||
# With tools
|
||||
@ai_function
|
||||
def get_weather(location: str) -> str:
|
||||
'''Get weather for a location.'''
|
||||
return f"Weather in {location}"
|
||||
|
||||
|
||||
options = ChatOptions(
|
||||
model_id="gpt-4",
|
||||
tools=get_weather,
|
||||
tool_choice="auto",
|
||||
)
|
||||
|
||||
# Require a specific tool to be called
|
||||
options_required = ChatOptions(
|
||||
model_id="gpt-4",
|
||||
tools=get_weather,
|
||||
tool_choice=ToolMode.REQUIRED(function_name="get_weather"),
|
||||
)
|
||||
|
||||
# Combine options
|
||||
base_options = ChatOptions(temperature=0.5)
|
||||
extended_options = ChatOptions(max_tokens=500, tools=get_weather)
|
||||
combined = base_options & extended_options
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"_tools"} # Internal field, use .tools property
|
||||
|
||||
|
||||
@@ -185,25 +185,26 @@ class ConcurrentBuilder:
|
||||
- `with_custom_aggregator(...)` overrides the default aggregator with an Executor or callback.
|
||||
|
||||
Usage:
|
||||
```python
|
||||
from agent_framework import ConcurrentBuilder
|
||||
|
||||
# Minimal: use default aggregator (returns list[ChatMessage])
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).build()
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ConcurrentBuilder
|
||||
|
||||
# Minimal: use default aggregator (returns list[ChatMessage])
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).build()
|
||||
|
||||
|
||||
# Custom aggregator via callback (sync or async). The callback receives
|
||||
# list[AgentExecutorResponse] and its return value becomes the workflow's output.
|
||||
def summarize(results):
|
||||
return " | ".join(r.agent_run_response.messages[-1].text for r in results)
|
||||
# Custom aggregator via callback (sync or async). The callback receives
|
||||
# list[AgentExecutorResponse] and its return value becomes the workflow's output.
|
||||
def summarize(results):
|
||||
return " | ".join(r.agent_run_response.messages[-1].text for r in results)
|
||||
|
||||
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_custom_aggregator(summarize).build()
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_custom_aggregator(summarize).build()
|
||||
|
||||
|
||||
# Enable checkpoint persistence so runs can resume
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_checkpointing(storage).build()
|
||||
```
|
||||
# Enable checkpoint persistence so runs can resume
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_checkpointing(storage).build()
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -223,12 +224,13 @@ class ConcurrentBuilder:
|
||||
TypeError: if any entry is not AgentProtocol or Executor
|
||||
|
||||
Example:
|
||||
```python
|
||||
wf = ConcurrentBuilder().participants([researcher_agent, marketer_agent, legal_agent]).build()
|
||||
|
||||
# Mixing agent(s) and executor(s) is supported
|
||||
wf2 = ConcurrentBuilder().participants([researcher_agent, my_custom_executor]).build()
|
||||
```
|
||||
.. code-block:: python
|
||||
|
||||
wf = ConcurrentBuilder().participants([researcher_agent, marketer_agent, legal_agent]).build()
|
||||
|
||||
# Mixing agent(s) and executor(s) is supported
|
||||
wf2 = ConcurrentBuilder().participants([researcher_agent, my_custom_executor]).build()
|
||||
"""
|
||||
if not participants:
|
||||
raise ValueError("participants cannot be empty")
|
||||
@@ -264,14 +266,15 @@ class ConcurrentBuilder:
|
||||
If the callback returns a non-None value, it becomes the workflow's output.
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Callback-based aggregator (string result)
|
||||
async def summarize(results):
|
||||
return " | ".join(r.agent_run_response.messages[-1].text for r in results)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Callback-based aggregator (string result)
|
||||
async def summarize(results):
|
||||
return " | ".join(r.agent_run_response.messages[-1].text for r in results)
|
||||
|
||||
|
||||
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_custom_aggregator(summarize).build()
|
||||
```
|
||||
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_custom_aggregator(summarize).build()
|
||||
"""
|
||||
if isinstance(aggregator, Executor):
|
||||
self._aggregator = aggregator
|
||||
@@ -303,9 +306,10 @@ class ConcurrentBuilder:
|
||||
ValueError: if no participants were defined
|
||||
|
||||
Example:
|
||||
```python
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2]).build()
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2]).build()
|
||||
"""
|
||||
if not self._participants:
|
||||
raise ValueError("No participants provided. Call .participants([...]) first.")
|
||||
|
||||
@@ -75,32 +75,35 @@ class Executor(DictConvertible):
|
||||
|
||||
### Input Types
|
||||
The types of messages an executor can process, discovered from handler method signatures:
|
||||
```python
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def handle_string(self, message: str, ctx: WorkflowContext) -> None:
|
||||
# This executor can handle 'str' input types
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def handle_string(self, message: str, ctx: WorkflowContext) -> None:
|
||||
# This executor can handle 'str' input types
|
||||
Access via the `input_types` property.
|
||||
|
||||
### Output Types
|
||||
The types of messages an executor can send to other executors via `ctx.send_message()`:
|
||||
```python
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def handle_data(self, message: str, ctx: WorkflowContext[int | bool]) -> None:
|
||||
# This executor can send 'int' or 'bool' messages
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def handle_data(self, message: str, ctx: WorkflowContext[int | bool]) -> None:
|
||||
# This executor can send 'int' or 'bool' messages
|
||||
Access via the `output_types` property.
|
||||
|
||||
### Workflow Output Types
|
||||
The types of data an executor can emit as workflow-level outputs via `ctx.yield_output()`:
|
||||
```python
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def process(self, message: str, ctx: WorkflowContext[int, str]) -> None:
|
||||
# Can send 'int' messages AND yield 'str' workflow outputs
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def process(self, message: str, ctx: WorkflowContext[int, str]) -> None:
|
||||
# Can send 'int' messages AND yield 'str' workflow outputs
|
||||
Access via the `workflow_output_types` property.
|
||||
|
||||
## Handler Discovery
|
||||
@@ -108,66 +111,73 @@ class Executor(DictConvertible):
|
||||
|
||||
### @handler Decorator
|
||||
Marks methods that process incoming messages:
|
||||
```python
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def handle_text(self, message: str, ctx: WorkflowContext[str]) -> None:
|
||||
await ctx.send_message(message.upper())
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExecutor(Executor):
|
||||
@handler
|
||||
async def handle_text(self, message: str, ctx: WorkflowContext[str]) -> None:
|
||||
await ctx.send_message(message.upper())
|
||||
|
||||
### Sub-workflow Request Interception
|
||||
Use @handler methods to intercept sub-workflow requests:
|
||||
```python
|
||||
class ParentExecutor(Executor):
|
||||
@handler
|
||||
async def handle_domain_request(
|
||||
self,
|
||||
request: DomainRequest, # Subclass of RequestInfoMessage
|
||||
ctx: WorkflowContext[RequestResponse[RequestInfoMessage, Any] | DomainRequest],
|
||||
) -> None:
|
||||
if self.is_allowed(request.domain):
|
||||
response = RequestResponse(data=True, original_request=request, request_id=request.request_id)
|
||||
await ctx.send_message(response, target_id=request.source_executor_id)
|
||||
else:
|
||||
await ctx.send_message(request) # Forward to external
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ParentExecutor(Executor):
|
||||
@handler
|
||||
async def handle_domain_request(
|
||||
self,
|
||||
request: DomainRequest, # Subclass of RequestInfoMessage
|
||||
ctx: WorkflowContext[RequestResponse[RequestInfoMessage, Any] | DomainRequest],
|
||||
) -> None:
|
||||
if self.is_allowed(request.domain):
|
||||
response = RequestResponse(data=True, original_request=request, request_id=request.request_id)
|
||||
await ctx.send_message(response, target_id=request.source_executor_id)
|
||||
else:
|
||||
await ctx.send_message(request) # Forward to external
|
||||
|
||||
## Context Types
|
||||
Handler methods receive different WorkflowContext variants based on their type annotations:
|
||||
|
||||
### WorkflowContext (no type parameters)
|
||||
For handlers that only perform side effects without sending messages or yielding outputs:
|
||||
```python
|
||||
class LoggingExecutor(Executor):
|
||||
@handler
|
||||
async def log_message(self, msg: str, ctx: WorkflowContext) -> None:
|
||||
print(f"Received: {msg}") # Only logging, no outputs
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class LoggingExecutor(Executor):
|
||||
@handler
|
||||
async def log_message(self, msg: str, ctx: WorkflowContext) -> None:
|
||||
print(f"Received: {msg}") # Only logging, no outputs
|
||||
|
||||
### WorkflowContext[T_Out]
|
||||
Enables sending messages of type T_Out via `ctx.send_message()`:
|
||||
```python
|
||||
class ProcessorExecutor(Executor):
|
||||
@handler
|
||||
async def handler(self, msg: str, ctx: WorkflowContext[int]) -> None:
|
||||
await ctx.send_message(42) # Can send int messages
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProcessorExecutor(Executor):
|
||||
@handler
|
||||
async def handler(self, msg: str, ctx: WorkflowContext[int]) -> None:
|
||||
await ctx.send_message(42) # Can send int messages
|
||||
|
||||
### WorkflowContext[T_Out, T_W_Out]
|
||||
Enables both sending messages (T_Out) and yielding workflow outputs (T_W_Out):
|
||||
```python
|
||||
class DualOutputExecutor(Executor):
|
||||
@handler
|
||||
async def handler(self, msg: str, ctx: WorkflowContext[int, str]) -> None:
|
||||
await ctx.send_message(42) # Send int message
|
||||
await ctx.yield_output("done") # Yield str workflow output
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DualOutputExecutor(Executor):
|
||||
@handler
|
||||
async def handler(self, msg: str, ctx: WorkflowContext[int, str]) -> None:
|
||||
await ctx.send_message(42) # Send int message
|
||||
await ctx.yield_output("done") # Yield str workflow output
|
||||
|
||||
## Function Executors
|
||||
Simple functions can be converted to executors using the `@executor` decorator:
|
||||
```python
|
||||
@executor
|
||||
async def process_text(text: str, ctx: WorkflowContext[str]) -> None:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@executor
|
||||
async def process_text(text: str, ctx: WorkflowContext[str]) -> None:
|
||||
await ctx.send_message(text.upper())
|
||||
|
||||
|
||||
|
||||
@@ -101,14 +101,15 @@ class SequentialBuilder:
|
||||
- The final output is the conversation produced by the last participant
|
||||
|
||||
Usage:
|
||||
```python
|
||||
from agent_framework import SequentialBuilder
|
||||
|
||||
workflow = SequentialBuilder().participants([agent1, agent2, summarizer_exec]).build()
|
||||
.. code-block:: python
|
||||
|
||||
# Enable checkpoint persistence
|
||||
workflow = SequentialBuilder().participants([agent1, agent2]).with_checkpointing(storage).build()
|
||||
```
|
||||
from agent_framework import SequentialBuilder
|
||||
|
||||
workflow = SequentialBuilder().participants([agent1, agent2, summarizer_exec]).build()
|
||||
|
||||
# Enable checkpoint persistence
|
||||
workflow = SequentialBuilder().participants([agent1, agent2]).with_checkpointing(storage).build()
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -283,34 +283,38 @@ class WorkflowContext(Generic[T_Out, T_W_Out]):
|
||||
|
||||
### WorkflowContext (no parameters)
|
||||
For executors that only perform side effects without sending messages or yielding outputs:
|
||||
```python
|
||||
async def log_handler(message: str, ctx: WorkflowContext) -> None:
|
||||
print(f"Received: {message}") # Only side effects
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def log_handler(message: str, ctx: WorkflowContext) -> None:
|
||||
print(f"Received: {message}") # Only side effects
|
||||
|
||||
### WorkflowContext[T_Out]
|
||||
Enables sending messages of type T_Out to other executors:
|
||||
```python
|
||||
async def processor(message: str, ctx: WorkflowContext[int]) -> None:
|
||||
result = len(message)
|
||||
await ctx.send_message(result) # Send int to downstream executors
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def processor(message: str, ctx: WorkflowContext[int]) -> None:
|
||||
result = len(message)
|
||||
await ctx.send_message(result) # Send int to downstream executors
|
||||
|
||||
### WorkflowContext[T_Out, T_W_Out]
|
||||
Enables both sending messages (T_Out) and yielding workflow outputs (T_W_Out):
|
||||
```python
|
||||
async def dual_output(message: str, ctx: WorkflowContext[int, str]) -> None:
|
||||
await ctx.send_message(42) # Send int message
|
||||
await ctx.yield_output("complete") # Yield str workflow output
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def dual_output(message: str, ctx: WorkflowContext[int, str]) -> None:
|
||||
await ctx.send_message(42) # Send int message
|
||||
await ctx.yield_output("complete") # Yield str workflow output
|
||||
|
||||
### Union Types
|
||||
Multiple types can be specified using union notation:
|
||||
```python
|
||||
async def flexible(message: str, ctx: WorkflowContext[int | str, bool | dict]) -> None:
|
||||
await ctx.send_message("text") # or send 42
|
||||
await ctx.yield_output(True) # or yield {"status": "done"}
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def flexible(message: str, ctx: WorkflowContext[int | str, bool | dict]) -> None:
|
||||
await ctx.send_message("text") # or send 42
|
||||
await ctx.yield_output(True) # or yield {"status": "done"}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -63,27 +63,29 @@ class WorkflowExecutor(Executor):
|
||||
|
||||
### Output Forwarding
|
||||
All outputs from the sub-workflow are automatically forwarded to the parent:
|
||||
```python
|
||||
# Sub-workflow yields outputs
|
||||
await ctx.yield_output("sub-workflow result")
|
||||
|
||||
# WorkflowExecutor forwards to parent via ctx.send_message()
|
||||
# Parent receives the output as a regular message
|
||||
```
|
||||
.. code-block:: python
|
||||
|
||||
# Sub-workflow yields outputs
|
||||
await ctx.yield_output("sub-workflow result")
|
||||
|
||||
# WorkflowExecutor forwards to parent via ctx.send_message()
|
||||
# Parent receives the output as a regular message
|
||||
|
||||
### Request/Response Coordination
|
||||
When sub-workflows need external information:
|
||||
```python
|
||||
# Sub-workflow makes request
|
||||
request = MyDataRequest(query="user info")
|
||||
# RequestInfoExecutor emits RequestInfoEvent
|
||||
|
||||
# WorkflowExecutor sets source_executor_id and forwards to parent
|
||||
request.source_executor_id = "child_workflow_executor_id"
|
||||
# Parent workflow can handle via @handler for RequestInfoMessage subclasses,
|
||||
# or directly forward to external source via a RequestInfoExecutor in the parent
|
||||
# workflow.
|
||||
```
|
||||
.. code-block:: python
|
||||
|
||||
# Sub-workflow makes request
|
||||
request = MyDataRequest(query="user info")
|
||||
# RequestInfoExecutor emits RequestInfoEvent
|
||||
|
||||
# WorkflowExecutor sets source_executor_id and forwards to parent
|
||||
request.source_executor_id = "child_workflow_executor_id"
|
||||
# Parent workflow can handle via @handler for RequestInfoMessage subclasses,
|
||||
# or directly forward to external source via a RequestInfoExecutor in the parent
|
||||
# workflow.
|
||||
|
||||
### State Management
|
||||
WorkflowExecutor maintains execution state across request/response cycles:
|
||||
@@ -97,17 +99,20 @@ class WorkflowExecutor(Executor):
|
||||
|
||||
### Input Types
|
||||
Matches the wrapped workflow's start executor input types:
|
||||
```python
|
||||
# If sub-workflow accepts str, WorkflowExecutor accepts str
|
||||
workflow_executor = WorkflowExecutor(my_workflow, id="wrapper")
|
||||
assert workflow_executor.input_types == my_workflow.input_types
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# If sub-workflow accepts str, WorkflowExecutor accepts str
|
||||
workflow_executor = WorkflowExecutor(my_workflow, id="wrapper")
|
||||
assert workflow_executor.input_types == my_workflow.input_types
|
||||
|
||||
### Output Types
|
||||
Combines sub-workflow outputs with request coordination types:
|
||||
```python
|
||||
# Includes all sub-workflow output types
|
||||
# Plus RequestInfoMessage if sub-workflow can make requests
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Includes all sub-workflow output types
|
||||
# Plus RequestInfoMessage if sub-workflow can make requests
|
||||
output_types = workflow.output_types + [RequestInfoMessage] # if applicable
|
||||
```
|
||||
|
||||
@@ -122,15 +127,16 @@ class WorkflowExecutor(Executor):
|
||||
|
||||
### Per-Execution State Isolation
|
||||
Each sub-workflow invocation creates an isolated ExecutionContext:
|
||||
```python
|
||||
# Multiple concurrent invocations are supported
|
||||
workflow_executor = WorkflowExecutor(my_workflow, id="concurrent_executor")
|
||||
|
||||
# Each invocation gets its own execution context
|
||||
# Execution 1: processes input_1 independently
|
||||
# Execution 2: processes input_2 independently
|
||||
# No state interference between executions
|
||||
```
|
||||
.. code-block:: python
|
||||
|
||||
# Multiple concurrent invocations are supported
|
||||
workflow_executor = WorkflowExecutor(my_workflow, id="concurrent_executor")
|
||||
|
||||
# Each invocation gets its own execution context
|
||||
# Execution 1: processes input_1 independently
|
||||
# Execution 2: processes input_2 independently
|
||||
# No state interference between executions
|
||||
|
||||
### Request/Response Coordination
|
||||
Responses are correctly routed to the originating execution:
|
||||
@@ -151,23 +157,24 @@ class WorkflowExecutor(Executor):
|
||||
- The wrapped workflow and its executors are stateless
|
||||
- Executors use WorkflowContext state management instead of instance variables
|
||||
- Any shared state is managed through WorkflowContext.get_shared_state/set_shared_state
|
||||
```python
|
||||
# Good: Stateless executor using context state
|
||||
class StatelessExecutor(Executor):
|
||||
@handler
|
||||
async def process(self, data: str, ctx: WorkflowContext[str]) -> None:
|
||||
# Use context state instead of instance variables
|
||||
state = await ctx.get_state() or {}
|
||||
state["processed"] = data
|
||||
await ctx.set_state(state)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Good: Stateless executor using context state
|
||||
class StatelessExecutor(Executor):
|
||||
@handler
|
||||
async def process(self, data: str, ctx: WorkflowContext[str]) -> None:
|
||||
# Use context state instead of instance variables
|
||||
state = await ctx.get_state() or {}
|
||||
state["processed"] = data
|
||||
await ctx.set_state(state)
|
||||
|
||||
|
||||
# Avoid: Stateful executor with instance variables
|
||||
class StatefulExecutor(Executor):
|
||||
def __init__(self):
|
||||
super().__init__(id="stateful")
|
||||
self.data = [] # This will be shared across concurrent executions!
|
||||
```
|
||||
# Avoid: Stateful executor with instance variables
|
||||
class StatefulExecutor(Executor):
|
||||
def __init__(self):
|
||||
super().__init__(id="stateful")
|
||||
self.data = [] # This will be shared across concurrent executions!
|
||||
|
||||
## Integration with Parent Workflows
|
||||
Parent workflows can intercept sub-workflow requests:
|
||||
|
||||
@@ -41,6 +41,16 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
FinishReason,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"OBSERVABILITY_SETTINGS",
|
||||
"OtelAttr",
|
||||
"get_meter",
|
||||
"get_tracer",
|
||||
"setup_observability",
|
||||
"use_agent_observability",
|
||||
"use_observability",
|
||||
]
|
||||
|
||||
|
||||
TAgent = TypeVar("TAgent", bound="AgentProtocol")
|
||||
TChatClient = TypeVar("TChatClient", bound="ChatClientProtocol")
|
||||
@@ -527,13 +537,42 @@ def get_tracer(
|
||||
schema_url: str | None = None,
|
||||
attributes: dict[str, Any] | None = None,
|
||||
) -> "trace.Tracer":
|
||||
"""Returns a `Tracer` for use by the given instrumentation library.
|
||||
|
||||
This function is a convenience wrapper for
|
||||
trace.get_tracer()
|
||||
replicating the behavior of opentelemetry.trace.TracerProvider.get_tracer.
|
||||
"""Returns a Tracer for use by the given instrumentation library.
|
||||
|
||||
This function is a convenience wrapper for trace.get_tracer() replicating
|
||||
the behavior of opentelemetry.trace.TracerProvider.get_tracer.
|
||||
If tracer_provider is omitted the current configured one is used.
|
||||
|
||||
Args:
|
||||
instrumenting_module_name: The name of the instrumenting library.
|
||||
Default is "agent_framework".
|
||||
instrumenting_library_version: The version of the instrumenting library.
|
||||
Default is the current agent_framework version.
|
||||
schema_url: Optional schema URL for the emitted telemetry.
|
||||
attributes: Optional attributes associated with the emitted telemetry.
|
||||
|
||||
Returns:
|
||||
A Tracer instance for creating spans.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import get_tracer
|
||||
|
||||
# Get default tracer
|
||||
tracer = get_tracer()
|
||||
|
||||
# Use tracer to create spans
|
||||
with tracer.start_as_current_span("my_operation") as span:
|
||||
span.set_attribute("custom.attribute", "value")
|
||||
# Your operation here
|
||||
pass
|
||||
|
||||
# Get tracer with custom module name
|
||||
custom_tracer = get_tracer(
|
||||
instrumenting_module_name="my_custom_module",
|
||||
instrumenting_library_version="1.0.0",
|
||||
)
|
||||
"""
|
||||
return trace.get_tracer(
|
||||
instrumenting_module_name=instrumenting_module_name,
|
||||
@@ -549,21 +588,44 @@ def get_meter(
|
||||
schema_url: str | None = None,
|
||||
attributes: dict[str, Any] | None = None,
|
||||
) -> "metrics.Meter":
|
||||
"""Returns a `Meter` for Agent Framework.
|
||||
"""Returns a Meter for Agent Framework.
|
||||
|
||||
This is a convenience wrapper for
|
||||
metrics.get_meter() replicating the behavior of
|
||||
opentelemetry.metrics.get_meter().
|
||||
This is a convenience wrapper for metrics.get_meter() replicating the behavior
|
||||
of opentelemetry.metrics.get_meter().
|
||||
|
||||
Args:
|
||||
name: Optional name, default is "agent_framework". The name of the
|
||||
instrumenting library.
|
||||
name: The name of the instrumenting library. Default is "agent_framework".
|
||||
version: The version of agent_framework. Default is the current version
|
||||
of the package.
|
||||
schema_url: Optional schema URL of the emitted telemetry.
|
||||
attributes: Optional attributes associated with the emitted telemetry.
|
||||
|
||||
version: Optional. The version of `agent_framework`, default is the
|
||||
current version of the package.
|
||||
Returns:
|
||||
A Meter instance for recording metrics.
|
||||
|
||||
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
|
||||
attributes: Optional. Attributes that are associated with the emitted telemetry.
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import get_meter
|
||||
|
||||
# Get default meter
|
||||
meter = get_meter()
|
||||
|
||||
# Create a counter metric
|
||||
request_counter = meter.create_counter(
|
||||
name="requests",
|
||||
description="Number of requests",
|
||||
unit="1",
|
||||
)
|
||||
request_counter.add(1, {"endpoint": "/api/chat"})
|
||||
|
||||
# Create a histogram metric
|
||||
duration_histogram = meter.create_histogram(
|
||||
name="request_duration",
|
||||
description="Request duration in seconds",
|
||||
unit="s",
|
||||
)
|
||||
duration_histogram.record(0.125, {"status": "success"})
|
||||
"""
|
||||
try:
|
||||
return metrics.get_meter(name=name, version=version, schema_url=schema_url, attributes=attributes)
|
||||
@@ -584,98 +646,75 @@ def setup_observability(
|
||||
exporters: list["LogExporter | SpanExporter | MetricExporter"] | None = None,
|
||||
vs_code_extension_port: int | None = None,
|
||||
) -> None:
|
||||
"""Convenient method to setup observability for the application.
|
||||
"""Setup observability for the application with OpenTelemetry.
|
||||
|
||||
This method will create the exporters and the providers for the application,
|
||||
based on the provided values and the environment variables.
|
||||
This method creates the exporters and providers for the application based on
|
||||
the provided values and environment variables.
|
||||
|
||||
Call this method once during application startup, before any telemetry is captured.
|
||||
DO NOT call this method multiple times, as it may lead to unexpected behavior.
|
||||
|
||||
If you have configured the providers manually, calling this method will not have any effect:
|
||||
|
||||
```python
|
||||
# Some where in your application startup code
|
||||
trace.set_tracer_provider(TracerProvider(...))
|
||||
|
||||
# After the above call, calling setup_observability will not have any effect
|
||||
setup_observability()
|
||||
```
|
||||
|
||||
The reverse is also true:
|
||||
|
||||
```python
|
||||
# Some where in your application startup code
|
||||
setup_observability()
|
||||
|
||||
# After the above call, calling trace.set_tracer_provider will not have any effect
|
||||
trace.set_tracer_provider(TracerProvider(...))
|
||||
```
|
||||
|
||||
The OTel endpoint and the Application Insights connection string can be set through
|
||||
environment variables or you can pass additional ones here. In the case where both
|
||||
are present, non-duplicate values will be added:
|
||||
|
||||
## With environment variables
|
||||
|
||||
This method will read the settings from the environment:
|
||||
|
||||
```python
|
||||
setup_observability()
|
||||
```
|
||||
|
||||
## Without environment variables and use parameters
|
||||
|
||||
It is also possible to pass the settings directly:
|
||||
|
||||
```python
|
||||
setup_observability(
|
||||
enable_sensitive_data=True,
|
||||
otlp_endpoint=["http://localhost:7431"],
|
||||
applicationinsights_connection_string=["..."],
|
||||
exporters=[...], # your custom exporters
|
||||
vs_code_extension_port=4317,
|
||||
)
|
||||
```
|
||||
|
||||
## Mixed
|
||||
|
||||
When both environment variables and parameters are used, the following settings will get overridden:
|
||||
- enable_sensitive_data
|
||||
- vs_code_extension_port
|
||||
|
||||
The endpoints and connection strings will be combined, excluding duplicates.
|
||||
|
||||
```env
|
||||
OTEL_ENDPOINT="http://localhost:7431"
|
||||
```
|
||||
|
||||
```python
|
||||
setup_observability(
|
||||
enable_sensitive_data=True,
|
||||
otlp_endpoint=["http://localhost:4317"],
|
||||
)
|
||||
```
|
||||
|
||||
Exporters will be created for both endpoints.
|
||||
Note:
|
||||
If you have configured the providers manually, calling this method will not
|
||||
have any effect. The reverse is also true - if you call this method first,
|
||||
subsequent provider configurations will not take effect.
|
||||
|
||||
Args:
|
||||
enable_sensitive_data: Enable OpenTelemetry sensitive events.
|
||||
If set, this will override the value set through the environment variable.
|
||||
enable_sensitive_data: Enable OpenTelemetry sensitive events. Overrides
|
||||
the environment variable if set. Default is None.
|
||||
otlp_endpoint: The OpenTelemetry Protocol (OTLP) endpoint. Will be used
|
||||
to create OTLPLogExporter, OTLPMetricExporter and OTLPSpanExporter.
|
||||
Default is None.
|
||||
otlp_endpoint: The OpenTelemetry Protocol (OTLP) endpoint. Default is None.
|
||||
Will be used to create a `OTLPLogExporter`, `OTLPMetricExporter` and `OTLPSpanExporter`
|
||||
applicationinsights_connection_string: The Azure Monitor connection string. Default is None.
|
||||
Will be used to create AzureMonitorExporters.
|
||||
applicationinsights_connection_string: The Azure Monitor connection string.
|
||||
Will be used to create AzureMonitorExporters. Default is None.
|
||||
credential: The credential to use for Azure Monitor Entra ID authentication.
|
||||
Default is None.
|
||||
exporters: A list of exporters, for logs, metrics or spans, or any combination.
|
||||
These will be added directly, and allows you to customize the spans completely.
|
||||
vs_code_extension_port: The port the AI Toolkit or AzureAI Foundry VS Code extensions are
|
||||
listening on. When this is set, additional OTEL exporters will be created with endpoint
|
||||
`http://localhost:{vs_code_extension_port}` unless this endpoint is already configured.
|
||||
This will override the value set through the environment variable.
|
||||
Default is None.
|
||||
exporters: A list of exporters for logs, metrics or spans, or any combination.
|
||||
These will be added directly, allowing complete customization. Default is None.
|
||||
vs_code_extension_port: The port the AI Toolkit or AzureAI Foundry VS Code
|
||||
extensions are listening on. When set, additional OTEL exporters will be
|
||||
created with endpoint `http://localhost:{vs_code_extension_port}` unless
|
||||
already configured. Overrides the environment variable if set. Default is None.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import setup_observability
|
||||
|
||||
# With environment variables
|
||||
# Set ENABLE_OTEL=true, OTLP_ENDPOINT=http://localhost:4317
|
||||
setup_observability()
|
||||
|
||||
# With parameters (no environment variables)
|
||||
setup_observability(
|
||||
enable_sensitive_data=True,
|
||||
otlp_endpoint="http://localhost:4317",
|
||||
)
|
||||
|
||||
# With Azure Monitor
|
||||
setup_observability(
|
||||
applicationinsights_connection_string="InstrumentationKey=...",
|
||||
)
|
||||
|
||||
# With custom exporters
|
||||
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
|
||||
|
||||
setup_observability(
|
||||
exporters=[ConsoleSpanExporter()],
|
||||
)
|
||||
|
||||
# Mixed: combine environment variables and parameters
|
||||
# Environment: OTLP_ENDPOINT=http://localhost:7431
|
||||
# Code adds additional endpoint
|
||||
setup_observability(
|
||||
enable_sensitive_data=True,
|
||||
otlp_endpoint="http://localhost:4317", # Both endpoints will be used
|
||||
)
|
||||
|
||||
# VS Code extension integration
|
||||
setup_observability(
|
||||
vs_code_extension_port=4317, # Connects to AI Toolkit
|
||||
)
|
||||
"""
|
||||
global OBSERVABILITY_SETTINGS
|
||||
# Update the observability settings with the provided values
|
||||
@@ -928,12 +967,53 @@ def _trace_get_streaming_response(
|
||||
def use_observability(
|
||||
chat_client: type[TChatClient],
|
||||
) -> type[TChatClient]:
|
||||
"""Class decorator that enables telemetry for a chat client.
|
||||
"""Class decorator that enables OpenTelemetry observability for a chat client.
|
||||
|
||||
This needs to be applied on the class itself, not a instance of it.
|
||||
This decorator automatically traces chat completion requests, captures metrics,
|
||||
and logs events for the decorated chat client class.
|
||||
|
||||
To set the proper provider name, the chat client class should have a class variable
|
||||
OTEL_PROVIDER_NAME.
|
||||
Note:
|
||||
This decorator must be applied to the class itself, not an instance.
|
||||
The chat client class should have a class variable OTEL_PROVIDER_NAME to
|
||||
set the proper provider name for telemetry.
|
||||
|
||||
Args:
|
||||
chat_client: The chat client class to enable observability for.
|
||||
|
||||
Returns:
|
||||
The decorated chat client class with observability enabled.
|
||||
|
||||
Raises:
|
||||
ChatClientInitializationError: If the chat client does not have required
|
||||
methods (get_response, get_streaming_response).
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import use_observability, setup_observability
|
||||
from agent_framework._clients import ChatClientProtocol
|
||||
|
||||
|
||||
# Decorate a custom chat client class
|
||||
@use_observability
|
||||
class MyCustomChatClient:
|
||||
OTEL_PROVIDER_NAME = "my_provider"
|
||||
|
||||
async def get_response(self, messages, **kwargs):
|
||||
# Your implementation
|
||||
pass
|
||||
|
||||
async def get_streaming_response(self, messages, **kwargs):
|
||||
# Your implementation
|
||||
pass
|
||||
|
||||
|
||||
# Setup observability
|
||||
setup_observability(otlp_endpoint="http://localhost:4317")
|
||||
|
||||
# Now all calls will be traced
|
||||
client = MyCustomChatClient()
|
||||
response = await client.get_response("Hello")
|
||||
"""
|
||||
if getattr(chat_client, OPEN_TELEMETRY_CHAT_CLIENT_MARKER, False):
|
||||
# Already decorated
|
||||
@@ -1107,7 +1187,54 @@ def _trace_agent_run_stream(
|
||||
def use_agent_observability(
|
||||
agent: type[TAgent],
|
||||
) -> type[TAgent]:
|
||||
"""Class decorator that enables telemetry for an agent."""
|
||||
"""Class decorator that enables OpenTelemetry observability for an agent.
|
||||
|
||||
This decorator automatically traces agent run requests, captures events,
|
||||
and logs interactions for the decorated agent class.
|
||||
|
||||
Note:
|
||||
This decorator must be applied to the agent class itself, not an instance.
|
||||
The agent class should have a class variable AGENT_SYSTEM_NAME to set the
|
||||
proper system name for telemetry.
|
||||
|
||||
Args:
|
||||
agent: The agent class to enable observability for.
|
||||
|
||||
Returns:
|
||||
The decorated agent class with observability enabled.
|
||||
|
||||
Raises:
|
||||
AgentInitializationError: If the agent does not have required methods
|
||||
(run, run_stream).
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import use_agent_observability, setup_observability
|
||||
from agent_framework._agents import AgentProtocol
|
||||
|
||||
|
||||
# Decorate a custom agent class
|
||||
@use_agent_observability
|
||||
class MyCustomAgent:
|
||||
AGENT_SYSTEM_NAME = "my_agent_system"
|
||||
|
||||
async def run(self, messages=None, *, thread=None, **kwargs):
|
||||
# Your implementation
|
||||
pass
|
||||
|
||||
async def run_stream(self, messages=None, *, thread=None, **kwargs):
|
||||
# Your implementation
|
||||
pass
|
||||
|
||||
|
||||
# Setup observability
|
||||
setup_observability(otlp_endpoint="http://localhost:4317")
|
||||
|
||||
# Now all agent runs will be traced
|
||||
agent = MyCustomAgent()
|
||||
response = await agent.run("Perform a task")
|
||||
"""
|
||||
provider_name = str(getattr(agent, "AGENT_SYSTEM_NAME", "Unknown"))
|
||||
try:
|
||||
agent.run = _trace_agent_run(agent.run, provider_name) # type: ignore
|
||||
|
||||
@@ -41,6 +41,15 @@ dependencies = [
|
||||
viz = [
|
||||
"graphviz>=0.20.0"
|
||||
]
|
||||
all = [
|
||||
"agent-framework-a2a",
|
||||
"agent-framework-azure-ai",
|
||||
"agent-framework-copilotstudio",
|
||||
"agent-framework-mem0",
|
||||
"agent-framework-redis",
|
||||
"agent-framework-devui",
|
||||
"graphviz>=0.20.0"
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "if-necessary-or-explicit"
|
||||
|
||||
@@ -168,10 +168,10 @@ class RedisChatMessageStore:
|
||||
- LTRIM operation is atomic for consistent message limits
|
||||
|
||||
Example:
|
||||
```python
|
||||
messages = [ChatMessage(role=Role.USER, text="Hello"), ChatMessage(role=Role.ASSISTANT, text="Hi there!")]
|
||||
await store.add_messages(messages)
|
||||
```
|
||||
.. code-block:: python
|
||||
|
||||
messages = [ChatMessage(role="user", text="Hello"), ChatMessage(role="assistant", text="Hi there!")]
|
||||
await store.add_messages(messages)
|
||||
"""
|
||||
if not messages:
|
||||
return
|
||||
@@ -200,10 +200,10 @@ class RedisChatMessageStore:
|
||||
Returns empty list if no messages exist or if Redis connection fails.
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get all conversation history
|
||||
messages = await store.list_messages()
|
||||
```
|
||||
.. code-block:: python
|
||||
|
||||
# Get all conversation history
|
||||
messages = await store.list_messages()
|
||||
"""
|
||||
# Ensure any initial messages are persisted to Redis first
|
||||
await self._ensure_initial_messages_added()
|
||||
@@ -320,14 +320,14 @@ class RedisChatMessageStore:
|
||||
- Consider exporting messages before clearing if backup is needed
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Clear conversation history
|
||||
await store.clear()
|
||||
.. code-block:: python
|
||||
|
||||
# Verify messages are gone
|
||||
messages = await store.list_messages()
|
||||
assert len(messages) == 0
|
||||
```
|
||||
# Clear conversation history
|
||||
await store.clear()
|
||||
|
||||
# Verify messages are gone
|
||||
messages = await store.list_messages()
|
||||
assert len(messages) == 0
|
||||
"""
|
||||
await self._redis_client.delete(self.redis_key)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ fmt = "ruff format"
|
||||
format.ref = "fmt"
|
||||
lint = "ruff check"
|
||||
pyright = "pyright"
|
||||
build = "python -m flit build"
|
||||
publish = "uv publish"
|
||||
clean-dist = "rm -rf dist"
|
||||
build-package = "python -m flit build"
|
||||
move-dist = "sh -c 'mkdir -p ../../dist && mv dist/* ../../dist/ 2>/dev/null || true'"
|
||||
build = ["build-package", "move-dist"]
|
||||
|
||||
Generated
+27
-11
@@ -206,16 +206,32 @@ dependencies = [
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
all = [
|
||||
{ name = "agent-framework-a2a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "agent-framework-azure-ai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "agent-framework-copilotstudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "agent-framework-devui", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "agent-framework-redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "graphviz", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
viz = [
|
||||
{ name = "graphviz", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "agent-framework-a2a", marker = "extra == 'all'", editable = "packages/a2a" },
|
||||
{ name = "agent-framework-azure-ai", marker = "extra == 'all'", editable = "packages/azure-ai" },
|
||||
{ name = "agent-framework-copilotstudio", marker = "extra == 'all'", editable = "packages/copilotstudio" },
|
||||
{ name = "agent-framework-devui", marker = "extra == 'all'", editable = "packages/devui" },
|
||||
{ name = "agent-framework-mem0", marker = "extra == 'all'", editable = "packages/mem0" },
|
||||
{ name = "agent-framework-redis", marker = "extra == 'all'", editable = "packages/redis" },
|
||||
{ name = "aiofiles", specifier = ">=24.1.0" },
|
||||
{ name = "azure-identity", specifier = ">=1,<2" },
|
||||
{ name = "azure-monitor-opentelemetry", specifier = ">=1.7.0" },
|
||||
{ name = "azure-monitor-opentelemetry-exporter", specifier = ">=1.0.0b41" },
|
||||
{ name = "graphviz", marker = "extra == 'all'", specifier = ">=0.20.0" },
|
||||
{ name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.20.0" },
|
||||
{ name = "mcp", extras = ["ws"], specifier = ">=1.13" },
|
||||
{ name = "openai", specifier = ">=1.99.0" },
|
||||
@@ -227,7 +243,7 @@ requires-dist = [
|
||||
{ name = "pydantic-settings", specifier = ">=2,<3" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
provides-extras = ["viz"]
|
||||
provides-extras = ["viz", "all"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-devui"
|
||||
@@ -1405,7 +1421,7 @@ name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
|
||||
wheels = [
|
||||
@@ -2354,7 +2370,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langfuse"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -2367,9 +2383,9 @@ dependencies = [
|
||||
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/36/c445ffa3a1ac856e9c54b2d9ee7969c5bfbea549c5389339f23b67d45653/langfuse-3.6.0.tar.gz", hash = "sha256:7b11f8510949f7b00afa6846e503ded80ddb5c6e2910e6fbcd773716bdcbec23", size = 189466, upload-time = "2025-10-01T12:01:18.475Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/ea/b1abad97af5e4dba0ea3135387efa139f11ac34e57da5a8b2ea14354bd95/langfuse-3.6.1.tar.gz", hash = "sha256:eac27ee5bbd8d05e7d665e822e0efb36766b20fe281930ff040f47eb22cc1b69", size = 189456, upload-time = "2025-10-02T08:33:17.363Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/10/fd8daf0a1847fe25c6804936ded4ab722cfbb98b5ce215de67ad8f515e9e/langfuse-3.6.0-py3-none-any.whl", hash = "sha256:672ac0db51780d10d98b32bd6c922fad5d5f6eea1344725537bbbfad10908b92", size = 350744, upload-time = "2025-10-01T12:01:16.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/49/4eae7cd4a1005c77808b3d8e3174412c4e198c8fb776b8847b0223a5f504/langfuse-3.6.1-py3-none-any.whl", hash = "sha256:134e0007fcfdd9fb70b491c882bb431c8095b3f5cc5e865756f46a2abd3675a2", size = 350756, upload-time = "2025-10-02T08:33:15.607Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4157,15 +4173,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.405"
|
||||
version = "1.1.406"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/6b4fbdd1fef59a0292cbb99f790b44983e390321eccbc5921b4d161da5d1/pyright-1.1.406.tar.gz", hash = "sha256:c4872bc58c9643dac09e8a2e74d472c62036910b3bd37a32813989ef7576ea2c", size = 4113151, upload-time = "2025-10-02T01:04:45.488Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/a2/e309afbb459f50507103793aaef85ca4348b66814c86bc73908bdeb66d12/pyright-1.1.406-py3-none-any.whl", hash = "sha256:1d81fb43c2407bf566e97e57abb01c811973fdb21b2df8df59f870f688bdca71", size = 5980982, upload-time = "2025-10-02T01:04:43.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4436,7 +4452,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "redisvl"
|
||||
version = "0.8.2"
|
||||
version = "0.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpath-ng", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -4449,9 +4465,9 @@ dependencies = [
|
||||
{ name = "redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/26/f3a5128d96eeeb5af0fc345156e48971ce0ce99689b62ba01dc855744c61/redisvl-0.8.2.tar.gz", hash = "sha256:3938ddcd093507c4c427cb431ac9faaa8bb999bb2ca116cbd57e4b7334fe18eb", size = 573106, upload-time = "2025-08-26T15:23:40.356Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/86/50d8cbd7df4849a602a27ba475a3e426f26ec14d26bdce14ed77e120b66d/redisvl-0.9.1.tar.gz", hash = "sha256:a735ecf3238e804800b54a513b85a8cf4300fe6d111fb055bd75528f77dd5419", size = 606980, upload-time = "2025-10-01T23:25:51.293Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/16/a9eb70249c518b9b6a19efb32089bda8ecc146bafee360abd375eae7053e/redisvl-0.8.2-py3-none-any.whl", hash = "sha256:67b413387d72849d571723c95fa1183539d6fa60d6ac533513ee8e3e31874600", size = 152593, upload-time = "2025-08-26T15:23:38.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/3e8d5d8a734dd0df731567147b19f0326544f633a5efbab7de0e3625da62/redisvl-0.9.1-py3-none-any.whl", hash = "sha256:aaec441cfcb37ce7cced028dcf9a748337a27422dcaf1b494a4c6198f577dcf4", size = 160335, upload-time = "2025-10-01T23:25:49.812Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user