" in result.text
+
+ def test_task_to_input_no_custom_task(self, converter):
+ """Test that non-custom tasks return None."""
+ from datetime import datetime
+
+ from chatkit.types import TaskItem, ThoughtTask
+
+ task_item = TaskItem(
+ id="task_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="task",
+ task=ThoughtTask(type="thought", title="Think", content="Thinking..."),
+ )
+
+ result = converter.task_to_input(task_item)
+ assert result is None
+
+ def test_workflow_to_input(self, converter):
+ """Test converting WorkflowItem to ChatMessages."""
+ from datetime import datetime
+
+ from chatkit.types import CustomTask, Workflow, WorkflowItem
+
+ workflow_item = WorkflowItem(
+ id="wf_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="workflow",
+ workflow=Workflow(
+ type="custom",
+ tasks=[
+ CustomTask(type="custom", title="Step 1", content="First step"),
+ CustomTask(type="custom", title="Step 2", content="Second step"),
+ ],
+ ),
+ )
+
+ result = converter.workflow_to_input(workflow_item)
+ assert isinstance(result, list)
+ assert len(result) == 2
+ assert all(isinstance(msg, ChatMessage) for msg in result)
+ assert "Step 1: First step" in result[0].text
+ assert "Step 2: Second step" in result[1].text
+
+ def test_workflow_to_input_empty(self, converter):
+ """Test that workflows with no custom tasks return None."""
+ from datetime import datetime
+
+ from chatkit.types import Workflow, WorkflowItem
+
+ workflow_item = WorkflowItem(
+ id="wf_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="workflow",
+ workflow=Workflow(type="custom", tasks=[]),
+ )
+
+ result = converter.workflow_to_input(workflow_item)
+ assert result is None
+
+ def test_widget_to_input(self, converter):
+ """Test converting WidgetItem to ChatMessage."""
+ from datetime import datetime
+
+ from chatkit.types import WidgetItem
+ from chatkit.widgets import Card, Text
+
+ widget_item = WidgetItem(
+ id="widget_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="widget",
+ widget=Card(key="card1", children=[Text(value="Hello")]),
+ )
+
+ result = converter.widget_to_input(widget_item)
+ assert isinstance(result, ChatMessage)
+ assert result.role == Role.USER
+ assert "widget_1" in result.text
+ assert "graphical UI widget" in result.text
+
+
+class TestSimpleToAgentInput:
+ """Tests for simple_to_agent_input helper function."""
+
+ async def test_simple_to_agent_input_empty_list(self):
+ """Test simple conversion with empty list."""
+ result = await simple_to_agent_input([])
+ assert result == []
+
+ async def test_simple_to_agent_input_with_text(self):
+ """Test simple conversion with text content."""
+ from datetime import datetime
+
+ from chatkit.types import UserMessageItem
+
+ input_item = UserMessageItem(
+ id="msg_1",
+ thread_id="thread_1",
+ created_at=datetime.now(),
+ type="user_message",
+ content=[UserMessageTextContent(text="Test message")],
+ attachments=[],
+ inference_options={},
+ )
+
+ result = await simple_to_agent_input(input_item)
+
+ assert len(result) == 1
+ assert isinstance(result[0], ChatMessage)
+ assert result[0].role == Role.USER
+ assert result[0].text == "Test message"
diff --git a/python/packages/chatkit/tests/test_streaming.py b/python/packages/chatkit/tests/test_streaming.py
new file mode 100644
index 0000000000..2e5041613a
--- /dev/null
+++ b/python/packages/chatkit/tests/test_streaming.py
@@ -0,0 +1,142 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""Tests for Agent Framework to ChatKit streaming utilities."""
+
+from unittest.mock import Mock
+
+from agent_framework import AgentRunResponseUpdate, Role, TextContent
+from chatkit.types import (
+ ThreadItemAddedEvent,
+ ThreadItemDoneEvent,
+ ThreadItemUpdated,
+)
+
+from agent_framework_chatkit import stream_agent_response
+
+
+class TestStreamAgentResponse:
+ """Tests for stream_agent_response function."""
+
+ async def test_stream_empty_response(self):
+ """Test streaming empty response."""
+
+ async def empty_stream():
+ return
+ yield # Make it a generator
+
+ events = []
+ async for event in stream_agent_response(empty_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ assert len(events) == 0
+
+ async def test_stream_single_text_update(self):
+ """Test streaming single text update."""
+
+ async def single_update_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Hello world")])
+
+ events = []
+ async for event in stream_agent_response(single_update_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have: item_added, item_updated (delta), item_done
+ assert len(events) == 3
+
+ # Check event types
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemUpdated)
+ assert isinstance(events[2], ThreadItemDoneEvent)
+
+ # Check delta event
+ assert events[1].update.delta == "Hello world"
+
+ # Check final message content
+ assert len(events[2].item.content) == 1
+ assert events[2].item.content[0].text == "Hello world"
+
+ async def test_stream_multiple_text_updates(self):
+ """Test streaming multiple text updates."""
+
+ async def multiple_updates_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Hello ")])
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="world!")])
+
+ events = []
+ async for event in stream_agent_response(multiple_updates_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have: item_added, item_updated (delta 1), item_updated (delta 2), item_done
+ assert len(events) == 4
+
+ # Check event types
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemUpdated)
+ assert isinstance(events[2], ThreadItemUpdated)
+ assert isinstance(events[3], ThreadItemDoneEvent)
+
+ # Check delta events
+ assert events[1].update.delta == "Hello "
+ assert events[2].update.delta == "world!"
+
+ # Check final accumulated text
+ final_message_event = events[-1]
+ assert isinstance(final_message_event, ThreadItemDoneEvent)
+ assert final_message_event.item.content[0].text == "Hello world!"
+
+ async def test_stream_with_custom_id_generator(self):
+ """Test streaming with custom ID generator."""
+
+ def custom_id_generator(item_type: str) -> str:
+ return f"custom_{item_type}_123"
+
+ async def single_update_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Test")])
+
+ events = []
+ async for event in stream_agent_response(
+ single_update_stream(), thread_id="test_thread", generate_id=custom_id_generator
+ ):
+ events.append(event)
+
+ # Check that custom IDs are used
+ message_added_event = events[0]
+ assert message_added_event.item.id == "custom_msg_123"
+
+ async def test_stream_empty_content_updates(self):
+ """Test streaming updates with empty content."""
+
+ async def empty_content_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[])
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=None)
+
+ events = []
+ async for event in stream_agent_response(empty_content_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have item_added and item_done
+ assert len(events) == 2
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemDoneEvent)
+
+ # Final message should have empty content
+ assert len(events[1].item.content) == 0
+
+ async def test_stream_non_text_content(self):
+ """Test streaming updates with non-text content."""
+ # Mock a content object without text attribute
+ non_text_content = Mock()
+ # Don't set text attribute
+ del non_text_content.text
+
+ async def non_text_stream():
+ yield AgentRunResponseUpdate(role=Role.ASSISTANT, contents=[non_text_content])
+
+ events = []
+ async for event in stream_agent_response(non_text_stream(), thread_id="test_thread"):
+ events.append(event)
+
+ # Should have item_added and item_done, but no content since no text
+ assert len(events) == 2
+ assert isinstance(events[0], ThreadItemAddedEvent)
+ assert isinstance(events[1], ThreadItemDoneEvent)
diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml
index b3325c76b5..3ad6aad137 100644
--- a/python/packages/copilotstudio/pyproject.toml
+++ b/python/packages/copilotstudio/pyproject.toml
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py
index e4b2d53cc6..3cac845ed3 100644
--- a/python/packages/core/agent_framework/_clients.py
+++ b/python/packages/core/agent_framework/_clients.py
@@ -19,7 +19,7 @@ from ._middleware import (
)
from ._serialization import SerializationMixin
from ._threads import ChatMessageStoreProtocol
-from ._tools import ToolProtocol
+from ._tools import FUNCTION_INVOKING_CHAT_CLIENT_MARKER, FunctionInvocationConfiguration, ToolProtocol
from ._types import ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, ToolMode, prepare_messages
if TYPE_CHECKING:
@@ -357,6 +357,10 @@ class BaseChatClient(SerializationMixin, ABC):
self.middleware = middleware
+ self.function_invocation_configuration = (
+ FunctionInvocationConfiguration() if hasattr(self.__class__, FUNCTION_INVOKING_CHAT_CLIENT_MARKER) else None
+ )
+
def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
"""Convert the instance to a dictionary.
diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py
index 22b9921e49..83df62e29d 100644
--- a/python/packages/core/agent_framework/_tools.py
+++ b/python/packages/core/agent_framework/_tools.py
@@ -71,6 +71,7 @@ logger = get_logger()
__all__ = [
"FUNCTION_INVOKING_CHAT_CLIENT_MARKER",
"AIFunction",
+ "FunctionInvocationConfiguration",
"HostedCodeInterpreterTool",
"HostedFileSearchTool",
"HostedMCPSpecificApproval",
@@ -84,7 +85,8 @@ __all__ = [
logger = get_logger()
FUNCTION_INVOKING_CHAT_CLIENT_MARKER: Final[str] = "__function_invoking_chat_client__"
-DEFAULT_MAX_ITERATIONS: Final[int] = 10
+DEFAULT_MAX_ITERATIONS: Final[int] = 40
+DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST: Final[int] = 3
TChatClient = TypeVar("TChatClient", bound="ChatClientProtocol")
# region Helpers
@@ -156,34 +158,19 @@ def _parse_inputs(
# region Tools
@runtime_checkable
class ToolProtocol(Protocol):
- """Represents a generic tool that can be specified to an AI service.
+ """Represents a generic tool.
This protocol defines the interface that all tools must implement to be compatible
- with the agent framework.
+ with the agent framework. It is implemented by various tool classes such as HostedMCPTool,
+ HostedWebSearchTool, and AIFunction's. A AIFunction is usually created by the `ai_function` decorator.
+
+ Since each connector needs to parse tools differently, users can pass a dict to
+ specify a service-specific tool when no abstraction is available.
Attributes:
name: The name 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
@@ -201,22 +188,11 @@ 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.
+ Used as the base class for the various tools in the agent framework, such as HostedMCPTool,
+ HostedWebSearchTool, and AIFunction.
- 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)
+ Since each connector needs to parse tools differently, this class is not exposed directly to end users.
+ In most cases, users can pass a dict to specify a service-specific tool when no abstraction is available.
"""
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"}
@@ -551,6 +527,10 @@ def _default_histogram() -> Histogram:
TClass = TypeVar("TClass", bound="SerializationMixin")
+class EmptyInputModel(BaseModel):
+ """An empty input model for functions with no parameters."""
+
+
class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
"""A tool that wraps a Python function to make it callable by AI models.
@@ -602,8 +582,10 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
name: str,
description: str = "",
approval_mode: Literal["always_require", "never_require"] | None = None,
+ max_invocations: int | None = None,
+ max_invocation_exceptions: int | None = None,
additional_properties: dict[str, Any] | None = None,
- func: Callable[..., Awaitable[ReturnT] | ReturnT],
+ func: Callable[..., Awaitable[ReturnT] | ReturnT] | None = None,
input_model: type[ArgsT] | Mapping[str, Any] | None = None,
**kwargs: Any,
) -> None:
@@ -614,6 +596,10 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
description: A description of the function.
approval_mode: Whether or not approval is required to run this tool.
Default is that approval is not needed.
+ max_invocations: The maximum number of times this function can be invoked.
+ If None, there is no limit. Should be at least 1.
+ max_invocation_exceptions: The maximum number of exceptions allowed during invocations.
+ If None, there is no limit. Should be at least 1.
additional_properties: Additional properties to set on the function.
func: The function to wrap.
input_model: The Pydantic model that defines the input parameters for the function.
@@ -630,21 +616,56 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
self.func = func
self.input_model = self._resolve_input_model(input_model)
self.approval_mode = approval_mode or "never_require"
+ if max_invocations is not None and max_invocations < 1:
+ raise ValueError("max_invocations must be at least 1 or None.")
+ if max_invocation_exceptions is not None and max_invocation_exceptions < 1:
+ raise ValueError("max_invocation_exceptions must be at least 1 or None.")
+ self.max_invocations = max_invocations
+ self.invocation_count = 0
+ self.max_invocation_exceptions = max_invocation_exceptions
+ self.invocation_exception_count = 0
self._invocation_duration_histogram = _default_histogram()
self.type: Literal["ai_function"] = "ai_function"
+ @property
+ def declaration_only(self) -> bool:
+ """Indicate whether the function is declaration only (i.e., has no implementation)."""
+ return self.func is None
+
def _resolve_input_model(self, input_model: type[ArgsT] | Mapping[str, Any] | None) -> type[ArgsT]:
- if input_model:
- if inspect.isclass(input_model) and issubclass(input_model, BaseModel):
- return input_model
- if isinstance(input_model, Mapping):
- return cast(type[ArgsT], _create_model_from_json_schema(self.name, input_model))
- raise TypeError("input_model must be a Pydantic BaseModel subclass or a JSON schema dict.")
- return cast(type[ArgsT], _create_input_model_from_func(self.func, self.name))
+ """Resolve the input model for the function."""
+ if input_model is None:
+ if self.func is None:
+ return cast(type[ArgsT], EmptyInputModel)
+ return cast(type[ArgsT], _create_input_model_from_func(func=self.func, name=self.name))
+ if inspect.isclass(input_model) and issubclass(input_model, BaseModel):
+ return input_model
+ if isinstance(input_model, Mapping):
+ return cast(type[ArgsT], _create_model_from_json_schema(self.name, input_model))
+ raise TypeError("input_model must be a Pydantic BaseModel subclass or a JSON schema dict.")
def __call__(self, *args: Any, **kwargs: Any) -> ReturnT | Awaitable[ReturnT]:
"""Call the wrapped function with the provided arguments."""
- return self.func(*args, **kwargs)
+ if self.func is None:
+ raise ToolException(f"Function '{self.name}' is declaration only and cannot be invoked.")
+ if self.max_invocations is not None and self.invocation_count >= self.max_invocations:
+ raise ToolException(
+ f"Function '{self.name}' has reached its maximum invocation limit, you can no longer use this tool."
+ )
+ if (
+ self.max_invocation_exceptions is not None
+ and self.invocation_exception_count >= self.max_invocation_exceptions
+ ):
+ raise ToolException(
+ f"Function '{self.name}' has reached its maximum exception limit, "
+ f"you tried to use this tool too many times and it kept failing."
+ )
+ self.invocation_count += 1
+ try:
+ return self.func(*args, **kwargs)
+ except Exception:
+ self.invocation_exception_count += 1
+ raise
async def invoke(
self,
@@ -664,6 +685,8 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
Raises:
TypeError: If arguments is not an instance of the expected input model.
"""
+ if self.declaration_only:
+ raise ToolException(f"Function '{self.name}' is declaration only and cannot be invoked.")
global OBSERVABILITY_SETTINGS
from .observability import OBSERVABILITY_SETTINGS
@@ -833,7 +856,7 @@ def _parse_annotation(annotation: Any) -> Any:
return annotation
-def _create_input_model_from_func(func: Callable[..., Any], tool_name: str) -> type[BaseModel]:
+def _create_input_model_from_func(func: Callable[..., Any], name: str) -> type[BaseModel]:
"""Create a Pydantic model from a function's signature."""
sig = inspect.signature(func)
fields = {
@@ -844,7 +867,7 @@ def _create_input_model_from_func(func: Callable[..., Any], tool_name: str) -> t
for pname, param in sig.parameters.items()
if pname not in {"self", "cls"}
}
- return create_model(f"{tool_name}_input", **fields) # type: ignore[call-overload, no-any-return]
+ return create_model(f"{name}_input", **fields) # type: ignore[call-overload, no-any-return]
# Map JSON Schema types to Pydantic types
@@ -907,6 +930,8 @@ def ai_function(
name: str | None = None,
description: str | None = None,
approval_mode: Literal["always_require", "never_require"] | None = None,
+ max_invocations: int | None = None,
+ max_invocation_exceptions: int | None = None,
additional_properties: dict[str, Any] | None = None,
) -> AIFunction[Any, ReturnT]: ...
@@ -918,6 +943,8 @@ def ai_function(
name: str | None = None,
description: str | None = None,
approval_mode: Literal["always_require", "never_require"] | None = None,
+ max_invocations: int | None = None,
+ max_invocation_exceptions: int | None = None,
additional_properties: dict[str, Any] | None = None,
) -> Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], AIFunction[Any, ReturnT]]: ...
@@ -928,6 +955,8 @@ def ai_function(
name: str | None = None,
description: str | None = None,
approval_mode: Literal["always_require", "never_require"] | None = None,
+ max_invocations: int | None = None,
+ max_invocation_exceptions: int | None = None,
additional_properties: dict[str, Any] | None = None,
) -> AIFunction[Any, ReturnT] | Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], AIFunction[Any, ReturnT]]:
"""Decorate a function to turn it into a AIFunction that can be passed to models and executed automatically.
@@ -940,6 +969,22 @@ def ai_function(
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 decorate.
+
+ Keyword Args:
+ name: The name of the function. If not provided, the function's ``__name__``
+ attribute will be used.
+ description: A description of the function. If not provided, the function's
+ docstring will be used.
+ approval_mode: Whether or not approval is required to run this tool.
+ Default is that approval is not needed.
+ max_invocations: The maximum number of times this function can be invoked.
+ If None, there is no limit, should be at least 1.
+ max_invocation_exceptions: The maximum number of exceptions allowed during invocations.
+ If None, there is no limit, should be at least 1.
+ additional_properties: Additional properties to set on the function.
+
Note:
When approval_mode is set to "always_require", the function will not be executed
until explicit approval is given, this only applies to the auto-invocation flow.
@@ -997,6 +1042,8 @@ def ai_function(
name=tool_name,
description=tool_desc,
approval_mode=approval_mode,
+ max_invocations=max_invocations,
+ max_invocation_exceptions=max_invocation_exceptions,
additional_properties=additional_properties or {},
func=f,
)
@@ -1009,10 +1056,123 @@ def ai_function(
# region Function Invoking Chat Client
+class FunctionInvocationConfiguration(SerializationMixin):
+ """Configuration for function invocation in chat clients.
+
+ This class is created automatically on every chat client that supports function invocation.
+ This means that for most cases you can just alter the attributes on the instance, rather then creating a new one.
+
+ Example:
+ .. code-block:: python
+ from agent_framework.openai import OpenAIChatClient
+
+ # Create an OpenAI chat client
+ client = OpenAIChatClient(api_key="your_api_key")
+
+ # Disable function invocation
+ client.function_invocation_config.enabled = False
+
+ # Set maximum iterations to 10
+ client.function_invocation_config.max_iterations = 10
+
+ # Enable termination on unknown function calls
+ client.function_invocation_config.terminate_on_unknown_calls = True
+
+ # Add additional tools for function execution
+ client.function_invocation_config.additional_tools = [my_custom_tool]
+
+ # Enable detailed error information in function results
+ client.function_invocation_config.include_detailed_errors = True
+
+ # You can also create a new configuration instance if needed
+ new_config = FunctionInvocationConfiguration(
+ enabled=True,
+ max_iterations=20,
+ terminate_on_unknown_calls=False,
+ additional_tools=[another_tool],
+ include_detailed_errors=False,
+ )
+
+ # and then assign it to the client
+ client.function_invocation_config = new_config
+
+
+ Attributes:
+ enabled: Whether function invocation is enabled.
+ When this is set to False, the client will not attempt to invoke any functions,
+ because the tool mode will be set to None.
+ max_iterations: Maximum number of function invocation iterations.
+ Each request to this client might end up making multiple requests to the model. Each time the model responds
+ with a function call request, this client might perform that invocation and send the results back to the
+ model in a new request. This property limits the number of times such a roundtrip is performed. The value
+ must be at least one, as it includes the initial request.
+ If you want to fully disable function invocation, use the ``enabled`` property.
+ The default is 40.
+ max_consecutive_errors_per_request: Maximum consecutive errors allowed per request.
+ The maximum number of consecutive function call errors allowed before stopping
+ further function calls for the request.
+ The default is 3.
+ terminate_on_unknown_calls: Whether to terminate on unknown function calls.
+ When False, call requests to any tools that aren't available to the client
+ will result in a response message automatically being created and returned to the inner client stating that
+ the tool couldn't be found. This behavior can help in cases where a model hallucinates a function, but it's
+ problematic if the model has been made aware of the existence of tools outside of the normal mechanisms, and
+ requests one of those. ``additional_tools`` can be used to help with that. But if instead the consumer wants
+ to know about all function call requests that the client can't handle, this can be set to True. Upon
+ receiving a request to call a function that the client doesn't know about, it will terminate the function
+ calling loop and return the response, leaving the handling of the function call requests to the consumer of
+ the client.
+ additional_tools: Additional tools to include for function execution.
+ These will not impact the requests sent by the client, which will pass through the
+ ``tools`` unmodified. However, if the inner client requests the invocation of a tool
+ that was not in ``ChatOptions.tools``, this ``additional_tools`` collection will also be consulted to look
+ for a corresponding tool. This is useful when the service might have been pre-configured to be aware of
+ certain tools that aren't also sent on each individual request. These tools are treated the same as
+ ``declaration_only`` tools and will be returned to the user.
+ include_detailed_errors: Whether to include detailed error information in function results.
+ When set to True, detailed error information such as exception type and message
+ will be included in the function result content when a function invocation fails.
+ When False, only a generic error message will be included.
+
+
+ """
+
+ def __init__(
+ self,
+ enabled: bool = True,
+ max_iterations: int = DEFAULT_MAX_ITERATIONS,
+ max_consecutive_errors_per_request: int = DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST,
+ terminate_on_unknown_calls: bool = False,
+ additional_tools: Sequence[ToolProtocol] | None = None,
+ include_detailed_errors: bool = False,
+ ) -> None:
+ """Initialize FunctionInvocationConfiguration.
+
+ Args:
+ enabled: Whether function invocation is enabled.
+ max_iterations: Maximum number of function invocation iterations.
+ max_consecutive_errors_per_request: Maximum consecutive errors allowed per request.
+ terminate_on_unknown_calls: Whether to terminate on unknown function calls.
+ additional_tools: Additional tools to include for function execution.
+ include_detailed_errors: Whether to include detailed error information in function results.
+ """
+ self.enabled = enabled
+ if max_iterations < 1:
+ raise ValueError("max_iterations must be at least 1.")
+ self.max_iterations = max_iterations
+ if max_consecutive_errors_per_request < 0:
+ raise ValueError("max_consecutive_errors_per_request must be 0 or more.")
+ self.max_consecutive_errors_per_request = max_consecutive_errors_per_request
+ self.terminate_on_unknown_calls = terminate_on_unknown_calls
+ self.additional_tools = additional_tools or []
+ self.include_detailed_errors = include_detailed_errors
+
+
async def _auto_invoke_function(
function_call_content: "FunctionCallContent | FunctionApprovalResponseContent",
custom_args: dict[str, Any] | None = None,
*,
+ config: FunctionInvocationConfiguration,
tool_map: dict[str, AIFunction[BaseModel, Any]],
sequence_index: int | None = None,
request_index: int | None = None,
@@ -1025,6 +1185,7 @@ async def _auto_invoke_function(
custom_args: Additional custom arguments to merge with parsed arguments.
Keyword Args:
+ config: The function invocation configuration.
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.
@@ -1037,29 +1198,33 @@ async def _auto_invoke_function(
KeyError: If the requested function is not found in the tool map.
"""
from ._types import (
- FunctionApprovalRequestContent,
- FunctionApprovalResponseContent,
- FunctionCallContent,
FunctionResultContent,
)
+ # Note: The scenarios for approval_mode="always_require", declaration_only, and
+ # terminate_on_unknown_calls are all handled in _try_execute_function_calls before
+ # this function is called. This function only handles the actual execution of approved,
+ # non-declaration-only functions.
+
tool: AIFunction[BaseModel, Any] | None = None
- if isinstance(function_call_content, FunctionCallContent):
+ if function_call_content.type == "function_call":
tool = tool_map.get(function_call_content.name)
+ # Tool should exist because _try_execute_function_calls validates this
if tool is None:
- raise KeyError(f"No tool or function named '{function_call_content.name}'")
- if tool.approval_mode == "always_require":
- return FunctionApprovalRequestContent(id=function_call_content.call_id, function_call=function_call_content)
+ exc = KeyError(f'Function "{function_call_content.name}" not found.')
+ return FunctionResultContent(
+ call_id=function_call_content.call_id,
+ result=f'Error: Requested function "{function_call_content.name}" not found.',
+ exception=exc,
+ )
else:
- if isinstance(function_call_content, FunctionApprovalResponseContent):
- if function_call_content.approved:
- tool = tool_map.get(function_call_content.function_call.name)
- if tool is None:
- # we assume it is a hosted tool
- return function_call_content
- function_call_content = function_call_content.function_call
- else:
- raise ToolException("Unapproved tool cannot be executed.")
+ # Note: Unapproved tools (approved=False) are handled in _replace_approval_contents_with_results
+ # and never reach this function, so we only handle approved=True cases here.
+ tool = tool_map.get(function_call_content.function_call.name)
+ if tool is None:
+ # we assume it is a hosted tool
+ return function_call_content
+ function_call_content = function_call_content.function_call
parsed_args: dict[str, Any] = dict(function_call_content.parse_arguments() or {})
@@ -1068,10 +1233,10 @@ async def _auto_invoke_function(
try:
args = tool.input_model.model_validate(merged_args)
except ValidationError as exc:
- return FunctionResultContent(
- call_id=function_call_content.call_id,
- exception=exc,
- )
+ message = "Error: Argument parsing failed."
+ if config.include_detailed_errors:
+ message = f"{message} Exception: {exc}"
+ return FunctionResultContent(call_id=function_call_content.call_id, result=message, exception=exc)
if not middleware_pipeline or (
not hasattr(middleware_pipeline, "has_middlewares") and not middleware_pipeline.has_middlewares
):
@@ -1086,10 +1251,10 @@ async def _auto_invoke_function(
result=function_result,
)
except Exception as exc:
- return FunctionResultContent(
- call_id=function_call_content.call_id,
- exception=exc,
- )
+ message = "Error: Function failed."
+ if config.include_detailed_errors:
+ message = f"{message} Exception: {exc}"
+ return FunctionResultContent(call_id=function_call_content.call_id, result=message, exception=exc)
# Execute through middleware pipeline if available
from ._middleware import FunctionInvocationContext
@@ -1117,10 +1282,10 @@ async def _auto_invoke_function(
result=function_result,
)
except Exception as exc:
- return FunctionResultContent(
- call_id=function_call_content.call_id,
- exception=exc,
- )
+ message = "Error: Function failed."
+ if config.include_detailed_errors:
+ message = f"{message} Exception: {exc}"
+ return FunctionResultContent(call_id=function_call_content.call_id, result=message, exception=exc)
def _get_tool_map(
@@ -1141,7 +1306,7 @@ def _get_tool_map(
return ai_function_list
-async def _execute_function_calls(
+async def _try_execute_function_calls(
custom_args: dict[str, Any],
attempt_idx: int,
function_calls: Sequence["FunctionCallContent"] | Sequence["FunctionApprovalResponseContent"],
@@ -1149,6 +1314,7 @@ async def _execute_function_calls(
| Callable[..., Any] \
| MutableMapping[str, Any] \
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]",
+ config: FunctionInvocationConfiguration,
middleware_pipeline: Any = None, # Optional MiddlewarePipeline to avoid circular imports
) -> Sequence["Contents"]:
"""Execute multiple function calls concurrently.
@@ -1158,22 +1324,33 @@ async def _execute_function_calls(
attempt_idx: The index of the current attempt iteration.
function_calls: A sequence of FunctionCallContent to execute.
tools: The tools available for execution.
+ config: Configuration for function invocation.
middleware_pipeline: Optional middleware pipeline to apply during execution.
Returns:
- A list of Contents containing the results of each function call.
+ A list of Contents containing the results of each function call,
+ or the approval requests if any function requires approval,
+ or the original function calls if any are declaration only.
"""
from ._types import FunctionApprovalRequestContent, FunctionCallContent
tool_map = _get_tool_map(tools)
approval_tools = [tool_name for tool_name, tool in tool_map.items() if tool.approval_mode == "always_require"]
+ declaration_only = [tool_name for tool_name, tool in tool_map.items() if tool.declaration_only]
+ additional_tool_names = [tool.name for tool in config.additional_tools] if config.additional_tools else []
# check if any are calling functions that need approval
# if so, we return approval request for all
approval_needed = False
+ declaration_only_flag = False
for fcc in function_calls:
if isinstance(fcc, FunctionCallContent) and fcc.name in approval_tools:
approval_needed = True
break
+ if isinstance(fcc, FunctionCallContent) and (fcc.name in declaration_only or fcc.name in additional_tool_names):
+ declaration_only_flag = True
+ break
+ if config.terminate_on_unknown_calls and isinstance(fcc, FunctionCallContent) and fcc.name not in tool_map:
+ raise KeyError(f'Error: Requested function "{fcc.name}" not found.')
if approval_needed:
# approval can only be needed for Function Call Contents, not Approval Responses.
return [
@@ -1181,6 +1358,9 @@ async def _execute_function_calls(
for fcc in function_calls
if isinstance(fcc, FunctionCallContent)
]
+ if declaration_only_flag:
+ # return the declaration only tools to the user, since we cannot execute them.
+ return [fcc for fcc in function_calls if isinstance(fcc, FunctionCallContent)]
# Run all function calls concurrently
return await asyncio.gather(*[
@@ -1191,6 +1371,7 @@ async def _execute_function_calls(
sequence_index=seq_idx,
request_index=attempt_idx,
middleware_pipeline=middleware_pipeline,
+ config=config,
)
for seq_idx, function_call in enumerate(function_calls)
])
@@ -1334,17 +1515,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)
+ # Get the config for function invocation (not part of ChatClientProtocol, hence getattr)
+ config: FunctionInvocationConfiguration | None = getattr(self, "function_invocation_configuration", None)
+ if not config:
+ # Default config if not set
+ config = FunctionInvocationConfiguration()
+ errors_in_a_row: int = 0
prepped_messages = prepare_messages(messages)
response: "ChatResponse | None" = None
fcc_messages: "list[ChatMessage]" = []
- for attempt_idx in range(instance_max_iterations):
+ for attempt_idx in range(config.max_iterations if config.enabled else 0):
fcc_todo = _collect_approval_responses(prepped_messages)
if fcc_todo:
tools = _extract_tools(kwargs)
@@ -1352,13 +1533,29 @@ def _handle_function_calls_response(
approved_responses = [resp for resp in fcc_todo.values() if resp.approved]
approved_function_results: list[Contents] = []
if approved_responses:
- approved_function_results = await _execute_function_calls(
+ approved_function_results = await _try_execute_function_calls(
custom_args=kwargs,
attempt_idx=attempt_idx,
function_calls=approved_responses,
tools=tools, # type: ignore
middleware_pipeline=stored_middleware_pipeline,
+ config=config,
)
+ if any(
+ fcr.exception is not None
+ for fcr in approved_function_results
+ if isinstance(fcr, FunctionResultContent)
+ ):
+ errors_in_a_row += 1
+ # no need to reset the counter here, since this is the start of a new attempt.
+ if errors_in_a_row >= config.max_consecutive_errors_per_request:
+ logger.warning(
+ "Maximum consecutive function call errors reached (%d). "
+ "Stopping further function calls for this request.",
+ config.max_consecutive_errors_per_request,
+ )
+ # break out of the loop and do the fallback response
+ break
_replace_approval_contents_with_results(prepped_messages, fcc_todo, approved_function_results)
response = await func(self, messages=prepped_messages, **kwargs)
@@ -1381,15 +1578,15 @@ def _handle_function_calls_response(
if function_calls and tools:
# Use the stored middleware pipeline instead of extracting from kwargs
# because kwargs may have been modified by the underlying function
- function_call_results: list[Contents] = await _execute_function_calls(
+ function_call_results: list[Contents] = await _try_execute_function_calls(
custom_args=kwargs,
attempt_idx=attempt_idx,
function_calls=function_calls,
tools=tools, # type: ignore
middleware_pipeline=stored_middleware_pipeline,
+ config=config,
)
-
- # Check if we have approval requests in the results
+ # Check if we have approval requests or function calls (not results) in the results
if any(isinstance(fccr, FunctionApprovalRequestContent) for fccr in function_call_results):
# Add approval requests to the existing assistant message (with tool_calls)
# instead of creating a separate tool message
@@ -1402,6 +1599,26 @@ def _handle_function_calls_response(
result_message = ChatMessage(role="assistant", contents=function_call_results)
response.messages.append(result_message)
return response
+ if any(isinstance(fccr, FunctionCallContent) for fccr in function_call_results):
+ # the function calls are already in the response, so we just continue
+ return response
+
+ if any(
+ fcr.exception is not None
+ for fcr in function_call_results
+ if isinstance(fcr, FunctionResultContent)
+ ):
+ errors_in_a_row += 1
+ if errors_in_a_row >= config.max_consecutive_errors_per_request:
+ logger.warning(
+ "Maximum consecutive function call errors reached (%d). "
+ "Stopping further function calls for this request.",
+ config.max_consecutive_errors_per_request,
+ )
+ # break out of the loop and do the fallback response
+ break
+ else:
+ errors_in_a_row = 0
# add a single ChatMessage to the response with the results
result_message = ChatMessage(role="tool", contents=function_call_results)
@@ -1482,16 +1699,16 @@ 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)
+ # Get the config for function invocation (not part of ChatClientProtocol, hence getattr)
+ config: FunctionInvocationConfiguration | None = getattr(self, "function_invocation_configuration", None)
+ if not config:
+ # Default config if not set
+ config = FunctionInvocationConfiguration()
+ errors_in_a_row: int = 0
prepped_messages = prepare_messages(messages)
fcc_messages: "list[ChatMessage]" = []
- for attempt_idx in range(instance_max_iterations):
+ for attempt_idx in range(config.max_iterations if config.enabled else 0):
fcc_todo = _collect_approval_responses(prepped_messages)
if fcc_todo:
tools = _extract_tools(kwargs)
@@ -1499,13 +1716,21 @@ def _handle_function_calls_streaming_response(
approved_responses = [resp for resp in fcc_todo.values() if resp.approved]
approved_function_results: list[Contents] = []
if approved_responses:
- approved_function_results = await _execute_function_calls(
+ approved_function_results = await _try_execute_function_calls(
custom_args=kwargs,
attempt_idx=attempt_idx,
function_calls=approved_responses,
tools=tools, # type: ignore
middleware_pipeline=stored_middleware_pipeline,
+ config=config,
)
+ if any(
+ fcr.exception is not None
+ for fcr in approved_function_results
+ if isinstance(fcr, FunctionResultContent)
+ ):
+ errors_in_a_row += 1
+ # no need to reset the counter here, since this is the start of a new attempt.
_replace_approval_contents_with_results(prepped_messages, fcc_todo, approved_function_results)
all_updates: list["ChatResponseUpdate"] = []
@@ -1551,15 +1776,16 @@ def _handle_function_calls_streaming_response(
if function_calls and tools:
# Use the stored middleware pipeline instead of extracting from kwargs
# because kwargs may have been modified by the underlying function
- function_call_results: list[Contents] = await _execute_function_calls(
+ function_call_results: list[Contents] = await _try_execute_function_calls(
custom_args=kwargs,
attempt_idx=attempt_idx,
function_calls=function_calls,
tools=tools, # type: ignore
middleware_pipeline=stored_middleware_pipeline,
+ config=config,
)
- # Check if we have approval requests in the results
+ # Check if we have approval requests or function calls (not results) in the results
if any(isinstance(fccr, FunctionApprovalRequestContent) for fccr in function_call_results):
# Add approval requests to the existing assistant message (with tool_calls)
# instead of creating a separate tool message
@@ -1575,6 +1801,26 @@ def _handle_function_calls_streaming_response(
yield ChatResponseUpdate(contents=function_call_results, role="assistant")
response.messages.append(result_message)
return
+ if any(isinstance(fccr, FunctionCallContent) for fccr in function_call_results):
+ # the function calls were already yielded.
+ return
+
+ if any(
+ fcr.exception is not None
+ for fcr in function_call_results
+ if isinstance(fcr, FunctionResultContent)
+ ):
+ errors_in_a_row += 1
+ if errors_in_a_row >= config.max_consecutive_errors_per_request:
+ logger.warning(
+ "Maximum consecutive function call errors reached (%d). "
+ "Stopping further function calls for this request.",
+ config.max_consecutive_errors_per_request,
+ )
+ # break out of the loop and do the fallback response
+ break
+ else:
+ errors_in_a_row = 0
# add a single ChatMessage to the response with the results
result_message = ChatMessage(role="tool", contents=function_call_results)
@@ -1648,10 +1894,6 @@ def use_function_invocation(
if getattr(chat_client, FUNCTION_INVOKING_CHAT_CLIENT_MARKER, False):
return chat_client
- # 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
diff --git a/python/packages/core/agent_framework/_workflows/_handoff.py b/python/packages/core/agent_framework/_workflows/_handoff.py
index c98d9e752c..3c5995aeaf 100644
--- a/python/packages/core/agent_framework/_workflows/_handoff.py
+++ b/python/packages/core/agent_framework/_workflows/_handoff.py
@@ -80,6 +80,14 @@ def _clone_chat_agent(agent: ChatAgent) -> ChatAgent:
options = agent.chat_options
middleware = list(agent.middleware or [])
+ # Reconstruct the original tools list by combining regular tools with MCP tools.
+ # ChatAgent.__init__ separates MCP tools into _local_mcp_tools during initialization,
+ # so we need to recombine them here to pass the complete tools list to the constructor.
+ # This makes sure MCP tools are preserved when cloning agents for handoff workflows.
+ all_tools = list(options.tools) if options.tools else []
+ if agent._local_mcp_tools:
+ all_tools.extend(agent._local_mcp_tools)
+
return ChatAgent(
chat_client=agent.chat_client,
instructions=options.instructions,
@@ -101,7 +109,7 @@ def _clone_chat_agent(agent: ChatAgent) -> ChatAgent:
store=options.store,
temperature=options.temperature,
tool_choice=options.tool_choice, # type: ignore[arg-type]
- tools=list(options.tools) if options.tools else None,
+ tools=all_tools if all_tools else None,
top_p=options.top_p,
user=options.user,
additional_chat_options=dict(options.additional_properties),
diff --git a/python/packages/core/agent_framework/chatkit/__init__.py b/python/packages/core/agent_framework/chatkit/__init__.py
new file mode 100644
index 0000000000..163e6b412d
--- /dev/null
+++ b/python/packages/core/agent_framework/chatkit/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import importlib
+from typing import Any
+
+PACKAGE_NAME = "agent_framework_chatkit"
+PACKAGE_EXTRA = "chatkit"
+_IMPORTS = ["__version__", "ThreadItemConverter", "simple_to_agent_input", "stream_agent_response"]
+
+
+def __getattr__(name: str) -> Any:
+ if name in _IMPORTS:
+ try:
+ return getattr(importlib.import_module(PACKAGE_NAME), name)
+ except ModuleNotFoundError as exc:
+ raise ModuleNotFoundError(
+ f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
+ ) from exc
+ raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
+
+
+def __dir__() -> list[str]:
+ return _IMPORTS
diff --git a/python/packages/core/agent_framework/chatkit/__init__.pyi b/python/packages/core/agent_framework/chatkit/__init__.pyi
new file mode 100644
index 0000000000..9bd90e638d
--- /dev/null
+++ b/python/packages/core/agent_framework/chatkit/__init__.pyi
@@ -0,0 +1,10 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from agent_framework_chatkit import (
+ ThreadItemConverter,
+ __version__,
+ simple_to_agent_input,
+ stream_agent_response,
+)
+
+__all__ = ["ThreadItemConverter", "__version__", "simple_to_agent_input", "stream_agent_response"]
diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py
index 239efb76e3..8a28075e62 100644
--- a/python/packages/core/agent_framework/openai/_assistants_client.py
+++ b/python/packages/core/agent_framework/openai/_assistants_client.py
@@ -502,8 +502,6 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
tool_outputs = []
if function_result_content.result:
output = prepare_function_call_results(function_result_content.result)
- elif function_result_content.exception:
- output = "Error: " + str(function_result_content.exception)
else:
output = "No output received."
tool_outputs.append(ToolOutput(tool_call_id=call_id, output=output))
diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py
index 70a37894d4..e6a4087508 100644
--- a/python/packages/core/agent_framework/openai/_chat_client.py
+++ b/python/packages/core/agent_framework/openai/_chat_client.py
@@ -380,11 +380,6 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
args["tool_call_id"] = content.call_id
if content.result is not None:
args["content"] = prepare_function_call_results(content.result)
- elif content.exception is not None:
- # Send the exception message to the model
- # Otherwise we won't have any channels to talk to OpenAI
- # TODO(yuge): This should ideally be customizable
- args["content"] = "Error: " + str(content.exception)
case _:
if "content" not in args:
args["content"] = []
diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py
index 83a7cb8098..301bfa8273 100644
--- a/python/packages/core/agent_framework/openai/_responses_client.py
+++ b/python/packages/core/agent_framework/openai/_responses_client.py
@@ -511,8 +511,6 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
}
if content.result:
args["output"] = prepare_function_call_results(content.result)
- if content.exception:
- args["output"] = "Error: " + str(content.exception)
return args
case FunctionApprovalRequestContent():
return {
diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml
index 71855049a4..77dae0ea63 100644
--- a/python/packages/core/pyproject.toml
+++ b/python/packages/core/pyproject.toml
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
@@ -32,7 +33,7 @@ dependencies = [
"opentelemetry-exporter-otlp-proto-grpc>=1.36.0",
"opentelemetry-semantic-conventions-ai>=0.4.13",
# connectors and functions
- "openai>=1.99.0,<2",
+ "openai>=1.99.0",
"azure-identity>=1,<2",
"mcp[ws]>=1.13",
]
diff --git a/python/packages/core/tests/core/test_function_invocation_logic.py b/python/packages/core/tests/core/test_function_invocation_logic.py
index 2812f19c9d..77b95d98a2 100644
--- a/python/packages/core/tests/core/test_function_invocation_logic.py
+++ b/python/packages/core/tests/core/test_function_invocation_logic.py
@@ -605,7 +605,7 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol):
]
# Set max_iterations to 1 in additional_properties
- chat_client_base.additional_properties = {"max_iterations": 1}
+ chat_client_base.function_invocation_configuration.max_iterations = 1
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
@@ -615,3 +615,1450 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol):
# 3. Fall back to asking for a plain answer with tool_choice="none"
assert exec_counter == 1 # Only first function executed
assert response.messages[-1].text == "I broke out of the function invocation loop..." # Failsafe response
+
+
+async def test_function_invocation_config_enabled_false(chat_client_base: ChatClientProtocol):
+ """Test that setting enabled=False disables function invocation."""
+ exec_counter = 0
+
+ @ai_function(name="test_function")
+ def ai_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Processed {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(messages=ChatMessage(role="assistant", text="response without function calling")),
+ ]
+
+ # Disable function invocation
+ chat_client_base.function_invocation_configuration.enabled = False
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
+
+ # Function should not be executed - when enabled=False, the loop doesn't run
+ assert exec_counter == 0
+ # The response should be from the mock client
+ assert len(response.messages) > 0
+
+
+async def test_function_invocation_config_max_consecutive_errors(chat_client_base: ChatClientProtocol):
+ """Test that max_consecutive_errors_per_request limits error retries."""
+
+ @ai_function(name="error_function")
+ def error_func(arg1: str) -> str:
+ raise ValueError("Function error")
+
+ # Set up multiple function call responses that will all error
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="error_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="2", name="error_function", arguments='{"arg1": "value2"}')],
+ )
+ ),
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="3", name="error_function", arguments='{"arg1": "value3"}')],
+ )
+ ),
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="4", name="error_function", arguments='{"arg1": "value4"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="final response")),
+ ]
+
+ # Set max_consecutive_errors to 2
+ chat_client_base.function_invocation_configuration.max_consecutive_errors_per_request = 2
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[error_func])
+
+ # Should stop after 2 consecutive errors and force a non-tool response
+ error_results = [
+ content
+ for msg in response.messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent) and content.exception
+ ]
+ # The first call errors, then the second call errors, hitting the limit
+ # So we get 2 function calls with errors, but the responses show the behavior stopped
+ assert len(error_results) >= 1 # At least one error occurred
+ # Should have stopped making new function calls after hitting the error limit
+ function_calls = [
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionCallContent)
+ ]
+ # Should have made at most 2 function calls before stopping
+ assert len(function_calls) <= 2
+
+
+async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_client_base: ChatClientProtocol):
+ """Test that terminate_on_unknown_calls=False returns error message for unknown functions."""
+ exec_counter = 0
+
+ @ai_function(name="known_function")
+ def known_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Processed {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="unknown_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set terminate_on_unknown_calls to False (default)
+ chat_client_base.function_invocation_configuration.terminate_on_unknown_calls = False
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[known_func])
+
+ # Should have a result message indicating the tool wasn't found
+ assert len(response.messages) == 3
+ assert isinstance(response.messages[1].contents[0], FunctionResultContent)
+ result_str = response.messages[1].contents[0].result or response.messages[1].contents[0].exception or ""
+ assert "not found" in result_str.lower()
+ assert exec_counter == 0 # Known function not executed
+
+
+async def test_function_invocation_config_terminate_on_unknown_calls_true(chat_client_base: ChatClientProtocol):
+ """Test that terminate_on_unknown_calls=True stops execution on unknown functions."""
+ exec_counter = 0
+
+ @ai_function(name="known_function")
+ def known_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Processed {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="unknown_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ]
+
+ # Set terminate_on_unknown_calls to True
+ chat_client_base.function_invocation_configuration.terminate_on_unknown_calls = True
+
+ # Should raise an exception when encountering an unknown function
+ with pytest.raises(KeyError, match='Error: Requested function "unknown_function" not found'):
+ await chat_client_base.get_response("hello", tool_choice="auto", tools=[known_func])
+
+ assert exec_counter == 0
+
+
+async def test_function_invocation_config_additional_tools(chat_client_base: ChatClientProtocol):
+ """Test that additional_tools are available but treated as declaration_only."""
+ exec_counter_visible = 0
+ exec_counter_hidden = 0
+
+ @ai_function(name="visible_function")
+ def visible_func(arg1: str) -> str:
+ nonlocal exec_counter_visible
+ exec_counter_visible += 1
+ return f"Visible {arg1}"
+
+ @ai_function(name="hidden_function")
+ def hidden_func(arg1: str) -> str:
+ nonlocal exec_counter_hidden
+ exec_counter_hidden += 1
+ return f"Hidden {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="hidden_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Add hidden_func to additional_tools
+ chat_client_base.function_invocation_configuration.additional_tools = [hidden_func]
+
+ # Only pass visible_func in the tools parameter
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[visible_func])
+
+ # Additional tools are treated as declaration_only, so not executed
+ # The function call should be in the messages but not executed
+ assert exec_counter_hidden == 0
+ assert exec_counter_visible == 0
+ # Should have the function call in messages (declaration_only behavior)
+ function_calls = [
+ content
+ for msg in response.messages
+ for content in msg.contents
+ if isinstance(content, FunctionCallContent) and content.name == "hidden_function"
+ ]
+ assert len(function_calls) >= 1
+
+
+async def test_function_invocation_config_include_detailed_errors_false(chat_client_base: ChatClientProtocol):
+ """Test that include_detailed_errors=False returns generic error messages."""
+
+ @ai_function(name="error_function")
+ def error_func(arg1: str) -> str:
+ raise ValueError("Specific error message that should not appear")
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="error_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set include_detailed_errors to False (default)
+ chat_client_base.function_invocation_configuration.include_detailed_errors = False
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[error_func])
+
+ # Should have a generic error message
+ error_result = next(
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Specific error message" not in error_result.result
+ assert "Error:" in error_result.result # Generic error prefix
+
+
+async def test_function_invocation_config_include_detailed_errors_true(chat_client_base: ChatClientProtocol):
+ """Test that include_detailed_errors=True returns detailed error information."""
+
+ @ai_function(name="error_function")
+ def error_func(arg1: str) -> str:
+ raise ValueError("Specific error message that should appear")
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="error_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set include_detailed_errors to True
+ chat_client_base.function_invocation_configuration.include_detailed_errors = True
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[error_func])
+
+ # Should have detailed error message
+ error_result = next(
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Specific error message that should appear" in error_result.result
+ # The error format includes "Function failed. Exception:" prefix
+ assert "Exception:" in error_result.result
+
+
+async def test_function_invocation_config_validation_max_iterations():
+ """Test that max_iterations validation works correctly."""
+ from agent_framework import FunctionInvocationConfiguration
+
+ # Valid values
+ config = FunctionInvocationConfiguration(max_iterations=1)
+ assert config.max_iterations == 1
+
+ config = FunctionInvocationConfiguration(max_iterations=100)
+ assert config.max_iterations == 100
+
+ # Invalid value (less than 1)
+ with pytest.raises(ValueError, match="max_iterations must be at least 1"):
+ FunctionInvocationConfiguration(max_iterations=0)
+
+ with pytest.raises(ValueError, match="max_iterations must be at least 1"):
+ FunctionInvocationConfiguration(max_iterations=-1)
+
+
+async def test_function_invocation_config_validation_max_consecutive_errors():
+ """Test that max_consecutive_errors_per_request validation works correctly."""
+ from agent_framework import FunctionInvocationConfiguration
+
+ # Valid values
+ config = FunctionInvocationConfiguration(max_consecutive_errors_per_request=0)
+ assert config.max_consecutive_errors_per_request == 0
+
+ config = FunctionInvocationConfiguration(max_consecutive_errors_per_request=5)
+ assert config.max_consecutive_errors_per_request == 5
+
+ # Invalid value (less than 0)
+ with pytest.raises(ValueError, match="max_consecutive_errors_per_request must be 0 or more"):
+ FunctionInvocationConfiguration(max_consecutive_errors_per_request=-1)
+
+
+async def test_argument_validation_error_with_detailed_errors(chat_client_base: ChatClientProtocol):
+ """Test that argument validation errors include details when include_detailed_errors=True."""
+
+ @ai_function(name="typed_function")
+ def typed_func(arg1: int) -> str: # Expects int, not str
+ return f"Got {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="typed_function", arguments='{"arg1": "not_an_int"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set include_detailed_errors to True
+ chat_client_base.function_invocation_configuration.include_detailed_errors = True
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[typed_func])
+
+ # Should have detailed validation error
+ error_result = next(
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Argument parsing failed" in error_result.result
+ assert "Exception:" in error_result.result # Detailed error included
+
+
+async def test_argument_validation_error_without_detailed_errors(chat_client_base: ChatClientProtocol):
+ """Test that argument validation errors are generic when include_detailed_errors=False."""
+
+ @ai_function(name="typed_function")
+ def typed_func(arg1: int) -> str: # Expects int, not str
+ return f"Got {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="typed_function", arguments='{"arg1": "not_an_int"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set include_detailed_errors to False (default)
+ chat_client_base.function_invocation_configuration.include_detailed_errors = False
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[typed_func])
+
+ # Should have generic validation error
+ error_result = next(
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Argument parsing failed" in error_result.result
+ assert "Exception:" not in error_result.result # No detailed error
+
+
+async def test_hosted_tool_approval_response(chat_client_base: ChatClientProtocol):
+ """Test handling of approval responses for hosted tools (tools not in tool_map)."""
+ from agent_framework import FunctionApprovalResponseContent
+
+ @ai_function(name="local_function")
+ def local_func(arg1: str) -> str:
+ return f"Local {arg1}"
+
+ # Create an approval response for a hosted tool that's not in our tool_map
+ hosted_function_call = FunctionCallContent(
+ call_id="hosted_1", name="hosted_function", arguments='{"arg1": "value"}'
+ )
+ approval_response = FunctionApprovalResponseContent(
+ id="approval_1",
+ function_call=hosted_function_call,
+ approved=True,
+ )
+
+ chat_client_base.run_responses = [
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Send the approval response
+ response = await chat_client_base.get_response(
+ [ChatMessage(role="user", contents=[approval_response])],
+ tool_choice="auto",
+ tools=[local_func],
+ )
+
+ # The hosted tool approval should be returned as-is (not executed)
+ # Check that we got a response without errors
+ assert response is not None
+
+
+async def test_unapproved_tool_execution_raises_exception(chat_client_base: ChatClientProtocol):
+ """Test that attempting to execute an unapproved tool raises ToolException."""
+ from agent_framework import FunctionApprovalResponseContent
+
+ @ai_function(name="test_function", approval_mode="always_require")
+ def test_func(arg1: str) -> str:
+ return f"Result {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[
+ FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}'),
+ ],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Get approval request
+ response1 = await chat_client_base.get_response("hello", tool_choice="auto", tools=[test_func])
+
+ approval_req = [c for c in response1.messages[0].contents if isinstance(c, FunctionApprovalRequestContent)][0]
+
+ # Create a rejection response (approved=False)
+ rejection_response = FunctionApprovalResponseContent(
+ id=approval_req.id,
+ function_call=approval_req.function_call,
+ approved=False,
+ )
+
+ # Continue conversation with rejection
+ all_messages = response1.messages + [ChatMessage(role="user", contents=[rejection_response])]
+
+ # This should handle the rejection gracefully (not raise ToolException to user)
+ await chat_client_base.get_response(all_messages, tool_choice="auto", tools=[test_func])
+
+ # Should have a rejection result
+ rejection_result = next(
+ (
+ content
+ for msg in all_messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent)
+ and "rejected" in (content.result or content.exception or "").lower()
+ ),
+ None,
+ )
+ assert rejection_result is not None
+
+
+async def test_approved_function_call_with_error_without_detailed_errors(chat_client_base: ChatClientProtocol):
+ """Test that approved functions that raise errors return generic error messages.
+
+ When include_detailed_errors=False.
+ """
+ from agent_framework import FunctionApprovalResponseContent
+
+ exec_counter = 0
+
+ @ai_function(name="error_func", approval_mode="always_require")
+ def error_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ raise ValueError("Specific error from approved function")
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="error_func", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set include_detailed_errors to False (default)
+ chat_client_base.function_invocation_configuration.include_detailed_errors = False
+
+ # Get approval request
+ response1 = await chat_client_base.get_response("hello", tool_choice="auto", tools=[error_func])
+
+ approval_req = [c for c in response1.messages[0].contents if isinstance(c, FunctionApprovalRequestContent)][0]
+
+ # Approve the function
+ approval_response = FunctionApprovalResponseContent(
+ id=approval_req.id,
+ function_call=approval_req.function_call,
+ approved=True,
+ )
+
+ all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])]
+
+ # Execute the approved function (which will error)
+ await chat_client_base.get_response(all_messages, tool_choice="auto", tools=[error_func])
+
+ # Should have executed the function
+ assert exec_counter == 1
+
+ # Should have an error result with generic message
+ error_result = next(
+ (
+ content
+ for msg in all_messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent) and content.exception is not None
+ ),
+ None,
+ )
+ assert error_result is not None
+ assert error_result.result is not None
+ assert "Error: Function failed." in error_result.result
+ assert "Specific error from approved function" not in error_result.result # Detail not included
+
+
+async def test_approved_function_call_with_error_with_detailed_errors(chat_client_base: ChatClientProtocol):
+ """Test that approved functions that raise errors return detailed error messages.
+
+ When include_detailed_errors=True.
+ """
+ from agent_framework import FunctionApprovalResponseContent
+
+ exec_counter = 0
+
+ @ai_function(name="error_func", approval_mode="always_require")
+ def error_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ raise ValueError("Specific error from approved function")
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="error_func", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set include_detailed_errors to True
+ chat_client_base.function_invocation_configuration.include_detailed_errors = True
+
+ # Get approval request
+ response1 = await chat_client_base.get_response("hello", tool_choice="auto", tools=[error_func])
+
+ approval_req = [c for c in response1.messages[0].contents if isinstance(c, FunctionApprovalRequestContent)][0]
+
+ # Approve the function
+ approval_response = FunctionApprovalResponseContent(
+ id=approval_req.id,
+ function_call=approval_req.function_call,
+ approved=True,
+ )
+
+ all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])]
+
+ # Execute the approved function (which will error)
+ await chat_client_base.get_response(all_messages, tool_choice="auto", tools=[error_func])
+
+ # Should have executed the function
+ assert exec_counter == 1
+
+ # Should have an error result with detailed message
+ error_result = next(
+ (
+ content
+ for msg in all_messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent) and content.exception is not None
+ ),
+ None,
+ )
+ assert error_result is not None
+ assert error_result.result is not None
+ assert "Error: Function failed." in error_result.result
+ assert "Exception:" in error_result.result
+ assert "Specific error from approved function" in error_result.result # Detail included
+
+
+async def test_approved_function_call_with_validation_error(chat_client_base: ChatClientProtocol):
+ """Test that approved functions with validation errors are handled correctly."""
+ from agent_framework import FunctionApprovalResponseContent
+
+ exec_counter = 0
+
+ @ai_function(name="typed_func", approval_mode="always_require")
+ def typed_func(arg1: int) -> str: # Expects int, not str
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Got {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="typed_func", arguments='{"arg1": "not_an_int"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Set include_detailed_errors to True to see validation details
+ chat_client_base.function_invocation_configuration.include_detailed_errors = True
+
+ # Get approval request
+ response1 = await chat_client_base.get_response("hello", tool_choice="auto", tools=[typed_func])
+
+ approval_req = [c for c in response1.messages[0].contents if isinstance(c, FunctionApprovalRequestContent)][0]
+
+ # Approve the function (even though it will fail validation)
+ approval_response = FunctionApprovalResponseContent(
+ id=approval_req.id,
+ function_call=approval_req.function_call,
+ approved=True,
+ )
+
+ all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])]
+
+ # Execute the approved function (which will fail validation)
+ await chat_client_base.get_response(all_messages, tool_choice="auto", tools=[typed_func])
+
+ # Should NOT have executed the function (validation failed before execution)
+ assert exec_counter == 0
+
+ # Should have a validation error result
+ error_result = next(
+ (
+ content
+ for msg in all_messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent) and content.exception is not None
+ ),
+ None,
+ )
+ assert error_result is not None
+ assert error_result.result is not None
+ assert "Argument parsing failed" in error_result.result
+
+
+async def test_approved_function_call_successful_execution(chat_client_base: ChatClientProtocol):
+ """Test that approved functions execute successfully when no errors occur."""
+ from agent_framework import FunctionApprovalResponseContent
+
+ exec_counter = 0
+
+ @ai_function(name="success_func", approval_mode="always_require")
+ def success_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Success {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="success_func", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Get approval request
+ response1 = await chat_client_base.get_response("hello", tool_choice="auto", tools=[success_func])
+
+ approval_req = [c for c in response1.messages[0].contents if isinstance(c, FunctionApprovalRequestContent)][0]
+
+ # Approve the function
+ approval_response = FunctionApprovalResponseContent(
+ id=approval_req.id,
+ function_call=approval_req.function_call,
+ approved=True,
+ )
+
+ all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])]
+
+ # Execute the approved function
+ await chat_client_base.get_response(all_messages, tool_choice="auto", tools=[success_func])
+
+ # Should have executed successfully
+ assert exec_counter == 1
+
+ # Should have a success result
+ success_result = next(
+ (
+ content
+ for msg in all_messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent) and content.exception is None
+ ),
+ None,
+ )
+ assert success_result is not None
+ assert success_result.result == "Success value1"
+
+
+async def test_declaration_only_tool_not_executed(chat_client_base: ChatClientProtocol):
+ """Test that declaration_only tools are not executed."""
+ exec_counter = 0
+
+ @ai_function(name="declaration_func")
+ def declaration_func_inner(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Result {arg1}"
+
+ # Create a new AIFunction with declaration_only set
+ from agent_framework import AIFunction
+
+ declaration_func = AIFunction(
+ name="declaration_func",
+ func=declaration_func_inner,
+ additional_properties={"declaration_only": True},
+ )
+ # Set declaration_only on the instance
+ object.__setattr__(declaration_func, "_declaration_only", True)
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="declaration_func", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[declaration_func])
+
+ # Function should NOT be executed
+ assert exec_counter == 0
+ # Should have the function call in messages but not a result
+ function_calls = [
+ content
+ for msg in response.messages
+ for content in msg.contents
+ if isinstance(content, FunctionCallContent) and content.name == "declaration_func"
+ ]
+ assert len(function_calls) >= 1
+
+
+async def test_multiple_function_calls_parallel_execution(chat_client_base: ChatClientProtocol):
+ """Test that multiple function calls are executed in parallel."""
+ import asyncio
+
+ exec_order = []
+
+ @ai_function(name="func1")
+ async def func1(arg1: str) -> str:
+ exec_order.append("func1_start")
+ await asyncio.sleep(0.01) # Small delay
+ exec_order.append("func1_end")
+ return f"Result1 {arg1}"
+
+ @ai_function(name="func2")
+ async def func2(arg1: str) -> str:
+ exec_order.append("func2_start")
+ await asyncio.sleep(0.01) # Small delay
+ exec_order.append("func2_end")
+ return f"Result2 {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[
+ FunctionCallContent(call_id="1", name="func1", arguments='{"arg1": "value1"}'),
+ FunctionCallContent(call_id="2", name="func2", arguments='{"arg1": "value2"}'),
+ ],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[func1, func2])
+
+ # Both functions should have been executed
+ assert "func1_start" in exec_order
+ assert "func1_end" in exec_order
+ assert "func2_start" in exec_order
+ assert "func2_end" in exec_order
+
+ # Should have results for both
+ results = [
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionResultContent)
+ ]
+ assert len(results) == 2
+
+
+async def test_callable_function_converted_to_ai_function(chat_client_base: ChatClientProtocol):
+ """Test that plain callable functions are converted to AIFunction."""
+ exec_counter = 0
+
+ def plain_function(arg1: str) -> str:
+ """A plain function without decorator."""
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Plain {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="plain_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ # Pass plain function (will be auto-converted)
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[plain_function])
+
+ # Function should be executed
+ assert exec_counter == 1
+ result = next(
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionResultContent)
+ )
+ assert result.result == "Plain value1"
+
+
+async def test_conversation_id_handling(chat_client_base: ChatClientProtocol):
+ """Test that conversation_id is properly handled and messages are cleared."""
+
+ @ai_function(name="test_function")
+ def test_func(arg1: str) -> str:
+ return f"Result {arg1}"
+
+ # Return a response with a conversation_id
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
+ ),
+ conversation_id="conv_123", # Simulate service-side thread
+ ),
+ ChatResponse(
+ messages=ChatMessage(role="assistant", text="done"),
+ conversation_id="conv_123",
+ ),
+ ]
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[test_func])
+
+ # Should have executed the function
+ results = [
+ content for msg in response.messages for content in msg.contents if isinstance(content, FunctionResultContent)
+ ]
+ assert len(results) >= 1
+ assert response.conversation_id == "conv_123"
+
+
+async def test_function_result_appended_to_existing_assistant_message(chat_client_base: ChatClientProtocol):
+ """Test that function results are appended to existing assistant message when appropriate."""
+
+ @ai_function(name="test_function")
+ def test_func(arg1: str) -> str:
+ return f"Result {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[test_func])
+
+ # Should have messages with both function call and function result
+ assert len(response.messages) >= 2
+ # Check that we have both a function call and a function result
+ has_call = any(isinstance(content, FunctionCallContent) for msg in response.messages for content in msg.contents)
+ has_result = any(
+ isinstance(content, FunctionResultContent) for msg in response.messages for content in msg.contents
+ )
+ assert has_call
+ assert has_result
+
+
+async def test_error_recovery_resets_counter(chat_client_base: ChatClientProtocol):
+ """Test that error counter resets after a successful function call."""
+
+ call_count = 0
+
+ @ai_function(name="sometimes_fails")
+ def sometimes_fails(arg1: str) -> str:
+ nonlocal call_count
+ call_count += 1
+ if call_count == 1:
+ raise ValueError("First call fails")
+ return f"Success {arg1}"
+
+ chat_client_base.run_responses = [
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="1", name="sometimes_fails", arguments='{"arg1": "value1"}')],
+ )
+ ),
+ ChatResponse(
+ messages=ChatMessage(
+ role="assistant",
+ contents=[FunctionCallContent(call_id="2", name="sometimes_fails", arguments='{"arg1": "value2"}')],
+ )
+ ),
+ ChatResponse(messages=ChatMessage(role="assistant", text="done")),
+ ]
+
+ response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[sometimes_fails])
+
+ # Should have both an error and a success
+ error_results = [
+ content
+ for msg in response.messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent) and content.exception
+ ]
+ success_results = [
+ content
+ for msg in response.messages
+ for content in msg.contents
+ if isinstance(content, FunctionResultContent) and content.result
+ ]
+
+ assert len(error_results) >= 1
+ assert len(success_results) >= 1
+ assert call_count == 2 # Both calls executed
+
+
+# ==================== STREAMING SCENARIO TESTS ====================
+
+
+async def test_streaming_approval_request_generated(chat_client_base: ChatClientProtocol):
+ """Test that approval requests are generated correctly in streaming mode."""
+ exec_counter = 0
+
+ @ai_function(name="test_func", approval_mode="always_require")
+ def func_with_approval(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Result {arg1}"
+
+ # Setup: function call that requires approval, streamed
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="test_func", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ],
+ ]
+
+ # Get the streaming response with approval request
+ updates = []
+ async for update in chat_client_base.get_streaming_response(
+ "hello", tool_choice="auto", tools=[func_with_approval]
+ ):
+ updates.append(update)
+
+ # Should have function call update and approval request
+ approval_requests = [
+ content
+ for update in updates
+ for content in update.contents
+ if isinstance(content, FunctionApprovalRequestContent)
+ ]
+ assert len(approval_requests) == 1
+ assert approval_requests[0].function_call.name == "test_func"
+ assert exec_counter == 0 # Function not executed yet due to approval requirement
+
+
+async def test_streaming_max_iterations_limit(chat_client_base: ChatClientProtocol):
+ """Test that MAX_ITERATIONS in streaming mode limits function call loops."""
+ exec_counter = 0
+
+ @ai_function(name="test_function")
+ def ai_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Processed {arg1}"
+
+ # Set up multiple function call responses to create a loop
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1":')],
+ role="assistant",
+ ),
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="test_function", arguments='"value1"}')],
+ role="assistant",
+ ),
+ ],
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="2", name="test_function", arguments='{"arg1":')],
+ role="assistant",
+ ),
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="2", name="test_function", arguments='"value2"}')],
+ role="assistant",
+ ),
+ ],
+ # Failsafe response when tool_choice is set to "none"
+ [ChatResponseUpdate(contents=[TextContent(text="giving up on tools")], role="assistant")],
+ ]
+
+ # Set max_iterations to 1 in additional_properties
+ chat_client_base.function_invocation_configuration.max_iterations = 1
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[ai_func]):
+ updates.append(update)
+
+ # With max_iterations=1, we should only execute first function
+ assert exec_counter == 1 # Only first function executed
+ # Should have the failsafe message
+ last_text = "".join(u.text or "" for u in updates if u.text)
+ assert "I broke out of the function invocation loop..." in last_text
+
+
+async def test_streaming_function_invocation_config_enabled_false(chat_client_base: ChatClientProtocol):
+ """Test that setting enabled=False disables function invocation in streaming mode."""
+ exec_counter = 0
+
+ @ai_function(name="test_function")
+ def ai_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Processed {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [ChatResponseUpdate(contents=[TextContent(text="response without function calling")], role="assistant")],
+ ]
+
+ # Disable function invocation
+ chat_client_base.function_invocation_configuration.enabled = False
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[ai_func]):
+ updates.append(update)
+
+ # Function should not be executed - when enabled=False, the loop doesn't run
+ assert exec_counter == 0
+ # The response should be from the mock client
+ assert len(updates) > 0
+
+
+async def test_streaming_function_invocation_config_max_consecutive_errors(chat_client_base: ChatClientProtocol):
+ """Test that max_consecutive_errors_per_request limits error retries in streaming mode."""
+
+ @ai_function(name="error_function")
+ def error_func(arg1: str) -> str:
+ raise ValueError("Function error")
+
+ # Set up multiple function call responses that will all error
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="error_function", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ],
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="2", name="error_function", arguments='{"arg1": "value2"}')],
+ role="assistant",
+ ),
+ ],
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="3", name="error_function", arguments='{"arg1": "value3"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="final response")], role="assistant")],
+ ]
+
+ # Set max_consecutive_errors to 2
+ chat_client_base.function_invocation_configuration.max_consecutive_errors_per_request = 2
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[error_func]):
+ updates.append(update)
+
+ # Should stop after 2 consecutive errors
+ error_results = [
+ content
+ for update in updates
+ for content in update.contents
+ if isinstance(content, FunctionResultContent) and content.exception
+ ]
+ # At least one error occurred
+ assert len(error_results) >= 1
+ # Should have stopped making new function calls after hitting the error limit
+ function_calls = [
+ content for update in updates for content in update.contents if isinstance(content, FunctionCallContent)
+ ]
+ # Should have made at most 2 function calls before stopping
+ assert len(function_calls) <= 2
+
+
+async def test_streaming_function_invocation_config_terminate_on_unknown_calls_false(
+ chat_client_base: ChatClientProtocol,
+):
+ """Test that terminate_on_unknown_calls=False returns error message for unknown functions in streaming mode."""
+ exec_counter = 0
+
+ @ai_function(name="known_function")
+ def known_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Processed {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="unknown_function", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")],
+ ]
+
+ # Set terminate_on_unknown_calls to False (default)
+ chat_client_base.function_invocation_configuration.terminate_on_unknown_calls = False
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[known_func]):
+ updates.append(update)
+
+ # Should have a result message indicating the tool wasn't found
+ result_contents = [
+ content for update in updates for content in update.contents if isinstance(content, FunctionResultContent)
+ ]
+ assert len(result_contents) >= 1
+ result_str = result_contents[0].result or result_contents[0].exception or ""
+ assert "not found" in result_str.lower()
+ assert exec_counter == 0 # Known function not executed
+
+
+async def test_streaming_function_invocation_config_terminate_on_unknown_calls_true(
+ chat_client_base: ChatClientProtocol,
+):
+ """Test that terminate_on_unknown_calls=True stops execution on unknown functions in streaming mode."""
+ exec_counter = 0
+
+ @ai_function(name="known_function")
+ def known_func(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Processed {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="unknown_function", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ],
+ ]
+
+ # Set terminate_on_unknown_calls to True
+ chat_client_base.function_invocation_configuration.terminate_on_unknown_calls = True
+
+ # Should raise an exception when encountering an unknown function
+ with pytest.raises(KeyError, match='Error: Requested function "unknown_function" not found'):
+ async for _ in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[known_func]):
+ pass
+
+ assert exec_counter == 0
+
+
+async def test_streaming_function_invocation_config_include_detailed_errors_true(chat_client_base: ChatClientProtocol):
+ """Test that include_detailed_errors=True returns detailed error information in streaming mode."""
+
+ @ai_function(name="error_function")
+ def error_func(arg1: str) -> str:
+ raise ValueError("Specific error message that should appear")
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="error_function", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")],
+ ]
+
+ # Set include_detailed_errors to True
+ chat_client_base.function_invocation_configuration.include_detailed_errors = True
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[error_func]):
+ updates.append(update)
+
+ # Should have detailed error message
+ error_result = next(
+ content for update in updates for content in update.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Specific error message that should appear" in error_result.result
+ assert "Exception:" in error_result.result
+
+
+async def test_streaming_function_invocation_config_include_detailed_errors_false(
+ chat_client_base: ChatClientProtocol,
+):
+ """Test that include_detailed_errors=False returns generic error messages in streaming mode."""
+
+ @ai_function(name="error_function")
+ def error_func(arg1: str) -> str:
+ raise ValueError("Specific error message that should not appear")
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="error_function", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")],
+ ]
+
+ # Set include_detailed_errors to False (default)
+ chat_client_base.function_invocation_configuration.include_detailed_errors = False
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[error_func]):
+ updates.append(update)
+
+ # Should have a generic error message
+ error_result = next(
+ content for update in updates for content in update.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Specific error message" not in error_result.result
+ assert "Error:" in error_result.result # Generic error prefix
+
+
+async def test_streaming_argument_validation_error_with_detailed_errors(chat_client_base: ChatClientProtocol):
+ """Test that argument validation errors include details when include_detailed_errors=True in streaming mode."""
+
+ @ai_function(name="typed_function")
+ def typed_func(arg1: int) -> str: # Expects int, not str
+ return f"Got {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="typed_function", arguments='{"arg1": "not_an_int"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")],
+ ]
+
+ # Set include_detailed_errors to True
+ chat_client_base.function_invocation_configuration.include_detailed_errors = True
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[typed_func]):
+ updates.append(update)
+
+ # Should have detailed validation error
+ error_result = next(
+ content for update in updates for content in update.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Argument parsing failed" in error_result.result
+ assert "Exception:" in error_result.result # Detailed error included
+
+
+async def test_streaming_argument_validation_error_without_detailed_errors(chat_client_base: ChatClientProtocol):
+ """Test that argument validation errors are generic when include_detailed_errors=False in streaming mode."""
+
+ @ai_function(name="typed_function")
+ def typed_func(arg1: int) -> str: # Expects int, not str
+ return f"Got {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="typed_function", arguments='{"arg1": "not_an_int"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")],
+ ]
+
+ # Set include_detailed_errors to False (default)
+ chat_client_base.function_invocation_configuration.include_detailed_errors = False
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[typed_func]):
+ updates.append(update)
+
+ # Should have generic validation error
+ error_result = next(
+ content for update in updates for content in update.contents if isinstance(content, FunctionResultContent)
+ )
+ assert error_result.result is not None
+ assert error_result.exception is not None
+ assert "Argument parsing failed" in error_result.result
+ assert "Exception:" not in error_result.result # No detailed error
+
+
+async def test_streaming_multiple_function_calls_parallel_execution(chat_client_base: ChatClientProtocol):
+ """Test that multiple function calls are executed in parallel in streaming mode."""
+ import asyncio
+
+ exec_order = []
+
+ @ai_function(name="func1")
+ async def func1(arg1: str) -> str:
+ exec_order.append("func1_start")
+ await asyncio.sleep(0.01) # Small delay
+ exec_order.append("func1_end")
+ return f"Result1 {arg1}"
+
+ @ai_function(name="func2")
+ async def func2(arg1: str) -> str:
+ exec_order.append("func2_start")
+ await asyncio.sleep(0.01) # Small delay
+ exec_order.append("func2_end")
+ return f"Result2 {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="func1", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="2", name="func2", arguments='{"arg1": "value2"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")],
+ ]
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[func1, func2]):
+ updates.append(update)
+
+ # Both functions should have been executed
+ assert "func1_start" in exec_order
+ assert "func1_end" in exec_order
+ assert "func2_start" in exec_order
+ assert "func2_end" in exec_order
+
+ # Should have results for both
+ results = [
+ content for update in updates for content in update.contents if isinstance(content, FunctionResultContent)
+ ]
+ assert len(results) == 2
+
+
+async def test_streaming_approval_requests_in_assistant_message(chat_client_base: ChatClientProtocol):
+ """Approval requests should be added to assistant updates in streaming mode."""
+ exec_counter = 0
+
+ @ai_function(name="test_func", approval_mode="always_require")
+ def func_with_approval(arg1: str) -> str:
+ nonlocal exec_counter
+ exec_counter += 1
+ return f"Result {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[
+ FunctionCallContent(call_id="1", name="test_func", arguments='{"arg1": "value1"}'),
+ ],
+ role="assistant",
+ ),
+ ],
+ ]
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response(
+ "hello", tool_choice="auto", tools=[func_with_approval]
+ ):
+ updates.append(update)
+
+ # Should have updates containing both the call and approval request
+ approval_requests = [
+ content
+ for update in updates
+ for content in update.contents
+ if isinstance(content, FunctionApprovalRequestContent)
+ ]
+ assert len(approval_requests) == 1
+ assert exec_counter == 0
+
+
+async def test_streaming_error_recovery_resets_counter(chat_client_base: ChatClientProtocol):
+ """Test that error counter resets after a successful function call in streaming mode."""
+
+ call_count = 0
+
+ @ai_function(name="sometimes_fails")
+ def sometimes_fails(arg1: str) -> str:
+ nonlocal call_count
+ call_count += 1
+ if call_count == 1:
+ raise ValueError("First call fails")
+ return f"Success {arg1}"
+
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="1", name="sometimes_fails", arguments='{"arg1": "value1"}')],
+ role="assistant",
+ ),
+ ],
+ [
+ ChatResponseUpdate(
+ contents=[FunctionCallContent(call_id="2", name="sometimes_fails", arguments='{"arg1": "value2"}')],
+ role="assistant",
+ ),
+ ],
+ [ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")],
+ ]
+
+ updates = []
+ async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[sometimes_fails]):
+ updates.append(update)
+
+ # Should have both an error and a success
+ error_results = [
+ content
+ for update in updates
+ for content in update.contents
+ if isinstance(content, FunctionResultContent) and content.exception
+ ]
+ success_results = [
+ content
+ for update in updates
+ for content in update.contents
+ if isinstance(content, FunctionResultContent) and content.result
+ ]
+
+ assert len(error_results) >= 1
+ assert len(success_results) >= 1
+ assert call_count == 2 # Both calls executed
diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py
index e2cf6b8d3d..acd9157363 100644
--- a/python/packages/core/tests/core/test_tools.py
+++ b/python/packages/core/tests/core/test_tools.py
@@ -63,6 +63,26 @@ def test_ai_function_decorator_without_args():
assert test_tool(1, 2) == 3
+def test_ai_function_without_args():
+ """Test the ai_function decorator."""
+
+ @ai_function
+ def test_tool() -> int:
+ """A simple function that adds two numbers."""
+ return 1 + 2
+
+ assert isinstance(test_tool, ToolProtocol)
+ assert isinstance(test_tool, AIFunction)
+ assert test_tool.name == "test_tool"
+ assert test_tool.description == "A simple function that adds two numbers."
+ assert test_tool.parameters() == {
+ "properties": {},
+ "title": "test_tool_input",
+ "type": "object",
+ }
+ assert test_tool() == 3
+
+
async def test_ai_function_decorator_with_async():
"""Test the ai_function decorator with an async function."""
diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/core/tests/openai/test_openai_chat_client.py
index d159091311..8af3ed61aa 100644
--- a/python/packages/core/tests/openai/test_openai_chat_client.py
+++ b/python/packages/core/tests/openai/test_openai_chat_client.py
@@ -689,12 +689,15 @@ def test_function_result_exception_handling(openai_unit_test_env: dict[str, str]
# Test with exception (no result)
test_exception = ValueError("Test error message")
message_with_exception = ChatMessage(
- role="tool", contents=[FunctionResultContent(call_id="call-123", exception=test_exception)]
+ role="tool",
+ contents=[
+ FunctionResultContent(call_id="call-123", result="Error: Function failed.", exception=test_exception)
+ ],
)
openai_messages = client._openai_chat_message_parser(message_with_exception)
assert len(openai_messages) == 1
- assert openai_messages[0]["content"] == "Error: Test error message"
+ assert openai_messages[0]["content"] == "Error: Function failed."
assert openai_messages[0]["tool_call_id"] == "call-123"
diff --git a/python/packages/core/tests/workflow/test_handoff.py b/python/packages/core/tests/workflow/test_handoff.py
index 8042c68e08..44a6403c6f 100644
--- a/python/packages/core/tests/workflow/test_handoff.py
+++ b/python/packages/core/tests/workflow/test_handoff.py
@@ -3,6 +3,7 @@
from collections.abc import AsyncIterable, AsyncIterator
from dataclasses import dataclass
from typing import Any, cast
+from unittest.mock import MagicMock
import pytest
@@ -10,6 +11,7 @@ from agent_framework import (
AgentRunResponse,
AgentRunResponseUpdate,
BaseAgent,
+ ChatAgent,
ChatMessage,
FunctionCallContent,
HandoffBuilder,
@@ -20,6 +22,8 @@ from agent_framework import (
WorkflowEvent,
WorkflowOutputEvent,
)
+from agent_framework._mcp import MCPTool
+from agent_framework._workflows._handoff import _clone_chat_agent
@dataclass
@@ -368,3 +372,32 @@ async def test_handoff_async_termination_condition() -> None:
user_messages = [msg for msg in final_conv_list if msg.role == Role.USER]
assert len(user_messages) == 2
assert termination_call_count > 0
+
+
+async def test_clone_chat_agent_preserves_mcp_tools() -> None:
+ """Test that _clone_chat_agent preserves MCP tools when cloning an agent."""
+ mock_chat_client = MagicMock()
+
+ mock_mcp_tool = MagicMock(spec=MCPTool)
+ mock_mcp_tool.name = "test_mcp_tool"
+
+ def sample_function() -> str:
+ return "test"
+
+ original_agent = ChatAgent(
+ chat_client=mock_chat_client,
+ name="TestAgent",
+ instructions="Test instructions",
+ tools=[mock_mcp_tool, sample_function],
+ )
+
+ assert hasattr(original_agent, "_local_mcp_tools")
+ assert len(original_agent._local_mcp_tools) == 1
+ assert original_agent._local_mcp_tools[0] == mock_mcp_tool
+
+ cloned_agent = _clone_chat_agent(original_agent)
+
+ assert hasattr(cloned_agent, "_local_mcp_tools")
+ assert len(cloned_agent._local_mcp_tools) == 1
+ assert cloned_agent._local_mcp_tools[0] == mock_mcp_tool
+ assert len(cloned_agent.chat_options.tools) == 1
diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml
index 1b22574ed1..2e84110e5d 100644
--- a/python/packages/devui/pyproject.toml
+++ b/python/packages/devui/pyproject.toml
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml
index 8b7f8f3c96..05abc5b6c1 100644
--- a/python/packages/lab/pyproject.toml
+++ b/python/packages/lab/pyproject.toml
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
]
dependencies = [
"agent-framework-core",
@@ -118,7 +119,7 @@ ignore = [
"INP001", # Ignore missing __init__.py in namespace packages.
"RUF029", # Allow use of 'assert' statements; assertions are used for internal checks in experimental code.
"ASYNC240", # Allow 'async for' outside of async functions in test and experimental code.
-]
+]
[tool.coverage.run]
omit = [
diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml
index 93df61461e..a02c266ae2 100644
--- a/python/packages/mem0/pyproject.toml
+++ b/python/packages/mem0/pyproject.toml
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
diff --git a/python/packages/purview/pyproject.toml b/python/packages/purview/pyproject.toml
index 4a498ac957..905793ebe9 100644
--- a/python/packages/purview/pyproject.toml
+++ b/python/packages/purview/pyproject.toml
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Framework :: Pydantic :: 2",
"Typing :: Typed",
]
diff --git a/python/packages/redis/pyproject.toml b/python/packages/redis/pyproject.toml
index 877b45bbc9..a5653d59f1 100644
--- a/python/packages/redis/pyproject.toml
+++ b/python/packages/redis/pyproject.toml
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 0696a923cf..d1cc65112f 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -19,13 +19,16 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
"agent-framework-core",
"agent-framework-a2a",
+ "agent-framework-ag-ui",
"agent-framework-anthropic",
"agent-framework-azure-ai",
+ "agent-framework-chatkit",
"agent-framework-copilotstudio",
"agent-framework-devui",
"agent-framework-lab",
@@ -88,7 +91,9 @@ members = [ "packages/*" ]
agent-framework = { workspace = true }
agent-framework-core = { workspace = true }
agent-framework-a2a = { workspace = true }
+agent-framework-ag-ui = { workspace = true }
agent-framework-azure-ai = { workspace = true }
+agent-framework-chatkit = { workspace = true }
agent-framework-copilotstudio = { workspace = true }
agent-framework-lab = { workspace = true }
agent-framework-mem0 = { workspace = true }
@@ -239,7 +244,9 @@ cmd = """
pytest --import-mode=importlib
--cov=agent_framework
--cov=agent_framework_a2a
+--cov=agent_framework_ag_ui
--cov=agent_framework_azure_ai
+--cov=agent_framework_chatkit
--cov=agent_framework_copilotstudio
--cov=agent_framework_mem0
--cov=agent_framework_redis
diff --git a/python/samples/README.md b/python/samples/README.md
index f8602b3385..f70a390892 100644
--- a/python/samples/README.md
+++ b/python/samples/README.md
@@ -218,9 +218,14 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
| File | Description |
|------|-------------|
-| [`getting_started/tools/ai_tool_with_approval.py`](./getting_started/tools/ai_tool_with_approval.py) | Demonstration of a tool with approvals |
-| [`getting_started/tools/ai_tool_with_approval_and_threads.py`](./getting_started/tools/ai_tool_with_approval_and_threads.py) | Tool Approvals with Threads |
-| [`getting_started/tools/failing_tools.py`](./getting_started/tools/failing_tools.py) | Tool exceptions handled by returning the error for the agent to recover from |
+| [`getting_started/tools/ai_function_declaration_only.py`](./getting_started/tools/ai_function_declaration_only.py) | Function declarations without implementations for testing agent reasoning |
+| [`getting_started/tools/ai_function_from_dict_with_dependency_injection.py`](./getting_started/tools/ai_function_from_dict_with_dependency_injection.py) | Creating AI functions from dictionary definitions using dependency injection |
+| [`getting_started/tools/ai_function_recover_from_failures.py`](./getting_started/tools/ai_function_recover_from_failures.py) | Graceful error handling when tools raise exceptions |
+| [`getting_started/tools/ai_function_with_approval.py`](./getting_started/tools/ai_function_with_approval.py) | User approval workflows for function calls without threads |
+| [`getting_started/tools/ai_function_with_approval_and_threads.py`](./getting_started/tools/ai_function_with_approval_and_threads.py) | Tool approval workflows using threads for conversation history management |
+| [`getting_started/tools/ai_function_with_max_exceptions.py`](./getting_started/tools/ai_function_with_max_exceptions.py) | Limiting tool failure exceptions using max_invocation_exceptions |
+| [`getting_started/tools/ai_function_with_max_invocations.py`](./getting_started/tools/ai_function_with_max_invocations.py) | Limiting total tool invocations using max_invocations |
+| [`getting_started/tools/ai_functions_in_class.py`](./getting_started/tools/ai_functions_in_class.py) | Using ai_function decorator with class methods for stateful tools |
## Workflows
diff --git a/python/samples/demos/chatkit-integration/.gitignore b/python/samples/demos/chatkit-integration/.gitignore
new file mode 100644
index 0000000000..deb912b2f6
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/.gitignore
@@ -0,0 +1,4 @@
+*.db
+*.db-shm
+*.db-wal
+uploads/
\ No newline at end of file
diff --git a/python/samples/demos/chatkit-integration/README.md b/python/samples/demos/chatkit-integration/README.md
new file mode 100644
index 0000000000..28dfef398e
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/README.md
@@ -0,0 +1,268 @@
+# ChatKit Integration Sample with Weather Agent and Image Analysis
+
+This sample demonstrates how to integrate Microsoft Agent Framework with OpenAI ChatKit. It provides a complete implementation of a weather assistant with interactive widget visualization, image analysis, and file upload support.
+
+**Features:**
+
+- Weather information with interactive widgets
+- Image analysis using vision models
+- Current time queries
+- File upload with attachment storage
+- Chat interface with streaming responses
+- City selector widget with one-click weather
+
+## Architecture
+
+```mermaid
+graph TB
+ subgraph Frontend["React Frontend (ChatKit UI)"]
+ UI[ChatKit Components]
+ Upload[File Upload]
+ end
+
+ subgraph Backend["FastAPI Server"]
+ FastAPI[FastAPI Endpoints]
+
+ subgraph ChatKit["WeatherChatKitServer"]
+ Respond[respond method]
+ Action[action method]
+ end
+
+ subgraph Stores["Data & Storage Layer"]
+ SQLite[SQLiteStore
Store Protocol]
+ AttStore[FileBasedAttachmentStore
AttachmentStore Protocol]
+ DB[(SQLite DB
chatkit_demo.db)]
+ Files[/uploads directory/]
+ end
+
+ subgraph Integration["Agent Framework Integration"]
+ Converter[ThreadItemConverter]
+ Streamer[stream_agent_response]
+ Agent[ChatAgent]
+ end
+
+ Widgets[Widget Rendering
render_weather_widget
render_city_selector_widget]
+ end
+
+ subgraph Azure["Azure AI"]
+ Foundry[GPT-5
with Vision]
+ end
+
+ UI -->|HTTP POST /chatkit| FastAPI
+ Upload -->|HTTP POST /upload/id| FastAPI
+
+ FastAPI --> ChatKit
+
+ ChatKit -->|save/load threads| SQLite
+ ChatKit -->|save/load attachments| AttStore
+ ChatKit -->|convert messages| Converter
+
+ SQLite -.->|persist| DB
+ AttStore -.->|save files| Files
+ AttStore -.->|save metadata| SQLite
+
+ Converter -->|ChatMessage array| Agent
+ Agent -->|AgentRunResponseUpdate| Streamer
+ Streamer -->|ThreadStreamEvent| ChatKit
+
+ ChatKit --> Widgets
+ Widgets -->|WidgetItem| ChatKit
+
+ Agent <-->|Chat Completions API| Foundry
+
+ ChatKit -->|ThreadStreamEvent| FastAPI
+ FastAPI -->|SSE Stream| UI
+
+ style ChatKit fill:#e1f5ff
+ style Stores fill:#fff4e1
+ style Integration fill:#f0e1ff
+ style Azure fill:#e1ffe1
+```
+
+### Server Implementation
+
+The sample implements a ChatKit server using the `ChatKitServer` base class from the `chatkit` package:
+
+**Core Components:**
+
+- **`WeatherChatKitServer`**: Custom ChatKit server implementation that:
+
+ - Extends `ChatKitServer[dict[str, Any]]`
+ - Uses Agent Framework's `ChatAgent` with Azure OpenAI
+ - Converts ChatKit messages to Agent Framework format using `ThreadItemConverter`
+ - Streams responses back to ChatKit using `stream_agent_response`
+ - Creates and streams interactive widgets after agent responses
+
+- **`SQLiteStore`**: Data persistence layer that:
+
+ - Implements the `Store[dict[str, Any]]` protocol from ChatKit
+ - Persists threads, messages, and attachment metadata in SQLite
+ - Provides thread management and item history
+ - Stores attachment metadata for the upload lifecycle
+
+- **`FileBasedAttachmentStore`**: File storage implementation that:
+ - Implements the `AttachmentStore[dict[str, Any]]` protocol from ChatKit
+ - Stores uploaded files on the local filesystem (in `./uploads` directory)
+ - Generates upload URLs for two-phase file upload
+ - Saves attachment metadata to the data store for upload tracking
+ - Provides preview URLs for images
+
+**Key Integration Points:**
+
+```python
+# Converting ChatKit messages to Agent Framework
+converter = ThreadItemConverter(
+ attachment_data_fetcher=self._fetch_attachment_data
+)
+agent_messages = await converter.to_agent_input(user_message_item)
+
+# Running agent and streaming back to ChatKit
+async for event in stream_agent_response(
+ self.weather_agent.run_stream(agent_messages),
+ thread_id=thread.id,
+):
+ yield event
+
+# Streaming widgets
+widget = render_weather_widget(weather_data)
+async for event in stream_widget(thread_id=thread.id, widget=widget):
+ yield event
+```
+
+## Installation and Setup
+
+### Prerequisites
+
+- Python 3.10+
+- Node.js 18.18+ and npm 9+
+- Azure OpenAI service configured
+- Azure CLI for authentication (`az login`)
+
+### Backend Setup
+
+1. **Install Python packages:**
+
+```bash
+cd python/samples/demos/chatkit-integration
+pip install agent-framework-chatkit fastapi uvicorn azure-identity
+```
+
+2. **Configure Azure OpenAI:**
+
+```bash
+export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
+export AZURE_OPENAI_API_VERSION="2024-06-01"
+export AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="gpt-4o"
+```
+
+3. **Authenticate with Azure:**
+
+```bash
+az login
+```
+
+### Frontend Setup
+
+Install the Node.js dependencies:
+
+```bash
+cd frontend
+npm install
+```
+
+## How to Run
+
+### Start the Backend Server
+
+From the `chatkit-integration` directory:
+
+```bash
+python app.py
+```
+
+Or with auto-reload for development:
+
+```bash
+uvicorn app:app --host 127.0.0.1 --port 8001 --reload
+```
+
+The backend will start on `http://localhost:8001`
+
+### Start the Frontend Development Server
+
+In a new terminal, from the `frontend` directory:
+
+```bash
+npm run dev
+```
+
+The frontend will start on `http://localhost:5171`
+
+### Access the Application
+
+Open your browser and navigate to:
+
+```
+http://localhost:5171
+```
+
+You can now:
+
+- Ask about weather in any location (weather widgets display automatically)
+- Upload images for analysis using the attachment button
+- Get the current time
+- Ask to see available cities and click city buttons for instant weather
+
+### Project Structure
+
+```
+chatkit-integration/
+├── app.py # FastAPI backend with ChatKitServer implementation
+├── store.py # SQLiteStore implementation
+├── attachment_store.py # FileBasedAttachmentStore implementation
+├── weather_widget.py # Widget rendering functions
+├── chatkit_demo.db # SQLite database (auto-created)
+├── uploads/ # Uploaded files directory (auto-created)
+└── frontend/
+ ├── package.json
+ ├── vite.config.ts
+ ├── index.html
+ └── src/
+ ├── main.tsx
+ └── App.tsx # ChatKit UI integration
+```
+
+### Configuration
+
+You can customize the application by editing constants at the top of `app.py`:
+
+```python
+# Server configuration
+SERVER_HOST = "127.0.0.1" # Bind to localhost only for security (local dev)
+SERVER_PORT = 8001
+SERVER_BASE_URL = f"http://localhost:{SERVER_PORT}"
+
+# Database configuration
+DATABASE_PATH = "chatkit_demo.db"
+
+# File storage configuration
+UPLOADS_DIRECTORY = "./uploads"
+
+# User context
+DEFAULT_USER_ID = "demo_user"
+```
+
+### Sample Conversations
+
+Try these example queries:
+
+- "What's the weather like in Tokyo?"
+- "Show me available cities" (displays interactive city selector)
+- "What's the current time?"
+- Upload an image and ask "What do you see in this image?"
+
+## Learn More
+
+- [Agent Framework Documentation](https://aka.ms/agent-framework)
+- [ChatKit Documentation](https://platform.openai.com/docs/guides/chatkit)
+- [Azure OpenAI Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/)
diff --git a/python/samples/demos/chatkit-integration/__init__.py b/python/samples/demos/chatkit-integration/__init__.py
new file mode 100644
index 0000000000..2a50eae894
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/__init__.py
@@ -0,0 +1 @@
+# Copyright (c) Microsoft. All rights reserved.
diff --git a/python/samples/demos/chatkit-integration/app.py b/python/samples/demos/chatkit-integration/app.py
new file mode 100644
index 0000000000..ed5fd2dd6e
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/app.py
@@ -0,0 +1,538 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""
+ChatKit Integration Sample with Weather Agent and Image Analysis
+
+This sample demonstrates how to integrate Microsoft Agent Framework with OpenAI ChatKit
+using a weather tool with widget visualization, image analysis, and Azure OpenAI. It shows
+a complete ChatKit server implementation using Agent Framework agents with proper FastAPI
+setup, interactive weather widgets, and vision capabilities for analyzing uploaded images.
+"""
+
+import logging
+from collections.abc import AsyncIterator, Callable
+from datetime import datetime, timezone
+from random import randint
+from typing import Annotated, Any
+
+import uvicorn
+from azure.identity import AzureCliCredential
+from fastapi import FastAPI, File, Request, UploadFile
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import FileResponse, JSONResponse, Response, StreamingResponse
+from pydantic import Field
+
+# ============================================================================
+# Configuration Constants
+# ============================================================================
+
+# Server configuration
+SERVER_HOST = "127.0.0.1" # Bind to localhost only for security (local dev)
+SERVER_PORT = 8001
+SERVER_BASE_URL = f"http://localhost:{SERVER_PORT}"
+
+# Database configuration
+DATABASE_PATH = "chatkit_demo.db"
+
+# File storage configuration
+UPLOADS_DIRECTORY = "./uploads"
+
+# User context
+DEFAULT_USER_ID = "demo_user"
+
+# Logging configuration
+LOG_LEVEL = logging.INFO
+LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+# ============================================================================
+# Logging Setup
+# ============================================================================
+
+logging.basicConfig(
+ level=LOG_LEVEL,
+ format=LOG_FORMAT,
+ datefmt=LOG_DATE_FORMAT,
+)
+logger = logging.getLogger(__name__)
+
+# Agent Framework imports
+from agent_framework import AgentRunResponseUpdate, ChatAgent, ChatMessage, FunctionResultContent, Role
+from agent_framework.azure import AzureOpenAIChatClient
+
+# Agent Framework ChatKit integration
+from agent_framework_chatkit import ThreadItemConverter, stream_agent_response
+
+# Local imports
+from attachment_store import FileBasedAttachmentStore
+
+# ChatKit imports
+from chatkit.actions import Action
+from chatkit.server import ChatKitServer
+from chatkit.store import StoreItemType, default_generate_id
+from chatkit.types import (
+ ThreadItemDoneEvent,
+ ThreadMetadata,
+ ThreadStreamEvent,
+ UserMessageItem,
+ WidgetItem,
+)
+from chatkit.widgets import WidgetRoot
+from store import SQLiteStore
+from weather_widget import (
+ WeatherData,
+ city_selector_copy_text,
+ render_city_selector_widget,
+ render_weather_widget,
+ weather_widget_copy_text,
+)
+
+
+class WeatherResponse(str):
+ """A string response that also carries WeatherData for widget creation."""
+
+ def __new__(cls, text: str, weather_data: WeatherData):
+ instance = super().__new__(cls, text)
+ instance.weather_data = weather_data # type: ignore
+ return instance
+
+
+async def stream_widget(
+ thread_id: str,
+ widget: WidgetRoot,
+ copy_text: str | None = None,
+ generate_id: Callable[[StoreItemType], str] = default_generate_id,
+) -> AsyncIterator[ThreadStreamEvent]:
+ """Stream a ChatKit widget as a ThreadStreamEvent.
+
+ This helper function creates a ChatKit widget item and yields it as a
+ ThreadItemDoneEvent that can be consumed by the ChatKit UI.
+
+ Args:
+ thread_id: The ChatKit thread ID for the conversation.
+ widget: The ChatKit widget to display.
+ copy_text: Optional text representation of the widget for copy/paste.
+ generate_id: Optional function to generate IDs for ChatKit items.
+
+ Yields:
+ ThreadStreamEvent: ChatKit event containing the widget.
+ """
+ item_id = generate_id("message")
+
+ widget_item = WidgetItem(
+ id=item_id,
+ thread_id=thread_id,
+ created_at=datetime.now(),
+ widget=widget,
+ copy_text=copy_text,
+ )
+
+ yield ThreadItemDoneEvent(type="thread.item.done", item=widget_item)
+
+
+def get_weather(
+ location: Annotated[str, Field(description="The location to get the weather for.")],
+) -> str:
+ """Get the weather for a given location.
+
+ Returns a string description with embedded WeatherData for widget creation.
+ """
+ logger.info(f"Fetching weather for location: {location}")
+
+ conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy", "foggy"]
+ temperature = randint(-5, 35)
+ condition = conditions[randint(0, len(conditions) - 1)]
+
+ # Add some realistic details
+ humidity = randint(30, 90)
+ wind_speed = randint(5, 25)
+
+ weather_data = WeatherData(
+ location=location,
+ condition=condition,
+ temperature=temperature,
+ humidity=humidity,
+ wind_speed=wind_speed,
+ )
+
+ logger.debug(f"Weather data generated: {condition}, {temperature}°C, {humidity}% humidity, {wind_speed} km/h wind")
+
+ # Return a WeatherResponse that is both a string (for the LLM) and carries structured data
+ text = (
+ f"Weather in {location}:\n"
+ f"• Condition: {condition.title()}\n"
+ f"• Temperature: {temperature}°C\n"
+ f"• Humidity: {humidity}%\n"
+ f"• Wind: {wind_speed} km/h"
+ )
+ return WeatherResponse(text, weather_data)
+
+
+def get_time() -> str:
+ """Get the current UTC time."""
+ current_time = datetime.now(timezone.utc)
+ logger.info("Getting current UTC time")
+ return f"Current UTC time: {current_time.strftime('%Y-%m-%d %H:%M:%S')} UTC"
+
+
+def show_city_selector() -> str:
+ """Show an interactive city selector widget to the user.
+
+ This function triggers the display of a widget that allows users
+ to select from popular cities to get weather information.
+
+ Returns a special marker string that will be detected to show the widget.
+ """
+ logger.info("Activating city selector widget")
+ return "__SHOW_CITY_SELECTOR__"
+
+
+class WeatherChatKitServer(ChatKitServer[dict[str, Any]]):
+ """ChatKit server implementation using Agent Framework.
+
+ This server integrates Agent Framework agents with ChatKit's server protocol,
+ providing weather information with interactive widgets and time queries through Azure OpenAI.
+ """
+
+ def __init__(self, data_store: SQLiteStore, attachment_store: FileBasedAttachmentStore):
+ super().__init__(data_store, attachment_store)
+
+ logger.info("Initializing WeatherChatKitServer")
+
+ # Create Agent Framework agent with Azure OpenAI
+ # For authentication, run `az login` command in terminal
+ try:
+ self.weather_agent = ChatAgent(
+ chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
+ instructions=(
+ "You are a helpful weather assistant with image analysis capabilities. "
+ "You can provide weather information for any location, tell the current time, "
+ "and analyze images that users upload. Be friendly and informative in your responses.\n\n"
+ "If a user asks to see a list of cities or wants to choose from available cities, "
+ "use the show_city_selector tool to display an interactive city selector.\n\n"
+ "When users upload images, you will automatically receive them and can analyze their content. "
+ "Describe what you see in detail and be helpful in answering questions about the images."
+ ),
+ tools=[get_weather, get_time, show_city_selector],
+ )
+ logger.info("Weather agent initialized successfully with Azure OpenAI")
+ except Exception as e:
+ logger.error(f"Failed to initialize weather agent: {e}")
+ raise
+
+ # Create ThreadItemConverter with attachment data fetcher
+ self.converter = ThreadItemConverter(
+ attachment_data_fetcher=self._fetch_attachment_data,
+ )
+
+ logger.info("WeatherChatKitServer initialized")
+
+ async def _fetch_attachment_data(self, attachment_id: str) -> bytes:
+ """Fetch attachment binary data for the converter.
+
+ Args:
+ attachment_id: The ID of the attachment to fetch.
+
+ Returns:
+ The binary data of the attachment.
+ """
+ return await attachment_store.read_attachment_bytes(attachment_id)
+
+ async def respond(
+ self,
+ thread: ThreadMetadata,
+ input_user_message: UserMessageItem | None,
+ context: dict[str, Any],
+ ) -> AsyncIterator[ThreadStreamEvent]:
+ """Handle incoming user messages and generate responses.
+
+ This method converts ChatKit messages to Agent Framework format using ThreadItemConverter,
+ runs the agent, converts the response back to ChatKit events using stream_agent_response,
+ and creates interactive weather widgets when weather data is queried.
+ """
+ from agent_framework import FunctionResultContent
+
+ if input_user_message is None:
+ logger.debug("Received None user message, skipping")
+ return
+
+ logger.info(f"Processing message for thread: {thread.id}")
+
+ try:
+ # Track weather data and city selector flag for this request
+ weather_data: WeatherData | None = None
+ show_city_selector = False
+
+ # Convert ChatKit user message to Agent Framework ChatMessage using ThreadItemConverter
+ agent_messages = await self.converter.to_agent_input(input_user_message)
+
+ if not agent_messages:
+ logger.warning("No messages after conversion")
+ return
+
+ logger.info(f"Running agent with {len(agent_messages)} message(s)")
+
+ # Run the Agent Framework agent with streaming
+ agent_stream = self.weather_agent.run_stream(agent_messages)
+
+ # Create an intercepting stream that extracts function results while passing through updates
+ async def intercept_stream() -> AsyncIterator[AgentRunResponseUpdate]:
+ nonlocal weather_data, show_city_selector
+ async for update in agent_stream:
+ # Check for function results in the update
+ if update.contents:
+ for content in update.contents:
+ if isinstance(content, FunctionResultContent):
+ result = content.result
+
+ # Check if it's a WeatherResponse (string subclass with weather_data attribute)
+ if isinstance(result, str) and hasattr(result, "weather_data"):
+ extracted_data = getattr(result, "weather_data", None)
+ if isinstance(extracted_data, WeatherData):
+ weather_data = extracted_data
+ logger.info(f"Weather data extracted: {weather_data.location}")
+ # Check if it's the city selector marker
+ elif isinstance(result, str) and result == "__SHOW_CITY_SELECTOR__":
+ show_city_selector = True
+ logger.info("City selector flag detected")
+ yield update
+
+ # Stream updates as ChatKit events with interception
+ async for event in stream_agent_response(
+ intercept_stream(),
+ thread_id=thread.id,
+ ):
+ yield event
+
+ # If weather data was collected during the tool call, create a widget
+ if weather_data is not None and isinstance(weather_data, WeatherData):
+ logger.info(f"Creating weather widget for location: {weather_data.location}")
+ # Create weather widget
+ widget = render_weather_widget(weather_data)
+ copy_text = weather_widget_copy_text(weather_data)
+
+ # Stream the widget
+ async for widget_event in stream_widget(thread_id=thread.id, widget=widget, copy_text=copy_text):
+ yield widget_event
+ logger.debug("Weather widget streamed successfully")
+
+ # If city selector should be shown, create and stream that widget
+ if show_city_selector:
+ logger.info("Creating city selector widget")
+ # Create city selector widget
+ selector_widget = render_city_selector_widget()
+ selector_copy_text = city_selector_copy_text()
+
+ # Stream the widget
+ async for widget_event in stream_widget(
+ thread_id=thread.id, widget=selector_widget, copy_text=selector_copy_text
+ ):
+ yield widget_event
+ logger.debug("City selector widget streamed successfully")
+
+ logger.info(f"Completed processing message for thread: {thread.id}")
+
+ except Exception as e:
+ logger.error(f"Error processing message for thread {thread.id}: {e}", exc_info=True)
+
+ async def action(
+ self,
+ thread: ThreadMetadata,
+ action: Action[str, Any],
+ sender: WidgetItem | None,
+ context: dict[str, Any],
+ ) -> AsyncIterator[ThreadStreamEvent]:
+ """Handle widget actions from the frontend.
+
+ This method processes actions triggered by interactive widgets,
+ such as city selection from the city selector widget.
+ """
+
+ logger.info(f"Received action: {action.type} for thread: {thread.id}")
+
+ if action.type == "city_selected":
+ # Extract city information from the action payload
+ city_label = action.payload.get("city_label", "Unknown")
+
+ logger.info(f"City selected: {city_label}")
+ logger.debug(f"Action payload: {action.payload}")
+
+ # Track weather data for this request
+ weather_data: WeatherData | None = None
+
+ # Create an agent message asking about the weather
+ agent_messages = [ChatMessage(role=Role.USER, text=f"What's the weather in {city_label}?")]
+
+ logger.debug(f"Processing weather query: {agent_messages[0].text}")
+
+ # Run the Agent Framework agent with streaming
+ agent_stream = self.weather_agent.run_stream(agent_messages)
+
+ # Create an intercepting stream that extracts function results while passing through updates
+ async def intercept_stream() -> AsyncIterator[AgentRunResponseUpdate]:
+ nonlocal weather_data
+ async for update in agent_stream:
+ # Check for function results in the update
+ if update.contents:
+ for content in update.contents:
+ if isinstance(content, FunctionResultContent):
+ result = content.result
+
+ # Check if it's a WeatherResponse (string subclass with weather_data attribute)
+ if isinstance(result, str) and hasattr(result, "weather_data"):
+ extracted_data = getattr(result, "weather_data", None)
+ if isinstance(extracted_data, WeatherData):
+ weather_data = extracted_data
+ logger.info(f"Weather data extracted: {weather_data.location}")
+ yield update
+
+ # Stream updates as ChatKit events with interception
+ async for event in stream_agent_response(
+ intercept_stream(),
+ thread_id=thread.id,
+ ):
+ yield event
+
+ # If weather data was collected during the tool call, create a widget
+ if weather_data is not None and isinstance(weather_data, WeatherData):
+ logger.info(f"Creating weather widget for: {weather_data.location}")
+ # Create weather widget
+ widget = render_weather_widget(weather_data)
+ copy_text = weather_widget_copy_text(weather_data)
+
+ # Stream the widget
+ async for widget_event in stream_widget(thread_id=thread.id, widget=widget, copy_text=copy_text):
+ yield widget_event
+ logger.debug("Weather widget created successfully from action")
+ else:
+ logger.warning("No weather data available to create widget after action")
+
+
+# FastAPI application setup
+app = FastAPI(
+ title="ChatKit Weather & Vision Agent",
+ description="Weather and image analysis assistant powered by Agent Framework and Azure OpenAI",
+ version="1.0.0",
+)
+
+# Add CORS middleware to allow frontend connections
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # In production, specify exact origins
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Initialize data store and ChatKit server
+logger.info("Initializing application components")
+data_store = SQLiteStore(db_path=DATABASE_PATH)
+attachment_store = FileBasedAttachmentStore(
+ uploads_dir=UPLOADS_DIRECTORY,
+ base_url=SERVER_BASE_URL,
+ data_store=data_store,
+)
+chatkit_server = WeatherChatKitServer(data_store, attachment_store)
+logger.info("Application initialization complete")
+
+
+@app.post("/chatkit")
+async def chatkit_endpoint(request: Request):
+ """Main ChatKit endpoint that handles all ChatKit requests.
+
+ This endpoint follows the ChatKit server protocol and handles both
+ streaming and non-streaming responses.
+ """
+ logger.debug(f"Received ChatKit request from {request.client}")
+ request_body = await request.body()
+
+ # Create context following the working examples pattern
+ context = {"request": request}
+
+ try:
+ # Process the request using ChatKit server
+ result = await chatkit_server.process(request_body, context)
+
+ # Return appropriate response type
+ if hasattr(result, "__aiter__"): # StreamingResult
+ logger.debug("Returning streaming response")
+ return StreamingResponse(result, media_type="text/event-stream") # type: ignore[arg-type]
+ # NonStreamingResult
+ logger.debug("Returning non-streaming response")
+ return Response(content=result.json, media_type="application/json") # type: ignore[union-attr]
+ except Exception as e:
+ logger.error(f"Error processing ChatKit request: {e}", exc_info=True)
+ raise
+
+
+@app.post("/upload/{attachment_id}")
+async def upload_file(attachment_id: str, file: UploadFile = File(...)):
+ """Handle file upload for two-phase upload.
+
+ The client POSTs the file bytes here after creating the attachment
+ via the ChatKit attachments.create endpoint.
+ """
+ logger.info(f"Receiving file upload for attachment: {attachment_id}")
+
+ try:
+ # Read file contents
+ contents = await file.read()
+
+ # Save to disk
+ file_path = attachment_store.get_file_path(attachment_id)
+ file_path.write_bytes(contents)
+
+ logger.info(f"Saved {len(contents)} bytes to {file_path}")
+
+ # Load the attachment metadata from the data store
+ attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID})
+
+ # Clear the upload_url since upload is complete
+ attachment.upload_url = None
+
+ # Save the updated attachment back to the store
+ await data_store.save_attachment(attachment, {"user_id": DEFAULT_USER_ID})
+
+ # Return the attachment metadata as JSON
+ return JSONResponse(content=attachment.model_dump(mode="json"))
+
+ except Exception as e:
+ logger.error(f"Error uploading file for attachment {attachment_id}: {e}", exc_info=True)
+ return JSONResponse(status_code=500, content={"error": f"Failed to upload file: {str(e)}"})
+
+
+@app.get("/preview/{attachment_id}")
+async def preview_image(attachment_id: str):
+ """Serve image preview/thumbnail.
+
+ For simplicity, this serves the full image. In production, you should
+ generate and cache thumbnails.
+ """
+ logger.debug(f"Serving preview for attachment: {attachment_id}")
+
+ try:
+ file_path = attachment_store.get_file_path(attachment_id)
+
+ if not file_path.exists():
+ return JSONResponse(status_code=404, content={"error": "File not found"})
+
+ # Determine media type from file extension or attachment metadata
+ # For simplicity, we'll try to load from the store
+ try:
+ attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID})
+ media_type = attachment.mime_type
+ except Exception:
+ # Default to binary if we can't determine
+ media_type = "application/octet-stream"
+
+ return FileResponse(file_path, media_type=media_type)
+
+ except Exception as e:
+ logger.error(f"Error serving preview for attachment {attachment_id}: {e}", exc_info=True)
+ return JSONResponse(status_code=500, content={"error": str(e)})
+
+
+if __name__ == "__main__":
+ # Run the server
+ logger.info(f"Starting ChatKit Weather Agent server on {SERVER_HOST}:{SERVER_PORT}")
+ uvicorn.run(app, host=SERVER_HOST, port=SERVER_PORT, log_level="info")
diff --git a/python/samples/demos/chatkit-integration/attachment_store.py b/python/samples/demos/chatkit-integration/attachment_store.py
new file mode 100644
index 0000000000..263af20f46
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/attachment_store.py
@@ -0,0 +1,121 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""File-based AttachmentStore implementation for ChatKit.
+
+This module provides a simple AttachmentStore implementation that stores
+uploaded files on the local filesystem. In production, you should use
+cloud storage like S3, Azure Blob Storage, or Google Cloud Storage.
+"""
+
+from pathlib import Path
+from typing import Any, TYPE_CHECKING
+
+from chatkit.store import AttachmentStore
+from chatkit.types import Attachment, AttachmentCreateParams, FileAttachment, ImageAttachment
+from pydantic import AnyUrl
+
+if TYPE_CHECKING:
+ from store import SQLiteStore
+
+
+class FileBasedAttachmentStore(AttachmentStore[dict[str, Any]]):
+ """File-based AttachmentStore that stores files on local disk.
+
+ This implementation stores uploaded files in a local directory and provides
+ upload URLs that point to the FastAPI upload endpoint. It supports both
+ image and file attachments.
+
+ Features:
+ - Stores files in a local uploads directory
+ - Generates upload URLs for two-phase upload
+ - Generates preview URLs for images
+ - Proper cleanup on deletion
+
+ Note: This is for demonstration purposes. In production, use cloud storage
+ with signed URLs for better security and scalability.
+ """
+
+ def __init__(
+ self,
+ uploads_dir: str = "./uploads",
+ base_url: str = "http://localhost:8001",
+ data_store: "SQLiteStore | None" = None,
+ ):
+ """Initialize the file-based attachment store.
+
+ Args:
+ uploads_dir: Directory where uploaded files will be stored
+ base_url: Base URL for generating upload and preview URLs
+ data_store: Optional data store to persist attachment metadata
+ """
+ self.uploads_dir = Path(uploads_dir)
+ self.base_url = base_url.rstrip("/")
+ self.data_store = data_store
+
+ # Create uploads directory if it doesn't exist
+ self.uploads_dir.mkdir(parents=True, exist_ok=True)
+
+ def get_file_path(self, attachment_id: str) -> Path:
+ """Get the filesystem path for an attachment."""
+ return self.uploads_dir / attachment_id
+
+ async def delete_attachment(self, attachment_id: str, context: dict[str, Any]) -> None:
+ """Delete an attachment and its file from disk."""
+ file_path = self.get_file_path(attachment_id)
+ if file_path.exists():
+ file_path.unlink()
+
+ async def create_attachment(
+ self, input: AttachmentCreateParams, context: dict[str, Any]
+ ) -> Attachment:
+ """Create an attachment with upload URL for two-phase upload.
+
+ This creates the attachment metadata and returns upload URLs that
+ the client will use to POST the actual file bytes.
+ """
+ # Generate unique ID for this attachment
+ attachment_id = self.generate_attachment_id(input.mime_type, context)
+
+ # Generate upload URL that points to our FastAPI upload endpoint
+ upload_url = f"{self.base_url}/upload/{attachment_id}"
+
+ # Create appropriate attachment type based on MIME type
+ if input.mime_type.startswith("image/"):
+ # For images, also provide a preview URL
+ preview_url = f"{self.base_url}/preview/{attachment_id}"
+
+ attachment = ImageAttachment(
+ id=attachment_id,
+ type="image",
+ mime_type=input.mime_type,
+ name=input.name,
+ upload_url=AnyUrl(upload_url),
+ preview_url=AnyUrl(preview_url),
+ )
+ else:
+ # For files, just provide upload URL
+ attachment = FileAttachment(
+ id=attachment_id,
+ type="file",
+ mime_type=input.mime_type,
+ name=input.name,
+ upload_url=AnyUrl(upload_url),
+ )
+
+ # Save attachment metadata to data store so it's available during upload
+ if self.data_store is not None:
+ await self.data_store.save_attachment(attachment, context)
+
+ return attachment
+
+ async def read_attachment_bytes(self, attachment_id: str) -> bytes:
+ """Read the raw bytes of an uploaded attachment.
+
+ This is used by the ThreadItemConverter to create base64-encoded
+ content for sending to the Agent Framework.
+ """
+ file_path = self.get_file_path(attachment_id)
+ if not file_path.exists():
+ raise FileNotFoundError(f"Attachment {attachment_id} not found on disk")
+
+ return file_path.read_bytes()
diff --git a/python/samples/demos/chatkit-integration/frontend/index.html b/python/samples/demos/chatkit-integration/frontend/index.html
new file mode 100644
index 0000000000..82837ef519
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+ ChatKit + Agent Framework Demo
+
+
+
+
+
+
+
+
+
diff --git a/python/samples/demos/chatkit-integration/frontend/package-lock.json b/python/samples/demos/chatkit-integration/frontend/package-lock.json
new file mode 100644
index 0000000000..9cf6bb6b86
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/package-lock.json
@@ -0,0 +1,1437 @@
+{
+ "name": "chatkit-agent-framework-demo",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "chatkit-agent-framework-demo",
+ "version": "0.1.0",
+ "dependencies": {
+ "@openai/chatkit-react": "^0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "typescript": "^5.4.0",
+ "vite": "^7.1.9"
+ },
+ "engines": {
+ "node": ">=18.18",
+ "npm": ">=9"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
+ "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
+ "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
+ "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
+ "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
+ "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
+ "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
+ "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
+ "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
+ "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
+ "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
+ "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
+ "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
+ "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
+ "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
+ "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
+ "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
+ "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
+ "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
+ "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
+ "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
+ "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@openai/chatkit": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/@openai/chatkit/-/chatkit-0.0.0.tgz",
+ "integrity": "sha512-9YomebDd2dpWFR3s1fiEtNknXmEC8QYt//2ConGjr/4geWdRqunEpO+i7yJXYEGLJbkmB4lxwKmbwWJA4pvpSg==",
+ "license": "MIT"
+ },
+ "node_modules/@openai/chatkit-react": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/@openai/chatkit-react/-/chatkit-react-0.0.0.tgz",
+ "integrity": "sha512-ppoAKiWKUJGIlKuFQ0mgPRVMAAjJ+PonAzdo1p7BQmTEZtwFI8vq6W7ZRN2UTfzZZIKbJ2diwU6ePbYSKsePuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@openai/chatkit": "0.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
+ "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
+ "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
+ "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
+ "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
+ "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
+ "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
+ "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
+ "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
+ "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
+ "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
+ "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
+ "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
+ "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
+ "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
+ "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
+ "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
+ "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
+ "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
+ "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
+ "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
+ "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
+ "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/core": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
+ "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.24"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.13.5",
+ "@swc/core-darwin-x64": "1.13.5",
+ "@swc/core-linux-arm-gnueabihf": "1.13.5",
+ "@swc/core-linux-arm64-gnu": "1.13.5",
+ "@swc/core-linux-arm64-musl": "1.13.5",
+ "@swc/core-linux-x64-gnu": "1.13.5",
+ "@swc/core-linux-x64-musl": "1.13.5",
+ "@swc/core-win32-arm64-msvc": "1.13.5",
+ "@swc/core-win32-ia32-msvc": "1.13.5",
+ "@swc/core-win32-x64-msvc": "1.13.5"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz",
+ "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz",
+ "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz",
+ "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz",
+ "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz",
+ "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz",
+ "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz",
+ "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz",
+ "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz",
+ "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz",
+ "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.25",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
+ "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.2",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
+ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
+ "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react-swc": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
+ "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@swc/core": "^1.12.11"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
+ "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.10",
+ "@esbuild/android-arm": "0.25.10",
+ "@esbuild/android-arm64": "0.25.10",
+ "@esbuild/android-x64": "0.25.10",
+ "@esbuild/darwin-arm64": "0.25.10",
+ "@esbuild/darwin-x64": "0.25.10",
+ "@esbuild/freebsd-arm64": "0.25.10",
+ "@esbuild/freebsd-x64": "0.25.10",
+ "@esbuild/linux-arm": "0.25.10",
+ "@esbuild/linux-arm64": "0.25.10",
+ "@esbuild/linux-ia32": "0.25.10",
+ "@esbuild/linux-loong64": "0.25.10",
+ "@esbuild/linux-mips64el": "0.25.10",
+ "@esbuild/linux-ppc64": "0.25.10",
+ "@esbuild/linux-riscv64": "0.25.10",
+ "@esbuild/linux-s390x": "0.25.10",
+ "@esbuild/linux-x64": "0.25.10",
+ "@esbuild/netbsd-arm64": "0.25.10",
+ "@esbuild/netbsd-x64": "0.25.10",
+ "@esbuild/openbsd-arm64": "0.25.10",
+ "@esbuild/openbsd-x64": "0.25.10",
+ "@esbuild/openharmony-arm64": "0.25.10",
+ "@esbuild/sunos-x64": "0.25.10",
+ "@esbuild/win32-arm64": "0.25.10",
+ "@esbuild/win32-ia32": "0.25.10",
+ "@esbuild/win32-x64": "0.25.10"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
+ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
+ "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.52.4",
+ "@rollup/rollup-android-arm64": "4.52.4",
+ "@rollup/rollup-darwin-arm64": "4.52.4",
+ "@rollup/rollup-darwin-x64": "4.52.4",
+ "@rollup/rollup-freebsd-arm64": "4.52.4",
+ "@rollup/rollup-freebsd-x64": "4.52.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.4",
+ "@rollup/rollup-linux-arm64-musl": "4.52.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.4",
+ "@rollup/rollup-linux-x64-gnu": "4.52.4",
+ "@rollup/rollup-linux-x64-musl": "4.52.4",
+ "@rollup/rollup-openharmony-arm64": "4.52.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.4",
+ "@rollup/rollup-win32-x64-gnu": "4.52.4",
+ "@rollup/rollup-win32-x64-msvc": "4.52.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.1.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
+ "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ }
+ }
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/package.json b/python/samples/demos/chatkit-integration/frontend/package.json
new file mode 100644
index 0000000000..65d65d1d53
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "chatkit-agent-framework-demo",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "engines": {
+ "node": ">=18.18",
+ "npm": ">=9"
+ },
+ "dependencies": {
+ "@openai/chatkit-react": "^0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "typescript": "^5.4.0",
+ "vite": "^7.1.9"
+ }
+}
\ No newline at end of file
diff --git a/python/samples/demos/chatkit-integration/frontend/src/App.tsx b/python/samples/demos/chatkit-integration/frontend/src/App.tsx
new file mode 100644
index 0000000000..13f42d17c9
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/src/App.tsx
@@ -0,0 +1,33 @@
+import { ChatKit, useChatKit } from "@openai/chatkit-react";
+
+const CHATKIT_API_URL = "/chatkit";
+const CHATKIT_API_DOMAIN_KEY =
+ import.meta.env.VITE_CHATKIT_API_DOMAIN_KEY ?? "domain_pk_localhost_dev";
+
+export default function App() {
+ const chatkit = useChatKit({
+ api: {
+ url: CHATKIT_API_URL,
+ domainKey: CHATKIT_API_DOMAIN_KEY,
+ uploadStrategy: { type: "two_phase" },
+ },
+ startScreen: {
+ greeting: "Hello! I'm your weather and image analysis assistant. Ask me about the weather in any location or upload images for me to analyze.",
+ prompts: [
+ { label: "Weather in New York", prompt: "What's the weather in New York?" },
+ { label: "Select City to Get Weather", prompt: "Show me the city selector for weather" },
+ { label: "Current Time", prompt: "What time is it?" },
+ { label: "Analyze an Image", prompt: "I'll upload an image for you to analyze" },
+ ],
+ },
+ composer: {
+ placeholder: "Ask about weather or upload an image...",
+ attachments: {
+ enabled: true,
+ accept: { "image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"] },
+ },
+ },
+ });
+
+ return ;
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/src/main.tsx b/python/samples/demos/chatkit-integration/frontend/src/main.tsx
new file mode 100644
index 0000000000..0937a0fa0f
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/src/main.tsx
@@ -0,0 +1,15 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App";
+
+const container = document.getElementById("root");
+
+if (!container) {
+ throw new Error("Root element with id 'root' not found");
+}
+
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/python/samples/demos/chatkit-integration/frontend/src/vite-env.d.ts b/python/samples/demos/chatkit-integration/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000000..11f02fe2a0
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/python/samples/demos/chatkit-integration/frontend/tsconfig.json b/python/samples/demos/chatkit-integration/frontend/tsconfig.json
new file mode 100644
index 0000000000..3934b8f6d6
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/tsconfig.node.json b/python/samples/demos/chatkit-integration/frontend/tsconfig.node.json
new file mode 100644
index 0000000000..42872c59f5
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/python/samples/demos/chatkit-integration/frontend/vite.config.ts b/python/samples/demos/chatkit-integration/frontend/vite.config.ts
new file mode 100644
index 0000000000..ebf0200e51
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/frontend/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+
+const backendTarget = process.env.BACKEND_URL ?? "http://127.0.0.1:8001";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ host: "0.0.0.0",
+ port: 5171,
+ proxy: {
+ "/chatkit": {
+ target: backendTarget,
+ changeOrigin: true,
+ },
+ },
+ // For production deployments, you need to add your public domains to this list
+ allowedHosts: [
+ // You can remove these examples added just to demonstrate how to configure the allowlist
+ ".ngrok.io",
+ ".trycloudflare.com",
+ ],
+ },
+});
diff --git a/python/samples/demos/chatkit-integration/store.py b/python/samples/demos/chatkit-integration/store.py
new file mode 100644
index 0000000000..17fb746bed
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/store.py
@@ -0,0 +1,361 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""SQLite-based store implementation for ChatKit data persistence.
+
+This module provides a complete Store implementation using SQLite for data persistence.
+It includes proper thread safety, user isolation, and follows the ChatKit Store protocol.
+"""
+
+import sqlite3
+import uuid
+from typing import Any
+
+from chatkit.store import Store, NotFoundError
+from chatkit.types import (
+ Attachment,
+ Page,
+ ThreadItem,
+ ThreadMetadata,
+)
+from pydantic import BaseModel
+
+
+class ThreadData(BaseModel):
+ """Model for serializing thread data to SQLite."""
+ thread: ThreadMetadata
+
+
+class ItemData(BaseModel):
+ """Model for serializing thread item data to SQLite."""
+ item: ThreadItem
+
+
+class AttachmentData(BaseModel):
+ """Model for serializing attachment data to SQLite."""
+ attachment: Attachment
+
+
+class SQLiteStore(Store[dict[str, Any]]):
+ """SQLite-based store implementation for ChatKit data.
+
+ This implementation follows the pattern from the ChatKit Python tests
+ and provides persistent storage for threads, messages, and attachments.
+
+ Features:
+ - Thread-safe SQLite connections with WAL mode
+ - User isolation for multi-tenant support
+ - Proper error handling and transaction management
+ - Complete Store protocol implementation
+
+ Note: This is for demonstration purposes. In production, you should
+ implement proper error handling, connection pooling, and migration strategies.
+ """
+
+ def __init__(self, db_path: str | None = None):
+ self.db_path = db_path or "chatkit_demo.db" # Use file-based DB for demo
+ self._create_tables()
+
+ def _create_connection(self):
+ # Enable thread safety and WAL mode for better concurrent access
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
+ conn.execute("PRAGMA journal_mode=WAL")
+ return conn
+
+ def _create_tables(self):
+ with self._create_connection() as conn:
+ # Create threads table
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS threads (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ data TEXT NOT NULL
+ )"""
+ )
+
+ # Create items table
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS items (
+ id TEXT PRIMARY KEY,
+ thread_id TEXT NOT NULL,
+ user_id TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ data TEXT NOT NULL
+ )"""
+ )
+
+ # Create attachments table
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS attachments (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ data TEXT NOT NULL
+ )"""
+ )
+ conn.commit()
+
+ def generate_thread_id(self, context: dict[str, Any]) -> str:
+ return f"thr_{uuid.uuid4().hex[:8]}"
+
+ def generate_item_id(
+ self,
+ item_type: str,
+ thread: ThreadMetadata,
+ context: dict[str, Any],
+ ) -> str:
+ prefix_map = {
+ "message": "msg",
+ "tool_call": "tc",
+ "task": "tsk",
+ "workflow": "wf",
+ "attachment": "atc",
+ }
+ prefix = prefix_map.get(item_type, "itm")
+ return f"{prefix}_{uuid.uuid4().hex[:8]}"
+
+ async def load_thread(self, thread_id: str, context: dict[str, Any]) -> ThreadMetadata:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ cursor = conn.execute(
+ "SELECT data FROM threads WHERE id = ? AND user_id = ?",
+ (thread_id, user_id),
+ ).fetchone()
+
+ if cursor is None:
+ raise NotFoundError(f"Thread {thread_id} not found")
+
+ thread_data = ThreadData.model_validate_json(cursor[0])
+ return thread_data.thread
+
+ async def save_thread(self, thread: ThreadMetadata, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ thread_data = ThreadData(thread=thread)
+
+ # Replace existing thread data
+ conn.execute(
+ "DELETE FROM threads WHERE id = ? AND user_id = ?",
+ (thread.id, user_id),
+ )
+ conn.execute(
+ "INSERT INTO threads (id, user_id, created_at, data) VALUES (?, ?, ?, ?)",
+ (
+ thread.id,
+ user_id,
+ thread.created_at.isoformat(),
+ thread_data.model_dump_json(),
+ ),
+ )
+ conn.commit()
+
+ async def load_thread_items(
+ self,
+ thread_id: str,
+ after: str | None,
+ limit: int,
+ order: str,
+ context: dict[str, Any],
+ ) -> Page[ThreadItem]:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ created_after: str | None = None
+ if after:
+ after_cursor = conn.execute(
+ "SELECT created_at FROM items WHERE id = ? AND user_id = ?",
+ (after, user_id),
+ ).fetchone()
+ if after_cursor is None:
+ raise NotFoundError(f"Item {after} not found")
+ created_after = after_cursor[0]
+
+ query = """
+ SELECT data FROM items
+ WHERE thread_id = ? AND user_id = ?
+ """
+ params: list[Any] = [thread_id, user_id]
+
+ if created_after:
+ query += " AND created_at > ?" if order == "asc" else " AND created_at < ?"
+ params.append(created_after)
+
+ query += f" ORDER BY created_at {order} LIMIT ?"
+ params.append(limit + 1)
+
+ items_cursor = conn.execute(query, params).fetchall()
+ items = [
+ ItemData.model_validate_json(row[0]).item for row in items_cursor
+ ]
+
+ has_more = len(items) > limit
+ if has_more:
+ items = items[:limit]
+
+ return Page[ThreadItem](
+ data=items,
+ has_more=has_more,
+ after=items[-1].id if items else None
+ )
+
+ async def save_attachment(self, attachment: Attachment, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ attachment_data = AttachmentData(attachment=attachment)
+ conn.execute(
+ "INSERT OR REPLACE INTO attachments (id, user_id, data) VALUES (?, ?, ?)",
+ (
+ attachment.id,
+ user_id,
+ attachment_data.model_dump_json(),
+ ),
+ )
+ conn.commit()
+
+ async def load_attachment(self, attachment_id: str, context: dict[str, Any]) -> Attachment:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ cursor = conn.execute(
+ "SELECT data FROM attachments WHERE id = ? AND user_id = ?",
+ (attachment_id, user_id),
+ ).fetchone()
+
+ if cursor is None:
+ raise NotFoundError(f"Attachment {attachment_id} not found")
+
+ attachment_data = AttachmentData.model_validate_json(cursor[0])
+ return attachment_data.attachment
+
+ async def delete_attachment(self, attachment_id: str, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ conn.execute(
+ "DELETE FROM attachments WHERE id = ? AND user_id = ?",
+ (attachment_id, user_id),
+ )
+ conn.commit()
+
+ async def load_threads(
+ self,
+ limit: int,
+ after: str | None,
+ order: str,
+ context: dict[str, Any],
+ ) -> Page[ThreadMetadata]:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ created_after: str | None = None
+ if after:
+ after_cursor = conn.execute(
+ "SELECT created_at FROM threads WHERE id = ? AND user_id = ?",
+ (after, user_id),
+ ).fetchone()
+ if after_cursor is None:
+ raise NotFoundError(f"Thread {after} not found")
+ created_after = after_cursor[0]
+
+ query = "SELECT data FROM threads WHERE user_id = ?"
+ params: list[Any] = [user_id]
+
+ if created_after:
+ query += " AND created_at > ?" if order == "asc" else " AND created_at < ?"
+ params.append(created_after)
+
+ query += f" ORDER BY created_at {order} LIMIT ?"
+ params.append(limit + 1)
+
+ threads_cursor = conn.execute(query, params).fetchall()
+ threads = [
+ ThreadData.model_validate_json(row[0]).thread for row in threads_cursor
+ ]
+
+ has_more = len(threads) > limit
+ if has_more:
+ threads = threads[:limit]
+
+ return Page[ThreadMetadata](
+ data=threads,
+ has_more=has_more,
+ after=threads[-1].id if threads else None
+ )
+
+ async def add_thread_item(
+ self, thread_id: str, item: ThreadItem, context: dict[str, Any]
+ ) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ item_data = ItemData(item=item)
+ conn.execute(
+ "INSERT INTO items (id, thread_id, user_id, created_at, data) VALUES (?, ?, ?, ?, ?)",
+ (
+ item.id,
+ thread_id,
+ user_id,
+ item.created_at.isoformat(),
+ item_data.model_dump_json(),
+ ),
+ )
+ conn.commit()
+
+ async def save_item(self, thread_id: str, item: ThreadItem, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ item_data = ItemData(item=item)
+ conn.execute(
+ "UPDATE items SET data = ? WHERE id = ? AND thread_id = ? AND user_id = ?",
+ (
+ item_data.model_dump_json(),
+ item.id,
+ thread_id,
+ user_id,
+ ),
+ )
+ conn.commit()
+
+ async def load_item(self, thread_id: str, item_id: str, context: dict[str, Any]) -> ThreadItem:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ cursor = conn.execute(
+ "SELECT data FROM items WHERE id = ? AND thread_id = ? AND user_id = ?",
+ (item_id, thread_id, user_id),
+ ).fetchone()
+
+ if cursor is None:
+ raise NotFoundError(f"Item {item_id} not found in thread {thread_id}")
+
+ item_data = ItemData.model_validate_json(cursor[0])
+ return item_data.item
+
+ async def delete_thread(self, thread_id: str, context: dict[str, Any]) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ conn.execute(
+ "DELETE FROM threads WHERE id = ? AND user_id = ?",
+ (thread_id, user_id),
+ )
+ conn.execute(
+ "DELETE FROM items WHERE thread_id = ? AND user_id = ?",
+ (thread_id, user_id),
+ )
+ conn.commit()
+
+ async def delete_thread_item(
+ self, thread_id: str, item_id: str, context: dict[str, Any]
+ ) -> None:
+ user_id = context.get("user_id", "demo_user")
+
+ with self._create_connection() as conn:
+ conn.execute(
+ "DELETE FROM items WHERE id = ? AND thread_id = ? AND user_id = ?",
+ (item_id, thread_id, user_id),
+ )
+ conn.commit()
diff --git a/python/samples/demos/chatkit-integration/weather_widget.py b/python/samples/demos/chatkit-integration/weather_widget.py
new file mode 100644
index 0000000000..834f7a031d
--- /dev/null
+++ b/python/samples/demos/chatkit-integration/weather_widget.py
@@ -0,0 +1,437 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""Weather widget rendering for ChatKit integration sample."""
+
+import base64
+from dataclasses import dataclass
+
+from chatkit.actions import ActionConfig
+from chatkit.widgets import Box, Button, Card, Col, Image, Row, Text, Title, WidgetRoot
+
+WEATHER_ICON_COLOR = "#1D4ED8"
+WEATHER_ICON_ACCENT = "#DBEAFE"
+
+# Popular cities for the selector
+POPULAR_CITIES = [
+ {"value": "seattle", "label": "Seattle, WA", "description": "Pacific Northwest"},
+ {"value": "new_york", "label": "New York, NY", "description": "East Coast"},
+ {"value": "san_francisco", "label": "San Francisco, CA", "description": "Bay Area"},
+ {"value": "chicago", "label": "Chicago, IL", "description": "Midwest"},
+ {"value": "miami", "label": "Miami, FL", "description": "Southeast"},
+ {"value": "austin", "label": "Austin, TX", "description": "Southwest"},
+ {"value": "boston", "label": "Boston, MA", "description": "New England"},
+ {"value": "denver", "label": "Denver, CO", "description": "Mountain West"},
+ {"value": "portland", "label": "Portland, OR", "description": "Pacific Northwest"},
+ {"value": "atlanta", "label": "Atlanta, GA", "description": "Southeast"},
+]
+
+# Mapping from city values to display names for weather queries
+CITY_VALUE_TO_NAME = {city["value"]: city["label"] for city in POPULAR_CITIES}
+
+
+
+def _sun_svg() -> str:
+ """Generate SVG for sunny weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _cloud_svg() -> str:
+ """Generate SVG for cloudy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _rain_svg() -> str:
+ """Generate SVG for rainy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _storm_svg() -> str:
+ """Generate SVG for stormy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _snow_svg() -> str:
+ """Generate SVG for snowy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _fog_svg() -> str:
+ """Generate SVG for foggy weather icon."""
+ color = WEATHER_ICON_COLOR
+ accent = WEATHER_ICON_ACCENT
+ return (
+ '"
+ )
+
+
+def _encode_svg(svg: str) -> str:
+ """Encode SVG as base64 data URI."""
+ encoded = base64.b64encode(svg.encode("utf-8")).decode("ascii")
+ return f"data:image/svg+xml;base64,{encoded}"
+
+
+# Weather condition to icon mapping
+WEATHER_ICONS = {
+ "sunny": _encode_svg(_sun_svg()),
+ "cloudy": _encode_svg(_cloud_svg()),
+ "rainy": _encode_svg(_rain_svg()),
+ "stormy": _encode_svg(_storm_svg()),
+ "snowy": _encode_svg(_snow_svg()),
+ "foggy": _encode_svg(_fog_svg()),
+}
+
+DEFAULT_WEATHER_ICON = _encode_svg(_cloud_svg())
+
+
+@dataclass
+class WeatherData:
+ """Weather data container."""
+
+ location: str
+ condition: str
+ temperature: int
+ humidity: int
+ wind_speed: int
+
+
+def render_weather_widget(data: WeatherData) -> WidgetRoot:
+ """Render a weather widget from weather data.
+
+ Args:
+ data: WeatherData containing weather information
+
+ Returns:
+ A ChatKit WidgetRoot (Card) displaying the weather information
+ """
+ # Get weather icon
+ weather_icon_src = WEATHER_ICONS.get(data.condition.lower(), DEFAULT_WEATHER_ICON)
+
+ # Build the widget
+ header = Box(
+ padding=5,
+ background="surface-tertiary",
+ children=[
+ Row(
+ justify="between",
+ align="center",
+ children=[
+ Col(
+ align="start",
+ gap=1,
+ children=[
+ Text(
+ value=data.location,
+ size="lg",
+ weight="semibold",
+ ),
+ Text(
+ value="Current conditions",
+ color="tertiary",
+ size="xs",
+ ),
+ ],
+ ),
+ Box(
+ padding=3,
+ radius="full",
+ background="blue-100",
+ children=[
+ Image(
+ src=weather_icon_src,
+ alt=data.condition,
+ size=28,
+ fit="contain",
+ )
+ ],
+ ),
+ ],
+ ),
+ Row(
+ align="start",
+ gap=4,
+ children=[
+ Title(
+ value=f"{data.temperature}°C",
+ size="lg",
+ weight="semibold",
+ ),
+ Col(
+ align="start",
+ gap=1,
+ children=[
+ Text(
+ value=data.condition.title(),
+ color="secondary",
+ size="sm",
+ weight="medium",
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ )
+
+ # Details section
+ details = Box(
+ padding=5,
+ gap=4,
+ children=[
+ Text(value="Weather details", weight="semibold", size="sm"),
+ Row(
+ gap=3,
+ wrap="wrap",
+ children=[
+ _detail_chip("Humidity", f"{data.humidity}%"),
+ _detail_chip("Wind", f"{data.wind_speed} km/h"),
+ ],
+ ),
+ ],
+ )
+
+ return Card(
+ key="weather",
+ padding=0,
+ children=[header, details],
+ )
+
+
+def _detail_chip(label: str, value: str) -> Box:
+ """Create a detail chip widget component."""
+ return Box(
+ padding=3,
+ radius="xl",
+ background="surface-tertiary",
+ width=150,
+ minWidth=150,
+ maxWidth=150,
+ minHeight=80,
+ maxHeight=80,
+ flex="0 0 auto",
+ children=[
+ Col(
+ align="stretch",
+ gap=2,
+ children=[
+ Text(value=label, size="xs", weight="medium", color="tertiary"),
+ Row(
+ justify="center",
+ margin={"top": 2},
+ children=[Text(value=value, weight="semibold", size="lg")],
+ ),
+ ],
+ )
+ ],
+ )
+
+
+def weather_widget_copy_text(data: WeatherData) -> str:
+ """Generate plain text representation of weather data.
+
+ Args:
+ data: WeatherData containing weather information
+
+ Returns:
+ Plain text description for copy/paste functionality
+ """
+ return (
+ f"Weather in {data.location}:\n"
+ f"• Condition: {data.condition.title()}\n"
+ f"• Temperature: {data.temperature}°C\n"
+ f"• Humidity: {data.humidity}%\n"
+ f"• Wind: {data.wind_speed} km/h"
+ )
+
+
+def render_city_selector_widget() -> WidgetRoot:
+ """Render an interactive city selector widget.
+
+ This widget displays popular cities as a visual selection interface.
+ Users can click or ask about any city to get weather information.
+
+ Returns:
+ A ChatKit WidgetRoot (Card) with city selection display
+ """
+ # Create location icon SVG
+ location_icon = _encode_svg(
+ '"
+ )
+
+ # Header section
+ header = Box(
+ padding=5,
+ background="surface-tertiary",
+ children=[
+ Row(
+ gap=3,
+ align="center",
+ children=[
+ Box(
+ padding=3,
+ radius="full",
+ background="blue-100",
+ children=[
+ Image(
+ src=location_icon,
+ alt="Location",
+ size=28,
+ fit="contain",
+ )
+ ],
+ ),
+ Col(
+ align="start",
+ gap=1,
+ children=[
+ Title(
+ value="Popular Cities",
+ size="md",
+ weight="semibold",
+ ),
+ Text(
+ value="Select a city or ask about any location",
+ color="tertiary",
+ size="xs",
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ )
+
+ # Create city chips in a grid layout
+ city_chips: list[Button] = []
+ for city in POPULAR_CITIES:
+ # Create a button that sends an action to query weather for the selected city
+ chip = Button(
+ label=city["label"],
+ variant="outline",
+ size="md",
+ onClickAction=ActionConfig(
+ type="city_selected",
+ payload={"city_value": city["value"], "city_label": city["label"]},
+ handler="server", # Handle on server-side
+ ),
+ )
+ city_chips.append(chip)
+
+ # Arrange in rows of 3
+ city_rows: list[Row] = []
+ for i in range(0, len(city_chips), 3):
+ row_chips: list[Button] = city_chips[i : i + 3]
+ city_rows.append(
+ Row(
+ gap=3,
+ wrap="wrap",
+ justify="start",
+ children=list(row_chips), # Convert to generic list
+ )
+ )
+
+ # Cities display section
+ cities_section = Box(
+ padding=5,
+ gap=3,
+ children=[
+ *city_rows,
+ Box(
+ padding=3,
+ radius="md",
+ background="blue-50",
+ children=[
+ Text(
+ value="💡 Click any city to get its weather, or ask about any other location!",
+ size="xs",
+ color="secondary",
+ ),
+ ],
+ ),
+ ],
+ )
+
+ return Card(
+ key="city_selector",
+ padding=0,
+ children=[header, cities_section],
+ )
+
+
+def city_selector_copy_text() -> str:
+ """Generate plain text representation of city selector.
+
+ Returns:
+ Plain text description for copy/paste functionality
+ """
+ cities_list = "\n".join([f"• {city['label']}" for city in POPULAR_CITIES])
+ return f"Popular cities (click to get weather):\n{cities_list}\n\nYou can also ask about weather in any other location!"
diff --git a/python/samples/getting_started/tools/README.md b/python/samples/getting_started/tools/README.md
index e69de29bb2..66ca227da6 100644
--- a/python/samples/getting_started/tools/README.md
+++ b/python/samples/getting_started/tools/README.md
@@ -0,0 +1,119 @@
+# Tools Examples
+
+This folder contains examples demonstrating how to use AI functions (tools) with the Agent Framework. AI functions allow agents to interact with external systems, perform computations, and execute custom logic.
+
+## Examples
+
+| File | Description |
+|------|-------------|
+| [`ai_function_declaration_only.py`](ai_function_declaration_only.py) | Demonstrates how to create function declarations without implementations. Useful for testing agent reasoning about tool usage or when tools are defined elsewhere. Shows how agents request tool calls even when the tool won't be executed. |
+| [`ai_function_from_dict_with_dependency_injection.py`](ai_function_from_dict_with_dependency_injection.py) | Shows how to create AI functions from dictionary definitions using dependency injection. The function implementation is injected at runtime during deserialization, enabling dynamic tool creation and configuration. Note: This serialization/deserialization feature is in active development. |
+| [`ai_function_recover_from_failures.py`](ai_function_recover_from_failures.py) | Demonstrates graceful error handling when tools raise exceptions. Shows how agents receive error information and can recover from failures, deciding whether to retry or respond differently based on the exception. |
+| [`ai_function_with_approval.py`](ai_function_with_approval.py) | Shows how to implement user approval workflows for function calls without using threads. Demonstrates both streaming and non-streaming approval patterns where users can approve or reject function executions before they run. |
+| [`ai_function_with_approval_and_threads.py`](ai_function_with_approval_and_threads.py) | Demonstrates tool approval workflows using threads for automatic conversation history management. Shows how threads simplify approval workflows by automatically storing and retrieving conversation context. Includes both approval and rejection examples. |
+| [`ai_function_with_max_exceptions.py`](ai_function_with_max_exceptions.py) | Shows how to limit the number of times a tool can fail with exceptions using `max_invocation_exceptions`. Useful for preventing expensive tools from being called repeatedly when they keep failing. |
+| [`ai_function_with_max_invocations.py`](ai_function_with_max_invocations.py) | Demonstrates limiting the total number of times a tool can be invoked using `max_invocations`. Useful for rate-limiting expensive operations or ensuring tools are only called a specific number of times per conversation. |
+| [`ai_functions_in_class.py`](ai_functions_in_class.py) | Shows how to use `ai_function` decorator with class methods to create stateful tools. Demonstrates how class state can control tool behavior dynamically, allowing you to adjust tool functionality at runtime by modifying class properties. |
+
+## Key Concepts
+
+### AI Function Features
+
+- **Function Declarations**: Define tool schemas without implementations for testing or external tools
+- **Dependency Injection**: Create tools from configurations with runtime-injected implementations
+- **Error Handling**: Gracefully handle and recover from tool execution failures
+- **Approval Workflows**: Require user approval before executing sensitive or important operations
+- **Invocation Limits**: Control how many times tools can be called or fail
+- **Stateful Tools**: Use class methods as tools to maintain state and dynamically control behavior
+
+### Common Patterns
+
+#### Basic Tool Definition
+
+```python
+from agent_framework import ai_function
+from typing import Annotated
+
+@ai_function
+def my_tool(param: Annotated[str, "Description"]) -> str:
+ """Tool description for the AI."""
+ return f"Result: {param}"
+```
+
+#### Tool with Approval
+
+```python
+@ai_function(approval_mode="always_require")
+def sensitive_operation(data: Annotated[str, "Data to process"]) -> str:
+ """This requires user approval before execution."""
+ return f"Processed: {data}"
+```
+
+#### Tool with Invocation Limits
+
+```python
+@ai_function(max_invocations=3)
+def limited_tool() -> str:
+ """Can only be called 3 times total."""
+ return "Result"
+
+@ai_function(max_invocation_exceptions=2)
+def fragile_tool() -> str:
+ """Can only fail 2 times before being disabled."""
+ return "Result"
+```
+
+#### Stateful Tools with Classes
+
+```python
+class MyTools:
+ def __init__(self, mode: str = "normal"):
+ self.mode = mode
+
+ def process(self, data: Annotated[str, "Data to process"]) -> str:
+ """Process data based on current mode."""
+ if self.mode == "safe":
+ return f"Safely processed: {data}"
+ return f"Processed: {data}"
+
+# Create instance and use methods as tools
+tools = MyTools(mode="safe")
+agent = client.create_agent(tools=tools.process)
+
+# Change behavior dynamically
+tools.mode = "normal"
+```
+
+### Error Handling
+
+When tools raise exceptions:
+1. The exception is captured and sent to the agent as a function result
+2. The agent receives the error message and can reason about what went wrong
+3. The agent can retry with different parameters, use alternative tools, or explain the issue to the user
+4. With invocation limits, tools can be disabled after repeated failures
+
+### Approval Workflows
+
+Two approaches for handling approvals:
+
+1. **Without Threads**: Manually manage conversation context, including the query, approval request, and response in each iteration
+2. **With Threads**: Thread automatically manages conversation history, simplifying the approval workflow
+
+## Usage Tips
+
+- Use **declaration-only** functions when you want to test agent reasoning without execution
+- Use **dependency injection** for dynamic tool configuration and plugin architectures
+- Implement **approval workflows** for operations that modify data, spend money, or require human oversight
+- Set **invocation limits** to prevent runaway costs or infinite loops with expensive tools
+- Handle **exceptions gracefully** to create robust agents that can recover from failures
+- Use **class-based tools** when you need to maintain state or dynamically adjust tool behavior at runtime
+
+## Running the Examples
+
+Each example is a standalone Python script that can be run directly:
+
+```bash
+uv run python ai_function_with_approval.py
+```
+
+Make sure you have the necessary environment variables configured (like `OPENAI_API_KEY` or Azure credentials) before running the examples.
diff --git a/python/samples/getting_started/tools/ai_function_declaration_only.py b/python/samples/getting_started/tools/ai_function_declaration_only.py
new file mode 100644
index 0000000000..03a2e8f8ed
--- /dev/null
+++ b/python/samples/getting_started/tools/ai_function_declaration_only.py
@@ -0,0 +1,75 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from agent_framework import AIFunction
+from agent_framework.openai import OpenAIResponsesClient
+
+"""
+Example of how to create a function that only consists of a declaration without an implementation.
+This is useful when you want the agent to use tools that are defined elsewhere or when you want
+to test the agent's ability to reason about tool usage without executing them.
+
+The only difference is that you provide an AIFunction without a function.
+If you need a input_model, you can still provide that as well.
+"""
+
+
+async def main():
+ function_declaration = AIFunction[None, None](
+ name="get_current_time",
+ description="Get the current time in ISO 8601 format.",
+ )
+
+ agent = OpenAIResponsesClient().create_agent(
+ name="DeclarationOnlyToolAgent",
+ instructions="You are a helpful agent that uses tools.",
+ tools=function_declaration,
+ )
+ query = "What is the current time?"
+ print(f"User: {query}")
+ result = await agent.run(query)
+ print(f"Result: {result.to_json(indent=2)}\n")
+
+
+"""
+Expected result:
+User: What is the current time?
+Result: {
+ "type": "agent_run_response",
+ "messages": [
+ {
+ "type": "chat_message",
+ "role": {
+ "type": "role",
+ "value": "assistant"
+ },
+ "contents": [
+ {
+ "type": "function_call",
+ "call_id": "call_0flN9rfGLK8LhORy4uMDiRSC",
+ "name": "get_current_time",
+ "arguments": "{}",
+ "fc_id": "fc_0fd5f269955c589f016904c46584348195b84a8736e61248de"
+ }
+ ],
+ "author_name": "DeclarationOnlyToolAgent",
+ "additional_properties": {}
+ }
+ ],
+ "response_id": "resp_0fd5f269955c589f016904c462d5cc819599d28384ba067edc",
+ "created_at": "2025-10-31T15:14:58.000000Z",
+ "usage_details": {
+ "type": "usage_details",
+ "input_token_count": 63,
+ "output_token_count": 145,
+ "total_token_count": 208,
+ "openai.reasoning_tokens": 128
+ },
+ "additional_properties": {}
+}
+"""
+
+
+if __name__ == "__main__":
+ import asyncio
+
+ asyncio.run(main())
diff --git a/python/samples/getting_started/tools/tool_with_injected_func.py b/python/samples/getting_started/tools/ai_function_from_dict_with_dependency_injection.py
similarity index 100%
rename from python/samples/getting_started/tools/tool_with_injected_func.py
rename to python/samples/getting_started/tools/ai_function_from_dict_with_dependency_injection.py
diff --git a/python/samples/getting_started/tools/failing_tools.py b/python/samples/getting_started/tools/ai_function_recover_from_failures.py
similarity index 100%
rename from python/samples/getting_started/tools/failing_tools.py
rename to python/samples/getting_started/tools/ai_function_recover_from_failures.py
diff --git a/python/samples/getting_started/tools/ai_tool_with_approval.py b/python/samples/getting_started/tools/ai_function_with_approval.py
similarity index 100%
rename from python/samples/getting_started/tools/ai_tool_with_approval.py
rename to python/samples/getting_started/tools/ai_function_with_approval.py
diff --git a/python/samples/getting_started/tools/ai_tool_with_approval_and_threads.py b/python/samples/getting_started/tools/ai_function_with_approval_and_threads.py
similarity index 100%
rename from python/samples/getting_started/tools/ai_tool_with_approval_and_threads.py
rename to python/samples/getting_started/tools/ai_function_with_approval_and_threads.py
diff --git a/python/samples/getting_started/tools/ai_function_with_max_exceptions.py b/python/samples/getting_started/tools/ai_function_with_max_exceptions.py
new file mode 100644
index 0000000000..b1600b7299
--- /dev/null
+++ b/python/samples/getting_started/tools/ai_function_with_max_exceptions.py
@@ -0,0 +1,188 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+from typing import Annotated
+
+from agent_framework import FunctionCallContent, FunctionResultContent, ai_function
+from agent_framework.openai import OpenAIResponsesClient
+
+"""
+Some tools are very expensive to run, so you may want to limit the number of times
+it tries to call them and fails. This sample shows a tool that can only raise exceptions a
+limited number of times.
+"""
+
+
+# we trick the AI into calling this function with 0 as denominator to trigger the exception
+@ai_function(max_invocation_exceptions=1)
+def safe_divide(
+ a: Annotated[int, "Numerator"],
+ b: Annotated[int, "Denominator"],
+) -> str:
+ """Divide two numbers can be used with 0 as denominator."""
+ try:
+ result = a / b # Will raise ZeroDivisionError
+ except ZeroDivisionError as exc:
+ print(f" Tool failed with error: {exc}")
+ raise
+
+ return f"{a} / {b} = {result}"
+
+
+async def main():
+ # tools = Tools()
+ agent = OpenAIResponsesClient().create_agent(
+ name="ToolAgent",
+ instructions="Use the provided tools.",
+ tools=[safe_divide],
+ )
+ thread = agent.get_new_thread()
+ print("=" * 60)
+ print("Step 1: Call divide(10, 0) - tool raises exception")
+ response = await agent.run("Divide 10 by 0", thread=thread)
+ print(f"Response: {response.text}")
+ print("=" * 60)
+ print("Step 2: Call divide(100, 0) - will refuse to execute due to max_invocation_exceptions")
+ response = await agent.run("Divide 100 by 0", thread=thread)
+ print(f"Response: {response.text}")
+ print("=" * 60)
+ print(f"Number of tool calls attempted: {safe_divide.invocation_count}")
+ print(f"Number of tool calls failed: {safe_divide.invocation_exception_count}")
+ print("Replay the conversation:")
+ assert thread.message_store
+ assert thread.message_store.list_messages
+ for idx, msg in enumerate(await thread.message_store.list_messages()):
+ if msg.text:
+ print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
+ for content in msg.contents:
+ if isinstance(content, FunctionCallContent):
+ print(
+ f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
+ )
+ if isinstance(content, FunctionResultContent):
+ print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
+
+
+"""
+Expected Output:
+============================================================
+Step 1: Call divide(10, 0) - tool raises exception
+ Tool failed with error: division by zero
+[2025-10-31 15:39:53 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
+Function failed. Error: division by zero
+Response: Division by zero is undefined in standard arithmetic. There is no finite value for 10 ÷ 0.
+
+If you want alternatives:
+- A valid example: 10 ÷ 2 = 5.
+- To handle safely in code, you can check the denominator first (e.g., in Python: if b == 0:
+ handle error else: compute a/b).
+- If you’re curious about limits: as x → 0+, 10/x → +∞; as x → 0−, 10/x → −∞; there is no finite limit.
+
+Would you like me to show a safe division snippet in a specific language, or compute something else?
+============================================================
+Step 2: Call divide(100, 0) - will refuse to execute due to max_invocations
+[2025-10-31 15:40:09 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
+Function failed. Error: Function 'safe_divide' has reached its maximum exception limit, you tried to use this
+tool too many times and it kept failing.
+Response: Division by zero is undefined in standard arithmetic, so 100 ÷ 0 has no finite value.
+
+If you’re coding and want safe handling, here are quick patterns in a few languages:
+
+- Python
+ def safe_divide(a, b):
+ if b == 0:
+ return None # or raise an exception
+ return a / b
+
+ safe_divide(100, 0) # -> None
+
+- JavaScript
+ function safeDivide(a, b) {
+ if (b === 0) return undefined; // or throw
+ return a / b;
+ }
+
+ safeDivide(100, 0) // -> undefined
+
+- Java
+ public static Double safeDivide(double a, double b) {
+ if (b == 0.0) throw new ArithmeticException("Divide by zero");
+ return a / b;
+ }
+
+ safeDivide(100, 0) // -> exception
+
+- C/C++
+ double safeDivide(double a, double b) {
+ if (b == 0.0) return std::numeric_limits::infinity(); // or handle error
+ return a / b;
+ }
+
+Note: In many languages, dividing by zero with floating-point numbers yields Infinity (or -Infinity) or NaN,
+but integer division typically raises an error.
+
+Would you like a snippet in a specific language or to see a math explanation (limits) for what happens as the
+divisor approaches zero?
+============================================================
+Number of tool calls attempted: 1
+Number of tool calls failed: 1
+Replay the conversation:
+1 user: Divide 10 by 0
+2 ToolAgent: calling function: safe_divide with arguments: {"a":10,"b":0}
+3 tool: division by zero
+4 ToolAgent: Division by zero is undefined in standard arithmetic. There is no finite value for 10 ÷ 0.
+
+If you want alternatives:
+- A valid example: 10 ÷ 2 = 5.
+- To handle safely in code, you can check the denominator first (e.g., in Python: if b == 0:
+ handle error else: compute a/b).
+- If you’re curious about limits: as x → 0+, 10/x → +∞; as x → 0−, 10/x → −∞; there is no finite limit.
+
+Would you like me to show a safe division snippet in a specific language, or compute something else?
+5 user: Divide 100 by 0
+6 ToolAgent: calling function: safe_divide with arguments: {"a":100,"b":0}
+7 tool: Function 'safe_divide' has reached its maximum exception limit, you tried to use this tool too many times
+ and it kept failing.
+8 ToolAgent: Division by zero is undefined in standard arithmetic, so 100 ÷ 0 has no finite value.
+
+If you’re coding and want safe handling, here are quick patterns in a few languages:
+
+- Python
+ def safe_divide(a, b):
+ if b == 0:
+ return None # or raise an exception
+ return a / b
+
+ safe_divide(100, 0) # -> None
+
+- JavaScript
+ function safeDivide(a, b) {
+ if (b === 0) return undefined; // or throw
+ return a / b;
+ }
+
+ safeDivide(100, 0) // -> undefined
+
+- Java
+ public static Double safeDivide(double a, double b) {
+ if (b == 0.0) throw new ArithmeticException("Divide by zero");
+ return a / b;
+ }
+
+ safeDivide(100, 0) // -> exception
+
+- C/C++
+ double safeDivide(double a, double b) {
+ if (b == 0.0) return std::numeric_limits::infinity(); // or handle error
+ return a / b;
+ }
+
+Note: In many languages, dividing by zero with floating-point numbers yields Infinity (or -Infinity) or NaN,
+but integer division typically raises an error.
+
+Would you like a snippet in a specific language or to see a math explanation (limits) for what happens as the
+divisor approaches zero?
+"""
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/python/samples/getting_started/tools/ai_function_with_max_invocations.py b/python/samples/getting_started/tools/ai_function_with_max_invocations.py
new file mode 100644
index 0000000000..6a52e91329
--- /dev/null
+++ b/python/samples/getting_started/tools/ai_function_with_max_invocations.py
@@ -0,0 +1,89 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+from typing import Annotated
+
+from agent_framework import FunctionCallContent, FunctionResultContent, ai_function
+from agent_framework.openai import OpenAIResponsesClient
+
+"""
+For tools you can specify if there is a maximum number of invocations allowed.
+This sample shows a tool that can only be invoked once.
+"""
+
+
+@ai_function(max_invocations=1)
+def unicorn_function(times: Annotated[int, "The number of unicorns to return."]) -> str:
+ """This function returns precious unicorns!"""
+ return f"{'🦄' * times}✨"
+
+
+async def main():
+ # tools = Tools()
+ agent = OpenAIResponsesClient().create_agent(
+ name="ToolAgent",
+ instructions="Use the provided tools.",
+ tools=[unicorn_function],
+ )
+ thread = agent.get_new_thread()
+ print("=" * 60)
+ print("Step 1: Call unicorn_function")
+ response = await agent.run("Call 5 unicorns!", thread=thread)
+ print(f"Response: {response.text}")
+ print("=" * 60)
+ print("Step 2: Call unicorn_function again - will refuse to execute due to max_invocations")
+ response = await agent.run("Call 10 unicorns and use the function to do it.", thread=thread)
+ print(f"Response: {response.text}")
+ print("=" * 60)
+ print(f"Number of tool calls attempted: {unicorn_function.invocation_count}")
+ print(f"Number of tool calls failed: {unicorn_function.invocation_exception_count}")
+ print("Replay the conversation:")
+ assert thread.message_store
+ assert thread.message_store.list_messages
+ for idx, msg in enumerate(await thread.message_store.list_messages()):
+ if msg.text:
+ print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
+ for content in msg.contents:
+ if isinstance(content, FunctionCallContent):
+ print(
+ f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
+ )
+ if isinstance(content, FunctionResultContent):
+ print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
+
+
+"""
+Expected Output:
+============================================================
+Step 1: Call unicorn_function
+Response: Five unicorns summoned: 🦄🦄🦄🦄🦄✨
+============================================================
+Step 2: Call unicorn_function again - will refuse to execute due to max_invocations
+[2025-10-31 15:54:40 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
+Function failed. Error: Function 'unicorn_function' has reached its maximum invocation limit,
+you can no longer use this tool.
+Response: The unicorn function has reached its maximum invocation limit. I can’t call it again right now.
+
+Here are 10 unicorns manually: 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄
+
+Would you like me to try again later, or generate something else?
+============================================================
+Number of tool calls attempted: 1
+Number of tool calls failed: 0
+Replay the conversation:
+1 user: Call 5 unicorns!
+2 ToolAgent: calling function: unicorn_function with arguments: {"times":5}
+3 tool: 🦄🦄🦄🦄🦄✨
+4 ToolAgent: Five unicorns summoned: 🦄🦄🦄🦄🦄✨
+5 user: Call 10 unicorns and use the function to do it.
+6 ToolAgent: calling function: unicorn_function with arguments: {"times":10}
+7 tool: Function 'unicorn_function' has reached its maximum invocation limit, you can no longer use this tool.
+8 ToolAgent: The unicorn function has reached its maximum invocation limit. I can’t call it again right now.
+
+Here are 10 unicorns manually: 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄
+
+Would you like me to try again later, or generate something else?
+"""
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/python/samples/getting_started/tools/ai_functions_in_class.py b/python/samples/getting_started/tools/ai_functions_in_class.py
new file mode 100644
index 0000000000..995383cc70
--- /dev/null
+++ b/python/samples/getting_started/tools/ai_functions_in_class.py
@@ -0,0 +1,100 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+from typing import Annotated
+
+from agent_framework import ai_function
+from agent_framework.openai import OpenAIResponsesClient
+
+"""
+This sample demonstrates using ai_function within a class,
+showing how to manage state within the class that affects tool behavior.
+
+And how to use ai_function-decorated methods as tools in an agent in order to adjust the behavior of a tool.
+"""
+
+
+class MyFunctionClass:
+ def __init__(self, safe: bool = False) -> None:
+ """Simple class with two ai_functions: divide and add.
+
+ The safe parameter controls whether divide raises on division by zero or returns `infinity` for divide by zero.
+ """
+ self.safe = safe
+
+ def divide(
+ self,
+ a: Annotated[int, "Numerator"],
+ b: Annotated[int, "Denominator"],
+ ) -> str:
+ """Divide two numbers, safe to use also with 0 as denominator."""
+ result = "∞" if b == 0 and self.safe else a / b
+ return f"{a} / {b} = {result}"
+
+ def add(
+ self,
+ x: Annotated[int, "First number"],
+ y: Annotated[int, "Second number"],
+ ) -> str:
+ return f"{x} + {y} = {x + y}"
+
+
+async def main():
+ # Creating my function class with safe division enabled
+ tools = MyFunctionClass(safe=True)
+ # Applying the ai_function decorator to one of the methods of the class
+ add_function = ai_function(description="Add two numbers.")(tools.add)
+
+ agent = OpenAIResponsesClient().create_agent(
+ name="ToolAgent",
+ instructions="Use the provided tools.",
+ )
+ print("=" * 60)
+ print("Step 1: Call divide(10, 0) - tool returns infinity")
+ query = "Divide 10 by 0"
+ response = await agent.run(
+ query,
+ tools=[add_function, tools.divide],
+ )
+ print(f"Response: {response.text}")
+ print("=" * 60)
+ print("Step 2: Call set safe to False and call again")
+ # Disabling safe mode to allow exceptions
+ tools.safe = False
+ response = await agent.run(query, tools=[add_function, tools.divide])
+ print(f"Response: {response.text}")
+ print("=" * 60)
+
+
+"""
+Expected Output:
+============================================================
+Step 1: Call divide(10, 0) - tool returns infinity
+Response: Division by zero is undefined in standard arithmetic. There is no real number that equals 10 divided by 0.
+
+- If you look at limits: as x → 0+ (denominator approaches 0 from the positive side), 10/x → +∞; as x → 0−, 10/x → −∞.
+- Some calculators may display "infinity" or give an error, but that's not a real number.
+
+If you want a numeric surrogate, you can use a small nonzero denominator, e.g., 10/0.001 = 10000. Would you like to
+see more on limits or handle it with a tiny epsilon?
+============================================================
+Step 2: Call set safe to False and call again
+[2025-10-31 16:17:44 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
+Function failed. Error: division by zero
+Response: Division by zero is undefined in standard arithmetic. There is no number y such that 0 × y = 10.
+
+If you’re looking at limits:
+- as x → 0+, 10/x → +∞
+- as x → 0−, 10/x → −∞
+So the limit does not exist.
+
+In programming, dividing by zero usually raises an error or results in special values (e.g., NaN or ∞) depending
+on the language.
+
+If you want, tell me what you’d like to do instead (e.g., compute 10 divided by 2, or handle division by zero safely
+in code), and I can help with examples.
+============================================================
+"""
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/python/samples/getting_started/tools/function_invocation_configuration.py b/python/samples/getting_started/tools/function_invocation_configuration.py
new file mode 100644
index 0000000000..bb0e6b0798
--- /dev/null
+++ b/python/samples/getting_started/tools/function_invocation_configuration.py
@@ -0,0 +1,58 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+from typing import Annotated
+
+from agent_framework.openai import OpenAIResponsesClient
+
+"""
+This sample demonstrates how to configure function invocation settings
+for an client and use a simple ai_function as a tool in an agent.
+
+This behavior is the same for all chat client types.
+"""
+
+
+def add(
+ x: Annotated[int, "First number"],
+ y: Annotated[int, "Second number"],
+) -> str:
+ return f"{x} + {y} = {x + y}"
+
+
+async def main():
+ client = OpenAIResponsesClient()
+ if client.function_invocation_configuration is not None:
+ client.function_invocation_configuration.include_detailed_errors = True
+ client.function_invocation_configuration.max_iterations = 40
+ print(f"Function invocation configured as: \n{client.function_invocation_configuration.to_json(indent=2)}")
+
+ agent = client.create_agent(name="ToolAgent", instructions="Use the provided tools.", tools=add)
+
+ print("=" * 60)
+ print("Call add(239847293, 29834)")
+ query = "Add 239847293 and 29834"
+ response = await agent.run(query)
+ print(f"Response: {response.text}")
+
+
+"""
+Expected Output:
+============================================================
+Function invocation configured as:
+{
+ "type": "function_invocation_configuration",
+ "enabled": true,
+ "max_iterations": 40,
+ "max_consecutive_errors_per_request": 3,
+ "terminate_on_unknown_calls": false,
+ "additional_tools": [],
+ "include_detailed_errors": true
+}
+============================================================
+Call add(239847293, 29834)
+Response: 239,877,127
+"""
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/python/uv.lock b/python/uv.lock
index 74d2cb1f8c..afddfee9b2 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -31,8 +31,10 @@ supported-markers = [
members = [
"agent-framework",
"agent-framework-a2a",
+ "agent-framework-ag-ui",
"agent-framework-anthropic",
"agent-framework-azure-ai",
+ "agent-framework-chatkit",
"agent-framework-copilotstudio",
"agent-framework-core",
"agent-framework-devui",
@@ -71,14 +73,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832 },
]
+[[package]]
+name = "ag-ui-protocol"
+version = "0.1.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7b/d7/a8f8789b3b8b5f7263a902361468e8dfefd85ec63d1d5398579b9175d76d/ag_ui_protocol-0.1.9.tar.gz", hash = "sha256:94d75e3919ff75e0b608a7eed445062ea0e6f11cd33b3386a7649047e0c7abd3", size = 4988 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/50/2bb71a2a9135f4d88706293773320d185789b592987c09f79e9bf2f4875f/ag_ui_protocol-0.1.9-py3-none-any.whl", hash = "sha256:44c1238b0576a3915b3a16e1b3855724e08e92ebc96b1ff29379fbd3bfbd400b", size = 7070 },
+]
+
[[package]]
name = "agent-framework"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { virtual = "." }
dependencies = [
{ name = "agent-framework-a2a", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "agent-framework-ag-ui", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-anthropic", 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-chatkit", 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-core", 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'" },
@@ -119,8 +135,10 @@ docs = [
[package.metadata]
requires-dist = [
{ name = "agent-framework-a2a", editable = "packages/a2a" },
+ { name = "agent-framework-ag-ui", editable = "packages/ag-ui" },
{ name = "agent-framework-anthropic", editable = "packages/anthropic" },
{ name = "agent-framework-azure-ai", editable = "packages/azure-ai" },
+ { name = "agent-framework-chatkit", editable = "packages/chatkit" },
{ name = "agent-framework-copilotstudio", editable = "packages/copilotstudio" },
{ name = "agent-framework-core", editable = "packages/core" },
{ name = "agent-framework-devui", editable = "packages/devui" },
@@ -160,7 +178,7 @@ docs = [
[[package]]
name = "agent-framework-a2a"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/a2a" }
dependencies = [
{ name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -173,9 +191,39 @@ requires-dist = [
{ name = "agent-framework-core", editable = "packages/core" },
]
+[[package]]
+name = "agent-framework-ag-ui"
+version = "1.0.0b251105"
+source = { editable = "packages/ag-ui" }
+dependencies = [
+ { name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pytest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "ag-ui-protocol", specifier = ">=0.1.9" },
+ { name = "agent-framework-core", editable = "packages/core" },
+ { name = "fastapi", specifier = ">=0.115.0" },
+ { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
+ { name = "uvicorn", specifier = ">=0.30.0" },
+]
+provides-extras = ["dev"]
+
[[package]]
name = "agent-framework-anthropic"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/anthropic" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -190,7 +238,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azure-ai"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/azure-ai" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -205,9 +253,24 @@ requires-dist = [
{ name = "azure-ai-projects", specifier = ">=2.0.0a20251103001", index = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple" },
]
+[[package]]
+name = "agent-framework-chatkit"
+version = "1.0.0b251001"
+source = { editable = "packages/chatkit" }
+dependencies = [
+ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "openai-chatkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "agent-framework-core", editable = "packages/core" },
+ { name = "openai-chatkit", specifier = ">=1.1.0,<2.0.0" },
+]
+
[[package]]
name = "agent-framework-copilotstudio"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/copilotstudio" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -222,7 +285,7 @@ requires-dist = [
[[package]]
name = "agent-framework-core"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/core" }
dependencies = [
{ name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -261,7 +324,7 @@ requires-dist = [
{ name = "agent-framework-redis", marker = "extra == 'all'", editable = "packages/redis" },
{ name = "azure-identity", specifier = ">=1,<2" },
{ name = "mcp", extras = ["ws"], specifier = ">=1.13" },
- { name = "openai", specifier = ">=1.99.0,<2" },
+ { name = "openai", specifier = ">=1.99.0" },
{ name = "opentelemetry-api", specifier = ">=1.24" },
{ name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.36.0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
@@ -274,7 +337,7 @@ provides-extras = ["all"]
[[package]]
name = "agent-framework-devui"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/devui" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -308,7 +371,7 @@ provides-extras = ["dev", "all"]
[[package]]
name = "agent-framework-lab"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/lab" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -399,7 +462,7 @@ dev = [
[[package]]
name = "agent-framework-mem0"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/mem0" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -414,7 +477,7 @@ requires-dist = [
[[package]]
name = "agent-framework-purview"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/purview" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -431,7 +494,7 @@ requires-dist = [
[[package]]
name = "agent-framework-redis"
-version = "1.0.0b251028"
+version = "1.0.0b251104"
source = { editable = "packages/redis" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -661,6 +724,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 },
]
+[[package]]
+name = "annotated-doc"
+version = "0.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488 },
+]
+
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -715,14 +787,14 @@ wheels = [
[[package]]
name = "apscheduler"
-version = "3.11.0"
+version = "3.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347 }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/81/192db4f8471de5bc1f0d098783decffb1e6e69c4f8b4bc6711094691950b/apscheduler-3.11.1.tar.gz", hash = "sha256:0db77af6400c84d1747fe98a04b8b58f0080c77d11d338c4f507a9752880f221", size = 108044 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004 },
+ { url = "https://files.pythonhosted.org/packages/58/9f/d3c76f76c73fcc959d28e9def45b8b1cc3d7722660c5003b19c1022fd7f4/apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2", size = 64278 },
]
[[package]]
@@ -807,7 +879,7 @@ wheels = [
[[package]]
name = "azure-ai-projects"
-version = "2.0.0a20251103001"
+version = "2.0.0a20251105001"
source = { registry = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple" }
dependencies = [
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -815,9 +887,9 @@ dependencies = [
{ name = "isodate", 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://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251103001/azure_ai_projects-2.0.0a20251103001.tar.gz", hash = "sha256:0041b8ccf890e7b8ecd09f9690eada0cc9370a6abc8d190b039173414c6d50a2" }
+sdist = { url = "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251105001/azure_ai_projects-2.0.0a20251105001.tar.gz", hash = "sha256:045740a4f9154b2aae5ae76a074a243ea29b26086e00948539b6725249535430" }
wheels = [
- { url = "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251103001/azure_ai_projects-2.0.0a20251103001-py3-none-any.whl", hash = "sha256:9c57b6294e6c5b50fbb6b2b0cc24ca4697934d710047ead5eee66cae5a3d82f1" },
+ { url = "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251105001/azure_ai_projects-2.0.0a20251105001-py3-none-any.whl", hash = "sha256:4fd25f169137533723ea41d40d08a226f10df63e091829a8d4a30e46f968574b" },
]
[[package]]
@@ -951,7 +1023,7 @@ name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pycparser", marker = "(python_full_version < '3.13' and implementation_name != 'PyPy' and sys_platform == 'darwin') or (python_full_version < '3.13' and implementation_name != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.13' and implementation_name != 'PyPy' and sys_platform == 'win32') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' and sys_platform == 'win32')" },
+ { name = "pycparser", marker = "(implementation_name != 'PyPy' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and sys_platform == 'linux') or (implementation_name != 'PyPy' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 }
wheels = [
@@ -1613,7 +1685,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 }
wheels = [
@@ -1631,16 +1703,17 @@ wheels = [
[[package]]
name = "fastapi"
-version = "0.115.14"
+version = "0.121.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "annotated-doc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "starlette", 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/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263 }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/77a2df0946703973b9905fd0cde6172c15e0781984320123b4f5079e7113/fastapi-0.121.0.tar.gz", hash = "sha256:06663356a0b1ee93e875bbf05a31fb22314f5bed455afaaad2b2dad7f26e98fa", size = 342412 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514 },
+ { url = "https://files.pythonhosted.org/packages/dd/2c/42277afc1ba1a18f8358561eee40785d27becab8f80a1f945c0a3051c6eb/fastapi-0.121.0-py3-none-any.whl", hash = "sha256:8bdf1b15a55f4e4b0d6201033da9109ea15632cb76cf156e7b8b4019f2172106", size = 109183 },
]
[[package]]
@@ -2085,6 +2158,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425 },
]
+[[package]]
+name = "griffe"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439 },
+]
+
[[package]]
name = "grpcio"
version = "1.76.0"
@@ -2233,11 +2318,11 @@ wheels = [
[[package]]
name = "httpdbg"
-version = "2.1.1"
+version = "2.1.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5c/07/bdc4b46bf9cda06b4bdb5b0dea2e1c5408fd91387823e1cb2cfebd79fde4/httpdbg-2.1.1.tar.gz", hash = "sha256:11b268e9224fdeccc7e5436b350154c287a1af65406047b5f6438461fc45486c", size = 81226 }
+sdist = { url = "https://files.pythonhosted.org/packages/69/c0/a54d8705ae57e76679cf21dbc6dba3eb4c5cb9f99fcd9cb99e159fb12a9d/httpdbg-2.1.3.tar.gz", hash = "sha256:da32fd7cab8032927ba4717c6c9108dd4aeb0d9a42636d34a43ab11541daac26", size = 80694 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/8e/8b0e91e4c5426f503149f86df5b2a142afb11abada57cf09a5990a933407/httpdbg-2.1.1-py3-none-any.whl", hash = "sha256:ef7137752cb2c79b3084b50a9534e7d1ba587d9ad531ac0807a3563ceb7a74e0", size = 89045 },
+ { url = "https://files.pythonhosted.org/packages/33/6e/567ace955933023403e4861d161de8b559d712b559e445cc6d9a95d8e26c/httpdbg-2.1.3-py3-none-any.whl", hash = "sha256:9faa4d66f308670ddde0c6b05281066cb10b56846e6c4d3eb712123c28ea019d", size = 88173 },
]
[[package]]
@@ -2271,7 +2356,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
-version = "1.0.1"
+version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2285,9 +2370,9 @@ dependencies = [
{ name = "typer-slim", 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/f7/e0/308849e8ff9590505815f4a300cb8941a21c5889fb94c955d992539b5bef/huggingface_hub-1.0.1.tar.gz", hash = "sha256:87b506d5b45f0d1af58df7cf8bab993ded25d6077c2e959af58444df8b9589f3", size = 419291 }
+sdist = { url = "https://files.pythonhosted.org/packages/bc/31/8a7be47df17e85052f41c794b6c69f29038e7c89306778622f0a1747f151/huggingface_hub-1.1.0.tar.gz", hash = "sha256:bee5b7ca3af48275523eae577a3e42adbc242d641bbc03a4612a357e13708d43", size = 606234 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/db/fb/d71f914bc69e6357cbde04db62ef15497cd27926d95f03b4930997c4c390/huggingface_hub-1.0.1-py3-none-any.whl", hash = "sha256:7e255cd9b3432287a34a86933057abb1b341d20b97fb01c40cbd4e053764ae13", size = 503841 },
+ { url = "https://files.pythonhosted.org/packages/78/cc/794af6d8ebb24dd0a76d74e9555ee7cf2088dbbb8ad79c188536e115e626/huggingface_hub-1.1.0-py3-none-any.whl", hash = "sha256:6486bf89f98177fa3db3b0510106cece2226dd090e267506a473a75be578f36c", size = 514940 },
]
[[package]]
@@ -2642,7 +2727,7 @@ wheels = [
[[package]]
name = "langfuse"
-version = "3.8.1"
+version = "3.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2656,14 +2741,14 @@ 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/ca/0b/81f9c6a982f79c112b7f10bfd6f3a4871e6fa3e4fe8d078b6112abfd3c08/langfuse-3.8.1.tar.gz", hash = "sha256:2464ae3f8386d80e1252a0e7406e3be4121e792a74f1b1c21d9950f658e5168d", size = 197401 }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/c6/1bdb6c68ebc2b7d3875861cf99715e227bcd909a758df8af329f81f6e7af/langfuse-3.9.0.tar.gz", hash = "sha256:ed02744ab184a320dba5662be09be21441a467cc84db7e9a67c8bb6baec9fb5c", size = 201850 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b2/f9/538af0fc4219eb2484ba319483bce3383146f7a0923d5f39e464ad9a504b/langfuse-3.8.1-py3-none-any.whl", hash = "sha256:5b94b66ec0b0de388a8ea1f078b32c1666b5825b36eab863a21fdee78c53b3bb", size = 364580 },
+ { url = "https://files.pythonhosted.org/packages/66/de/66ab298aecc0b50465824e7db5df77e43f872dcd8642d3c91d11be3ac6f7/langfuse-3.9.0-py3-none-any.whl", hash = "sha256:de46c47717822de46ad4a2563be5d775ca896dc4d0955a83b4d12e1ce5e249a9", size = 369620 },
]
[[package]]
name = "litellm"
-version = "1.79.0"
+version = "1.79.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2679,9 +2764,9 @@ dependencies = [
{ name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/90/52/2853febf8ea3072d8c76e3ee22d3168e6a4f97ebd8f21905e815a381c58b/litellm-1.79.0.tar.gz", hash = "sha256:f58bb751222ee0e1ffecb2d44987999f9fa94130a6d1a478e19a3e5e8b9a7414", size = 11146414 }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/12/1c30f1019892399a488ed60ebcdfed3e2603123d9591030abc8c702ff37a/litellm-1.79.1.tar.gz", hash = "sha256:c1cf0232c01e7ad4b8442d2cdd78973ce74dfda37ad1d9f0ec3c911510e26523", size = 11216675 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/26/a5fef380af5d6a2f47cda979d88561af1e1a8efc07da2ef72c0e8cb6842c/litellm-1.79.0-py3-none-any.whl", hash = "sha256:93414b6ed55fa9e3268e8cb3100faab960c9ecd18173129ccd85471cf3db4f1a", size = 10197864 },
+ { url = "https://files.pythonhosted.org/packages/2f/e4/ac5905dfe9c0c195e59c36ea431277090dd2aa1acbcc514f781fa87a5903/litellm-1.79.1-py3-none-any.whl", hash = "sha256:738f7bf36b31514ac11cc71f65718238b57696fcf22f8b3f1e57c44daf17a569", size = 10285849 },
]
[package.optional-dependencies]
@@ -2722,11 +2807,11 @@ wheels = [
[[package]]
name = "litellm-proxy-extras"
-version = "0.2.29"
+version = "0.2.31"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/47/23/8b262f0301e02f7a70f299e68d06752934f6dd95d0a6b82ce871e5de4d81/litellm_proxy_extras-0.2.29.tar.gz", hash = "sha256:236c1cf8d9b0128392bb843ff8553918b0a9c299f2b3bfdc9ecc6b4547ce195e", size = 16500 }
+sdist = { url = "https://files.pythonhosted.org/packages/30/ce/007a87b17834c5a24e15798ae32dd156d77528b12f086f4176bb7e3f4401/litellm_proxy_extras-0.2.31.tar.gz", hash = "sha256:6d4c96dfe28fa439eaf4e8d19b73718530bc2c59cd1e4cf560388c6bce5476bb", size = 16648 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/70/c8ec18235f4bbe3c8486b2909c3d5fc23cdbd08b2c7504ae8c02ed813c83/litellm_proxy_extras-0.2.29-py3-none-any.whl", hash = "sha256:27b7efc69829ed8745de7f469110c1f6a82e4f994bd8de3ac6b16dc2806a14b0", size = 33565 },
+ { url = "https://files.pythonhosted.org/packages/e4/5f/6a0add2cac34a370da62d3bf7476035f5f10519740dfe78410256f8945b1/litellm_proxy_extras-0.2.31-py3-none-any.whl", hash = "sha256:7a66ae2810e451977fb1dfed6dac81971c6a4efbce7d57c896dce280b50ce359", size = 34130 },
]
[[package]]
@@ -2916,7 +3001,7 @@ wheels = [
[[package]]
name = "mcp"
-version = "1.19.0"
+version = "1.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2925,15 +3010,16 @@ dependencies = [
{ name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "python-multipart", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sse-starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/69/2b/916852a5668f45d8787378461eaa1244876d77575ffef024483c94c0649c/mcp-1.19.0.tar.gz", hash = "sha256:213de0d3cd63f71bc08ffe9cc8d4409cc87acffd383f6195d2ce0457c021b5c1", size = 444163 }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/22/fae38092e6c2995c03232635028510d77e7decff31b4ae79dfa0ba99c635/mcp-1.20.0.tar.gz", hash = "sha256:9ccc09eaadbfbcbbdab1c9723cfe2e0d1d9e324d7d3ce7e332ef90b09ed35177", size = 451377 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/a3/3e71a875a08b6a830b88c40bc413bff01f1650f1efe8a054b5e90a9d4f56/mcp-1.19.0-py3-none-any.whl", hash = "sha256:f5907fe1c0167255f916718f376d05f09a830a215327a3ccdd5ec8a519f2e572", size = 170105 },
+ { url = "https://files.pythonhosted.org/packages/df/00/76fc92f4892d47fecb37131d0e95ea69259f077d84c68f6793a0d96cfe80/mcp-1.20.0-py3-none-any.whl", hash = "sha256:d0dc06f93653f7432ff89f694721c87f79876b6f93741bf628ad1e48f7ac5e5d", size = 173136 },
]
[package.optional-dependencies]
@@ -2970,31 +3056,31 @@ wheels = [
[[package]]
name = "microsoft-agents-activity"
-version = "0.5.1"
+version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/87/96/4416c5b3f13309d7503f3db3c2bfc321824366b68a240ed71e8145634c3d/microsoft_agents_activity-0.5.1.tar.gz", hash = "sha256:07be29aca58ea9d8279155cfa4c00261e3a18bdf718c8164c1d87e3e57ad527b", size = 55830 }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/51/2698980f425cda122f5b755a957c3c2db604c0b9a787c6add5aa4649c237/microsoft_agents_activity-0.5.3.tar.gz", hash = "sha256:d80b055591df561df8cebda9e1712012352581a396b36459133a951982b3a760", size = 55892 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/04/47/333591538c134b5b4637ffc8ab4f5d0bf1c1b6310e3cfb5adc4002aa5940/microsoft_agents_activity-0.5.1-py3-none-any.whl", hash = "sha256:07562064125f2bc8066c2c8e9a60ff6f038f7413ccd01a9d9b0aa426e47467cd", size = 127817 },
+ { url = "https://files.pythonhosted.org/packages/75/3d/9618243e7b6f1f6295642c4e2dfca65b3a37794efbe1bdec15f0a93827d9/microsoft_agents_activity-0.5.3-py3-none-any.whl", hash = "sha256:5ae2447ac47c32f03c614694f520817cd225c9c502ec08b90d448311fb5bf3b4", size = 127861 },
]
[[package]]
name = "microsoft-agents-copilotstudio-client"
-version = "0.5.1"
+version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-agents-hosting-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/de/53/e6dde964b677358ec7c177c4aa2d408cd31acba4abe3d24c4728c2607b3d/microsoft_agents_copilotstudio_client-0.5.1.tar.gz", hash = "sha256:0b730045b4f8e8f61291279e64e0669868ace39beb63688ec38ba181020f5c3f", size = 11153 }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/22/109164fb585c4baee40d2372c5d76254ec4a28219908f11cd27ac92aa6c1/microsoft_agents_copilotstudio_client-0.5.3.tar.gz", hash = "sha256:a57ea6b3cb47dbb5ad22e59c986208ace6479e35da3f644e6346f4dfd85db57c", size = 11161 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4e/cd/50576ae8cbb2cd7fe0ebaa3ae882fc69cfb29183a5304cda29ba09084faa/microsoft_agents_copilotstudio_client-0.5.1-py3-none-any.whl", hash = "sha256:115f6aff0e44b97fd23128b7d4d53b6ed10ec54f93494c569c1cb48ac2b8a468", size = 11091 },
+ { url = "https://files.pythonhosted.org/packages/c4/65/984e139c85657ff0c8df0ed98a167c8b9434f4fd4f32862b4a6490b8c714/microsoft_agents_copilotstudio_client-0.5.3-py3-none-any.whl", hash = "sha256:6a36fce5c8c1a2df6f5142e35b12c69be80959ecff6d60cc309661018c40f00a", size = 11091 },
]
[[package]]
name = "microsoft-agents-hosting-core"
-version = "0.5.1"
+version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -3003,9 +3089,9 @@ dependencies = [
{ name = "pyjwt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f2/0b/71bc8f2fd673de9f8a0d7e9bef30dd15892d8539c4557129a5aead2c5882/microsoft_agents_hosting_core-0.5.1.tar.gz", hash = "sha256:d9b64095bf7624d4fc9f1d48cea5a3c66cc2dee9e1c3fb6ea3e9b6dfc03ace8f", size = 81277 }
+sdist = { url = "https://files.pythonhosted.org/packages/b1/98/7755c07b2ae5faf3e4dc14b17e44680a600c8b840b3003fb326d5720dea1/microsoft_agents_hosting_core-0.5.3.tar.gz", hash = "sha256:b113d4ea5c9e555bbf61037bb2a1a7a3ce7e5e4a7a0f681a3bd4719ba72ff821", size = 81672 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/84/2c/bcb8d66ebfe59cf6093c5eac1fc19a7797b5b80ce3ceaec07f2954a21493/microsoft_agents_hosting_core-0.5.1-py3-none-any.whl", hash = "sha256:10a1f394d8e444f6e2e74ab935f5c0a04ebfa43d136be4658fbaccab1321c37e", size = 120190 },
+ { url = "https://files.pythonhosted.org/packages/95/57/c9e98475971c9da9cc9ff88195bbfcfae90dba511ebe14610be79f23ab3f/microsoft_agents_hosting_core-0.5.3-py3-none-any.whl", hash = "sha256:8c228a8814dcf1a86dd60e4c7574a2e86078962695fabd693a118097e703e982", size = 120668 },
]
[[package]]
@@ -3279,11 +3365,11 @@ wheels = [
[[package]]
name = "narwhals"
-version = "2.10.0"
+version = "2.10.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/56/e5/ef07d31c2e07d99eecac8e14ace5c20aeb00ecba4ed5bb00343136380524/narwhals-2.10.0.tar.gz", hash = "sha256:1c05bbef2048a4045263de7d98c3d06140583eb13d796dd733b2157f05d24485", size = 582423 }
+sdist = { url = "https://files.pythonhosted.org/packages/c5/dc/8db74daf8c2690ec696c1d772a33cc01511559ee8a9e92d7ed85a18e3c22/narwhals-2.10.2.tar.gz", hash = "sha256:ff738a08bc993cbb792266bec15346c1d85cc68fdfe82a23283c3713f78bd354", size = 584954 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/29/13/024ae0586d901f8a6f99e2d29b4ae217e8ef11d3fd944cdfc3bbde5f2a08/narwhals-2.10.0-py3-none-any.whl", hash = "sha256:baed44e8fc38e800e3a585e3fa9843a7079a6fad5fbffbecee4348d6ac52298c", size = 418077 },
+ { url = "https://files.pythonhosted.org/packages/47/a9/9e02fa97e421a355fc5e818e9c488080fce04a8e0eebb3ed75a84f041c4a/narwhals-2.10.2-py3-none-any.whl", hash = "sha256:059cd5c6751161b97baedcaf17a514c972af6a70f36a89af17de1a0caf519c43", size = 419573 },
]
[[package]]
@@ -3471,7 +3557,7 @@ wheels = [
[[package]]
name = "openai"
-version = "1.109.1"
+version = "2.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -3483,9 +3569,42 @@ dependencies = [
{ name = "tqdm", 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/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133 }
+sdist = { url = "https://files.pythonhosted.org/packages/51/a2/f4023c1e0c868a6a5854955b3374f17153388aed95e835af114a17eac95b/openai-2.7.1.tar.gz", hash = "sha256:df4d4a3622b2df3475ead8eb0fbb3c27fd1c070fa2e55d778ca4f40e0186c726", size = 595933 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627 },
+ { url = "https://files.pythonhosted.org/packages/8c/74/6bfc3adc81f6c2cea4439f2a734c40e3a420703bbcdc539890096a732bbd/openai-2.7.1-py3-none-any.whl", hash = "sha256:2f2530354d94c59c614645a4662b9dab0a5b881c5cd767a8587398feac0c9021", size = 1008780 },
+]
+
+[[package]]
+name = "openai-agents"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "types-requests", 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/00/b3/c25c3b3084c113ffbd161c37ed7c02e0cc1296eb2f2d582d85c461f60c7a/openai_agents-0.5.0.tar.gz", hash = "sha256:776dde4025442164e3e860ff5b239b5c0ebc30f9445b0d75295c385a8ca1f696", size = 1958702 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/f5/c43a84a64aa3328c628cc19365dc514ce02abf31e31c861ad489d6d3075b/openai_agents-0.5.0-py3-none-any.whl", hash = "sha256:5ef062273815de197315ec760f571625d0f2766ceb83ab189ba6cdd9b26a10e9", size = 223272 },
+]
+
+[[package]]
+name = "openai-chatkit"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "openai-agents", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/19/9948f2996c224aff01f6ef415784042c3d710c1e950937b16d9a2c07e47e/openai_chatkit-1.1.0.tar.gz", hash = "sha256:5594341aab29b56fd3396e8d3ad1962ebdb8c44f062a8e315663ac8cf1371c6b", size = 49480 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/82/07db74ee63d54f3cadab3baaa1534bef0d3699a94d2618c76050cccb0cfe/openai_chatkit-1.1.0-py3-none-any.whl", hash = "sha256:e78f021899fbef1323f3adc3a686f9fe5ee184cd997799a917e9013833e760ba", size = 35424 },
]
[[package]]
@@ -3931,15 +4050,15 @@ wheels = [
[[package]]
name = "plotly"
-version = "6.3.1"
+version = "6.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "narwhals", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0c/63/961d47c9ffd592a575495891cdcf7875dc0903ebb33ac238935714213789/plotly-6.3.1.tar.gz", hash = "sha256:dd896e3d940e653a7ce0470087e82c2bd903969a55e30d1b01bb389319461bb0", size = 6956460 }
+sdist = { url = "https://files.pythonhosted.org/packages/06/e6/b768650072837505804bed4790c5449ba348a3b720e27ca7605414e998cd/plotly-6.4.0.tar.gz", hash = "sha256:68c6db2ed2180289ef978f087841148b7efda687552276da15a6e9b92107052a", size = 7012379 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/93/023955c26b0ce614342d11cc0652f1e45e32393b6ab9d11a664a60e9b7b7/plotly-6.3.1-py3-none-any.whl", hash = "sha256:8b4420d1dcf2b040f5983eed433f95732ed24930e496d36eb70d211923532e64", size = 9833698 },
+ { url = "https://files.pythonhosted.org/packages/78/ae/89b45ccccfeebc464c9233de5675990f75241b8ee4cd63227800fdf577d1/plotly-6.4.0-py3-none-any.whl", hash = "sha256:a1062eafbdc657976c2eedd276c90e184ccd6c21282a5e9ee8f20efca9c9a4c5", size = 9892458 },
]
[[package]]
@@ -4014,7 +4133,7 @@ wheels = [
[[package]]
name = "posthog"
-version = "6.7.11"
+version = "6.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -4024,9 +4143,9 @@ dependencies = [
{ name = "six", 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/cb/32/3668d5e0f8b852fad81770743ee17893854fd8e5f7cea897a0a9199b0370/posthog-6.7.11.tar.gz", hash = "sha256:62db3e97cbd95351fe081c1ea8805393293de6fabad6d2e9024bf940aca4ddbf", size = 120407 }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/26/fbd8a29d094c1b3df109b79f7165ddb20dc37ec1e5b55717585de9ee9b65/posthog-6.8.0.tar.gz", hash = "sha256:40bc3bffe4818d37de63a4f4f13d2e90a78efe14f0d808c962f0ffebc3b15256", size = 122781 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/00/bf284e0aae5dec7c217c176f291867cfac2f7bfd5692c9ce041e80986fa7/posthog-6.7.11-py3-none-any.whl", hash = "sha256:31421a88437cef2ce20f60c14ee8d298b2e765a6de0617cb95d1fcef54170749", size = 138713 },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/970fe48b888c53de5768f67524444c2adf2ea86fba97a672434deb8db971/posthog-6.8.0-py3-none-any.whl", hash = "sha256:b30b3cb06234d9177cecabe6f3e04e34e1e15fe7b60428771a67be57920a6308", size = 141210 },
]
[[package]]
@@ -4302,7 +4421,7 @@ wheels = [
[[package]]
name = "pydantic"
-version = "2.12.3"
+version = "2.12.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -4310,9 +4429,9 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383 }
+sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431 },
+ { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400 },
]
[package.optional-dependencies]
@@ -4334,108 +4453,112 @@ wheels = [
[[package]]
name = "pydantic-core"
-version = "2.41.4"
+version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557 }
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197 },
- { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909 },
- { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905 },
- { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938 },
- { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710 },
- { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445 },
- { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875 },
- { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329 },
- { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658 },
- { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777 },
- { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705 },
- { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464 },
- { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497 },
- { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062 },
- { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301 },
- { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728 },
- { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238 },
- { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424 },
- { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047 },
- { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163 },
- { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585 },
- { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109 },
- { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078 },
- { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737 },
- { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160 },
- { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883 },
- { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026 },
- { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043 },
- { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699 },
- { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121 },
- { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590 },
- { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869 },
- { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169 },
- { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165 },
- { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067 },
- { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997 },
- { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187 },
- { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204 },
- { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536 },
- { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132 },
- { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483 },
- { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688 },
- { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807 },
- { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669 },
- { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629 },
- { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049 },
- { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409 },
- { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635 },
- { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284 },
- { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566 },
- { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809 },
- { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119 },
- { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398 },
- { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735 },
- { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209 },
- { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324 },
- { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515 },
- { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819 },
- { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866 },
- { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034 },
- { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022 },
- { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495 },
- { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131 },
- { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236 },
- { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573 },
- { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467 },
- { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754 },
- { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754 },
- { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115 },
- { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400 },
- { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070 },
- { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277 },
- { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608 },
- { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614 },
- { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904 },
- { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538 },
- { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183 },
- { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542 },
- { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897 },
- { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739 },
- { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549 },
- { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093 },
- { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971 },
- { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939 },
- { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400 },
- { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840 },
- { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135 },
- { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721 },
- { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608 },
- { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986 },
- { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516 },
- { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146 },
- { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296 },
- { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386 },
- { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775 },
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298 },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475 },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815 },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567 },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442 },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956 },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253 },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050 },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178 },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833 },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156 },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378 },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622 },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351 },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363 },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615 },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369 },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218 },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951 },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428 },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009 },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 },
]
[[package]]
@@ -4836,109 +4959,109 @@ wheels = [
[[package]]
name = "regex"
-version = "2025.10.23"
+version = "2025.11.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f8/c8/1d2160d36b11fbe0a61acb7c3c81ab032d9ec8ad888ac9e0a61b85ab99dd/regex-2025.10.23.tar.gz", hash = "sha256:8cbaf8ceb88f96ae2356d01b9adf5e6306fa42fa6f7eab6b97794e37c959ac26", size = 401266 }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/88/11/849d5d23633a77047465eaae4cc0cbf24ded7aa496c02e8b9710e28b1687/regex-2025.10.23-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:17bbcde374bef1c5fad9b131f0e28a6a24856dd90368d8c0201e2b5a69533daa", size = 487957 },
- { url = "https://files.pythonhosted.org/packages/87/12/5985386e7e3200a0d6a6417026d2c758d783a932428a5efc0a42ca1ddf74/regex-2025.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4e10434279cc8567f99ca6e018e9025d14f2fded2a603380b6be2090f476426", size = 290419 },
- { url = "https://files.pythonhosted.org/packages/67/cf/a8615923f962f8fdc41a3a6093a48726955e8b1993f4614b26a41d249f9b/regex-2025.10.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c9bb421cbe7012c744a5a56cf4d6c80829c72edb1a2991677299c988d6339c8", size = 288285 },
- { url = "https://files.pythonhosted.org/packages/4e/3d/6a3a1e12c86354cd0b3cbf8c3dd6acbe853609ee3b39d47ecd3ce95caf84/regex-2025.10.23-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:275cd1c2ed8c4a78ebfa489618d7aee762e8b4732da73573c3e38236ec5f65de", size = 781458 },
- { url = "https://files.pythonhosted.org/packages/46/47/76a8da004489f2700361754859e373b87a53d043de8c47f4d1583fd39d78/regex-2025.10.23-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b426ae7952f3dc1e73a86056d520bd4e5f021397484a6835902fc5648bcacce", size = 850605 },
- { url = "https://files.pythonhosted.org/packages/67/05/fa886461f97d45a6f4b209699cb994dc6d6212d6e219d29444dac5005775/regex-2025.10.23-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5cdaf5b6d37c7da1967dbe729d819461aab6a98a072feef65bbcff0a6e60649", size = 898563 },
- { url = "https://files.pythonhosted.org/packages/2d/db/3ddd8d01455f23cabad7499f4199de0df92f5e96d39633203ff9d0b592dc/regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bfeff0b08f296ab28b4332a7e03ca31c437ee78b541ebc874bbf540e5932f8d", size = 791535 },
- { url = "https://files.pythonhosted.org/packages/7c/ae/0fa5cbf41ca92b6ec3370222fcb6c68b240d68ab10e803d086c03a19fd9e/regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f97236a67307b775f30a74ef722b64b38b7ab7ba3bb4a2508518a5de545459c", size = 782461 },
- { url = "https://files.pythonhosted.org/packages/d4/23/70af22a016df11af4def27870eb175c2c7235b72d411ecf75a4b4a422cb6/regex-2025.10.23-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be19e7de499940cd72475fb8e46ab2ecb1cf5906bebdd18a89f9329afb1df82f", size = 774583 },
- { url = "https://files.pythonhosted.org/packages/7a/ee/a54a6851f6905f33d3c4ed64e8737b1d85ed01b5724712530ddc0f9abdb1/regex-2025.10.23-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:883df76ee42d9ecb82b37ff8d01caea5895b3f49630a64d21111078bbf8ef64c", size = 845649 },
- { url = "https://files.pythonhosted.org/packages/80/7d/c3ec1cae14e01fab00e38c41ed35f47a853359e95e9c023e9a4381bb122c/regex-2025.10.23-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2e9117d1d35fc2addae6281019ecc70dc21c30014b0004f657558b91c6a8f1a7", size = 836037 },
- { url = "https://files.pythonhosted.org/packages/15/ae/45771140dd43c4d67c87b54d3728078ed6a96599d9fc7ba6825086236782/regex-2025.10.23-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ff1307f531a5d8cf5c20ea517254551ff0a8dc722193aab66c656c5a900ea68", size = 779705 },
- { url = "https://files.pythonhosted.org/packages/b8/95/074e2581760eafce7c816a352b7d3a322536e5b68c346d1a8bacd895545c/regex-2025.10.23-cp310-cp310-win32.whl", hash = "sha256:7888475787cbfee4a7cd32998eeffe9a28129fa44ae0f691b96cb3939183ef41", size = 265663 },
- { url = "https://files.pythonhosted.org/packages/f7/c7/a25f56a718847e34d3f1608c72eadeb67653bff1a0411da023dd8f4c647b/regex-2025.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ec41a905908496ce4906dab20fb103c814558db1d69afc12c2f384549c17936a", size = 277587 },
- { url = "https://files.pythonhosted.org/packages/d3/e5/63eb17c6b5deaefd93c2bbb1feae7c0a8d2157da25883a6ca2569cf7a663/regex-2025.10.23-cp310-cp310-win_arm64.whl", hash = "sha256:b2b7f19a764d5e966d5a62bf2c28a8b4093cc864c6734510bdb4aeb840aec5e6", size = 269979 },
- { url = "https://files.pythonhosted.org/packages/82/e5/74b7cd5cd76b4171f9793042045bb1726f7856dd56e582fc3e058a7a8a5e/regex-2025.10.23-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c531155bf9179345e85032052a1e5fe1a696a6abf9cea54b97e8baefff970fd", size = 487960 },
- { url = "https://files.pythonhosted.org/packages/b9/08/854fa4b3b20471d1df1c71e831b6a1aa480281e37791e52a2df9641ec5c6/regex-2025.10.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:912e9df4e89d383681268d38ad8f5780d7cccd94ba0e9aa09ca7ab7ab4f8e7eb", size = 290425 },
- { url = "https://files.pythonhosted.org/packages/ab/d3/6272b1dd3ca1271661e168762b234ad3e00dbdf4ef0c7b9b72d2d159efa7/regex-2025.10.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f375c61bfc3138b13e762fe0ae76e3bdca92497816936534a0177201666f44f", size = 288278 },
- { url = "https://files.pythonhosted.org/packages/14/8f/c7b365dd9d9bc0a36e018cb96f2ffb60d2ba8deb589a712b437f67de2920/regex-2025.10.23-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e248cc9446081119128ed002a3801f8031e0c219b5d3c64d3cc627da29ac0a33", size = 793289 },
- { url = "https://files.pythonhosted.org/packages/d4/fb/b8fbe9aa16cf0c21f45ec5a6c74b4cecbf1a1c0deb7089d4a6f83a9c1caa/regex-2025.10.23-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b52bf9282fdf401e4f4e721f0f61fc4b159b1307244517789702407dd74e38ca", size = 860321 },
- { url = "https://files.pythonhosted.org/packages/b0/81/bf41405c772324926a9bd8a640dedaa42da0e929241834dfce0733070437/regex-2025.10.23-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c084889ab2c59765a0d5ac602fd1c3c244f9b3fcc9a65fdc7ba6b74c5287490", size = 907011 },
- { url = "https://files.pythonhosted.org/packages/a4/fb/5ad6a8b92d3f88f3797b51bb4ef47499acc2d0b53d2fbe4487a892f37a73/regex-2025.10.23-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80e8eb79009bdb0936658c44ca06e2fbbca67792013e3818eea3f5f228971c2", size = 800312 },
- { url = "https://files.pythonhosted.org/packages/42/48/b4efba0168a2b57f944205d823f8e8a3a1ae6211a34508f014ec2c712f4f/regex-2025.10.23-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6f259118ba87b814a8ec475380aee5f5ae97a75852a3507cf31d055b01b5b40", size = 782839 },
- { url = "https://files.pythonhosted.org/packages/13/2a/c9efb4c6c535b0559c1fa8e431e0574d229707c9ca718600366fcfef6801/regex-2025.10.23-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9b8c72a242683dcc72d37595c4f1278dfd7642b769e46700a8df11eab19dfd82", size = 854270 },
- { url = "https://files.pythonhosted.org/packages/34/2d/68eecc1bdaee020e8ba549502291c9450d90d8590d0552247c9b543ebf7b/regex-2025.10.23-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d7b7a0a3df9952f9965342159e0c1f05384c0f056a47ce8b61034f8cecbe83", size = 845771 },
- { url = "https://files.pythonhosted.org/packages/a5/cd/a1ae499cf9b87afb47a67316bbf1037a7c681ffe447c510ed98c0aa2c01c/regex-2025.10.23-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:413bfea20a484c524858125e92b9ce6ffdd0a4b97d4ff96b5859aa119b0f1bdd", size = 788778 },
- { url = "https://files.pythonhosted.org/packages/38/f9/70765e63f5ea7d43b2b6cd4ee9d3323f16267e530fb2a420d92d991cf0fc/regex-2025.10.23-cp311-cp311-win32.whl", hash = "sha256:f76deef1f1019a17dad98f408b8f7afc4bd007cbe835ae77b737e8c7f19ae575", size = 265666 },
- { url = "https://files.pythonhosted.org/packages/9c/1a/18e9476ee1b63aaec3844d8e1cb21842dc19272c7e86d879bfc0dcc60db3/regex-2025.10.23-cp311-cp311-win_amd64.whl", hash = "sha256:59bba9f7125536f23fdab5deeea08da0c287a64c1d3acc1c7e99515809824de8", size = 277600 },
- { url = "https://files.pythonhosted.org/packages/1d/1b/c019167b1f7a8ec77251457e3ff0339ed74ca8bce1ea13138dc98309c923/regex-2025.10.23-cp311-cp311-win_arm64.whl", hash = "sha256:b103a752b6f1632ca420225718d6ed83f6a6ced3016dd0a4ab9a6825312de566", size = 269974 },
- { url = "https://files.pythonhosted.org/packages/f6/57/eeb274d83ab189d02d778851b1ac478477522a92b52edfa6e2ae9ff84679/regex-2025.10.23-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7a44d9c00f7a0a02d3b777429281376370f3d13d2c75ae74eb94e11ebcf4a7fc", size = 489187 },
- { url = "https://files.pythonhosted.org/packages/55/5c/7dad43a9b6ea88bf77e0b8b7729a4c36978e1043165034212fd2702880c6/regex-2025.10.23-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b83601f84fde939ae3478bb32a3aef36f61b58c3208d825c7e8ce1a735f143f2", size = 291122 },
- { url = "https://files.pythonhosted.org/packages/66/21/38b71e6f2818f0f4b281c8fba8d9d57cfca7b032a648fa59696e0a54376a/regex-2025.10.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec13647907bb9d15fd192bbfe89ff06612e098a5709e7d6ecabbdd8f7908fc45", size = 288797 },
- { url = "https://files.pythonhosted.org/packages/be/95/888f069c89e7729732a6d7cca37f76b44bfb53a1e35dda8a2c7b65c1b992/regex-2025.10.23-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78d76dd2957d62501084e7012ddafc5fcd406dd982b7a9ca1ea76e8eaaf73e7e", size = 798442 },
- { url = "https://files.pythonhosted.org/packages/76/70/4f903c608faf786627a8ee17c06e0067b5acade473678b69c8094b248705/regex-2025.10.23-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8668e5f067e31a47699ebb354f43aeb9c0ef136f915bd864243098524482ac43", size = 864039 },
- { url = "https://files.pythonhosted.org/packages/62/19/2df67b526bf25756c7f447dde554fc10a220fd839cc642f50857d01e4a7b/regex-2025.10.23-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a32433fe3deb4b2d8eda88790d2808fed0dc097e84f5e683b4cd4f42edef6cca", size = 912057 },
- { url = "https://files.pythonhosted.org/packages/99/14/9a39b7c9e007968411bc3c843cc14cf15437510c0a9991f080cab654fd16/regex-2025.10.23-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d97d73818c642c938db14c0668167f8d39520ca9d983604575ade3fda193afcc", size = 803374 },
- { url = "https://files.pythonhosted.org/packages/d4/f7/3495151dd3ca79949599b6d069b72a61a2c5e24fc441dccc79dcaf708fe6/regex-2025.10.23-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bca7feecc72ee33579e9f6ddf8babbe473045717a0e7dbc347099530f96e8b9a", size = 787714 },
- { url = "https://files.pythonhosted.org/packages/28/65/ee882455e051131869957ee8597faea45188c9a98c0dad724cfb302d4580/regex-2025.10.23-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7e24af51e907d7457cc4a72691ec458320b9ae67dc492f63209f01eecb09de32", size = 858392 },
- { url = "https://files.pythonhosted.org/packages/53/25/9287fef5be97529ebd3ac79d256159cb709a07eb58d4be780d1ca3885da8/regex-2025.10.23-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d10bcde58bbdf18146f3a69ec46dd03233b94a4a5632af97aa5378da3a47d288", size = 850484 },
- { url = "https://files.pythonhosted.org/packages/f3/b4/b49b88b4fea2f14dc73e5b5842755e782fc2e52f74423d6f4adc130d5880/regex-2025.10.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:44383bc0c933388516c2692c9a7503e1f4a67e982f20b9a29d2fb70c6494f147", size = 789634 },
- { url = "https://files.pythonhosted.org/packages/b6/3c/2f8d199d0e84e78bcd6bdc2be9b62410624f6b796e2893d1837ae738b160/regex-2025.10.23-cp312-cp312-win32.whl", hash = "sha256:6040a86f95438a0114bba16e51dfe27f1bc004fd29fe725f54a586f6d522b079", size = 266060 },
- { url = "https://files.pythonhosted.org/packages/d7/67/c35e80969f6ded306ad70b0698863310bdf36aca57ad792f45ddc0e2271f/regex-2025.10.23-cp312-cp312-win_amd64.whl", hash = "sha256:436b4c4352fe0762e3bfa34a5567079baa2ef22aa9c37cf4d128979ccfcad842", size = 276931 },
- { url = "https://files.pythonhosted.org/packages/f5/a1/4ed147de7d2b60174f758412c87fa51ada15cd3296a0ff047f4280aaa7ca/regex-2025.10.23-cp312-cp312-win_arm64.whl", hash = "sha256:f4b1b1991617055b46aff6f6db24888c1f05f4db9801349d23f09ed0714a9335", size = 270103 },
- { url = "https://files.pythonhosted.org/packages/28/c6/195a6217a43719d5a6a12cc192a22d12c40290cecfa577f00f4fb822f07d/regex-2025.10.23-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7690f95404a1293923a296981fd943cca12c31a41af9c21ba3edd06398fc193", size = 488956 },
- { url = "https://files.pythonhosted.org/packages/4c/93/181070cd1aa2fa541ff2d3afcf763ceecd4937b34c615fa92765020a6c90/regex-2025.10.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a32d77aeaea58a13230100dd8797ac1a84c457f3af2fdf0d81ea689d5a9105b", size = 290997 },
- { url = "https://files.pythonhosted.org/packages/b6/c5/9d37fbe3a40ed8dda78c23e1263002497540c0d1522ed75482ef6c2000f0/regex-2025.10.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b24b29402f264f70a3c81f45974323b41764ff7159655360543b7cabb73e7d2f", size = 288686 },
- { url = "https://files.pythonhosted.org/packages/5f/e7/db610ff9f10c2921f9b6ac0c8d8be4681b28ddd40fc0549429366967e61f/regex-2025.10.23-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563824a08c7c03d96856d84b46fdb3bbb7cfbdf79da7ef68725cda2ce169c72a", size = 798466 },
- { url = "https://files.pythonhosted.org/packages/90/10/aab883e1fa7fe2feb15ac663026e70ca0ae1411efa0c7a4a0342d9545015/regex-2025.10.23-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0ec8bdd88d2e2659c3518087ee34b37e20bd169419ffead4240a7004e8ed03b", size = 863996 },
- { url = "https://files.pythonhosted.org/packages/a2/b0/8f686dd97a51f3b37d0238cd00a6d0f9ccabe701f05b56de1918571d0d61/regex-2025.10.23-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b577601bfe1d33913fcd9276d7607bbac827c4798d9e14d04bf37d417a6c41cb", size = 912145 },
- { url = "https://files.pythonhosted.org/packages/a3/ca/639f8cd5b08797bca38fc5e7e07f76641a428cf8c7fca05894caf045aa32/regex-2025.10.23-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9f2c68ac6cb3de94eea08a437a75eaa2bd33f9e97c84836ca0b610a5804368", size = 803370 },
- { url = "https://files.pythonhosted.org/packages/0d/1e/a40725bb76959eddf8abc42a967bed6f4851b39f5ac4f20e9794d7832aa5/regex-2025.10.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89f8b9ea3830c79468e26b0e21c3585f69f105157c2154a36f6b7839f8afb351", size = 787767 },
- { url = "https://files.pythonhosted.org/packages/3d/d8/8ee9858062936b0f99656dce390aa667c6e7fb0c357b1b9bf76fb5e2e708/regex-2025.10.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:98fd84c4e4ea185b3bb5bf065261ab45867d8875032f358a435647285c722673", size = 858335 },
- { url = "https://files.pythonhosted.org/packages/d8/0a/ed5faaa63fa8e3064ab670e08061fbf09e3a10235b19630cf0cbb9e48c0a/regex-2025.10.23-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1e11d3e5887b8b096f96b4154dfb902f29c723a9556639586cd140e77e28b313", size = 850402 },
- { url = "https://files.pythonhosted.org/packages/79/14/d05f617342f4b2b4a23561da500ca2beab062bfcc408d60680e77ecaf04d/regex-2025.10.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f13450328a6634348d47a88367e06b64c9d84980ef6a748f717b13f8ce64e87", size = 789739 },
- { url = "https://files.pythonhosted.org/packages/f9/7b/e8ce8eef42a15f2c3461f8b3e6e924bbc86e9605cb534a393aadc8d3aff8/regex-2025.10.23-cp313-cp313-win32.whl", hash = "sha256:37be9296598a30c6a20236248cb8b2c07ffd54d095b75d3a2a2ee5babdc51df1", size = 266054 },
- { url = "https://files.pythonhosted.org/packages/71/2d/55184ed6be6473187868d2f2e6a0708195fc58270e62a22cbf26028f2570/regex-2025.10.23-cp313-cp313-win_amd64.whl", hash = "sha256:ea7a3c283ce0f06fe789365841e9174ba05f8db16e2fd6ae00a02df9572c04c0", size = 276917 },
- { url = "https://files.pythonhosted.org/packages/9c/d4/927eced0e2bd45c45839e556f987f8c8f8683268dd3c00ad327deb3b0172/regex-2025.10.23-cp313-cp313-win_arm64.whl", hash = "sha256:d9a4953575f300a7bab71afa4cd4ac061c7697c89590a2902b536783eeb49a4f", size = 270105 },
- { url = "https://files.pythonhosted.org/packages/3e/b3/95b310605285573341fc062d1d30b19a54f857530e86c805f942c4ff7941/regex-2025.10.23-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7d6606524fa77b3912c9ef52a42ef63c6cfbfc1077e9dc6296cd5da0da286044", size = 491850 },
- { url = "https://files.pythonhosted.org/packages/a4/8f/207c2cec01e34e56db1eff606eef46644a60cf1739ecd474627db90ad90b/regex-2025.10.23-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c037aadf4d64bdc38af7db3dbd34877a057ce6524eefcb2914d6d41c56f968cc", size = 292537 },
- { url = "https://files.pythonhosted.org/packages/98/3b/025240af4ada1dc0b5f10d73f3e5122d04ce7f8908ab8881e5d82b9d61b6/regex-2025.10.23-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:99018c331fb2529084a0c9b4c713dfa49fafb47c7712422e49467c13a636c656", size = 290904 },
- { url = "https://files.pythonhosted.org/packages/81/8e/104ac14e2d3450c43db18ec03e1b96b445a94ae510b60138f00ce2cb7ca1/regex-2025.10.23-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8aba965604d70306eb90a35528f776e59112a7114a5162824d43b76fa27f58", size = 807311 },
- { url = "https://files.pythonhosted.org/packages/19/63/78aef90141b7ce0be8a18e1782f764f6997ad09de0e05251f0d2503a914a/regex-2025.10.23-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:238e67264b4013e74136c49f883734f68656adf8257bfa13b515626b31b20f8e", size = 873241 },
- { url = "https://files.pythonhosted.org/packages/b3/a8/80eb1201bb49ae4dba68a1b284b4211ed9daa8e74dc600018a10a90399fb/regex-2025.10.23-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2eb48bd9848d66fd04826382f5e8491ae633de3233a3d64d58ceb4ecfa2113a", size = 914794 },
- { url = "https://files.pythonhosted.org/packages/f0/d5/1984b6ee93281f360a119a5ca1af6a8ca7d8417861671388bf750becc29b/regex-2025.10.23-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36591ce06d047d0c0fe2fc5f14bfbd5b4525d08a7b6a279379085e13f0e3d0e", size = 812581 },
- { url = "https://files.pythonhosted.org/packages/c4/39/11ebdc6d9927172a64ae237d16763145db6bd45ebb4055c17b88edab72a7/regex-2025.10.23-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5d4ece8628d6e364302006366cea3ee887db397faebacc5dacf8ef19e064cf8", size = 795346 },
- { url = "https://files.pythonhosted.org/packages/3b/b4/89a591bcc08b5e436af43315284bd233ba77daf0cf20e098d7af12f006c1/regex-2025.10.23-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:39a7e8083959cb1c4ff74e483eecb5a65d3b3e1d821b256e54baf61782c906c6", size = 868214 },
- { url = "https://files.pythonhosted.org/packages/3d/ff/58ba98409c1dbc8316cdb20dafbc63ed267380a07780cafecaf5012dabc9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:842d449a8fefe546f311656cf8c0d6729b08c09a185f1cad94c756210286d6a8", size = 854540 },
- { url = "https://files.pythonhosted.org/packages/9a/f2/4a9e9338d67626e2071b643f828a482712ad15889d7268e11e9a63d6f7e9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d614986dc68506be8f00474f4f6960e03e4ca9883f7df47744800e7d7c08a494", size = 799346 },
- { url = "https://files.pythonhosted.org/packages/63/be/543d35c46bebf6f7bf2be538cca74d6585f25714700c36f37f01b92df551/regex-2025.10.23-cp313-cp313t-win32.whl", hash = "sha256:a5b7a26b51a9df473ec16a1934d117443a775ceb7b39b78670b2e21893c330c9", size = 268657 },
- { url = "https://files.pythonhosted.org/packages/14/9f/4dd6b7b612037158bb2c9bcaa710e6fb3c40ad54af441b9c53b3a137a9f1/regex-2025.10.23-cp313-cp313t-win_amd64.whl", hash = "sha256:ce81c5544a5453f61cb6f548ed358cfb111e3b23f3cd42d250a4077a6be2a7b6", size = 280075 },
- { url = "https://files.pythonhosted.org/packages/81/7a/5bd0672aa65d38c8da6747c17c8b441bdb53d816c569e3261013af8e83cf/regex-2025.10.23-cp313-cp313t-win_arm64.whl", hash = "sha256:e9bf7f6699f490e4e43c44757aa179dab24d1960999c84ab5c3d5377714ed473", size = 271219 },
- { url = "https://files.pythonhosted.org/packages/73/f6/0caf29fec943f201fbc8822879c99d31e59c1d51a983d9843ee5cf398539/regex-2025.10.23-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5b5cb5b6344c4c4c24b2dc87b0bfee78202b07ef7633385df70da7fcf6f7cec6", size = 488960 },
- { url = "https://files.pythonhosted.org/packages/8e/7d/ebb7085b8fa31c24ce0355107cea2b92229d9050552a01c5d291c42aecea/regex-2025.10.23-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a6ce7973384c37bdf0f371a843f95a6e6f4e1489e10e0cf57330198df72959c5", size = 290932 },
- { url = "https://files.pythonhosted.org/packages/27/41/43906867287cbb5ca4cee671c3cc8081e15deef86a8189c3aad9ac9f6b4d/regex-2025.10.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2ee3663f2c334959016b56e3bd0dd187cbc73f948e3a3af14c3caaa0c3035d10", size = 288766 },
- { url = "https://files.pythonhosted.org/packages/ab/9e/ea66132776700fc77a39b1056e7a5f1308032fead94507e208dc6716b7cd/regex-2025.10.23-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2003cc82a579107e70d013482acce8ba773293f2db534fb532738395c557ff34", size = 798884 },
- { url = "https://files.pythonhosted.org/packages/d5/99/aed1453687ab63819a443930770db972c5c8064421f0d9f5da9ad029f26b/regex-2025.10.23-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:182c452279365a93a9f45874f7f191ec1c51e1f1eb41bf2b16563f1a40c1da3a", size = 864768 },
- { url = "https://files.pythonhosted.org/packages/99/5d/732fe747a1304805eb3853ce6337eea16b169f7105a0d0dd9c6a5ffa9948/regex-2025.10.23-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b1249e9ff581c5b658c8f0437f883b01f1edcf424a16388591e7c05e5e9e8b0c", size = 911394 },
- { url = "https://files.pythonhosted.org/packages/5e/48/58a1f6623466522352a6efa153b9a3714fc559d9f930e9bc947b4a88a2c3/regex-2025.10.23-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b841698f93db3ccc36caa1900d2a3be281d9539b822dc012f08fc80b46a3224", size = 803145 },
- { url = "https://files.pythonhosted.org/packages/ea/f6/7dea79be2681a5574ab3fc237aa53b2c1dfd6bd2b44d4640b6c76f33f4c1/regex-2025.10.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:956d89e0c92d471e8f7eee73f73fdff5ed345886378c45a43175a77538a1ffe4", size = 787831 },
- { url = "https://files.pythonhosted.org/packages/3a/ad/07b76950fbbe65f88120ca2d8d845047c401450f607c99ed38862904671d/regex-2025.10.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5c259cb363299a0d90d63b5c0d7568ee98419861618a95ee9d91a41cb9954462", size = 859162 },
- { url = "https://files.pythonhosted.org/packages/41/87/374f3b2021b22aa6a4fc0b750d63f9721e53d1631a238f7a1c343c1cd288/regex-2025.10.23-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:185d2b18c062820b3a40d8fefa223a83f10b20a674bf6e8c4a432e8dfd844627", size = 849899 },
- { url = "https://files.pythonhosted.org/packages/12/4a/7f7bb17c5a5a9747249807210e348450dab9212a46ae6d23ebce86ba6a2b/regex-2025.10.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:281d87fa790049c2b7c1b4253121edd80b392b19b5a3d28dc2a77579cb2a58ec", size = 789372 },
- { url = "https://files.pythonhosted.org/packages/c9/dd/9c7728ff544fea09bbc8635e4c9e7c423b11c24f1a7a14e6ac4831466709/regex-2025.10.23-cp314-cp314-win32.whl", hash = "sha256:63b81eef3656072e4ca87c58084c7a9c2b81d41a300b157be635a8a675aacfb8", size = 271451 },
- { url = "https://files.pythonhosted.org/packages/48/f8/ef7837ff858eb74079c4804c10b0403c0b740762e6eedba41062225f7117/regex-2025.10.23-cp314-cp314-win_amd64.whl", hash = "sha256:0967c5b86f274800a34a4ed862dfab56928144d03cb18821c5153f8777947796", size = 280173 },
- { url = "https://files.pythonhosted.org/packages/8e/d0/d576e1dbd9885bfcd83d0e90762beea48d9373a6f7ed39170f44ed22e336/regex-2025.10.23-cp314-cp314-win_arm64.whl", hash = "sha256:c70dfe58b0a00b36aa04cdb0f798bf3e0adc31747641f69e191109fd8572c9a9", size = 273206 },
- { url = "https://files.pythonhosted.org/packages/a6/d0/2025268315e8b2b7b660039824cb7765a41623e97d4cd421510925400487/regex-2025.10.23-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1f5799ea1787aa6de6c150377d11afad39a38afd033f0c5247aecb997978c422", size = 491854 },
- { url = "https://files.pythonhosted.org/packages/44/35/5681c2fec5e8b33454390af209c4353dfc44606bf06d714b0b8bd0454ffe/regex-2025.10.23-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a9639ab7540cfea45ef57d16dcbea2e22de351998d614c3ad2f9778fa3bdd788", size = 292542 },
- { url = "https://files.pythonhosted.org/packages/5d/17/184eed05543b724132e4a18149e900f5189001fcfe2d64edaae4fbaf36b4/regex-2025.10.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08f52122c352eb44c3421dab78b9b73a8a77a282cc8314ae576fcaa92b780d10", size = 290903 },
- { url = "https://files.pythonhosted.org/packages/25/d0/5e3347aa0db0de382dddfa133a7b0ae72f24b4344f3989398980b44a3924/regex-2025.10.23-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebf1baebef1c4088ad5a5623decec6b52950f0e4d7a0ae4d48f0a99f8c9cb7d7", size = 807546 },
- { url = "https://files.pythonhosted.org/packages/d2/bb/40c589bbdce1be0c55e9f8159789d58d47a22014f2f820cf2b517a5cd193/regex-2025.10.23-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:16b0f1c2e2d566c562d5c384c2b492646be0a19798532fdc1fdedacc66e3223f", size = 873322 },
- { url = "https://files.pythonhosted.org/packages/fe/56/a7e40c01575ac93360e606278d359f91829781a9f7fb6e5aa435039edbda/regex-2025.10.23-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7ada5d9dceafaab92646aa00c10a9efd9b09942dd9b0d7c5a4b73db92cc7e61", size = 914855 },
- { url = "https://files.pythonhosted.org/packages/5c/4b/d55587b192763db3163c3f508b3b67b31bb6f5e7a0e08b83013d0a59500a/regex-2025.10.23-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a36b4005770044bf08edecc798f0e41a75795b9e7c9c12fe29da8d792ef870c", size = 812724 },
- { url = "https://files.pythonhosted.org/packages/33/20/18bac334955fbe99d17229f4f8e98d05e4a501ac03a442be8facbb37c304/regex-2025.10.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:af7b2661dcc032da1fae82069b5ebf2ac1dfcd5359ef8b35e1367bfc92181432", size = 795439 },
- { url = "https://files.pythonhosted.org/packages/67/46/c57266be9df8549c7d85deb4cb82280cb0019e46fff677534c5fa1badfa4/regex-2025.10.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb976810ac1416a67562c2e5ba0accf6f928932320fef302e08100ed681b38e", size = 868336 },
- { url = "https://files.pythonhosted.org/packages/b8/f3/bd5879e41ef8187fec5e678e94b526a93f99e7bbe0437b0f2b47f9101694/regex-2025.10.23-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1a56a54be3897d62f54290190fbcd754bff6932934529fbf5b29933da28fcd43", size = 854567 },
- { url = "https://files.pythonhosted.org/packages/e6/57/2b6bbdbd2f24dfed5b028033aa17ad8f7d86bb28f1a892cac8b3bc89d059/regex-2025.10.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f3e6d202fb52c2153f532043bbcf618fd177df47b0b306741eb9b60ba96edc3", size = 799565 },
- { url = "https://files.pythonhosted.org/packages/c7/ba/a6168f542ba73b151ed81237adf6b869c7b2f7f8d51618111296674e20ee/regex-2025.10.23-cp314-cp314t-win32.whl", hash = "sha256:1fa1186966b2621b1769fd467c7b22e317e6ba2d2cdcecc42ea3089ef04a8521", size = 274428 },
- { url = "https://files.pythonhosted.org/packages/ef/a0/c84475e14a2829e9b0864ebf77c3f7da909df9d8acfe2bb540ff0072047c/regex-2025.10.23-cp314-cp314t-win_amd64.whl", hash = "sha256:08a15d40ce28362eac3e78e83d75475147869c1ff86bc93285f43b4f4431a741", size = 284140 },
- { url = "https://files.pythonhosted.org/packages/51/33/6a08ade0eee5b8ba79386869fa6f77afeb835b60510f3525db987e2fffc4/regex-2025.10.23-cp314-cp314t-win_arm64.whl", hash = "sha256:a93e97338e1c8ea2649e130dcfbe8cd69bba5e1e163834752ab64dcb4de6d5ed", size = 274497 },
+ { url = "https://files.pythonhosted.org/packages/8a/d6/d788d52da01280a30a3f6268aef2aa71043bff359c618fea4c5b536654d5/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087 },
+ { url = "https://files.pythonhosted.org/packages/69/39/abec3bd688ec9bbea3562de0fd764ff802976185f5ff22807bf0a2697992/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544 },
+ { url = "https://files.pythonhosted.org/packages/39/b3/9a231475d5653e60002508f41205c61684bb2ffbf2401351ae2186897fc4/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408 },
+ { url = "https://files.pythonhosted.org/packages/c3/c5/1929a0491bd5ac2d1539a866768b88965fa8c405f3e16a8cef84313098d6/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584 },
+ { url = "https://files.pythonhosted.org/packages/ce/fd/16aa16cf5d497ef727ec966f74164fbe75d6516d3d58ac9aa989bc9cdaad/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733 },
+ { url = "https://files.pythonhosted.org/packages/e6/49/3294b988855a221cb6565189edf5dc43239957427df2d81d4a6b15244f64/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691 },
+ { url = "https://files.pythonhosted.org/packages/14/62/b56d29e70b03666193369bdbdedfdc23946dbe9f81dd78ce262c74d988ab/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662 },
+ { url = "https://files.pythonhosted.org/packages/15/fc/e4c31d061eced63fbf1ce9d853975f912c61a7d406ea14eda2dd355f48e7/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587 },
+ { url = "https://files.pythonhosted.org/packages/b2/bb/5e30c7394bcf63f0537121c23e796be67b55a8847c3956ae6068f4c70702/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709 },
+ { url = "https://files.pythonhosted.org/packages/c5/c4/fce773710af81b0cb37cb4ff0947e75d5d17dee304b93d940b87a67fc2f4/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773 },
+ { url = "https://files.pythonhosted.org/packages/7b/5e/9466a7ec4b8ec282077095c6eb50a12a389d2e036581134d4919e8ca518c/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164 },
+ { url = "https://files.pythonhosted.org/packages/95/18/82980a60e8ed1594eb3c89eb814fb276ef51b9af7caeab1340bfd8564af6/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832 },
+ { url = "https://files.pythonhosted.org/packages/03/cc/90ab0fdbe6dce064a42015433f9152710139fb04a8b81b4fb57a1cb63ffa/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802 },
+ { url = "https://files.pythonhosted.org/packages/34/9d/e9e8493a85f3b1ddc4a5014465f5c2b78c3ea1cbf238dcfde78956378041/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722 },
+ { url = "https://files.pythonhosted.org/packages/15/c4/b54b24f553966564506dbf873a3e080aef47b356a3b39b5d5aba992b50db/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289 },
+ { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081 },
+ { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554 },
+ { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407 },
+ { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418 },
+ { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448 },
+ { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139 },
+ { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439 },
+ { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965 },
+ { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398 },
+ { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897 },
+ { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906 },
+ { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812 },
+ { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737 },
+ { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290 },
+ { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312 },
+ { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256 },
+ { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921 },
+ { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568 },
+ { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165 },
+ { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182 },
+ { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501 },
+ { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842 },
+ { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519 },
+ { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611 },
+ { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759 },
+ { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194 },
+ { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069 },
+ { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330 },
+ { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 },
+ { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 },
+ { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 },
+ { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 },
+ { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 },
+ { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 },
+ { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 },
+ { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 },
+ { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 },
+ { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 },
+ { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 },
+ { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 },
+ { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 },
+ { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 },
+ { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 },
+ { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 },
+ { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 },
+ { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 },
+ { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 },
+ { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 },
+ { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 },
+ { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 },
+ { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 },
+ { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 },
+ { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 },
+ { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 },
+ { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 },
+ { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 },
+ { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089 },
+ { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059 },
+ { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900 },
+ { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010 },
+ { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893 },
+ { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522 },
+ { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272 },
+ { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958 },
+ { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289 },
+ { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026 },
+ { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499 },
+ { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604 },
+ { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320 },
+ { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372 },
+ { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985 },
+ { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669 },
+ { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030 },
+ { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674 },
+ { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451 },
+ { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980 },
+ { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852 },
+ { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566 },
+ { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463 },
+ { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694 },
+ { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691 },
+ { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583 },
+ { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286 },
+ { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741 },
]
[[package]]
@@ -5119,28 +5242,28 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.14.2"
+version = "0.14.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663 }
+sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390 },
- { url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187 },
- { url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177 },
- { url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285 },
- { url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832 },
- { url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995 },
- { url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649 },
- { url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182 },
- { url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516 },
- { url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690 },
- { url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497 },
- { url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116 },
- { url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345 },
- { url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296 },
- { url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610 },
- { url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318 },
- { url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279 },
- { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956 },
+ { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613 },
+ { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812 },
+ { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026 },
+ { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818 },
+ { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745 },
+ { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684 },
+ { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000 },
+ { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450 },
+ { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414 },
+ { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293 },
+ { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444 },
+ { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581 },
+ { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503 },
+ { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457 },
+ { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980 },
+ { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045 },
+ { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005 },
+ { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017 },
]
[[package]]
@@ -5634,14 +5757,15 @@ wheels = [
[[package]]
name = "starlette"
-version = "0.46.2"
+version = "0.49.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or 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/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 }
+sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
+ { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340 },
]
[[package]]
@@ -5902,6 +6026,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087 },
]
+[[package]]
+name = "types-requests"
+version = "2.32.4.20250913"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658 },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"