From 9c53abe8c15ab2cf1c5591c1db1f61b52a1b24dc Mon Sep 17 00:00:00 2001 From: Eric Peter Date: Sun, 30 Mar 2025 20:53:09 -0700 Subject: [PATCH 01/85] Databricks MLflow tracing integration (#401) --- docs/tracing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tracing.md b/docs/tracing.md index b5412de0..b5dd54a6 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -101,7 +101,8 @@ To customize this default setup, to send traces to alternative or additional bac - [Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents) - [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) -- [MLflow](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) +- [MLflow (self-hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) +- [MLflow (Databricks hosted](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) - [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) - [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents) - [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk) From 8b2bc0b4ba39e0369b76e0c570d0ed40ae426afe Mon Sep 17 00:00:00 2001 From: Ddper Date: Tue, 1 Apr 2025 23:48:02 +0800 Subject: [PATCH 02/85] fix duplicate declaration of _shutdown_event (#410) fix duplicate declaration of _shutdown_event reported by issue https://github.com/openai/openai-agents-python/issues/314 --- src/agents/tracing/processors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/agents/tracing/processors.py b/src/agents/tracing/processors.py index 5d8f4d8a..f929d05d 100644 --- a/src/agents/tracing/processors.py +++ b/src/agents/tracing/processors.py @@ -182,7 +182,6 @@ def __init__( # Track when we next *must* perform a scheduled export self._next_export_time = time.time() + self._schedule_delay - self._shutdown_event = threading.Event() self._worker_thread = threading.Thread(target=self._run, daemon=True) self._worker_thread.start() From 757b7dafb1d5367a56786df33e5c0d708541c44c Mon Sep 17 00:00:00 2001 From: Toufik Airane Date: Tue, 1 Apr 2025 17:50:40 +0200 Subject: [PATCH 03/85] Update README.md (#405) fix typo --- examples/mcp/sse_example/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mcp/sse_example/README.md b/examples/mcp/sse_example/README.md index 605c2cc9..9a667d31 100644 --- a/examples/mcp/sse_example/README.md +++ b/examples/mcp/sse_example/README.md @@ -5,7 +5,7 @@ This example uses a local SSE server in [server.py](server.py). Run the example via: ``` -uv run python python examples/mcp/sse_example/main.py +uv run python examples/mcp/sse_example/main.py ``` ## Details From e827dfe75f869f9bcbbc2e677d8ef97853bc5923 Mon Sep 17 00:00:00 2001 From: "Carmen(Jia) Liu" Date: Tue, 1 Apr 2025 23:51:52 +0800 Subject: [PATCH 04/85] Fix: Correct the trace path in examples (#395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What changed? The trace URL was using the wrong path format: - Old: `/traces/{trace_id}` - Fixed: `/traces/trace?trace_id={trace_id}` Screenshot 2025-03-30 at 5 54 11 pm --- examples/financial_research_agent/manager.py | 2 +- examples/mcp/filesystem_example/main.py | 2 +- examples/mcp/sse_example/main.py | 2 +- examples/research_bot/manager.py | 2 +- examples/research_bot/sample_outputs/product_recs.txt | 2 +- examples/research_bot/sample_outputs/vacation.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/financial_research_agent/manager.py b/examples/financial_research_agent/manager.py index a996296d..58ec11bf 100644 --- a/examples/financial_research_agent/manager.py +++ b/examples/financial_research_agent/manager.py @@ -38,7 +38,7 @@ async def run(self, query: str) -> None: with trace("Financial research trace", trace_id=trace_id): self.printer.update_item( "trace_id", - f"View trace: https://platform.openai.com/traces/{trace_id}", + f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}", is_done=True, hide_checkmark=True, ) diff --git a/examples/mcp/filesystem_example/main.py b/examples/mcp/filesystem_example/main.py index ae6fadd2..92c2b2db 100644 --- a/examples/mcp/filesystem_example/main.py +++ b/examples/mcp/filesystem_example/main.py @@ -45,7 +45,7 @@ async def main(): ) as server: trace_id = gen_trace_id() with trace(workflow_name="MCP Filesystem Example", trace_id=trace_id): - print(f"View trace: https://platform.openai.com/traces/{trace_id}\n") + print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n") await run(server) diff --git a/examples/mcp/sse_example/main.py b/examples/mcp/sse_example/main.py index 024b087e..7c1137d2 100644 --- a/examples/mcp/sse_example/main.py +++ b/examples/mcp/sse_example/main.py @@ -46,7 +46,7 @@ async def main(): ) as server: trace_id = gen_trace_id() with trace(workflow_name="SSE Example", trace_id=trace_id): - print(f"View trace: https://platform.openai.com/traces/{trace_id}\n") + print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n") await run(server) diff --git a/examples/research_bot/manager.py b/examples/research_bot/manager.py index 47306f14..dab68569 100644 --- a/examples/research_bot/manager.py +++ b/examples/research_bot/manager.py @@ -23,7 +23,7 @@ async def run(self, query: str) -> None: with trace("Research trace", trace_id=trace_id): self.printer.update_item( "trace_id", - f"View trace: https://platform.openai.com/traces/{trace_id}", + f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}", is_done=True, hide_checkmark=True, ) diff --git a/examples/research_bot/sample_outputs/product_recs.txt b/examples/research_bot/sample_outputs/product_recs.txt index 78865f23..fd14d533 100644 --- a/examples/research_bot/sample_outputs/product_recs.txt +++ b/examples/research_bot/sample_outputs/product_recs.txt @@ -3,7 +3,7 @@ $ uv run python -m examples.research_bot.main What would you like to research? Best surfboards for beginners. I can catch my own waves, but previously used an 11ft board. What should I look for, what are my options? Various budget ranges. -View trace: https://platform.openai.com/traces/trace_... +View trace: https://platform.openai.com/traces/trace?trace_id=trace_... Starting research... ✅ Will perform 15 searches ✅ Searching... 15/15 completed diff --git a/examples/research_bot/sample_outputs/vacation.txt b/examples/research_bot/sample_outputs/vacation.txt index b2649981..491c0005 100644 --- a/examples/research_bot/sample_outputs/vacation.txt +++ b/examples/research_bot/sample_outputs/vacation.txt @@ -2,7 +2,7 @@ $ uv run python -m examples.research_bot.main What would you like to research? Caribbean vacation spots in April, optimizing for surfing, hiking and water sports -View trace: https://platform.openai.com/traces/trace_.... +View trace: https://platform.openai.com/traces/trace?trace_id=trace_.... Starting research... ✅ Will perform 15 searches ✅ Searching... 15/15 completed From db3975f06ce77f6b63de06a4099593f09260c839 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 1 Apr 2025 08:51:58 -0700 Subject: [PATCH 05/85] Fix incorrect slot name on `SpeechSpanData` (#386) Slot had inconsistent name. `"first_byte_at"` -> `"first_content_at"` --- src/agents/tracing/span_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/tracing/span_data.py b/src/agents/tracing/span_data.py index ed2a3f2d..fe2e0618 100644 --- a/src/agents/tracing/span_data.py +++ b/src/agents/tracing/span_data.py @@ -236,7 +236,7 @@ def export(self) -> dict[str, Any]: class SpeechSpanData(SpanData): - __slots__ = ("input", "output", "model", "model_config", "first_byte_at") + __slots__ = ("input", "output", "model", "model_config", "first_content_at") def __init__( self, From 3454e3b32314b991eb37f2f2b2db7a1e436cc5b8 Mon Sep 17 00:00:00 2001 From: Hoc Phan Date: Tue, 1 Apr 2025 08:57:43 -0700 Subject: [PATCH 06/85] Update tracing.md - Added Okahu-Monocle (#385) Monocle is an open source tracing and eval for GenAI apps. --- docs/tracing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tracing.md b/docs/tracing.md index b5dd54a6..ea48a2e2 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -113,3 +113,4 @@ To customize this default setup, to send traces to alternative or additional bac - [Comet Opik](https://www.comet.com/docs/opik/tracing/integrations/openai_agents) - [Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents) - [Langtrace](https://docs.langtrace.ai/supported-integrations/llm-frameworks/openai-agents-sdk) +- [Okahu-Monocle](https://github.com/monocle2ai/monocle) From 45c25f8ab0ba6ba455997aa284e966956d9e0b2b Mon Sep 17 00:00:00 2001 From: Muhammad Anas Baig Date: Tue, 1 Apr 2025 23:09:26 +0500 Subject: [PATCH 07/85] Fix Docstring Reference in 'ToolsToFinalOutputFunction' in 'agent.py' (#391) ### **Summary** This pull request fixes docstring in `ToolsToFinalOutputFunction`, where `ToolToFinalOutputResult` was incorrectly referenced instead of `ToolsToFinalOutputResult`. ### **Changes Made** - Updated the docstring of `ToolsToFinalOutputFunction` to correctly reference `ToolsToFinalOutputResult`. ### **Before** ```python """A function that takes a run context and a list of tool results, and returns a `ToolToFinalOutputResult`. """ ``` ### **After** ```python """A function that takes a run context and a list of tool results, and returns a `ToolsToFinalOutputResult`. """ ``` ### **Why This Change?** - The incorrect reference could cause confusion for developers reading the code. - This aligns the documentation with the actual return type of the function. ### **Checklist** - [x] Verify correct reference to `ToolsToFinalOutputResult` - [x] Ensure no functionality is affected ### **Testing** This is a documentation-only change and does not affect runtime behavior. ### **Reviewer Notes** - Please review for accuracy in documentation. - No additional tests are needed since this is a non-functional change. --- src/agents/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index 13bb464e..b2b9f7b2 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -44,7 +44,7 @@ class ToolsToFinalOutputResult: MaybeAwaitable[ToolsToFinalOutputResult], ] """A function that takes a run context and a list of tool results, and returns a -`ToolToFinalOutputResult`. +`ToolsToFinalOutputResult`. """ From 01f5e86ea546905c3d935b6f25c9b074acbf85d6 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Tue, 1 Apr 2025 16:50:13 -0400 Subject: [PATCH 08/85] Convert MCP schemas to strict where possible (#414) ## Summary: Towards #404. I made this configurable because it's not clear this is always a good thing to do. I also made it default to False because I'm not sure if this could cause errors. If it works out well, we can switch the default in the future as a small breaking changes ## Test Plan: Unit tests --- src/agents/agent.py | 17 +++- src/agents/mcp/util.py | 30 +++++-- tests/mcp/test_mcp_util.py | 161 ++++++++++++++++++++++++++++++++++- tests/test_agent_runner.py | 4 +- tests/test_tracing_errors.py | 14 ++- 5 files changed, 202 insertions(+), 24 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index b2b9f7b2..4c6de245 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, cast -from typing_extensions import TypeAlias, TypedDict +from typing_extensions import NotRequired, TypeAlias, TypedDict from .guardrail import InputGuardrail, OutputGuardrail from .handoffs import Handoff @@ -53,6 +53,15 @@ class StopAtTools(TypedDict): """A list of tool names, any of which will stop the agent from running further.""" +class MCPConfig(TypedDict): + """Configuration for MCP servers.""" + + convert_schemas_to_strict: NotRequired[bool] + """If True, we will attempt to convert the MCP schemas to strict-mode schemas. This is a + best-effort conversion, so some schemas may not be convertible. Defaults to False. + """ + + @dataclass class Agent(Generic[TContext]): """An agent is an AI model configured with instructions, tools, guardrails, handoffs and more. @@ -119,6 +128,9 @@ class Agent(Generic[TContext]): longer needed. """ + mcp_config: MCPConfig = field(default_factory=lambda: MCPConfig()) + """Configuration for MCP servers.""" + input_guardrails: list[InputGuardrail[TContext]] = field(default_factory=list) """A list of checks that run in parallel to the agent's execution, before generating a response. Runs only if the agent is the first agent in the chain. @@ -224,7 +236,8 @@ async def get_system_prompt(self, run_context: RunContextWrapper[TContext]) -> s async def get_mcp_tools(self) -> list[Tool]: """Fetches the available tools from the MCP servers.""" - return await MCPUtil.get_all_function_tools(self.mcp_servers) + convert_schemas_to_strict = self.mcp_config.get("convert_schemas_to_strict", False) + return await MCPUtil.get_all_function_tools(self.mcp_servers, convert_schemas_to_strict) async def get_all_tools(self) -> list[Tool]: """All agent tools, including MCP tools and function tools.""" diff --git a/src/agents/mcp/util.py b/src/agents/mcp/util.py index 36c18bea..770ae8dd 100644 --- a/src/agents/mcp/util.py +++ b/src/agents/mcp/util.py @@ -2,6 +2,8 @@ import json from typing import TYPE_CHECKING, Any +from agents.strict_schema import ensure_strict_json_schema + from .. import _debug from ..exceptions import AgentsException, ModelBehaviorError, UserError from ..logger import logger @@ -19,12 +21,14 @@ class MCPUtil: """Set of utilities for interop between MCP and Agents SDK tools.""" @classmethod - async def get_all_function_tools(cls, servers: list["MCPServer"]) -> list[Tool]: + async def get_all_function_tools( + cls, servers: list["MCPServer"], convert_schemas_to_strict: bool + ) -> list[Tool]: """Get all function tools from a list of MCP servers.""" tools = [] tool_names: set[str] = set() for server in servers: - server_tools = await cls.get_function_tools(server) + server_tools = await cls.get_function_tools(server, convert_schemas_to_strict) server_tool_names = {tool.name for tool in server_tools} if len(server_tool_names & tool_names) > 0: raise UserError( @@ -37,25 +41,37 @@ async def get_all_function_tools(cls, servers: list["MCPServer"]) -> list[Tool]: return tools @classmethod - async def get_function_tools(cls, server: "MCPServer") -> list[Tool]: + async def get_function_tools( + cls, server: "MCPServer", convert_schemas_to_strict: bool + ) -> list[Tool]: """Get all function tools from a single MCP server.""" with mcp_tools_span(server=server.name) as span: tools = await server.list_tools() span.span_data.result = [tool.name for tool in tools] - return [cls.to_function_tool(tool, server) for tool in tools] + return [cls.to_function_tool(tool, server, convert_schemas_to_strict) for tool in tools] @classmethod - def to_function_tool(cls, tool: "MCPTool", server: "MCPServer") -> FunctionTool: + def to_function_tool( + cls, tool: "MCPTool", server: "MCPServer", convert_schemas_to_strict: bool + ) -> FunctionTool: """Convert an MCP tool to an Agents SDK function tool.""" invoke_func = functools.partial(cls.invoke_mcp_tool, server, tool) + schema, is_strict = tool.inputSchema, False + if convert_schemas_to_strict: + try: + schema = ensure_strict_json_schema(schema) + is_strict = True + except Exception as e: + logger.info(f"Error converting MCP schema to strict mode: {e}") + return FunctionTool( name=tool.name, description=tool.description or "", - params_json_schema=tool.inputSchema, + params_json_schema=schema, on_invoke_tool=invoke_func, - strict_json_schema=False, + strict_json_schema=is_strict, ) @classmethod diff --git a/tests/mcp/test_mcp_util.py b/tests/mcp/test_mcp_util.py index 345df996..64378b59 100644 --- a/tests/mcp/test_mcp_util.py +++ b/tests/mcp/test_mcp_util.py @@ -2,10 +2,11 @@ from typing import Any import pytest +from inline_snapshot import snapshot from mcp.types import Tool as MCPTool -from pydantic import BaseModel +from pydantic import BaseModel, TypeAdapter -from agents import FunctionTool, RunContextWrapper +from agents import Agent, FunctionTool, RunContextWrapper from agents.exceptions import AgentsException, ModelBehaviorError from agents.mcp import MCPServer, MCPUtil @@ -18,7 +19,16 @@ class Foo(BaseModel): class Bar(BaseModel): - qux: str + qux: dict[str, str] + + +Baz = TypeAdapter(dict[str, str]) + + +def _convertible_schema() -> dict[str, Any]: + schema = Foo.model_json_schema() + schema["additionalProperties"] = False + return schema @pytest.mark.asyncio @@ -47,7 +57,7 @@ async def test_get_all_function_tools(): server3.add_tool(names[4], schemas[4]) servers: list[MCPServer] = [server1, server2, server3] - tools = await MCPUtil.get_all_function_tools(servers) + tools = await MCPUtil.get_all_function_tools(servers, convert_schemas_to_strict=False) assert len(tools) == 5 assert all(tool.name in names for tool in tools) @@ -56,6 +66,11 @@ async def test_get_all_function_tools(): assert tool.params_json_schema == schemas[idx] assert tool.name == names[idx] + # Also make sure it works with strict schemas + tools = await MCPUtil.get_all_function_tools(servers, convert_schemas_to_strict=True) + assert len(tools) == 5 + assert all(tool.name in names for tool in tools) + @pytest.mark.asyncio async def test_invoke_mcp_tool(): @@ -107,3 +122,141 @@ async def test_mcp_invocation_crash_causes_error(caplog: pytest.LogCaptureFixtur await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") assert "Error invoking MCP tool test_tool_1" in caplog.text + + +@pytest.mark.asyncio +async def test_agent_convert_schemas_true(): + """Test that setting convert_schemas_to_strict to True converts non-strict schemas to strict. + - 'foo' tool is already strict and remains strict. + - 'bar' tool is non-strict and becomes strict (additionalProperties set to False, etc). + """ + strict_schema = Foo.model_json_schema() + non_strict_schema = Baz.json_schema() + possible_to_convert_schema = _convertible_schema() + + server = FakeMCPServer() + server.add_tool("foo", strict_schema) + server.add_tool("bar", non_strict_schema) + server.add_tool("baz", possible_to_convert_schema) + agent = Agent( + name="test_agent", mcp_servers=[server], mcp_config={"convert_schemas_to_strict": True} + ) + tools = await agent.get_mcp_tools() + + foo_tool = next(tool for tool in tools if tool.name == "foo") + assert isinstance(foo_tool, FunctionTool) + bar_tool = next(tool for tool in tools if tool.name == "bar") + assert isinstance(bar_tool, FunctionTool) + baz_tool = next(tool for tool in tools if tool.name == "baz") + assert isinstance(baz_tool, FunctionTool) + + # Checks that additionalProperties is set to False + assert foo_tool.params_json_schema == snapshot( + { + "properties": { + "bar": {"title": "Bar", "type": "string"}, + "baz": {"title": "Baz", "type": "integer"}, + }, + "required": ["bar", "baz"], + "title": "Foo", + "type": "object", + "additionalProperties": False, + } + ) + assert foo_tool.strict_json_schema is True, "foo_tool should be strict" + + # Checks that additionalProperties is set to False + assert bar_tool.params_json_schema == snapshot( + { + "type": "object", + "additionalProperties": {"type": "string"}, + } + ) + assert bar_tool.strict_json_schema is False, "bar_tool should not be strict" + + # Checks that additionalProperties is set to False + assert baz_tool.params_json_schema == snapshot( + { + "properties": { + "bar": {"title": "Bar", "type": "string"}, + "baz": {"title": "Baz", "type": "integer"}, + }, + "required": ["bar", "baz"], + "title": "Foo", + "type": "object", + "additionalProperties": False, + } + ) + assert baz_tool.strict_json_schema is True, "baz_tool should be strict" + + +@pytest.mark.asyncio +async def test_agent_convert_schemas_false(): + """Test that setting convert_schemas_to_strict to False leaves tool schemas as non-strict. + - 'foo' tool remains strict. + - 'bar' tool remains non-strict (additionalProperties remains True). + """ + strict_schema = Foo.model_json_schema() + non_strict_schema = Baz.json_schema() + possible_to_convert_schema = _convertible_schema() + + server = FakeMCPServer() + server.add_tool("foo", strict_schema) + server.add_tool("bar", non_strict_schema) + server.add_tool("baz", possible_to_convert_schema) + + agent = Agent( + name="test_agent", mcp_servers=[server], mcp_config={"convert_schemas_to_strict": False} + ) + tools = await agent.get_mcp_tools() + + foo_tool = next(tool for tool in tools if tool.name == "foo") + assert isinstance(foo_tool, FunctionTool) + bar_tool = next(tool for tool in tools if tool.name == "bar") + assert isinstance(bar_tool, FunctionTool) + baz_tool = next(tool for tool in tools if tool.name == "baz") + assert isinstance(baz_tool, FunctionTool) + + assert foo_tool.params_json_schema == strict_schema + assert foo_tool.strict_json_schema is False, "Shouldn't be converted unless specified" + + assert bar_tool.params_json_schema == non_strict_schema + assert bar_tool.strict_json_schema is False + + assert baz_tool.params_json_schema == possible_to_convert_schema + assert baz_tool.strict_json_schema is False, "Shouldn't be converted unless specified" + + +@pytest.mark.asyncio +async def test_agent_convert_schemas_unset(): + """Test that leaving convert_schemas_to_strict unset (defaulting to False) leaves tool schemas + as non-strict. + - 'foo' tool remains strict. + - 'bar' tool remains non-strict. + """ + strict_schema = Foo.model_json_schema() + non_strict_schema = Baz.json_schema() + possible_to_convert_schema = _convertible_schema() + + server = FakeMCPServer() + server.add_tool("foo", strict_schema) + server.add_tool("bar", non_strict_schema) + server.add_tool("baz", possible_to_convert_schema) + agent = Agent(name="test_agent", mcp_servers=[server]) + tools = await agent.get_mcp_tools() + + foo_tool = next(tool for tool in tools if tool.name == "foo") + assert isinstance(foo_tool, FunctionTool) + bar_tool = next(tool for tool in tools if tool.name == "bar") + assert isinstance(bar_tool, FunctionTool) + baz_tool = next(tool for tool in tools if tool.name == "baz") + assert isinstance(baz_tool, FunctionTool) + + assert foo_tool.params_json_schema == strict_schema + assert foo_tool.strict_json_schema is False, "Shouldn't be converted unless specified" + + assert bar_tool.params_json_schema == non_strict_schema + assert bar_tool.strict_json_schema is False + + assert baz_tool.params_json_schema == possible_to_convert_schema + assert baz_tool.strict_json_schema is False, "Shouldn't be converted unless specified" diff --git a/tests/test_agent_runner.py b/tests/test_agent_runner.py index ce0c5804..4f277656 100644 --- a/tests/test_agent_runner.py +++ b/tests/test_agent_runner.py @@ -642,9 +642,7 @@ async def test_tool_use_behavior_custom_function(): async def test_model_settings_override(): model = FakeModel() agent = Agent( - name="test", - model=model, - model_settings=ModelSettings(temperature=1.0, max_tokens=1000) + name="test", model=model, model_settings=ModelSettings(temperature=1.0, max_tokens=1000) ) model.add_multiple_turn_outputs( diff --git a/tests/test_tracing_errors.py b/tests/test_tracing_errors.py index 6d698bcc..72bd39ed 100644 --- a/tests/test_tracing_errors.py +++ b/tests/test_tracing_errors.py @@ -244,9 +244,10 @@ async def test_multiple_handoff_doesnt_error(): }, }, {"type": "generation"}, - {"type": "handoff", - "data": {"from_agent": "test", "to_agent": "test"}, - "error": { + { + "type": "handoff", + "data": {"from_agent": "test", "to_agent": "test"}, + "error": { "data": { "requested_agents": [ "test", @@ -255,7 +256,7 @@ async def test_multiple_handoff_doesnt_error(): }, "message": "Multiple handoffs requested", }, - }, + }, ], }, { @@ -383,10 +384,7 @@ async def test_handoffs_lead_to_correct_agent_spans(): {"type": "generation"}, { "type": "handoff", - "data": { - "from_agent": "test_agent_3", - "to_agent": "test_agent_1" - }, + "data": {"from_agent": "test_agent_3", "to_agent": "test_agent_1"}, "error": { "data": { "requested_agents": [ From bf302c574ce098efd5b2d3ce1db2452025bd3cd1 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 2 Apr 2025 12:27:42 -0400 Subject: [PATCH 09/85] Use "{}" instead of empty string for arguments (#422) --- src/agents/models/openai_chatcompletions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index de7b1ae4..2ba18872 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -923,12 +923,13 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam: elif func_call := cls.maybe_function_tool_call(item): asst = ensure_assistant_message() tool_calls = list(asst.get("tool_calls", [])) + arguments = func_call["arguments"] if func_call["arguments"] else "{}" new_tool_call = ChatCompletionMessageToolCallParam( id=func_call["call_id"], type="function", function={ "name": func_call["name"], - "arguments": func_call["arguments"], + "arguments": arguments, }, ) tool_calls.append(new_tool_call) @@ -971,7 +972,7 @@ def to_openai(cls, tool: Tool) -> ChatCompletionToolParam: } raise UserError( - f"Hosted tools are not supported with the ChatCompletions API. FGot tool type: " + f"Hosted tools are not supported with the ChatCompletions API. Got tool type: " f"{type(tool)}, tool: {tool}" ) From 7c2d7f4abd7e6dc2c452134cf536e7e3c0c7a02d Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 3 Apr 2025 12:08:01 -0400 Subject: [PATCH 10/85] Misc small fixes - mcp version, test for function_schema, version gen (#429) Summary: 1. Use <2 for MCP version so it doesn't break if the MCP sdk upgrades. 2. Test the func schema extraction logic. 3. Fix the logic to get the version nuber of the framework Test Plan: unit tests --- .vscode/settings.json | 7 ++++++ pyproject.toml | 2 +- src/agents/__init__.py | 2 ++ src/agents/version.py | 2 +- tests/test_function_tool_decorator.py | 35 +++++++++++++++++++++++++++ uv.lock | 10 +++++--- 6 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9b388533 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8518301f..75dd518c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "typing-extensions>=4.12.2, <5", "requests>=2.0, <3", "types-requests>=2.0, <3", - "mcp; python_version >= '3.10'", + "mcp>=1.6.0, <2; python_version >= '3.10'", ] classifiers = [ "Typing :: Typed", diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 242f5649..db7d312e 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -100,6 +100,7 @@ transcription_span, ) from .usage import Usage +from .version import __version__ def set_default_openai_key(key: str, use_for_tracing: bool = True) -> None: @@ -247,4 +248,5 @@ def enable_verbose_stdout_logging(): "gen_trace_id", "gen_span_id", "default_tool_error_function", + "__version__", ] diff --git a/src/agents/version.py b/src/agents/version.py index a0b7e9be..9b22499e 100644 --- a/src/agents/version.py +++ b/src/agents/version.py @@ -1,7 +1,7 @@ import importlib.metadata try: - __version__ = importlib.metadata.version("agents") + __version__ = importlib.metadata.version("openai-agents") except importlib.metadata.PackageNotFoundError: # Fallback if running from source without being installed __version__ = "0.0.0" diff --git a/tests/test_function_tool_decorator.py b/tests/test_function_tool_decorator.py index 903dd123..3b52788f 100644 --- a/tests/test_function_tool_decorator.py +++ b/tests/test_function_tool_decorator.py @@ -3,6 +3,7 @@ from typing import Any, Optional import pytest +from inline_snapshot import snapshot from agents import function_tool from agents.run_context import RunContextWrapper @@ -198,3 +199,37 @@ async def test_all_optional_params_function(): input_data = {"x": 10, "y": "world", "z": 99} output = await tool.on_invoke_tool(ctx_wrapper(), json.dumps(input_data)) assert output == "10_world_99" + + +@function_tool +def get_weather(city: str) -> str: + """Get the weather for a given city. + + Args: + city: The city to get the weather for. + """ + return f"The weather in {city} is sunny." + + +@pytest.mark.asyncio +async def test_extract_descriptions_from_docstring(): + """Ensure that we extract function and param descriptions from docstrings.""" + + tool = get_weather + assert tool.description == "Get the weather for a given city." + params_json_schema = tool.params_json_schema + assert params_json_schema == snapshot( + { + "type": "object", + "properties": { + "city": { + "description": "The city to get the weather for.", + "title": "City", + "type": "string", + } + }, + "title": "get_weather_args", + "required": ["city"], + "additionalProperties": False, + } + ) diff --git a/uv.lock b/uv.lock index a9e8f1e7..e443c009 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.10'", @@ -718,7 +719,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, @@ -730,9 +731,9 @@ dependencies = [ { name = "starlette", marker = "python_full_version >= '3.10'" }, { name = "uvicorn", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c9/c55764824e893fdebe777ac7223200986a275c3191dba9169f8eb6d7c978/mcp-1.5.0.tar.gz", hash = "sha256:5b2766c05e68e01a2034875e250139839498c61792163a7b221fc170c12f5aa9", size = 159128 } +sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d1/3ff566ecf322077d861f1a68a1ff025cad337417bd66ad22a7c6f7dfcfaf/mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527", size = 73734 }, + { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, ] [[package]] @@ -1133,7 +1134,7 @@ dev = [ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, - { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.66.5" }, { name = "pydantic", specifier = ">=2.10,<3" }, @@ -1142,6 +1143,7 @@ requires-dist = [ { name = "typing-extensions", specifier = ">=4.12.2,<5" }, { name = "websockets", marker = "extra == 'voice'", specifier = ">=15.0,<16" }, ] +provides-extras = ["voice", "viz"] [package.metadata.requires-dev] dev = [ From 506d73cfa4a61ccdeb23e7b78b09aeeab909a748 Mon Sep 17 00:00:00 2001 From: amri369 <39493555+amri369@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:34:53 +0400 Subject: [PATCH 11/85] Fix small typos and mcp example command (#417) --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index edd7681a..73586eaa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -10,7 +10,7 @@ assignees: '' ### Please read this first - **Have you read the docs?**[Agents SDK docs](https://openai.github.io/openai-agents-python/) -- **Have you searched for related issues?** Others may have had similar requesrs +- **Have you searched for related issues?** Others may have had similar requests ### Describe the feature What is the feature you're requesting? How would it work? Please provide examples and details if possible. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index cb4a05dc..6c639d72 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -10,7 +10,7 @@ assignees: '' ### Please read this first - **Have you read the docs?**[Agents SDK docs](https://openai.github.io/openai-agents-python/) -- **Have you searched for related issues?** Others may have had similar requesrs +- **Have you searched for related issues?** Others may have had similar requests ### Question Describe your question. Provide details if available. From bef33948597114b758d00bca8f9bf12a14ea789b Mon Sep 17 00:00:00 2001 From: Drew Youngwerth Date: Thu, 3 Apr 2025 12:36:35 -0700 Subject: [PATCH 12/85] Fix reference to group id as session id (#430) Group ID is being mistakenly referenced as Session ID, which is a bit confusing. --- docs/running_agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/running_agents.md b/docs/running_agents.md index 32abf9d5..f631cf46 100644 --- a/docs/running_agents.md +++ b/docs/running_agents.md @@ -53,7 +53,7 @@ The `run_config` parameter lets you configure some global settings for the agent - [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]: A global input filter to apply to all handoffs, if the handoff doesn't already have one. The input filter allows you to edit the inputs that are sent to the new agent. See the documentation in [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] for more details. - [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]: Allows you to disable [tracing](tracing.md) for the entire run. - [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]: Configures whether traces will include potentially sensitive data, such as LLM and tool call inputs/outputs. -- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]: Sets the tracing workflow name, trace ID and trace group ID for the run. We recommend at least setting `workflow_name`. The session ID is an optional field that lets you link traces across multiple runs. +- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]: Sets the tracing workflow name, trace ID and trace group ID for the run. We recommend at least setting `workflow_name`. The group ID is an optional field that lets you link traces across multiple runs. - [`trace_metadata`][agents.run.RunConfig.trace_metadata]: Metadata to include on all traces. ## Conversations/chat threads From 0110f3ad961564011f59df5e8534489c97e4b971 Mon Sep 17 00:00:00 2001 From: Drew Youngwerth Date: Thu, 3 Apr 2025 14:50:24 -0700 Subject: [PATCH 13/85] Add metadata to ModelSettings (#431) --- src/agents/model_settings.py | 3 +++ src/agents/models/openai_chatcompletions.py | 1 + src/agents/models/openai_responses.py | 1 + 3 files changed, 5 insertions(+) diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index 2b0885ab..bef7b9a6 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -40,6 +40,9 @@ class ModelSettings: max_tokens: int | None = None """The maximum number of output tokens to generate.""" + metadata: dict[str, str] | None = None + """Metadata to include with the model response call.""" + store: bool | None = None """Whether to store the generated model response for later retrieval. Defaults to True if not provided.""" diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 2ba18872..f90140bf 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -537,6 +537,7 @@ async def _fetch_response( stream_options={"include_usage": True} if stream else NOT_GIVEN, store=store, extra_headers=_HEADERS, + metadata=model_settings.metadata, ) if isinstance(ret, ChatCompletion): diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 17803fda..0a153910 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -247,6 +247,7 @@ async def _fetch_response( extra_headers=_HEADERS, text=response_format, store=self._non_null_or_not_given(model_settings.store), + metadata=model_settings.metadata, ) def _get_client(self) -> AsyncOpenAI: From 07a627e8ebeda84b3dddd570b77cd75a729e536e Mon Sep 17 00:00:00 2001 From: Suveen Ellawela <75742713+SuveenE@users.noreply.github.com> Date: Thu, 3 Apr 2025 16:35:59 -0700 Subject: [PATCH 14/85] Add reasoning parameter to ModelSettings (#388) fixes #189 @rm-openai Would really appreciate if this can get a quick review. --------- Co-authored-by: Rohan Mehta --- src/agents/model_settings.py | 7 +++++++ src/agents/models/openai_chatcompletions.py | 4 ++++ src/agents/models/openai_responses.py | 1 + 3 files changed, 12 insertions(+) diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index bef7b9a6..bac71f58 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -3,6 +3,8 @@ from dataclasses import dataclass, fields, replace from typing import Literal +from openai.types.shared import Reasoning + @dataclass class ModelSettings: @@ -40,6 +42,11 @@ class ModelSettings: max_tokens: int | None = None """The maximum number of output tokens to generate.""" + reasoning: Reasoning | None = None + """Configuration options for + [reasoning models](https://platform.openai.com/docs/guides/reasoning). + """ + metadata: dict[str, str] | None = None """Metadata to include with the model response call.""" diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index f90140bf..cbc48c50 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -521,6 +521,8 @@ async def _fetch_response( # Match the behavior of Responses where store is True when not given store = model_settings.store if model_settings.store is not None else True + reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None + ret = await self._get_client().chat.completions.create( model=self.model, messages=converted_messages, @@ -536,6 +538,7 @@ async def _fetch_response( stream=stream, stream_options={"include_usage": True} if stream else NOT_GIVEN, store=store, + reasoning_effort=self._non_null_or_not_given(reasoning_effort), extra_headers=_HEADERS, metadata=model_settings.metadata, ) @@ -556,6 +559,7 @@ async def _fetch_response( temperature=model_settings.temperature, tools=[], parallel_tool_calls=parallel_tool_calls or False, + reasoning=model_settings.reasoning, ) return response, ret diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 0a153910..6fda4bb3 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -247,6 +247,7 @@ async def _fetch_response( extra_headers=_HEADERS, text=response_format, store=self._non_null_or_not_given(model_settings.store), + reasoning=self._non_null_or_not_given(model_settings.reasoning), metadata=model_settings.metadata, ) From 0396052dcd0290fceae0cec88ce2a8e5831fd6b0 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 3 Apr 2025 19:46:26 -0400 Subject: [PATCH 15/85] v0.0.8 (#432) bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 75dd518c..3ade2c52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.7" +version = "0.0.8" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" From 064e25b01b5c82c08aea66ff898ff27adbb013d8 Mon Sep 17 00:00:00 2001 From: James Hills <70035505+jhills20@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:53:12 -0700 Subject: [PATCH 16/85] add links and mcp + voice examples (#438) --- docs/examples.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 1d3ebde7..30d60282 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -5,32 +5,38 @@ Check out a variety of sample implementations of the SDK in the examples section ## Categories -- **agent_patterns:** +- **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** Examples in this category illustrate common agent design patterns, such as - + - Deterministic workflows - Agents as tools - Parallel agent execution -- **basic:** +- **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** These examples showcase foundational capabilities of the SDK, such as - + - Dynamic system prompts - Streaming outputs - Lifecycle events -- **tool examples:** +- **[tool examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** Learn how to implement OAI hosted tools such as web search and file search, and integrate them into your agents. -- **model providers:** +- **[model providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** Explore how to use non-OpenAI models with the SDK. -- **handoffs:** +- **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** See practical examples of agent handoffs. -- **customer_service** and **research_bot:** +- **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** + Learn how to build agents with MCP. + +- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** and **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** Two more built-out examples that illustrate real-world applications - + - **customer_service**: Example customer service system for an airline. - **research_bot**: Simple deep research clone. + +- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** + See examples of voice agents, using our TTS and STT models. From 50bbfdd8beb89a9c864a5e4fea4dd2a79e99f724 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 7 Apr 2025 18:38:36 -0400 Subject: [PATCH 17/85] Ensure MCP works when inputSchema.properties is missing (#454) Resolves #449 - TLDR, [OpenAI's API](https://platform.openai.com/docs/api-reference/responses/create) expects the properties field to be present, whereas the MCP schema explicitly allows omitting the properties field. [MCP Spec](https://github.com/modelcontextprotocol/specification/blob/main/schema/2025-03-26/schema.json) --- src/agents/mcp/util.py | 5 +++++ tests/mcp/test_mcp_util.py | 43 +++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/agents/mcp/util.py b/src/agents/mcp/util.py index 770ae8dd..bbfe1885 100644 --- a/src/agents/mcp/util.py +++ b/src/agents/mcp/util.py @@ -59,6 +59,11 @@ def to_function_tool( """Convert an MCP tool to an Agents SDK function tool.""" invoke_func = functools.partial(cls.invoke_mcp_tool, server, tool) schema, is_strict = tool.inputSchema, False + + # MCP spec doesn't require the inputSchema to have `properties`, but OpenAI spec does. + if "properties" not in schema: + schema["properties"] = {} + if convert_schemas_to_strict: try: schema = ensure_strict_json_schema(schema) diff --git a/tests/mcp/test_mcp_util.py b/tests/mcp/test_mcp_util.py index 64378b59..74356a16 100644 --- a/tests/mcp/test_mcp_util.py +++ b/tests/mcp/test_mcp_util.py @@ -63,7 +63,10 @@ async def test_get_all_function_tools(): for idx, tool in enumerate(tools): assert isinstance(tool, FunctionTool) - assert tool.params_json_schema == schemas[idx] + if schemas[idx] == {}: + assert tool.params_json_schema == snapshot({"properties": {}}) + else: + assert tool.params_json_schema == schemas[idx] assert tool.name == names[idx] # Also make sure it works with strict schemas @@ -167,10 +170,7 @@ async def test_agent_convert_schemas_true(): # Checks that additionalProperties is set to False assert bar_tool.params_json_schema == snapshot( - { - "type": "object", - "additionalProperties": {"type": "string"}, - } + {"type": "object", "additionalProperties": {"type": "string"}, "properties": {}} ) assert bar_tool.strict_json_schema is False, "bar_tool should not be strict" @@ -220,7 +220,9 @@ async def test_agent_convert_schemas_false(): assert foo_tool.params_json_schema == strict_schema assert foo_tool.strict_json_schema is False, "Shouldn't be converted unless specified" - assert bar_tool.params_json_schema == non_strict_schema + assert bar_tool.params_json_schema == snapshot( + {"type": "object", "additionalProperties": {"type": "string"}, "properties": {}} + ) assert bar_tool.strict_json_schema is False assert baz_tool.params_json_schema == possible_to_convert_schema @@ -255,8 +257,35 @@ async def test_agent_convert_schemas_unset(): assert foo_tool.params_json_schema == strict_schema assert foo_tool.strict_json_schema is False, "Shouldn't be converted unless specified" - assert bar_tool.params_json_schema == non_strict_schema + assert bar_tool.params_json_schema == snapshot( + {"type": "object", "additionalProperties": {"type": "string"}, "properties": {}} + ) assert bar_tool.strict_json_schema is False assert baz_tool.params_json_schema == possible_to_convert_schema assert baz_tool.strict_json_schema is False, "Shouldn't be converted unless specified" + + +@pytest.mark.asyncio +async def test_util_adds_properties(): + """The MCP spec doesn't require the inputSchema to have `properties`, so we need to add it + if it's missing. + """ + schema = { + "type": "object", + "description": "Test tool", + } + + server = FakeMCPServer() + server.add_tool("test_tool", schema) + + tools = await MCPUtil.get_all_function_tools([server], convert_schemas_to_strict=False) + tool = next(tool for tool in tools if tool.name == "test_tool") + + assert isinstance(tool, FunctionTool) + assert "properties" in tool.params_json_schema + assert tool.params_json_schema["properties"] == {} + + assert tool.params_json_schema == snapshot( + {"type": "object", "description": "Test tool", "properties": {}} + ) From 2bcc864b81ba74c2e5252ce58b8f765bce98a329 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 7 Apr 2025 19:13:08 -0400 Subject: [PATCH 18/85] Don't send the "store" param unless its hitting OpenAI (#455) Summary: See #443. Causes issues with Gemini. Test Plan: Tests. Also tested with Gemini to ensure it works. --- src/agents/models/openai_chatcompletions.py | 14 ++++--- tests/test_openai_chatcompletions.py | 43 +++++++++++++++++++-- uv.lock | 2 +- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index cbc48c50..e0aafad0 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -518,10 +518,8 @@ async def _fetch_response( f"Response format: {response_format}\n" ) - # Match the behavior of Responses where store is True when not given - store = model_settings.store if model_settings.store is not None else True - reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None + store = _Converter.get_store_param(self._get_client(), model_settings) ret = await self._get_client().chat.completions.create( model=self.model, @@ -537,10 +535,10 @@ async def _fetch_response( parallel_tool_calls=parallel_tool_calls, stream=stream, stream_options={"include_usage": True} if stream else NOT_GIVEN, - store=store, + store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), extra_headers=_HEADERS, - metadata=model_settings.metadata, + metadata=self._non_null_or_not_given(model_settings.metadata), ) if isinstance(ret, ChatCompletion): @@ -570,6 +568,12 @@ def _get_client(self) -> AsyncOpenAI: class _Converter: + @classmethod + def get_store_param(cls, client: AsyncOpenAI, model_settings: ModelSettings) -> bool | None: + # Match the behavior of Responses where store is True when not given + default_store = True if str(client.base_url).startswith("https://api.openai.com") else None + return model_settings.store if model_settings.store is not None else default_store + @classmethod def convert_tool_choice( cls, tool_choice: Literal["auto", "required", "none"] | str | None diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index 9a53e2b7..a3198d33 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -5,7 +5,7 @@ import httpx import pytest -from openai import NOT_GIVEN +from openai import NOT_GIVEN, AsyncOpenAI from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.chat.chat_completion_message import ChatCompletionMessage @@ -31,6 +31,7 @@ generation_span, ) from agents.models.fake_id import FAKE_RESPONSES_ID +from agents.models.openai_chatcompletions import _Converter @pytest.mark.allow_call_model_methods @@ -226,7 +227,7 @@ def __init__(self, completions: DummyCompletions) -> None: # Ensure expected args were passed through to OpenAI client. kwargs = completions.kwargs assert kwargs["stream"] is False - assert kwargs["store"] is True + assert kwargs["store"] is NOT_GIVEN assert kwargs["model"] == "gpt-4" assert kwargs["messages"][0]["role"] == "system" assert kwargs["messages"][0]["content"] == "sys" @@ -280,7 +281,7 @@ def __init__(self, completions: DummyCompletions) -> None: ) # Check OpenAI client was called for streaming assert completions.kwargs["stream"] is True - assert completions.kwargs["store"] is True + assert completions.kwargs["store"] is NOT_GIVEN assert completions.kwargs["stream_options"] == {"include_usage": True} # Response is a proper openai Response assert isinstance(response, Response) @@ -290,3 +291,39 @@ def __init__(self, completions: DummyCompletions) -> None: assert response.output == [] # We returned the async iterator produced by our dummy. assert hasattr(stream, "__aiter__") + + +def test_store_param(): + """Should default to True for OpenAI API calls, and False otherwise.""" + + model_settings = ModelSettings() + client = AsyncOpenAI() + assert _Converter.get_store_param(client, model_settings) is True, ( + "Should default to True for OpenAI API calls" + ) + + model_settings = ModelSettings(store=False) + assert _Converter.get_store_param(client, model_settings) is False, ( + "Should respect explicitly set store=False" + ) + + model_settings = ModelSettings(store=True) + assert _Converter.get_store_param(client, model_settings) is True, ( + "Should respect explicitly set store=True" + ) + + client = AsyncOpenAI(base_url="http://www.notopenai.com") + model_settings = ModelSettings() + assert _Converter.get_store_param(client, model_settings) is None, ( + "Should default to None for non-OpenAI API calls" + ) + + model_settings = ModelSettings(store=False) + assert _Converter.get_store_param(client, model_settings) is False, ( + "Should respect explicitly set store=False" + ) + + model_settings = ModelSettings(store=True) + assert _Converter.get_store_param(client, model_settings) is True, ( + "Should respect explicitly set store=True" + ) diff --git a/uv.lock b/uv.lock index e443c009..a6018eeb 100644 --- a/uv.lock +++ b/uv.lock @@ -1087,7 +1087,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.7" +version = "0.0.8" source = { editable = "." } dependencies = [ { name = "griffe" }, From 9d3d6a58e56e1f2c114ed0530666bd4b50e54449 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 7 Apr 2025 19:27:19 -0400 Subject: [PATCH 19/85] v0.0.9 (#456) bump version. --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ade2c52..f850d629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.8" +version = "0.0.9" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index a6018eeb..d1c27225 100644 --- a/uv.lock +++ b/uv.lock @@ -1087,7 +1087,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.8" +version = "0.0.9" source = { editable = "." } dependencies = [ { name = "griffe" }, From ece647b93f02e8ed3c8c3ee1d10c035e10c65984 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 8 Apr 2025 22:41:48 +0900 Subject: [PATCH 20/85] Add i18n support to the documents (#460) --- Makefile | 5 + docs/ja/agents.md | 147 ++++++++++++++++++ docs/ja/config.md | 94 ++++++++++++ docs/ja/context.md | 77 ++++++++++ docs/ja/examples.md | 40 +++++ docs/ja/guardrails.md | 154 +++++++++++++++++++ docs/ja/handoffs.md | 113 ++++++++++++++ docs/ja/index.md | 52 +++++++ docs/ja/mcp.md | 59 ++++++++ docs/ja/models.md | 93 ++++++++++++ docs/ja/multi_agent.md | 37 +++++ docs/ja/quickstart.md | 188 +++++++++++++++++++++++ docs/ja/results.md | 52 +++++++ docs/ja/running_agents.md | 95 ++++++++++++ docs/ja/streaming.md | 87 +++++++++++ docs/ja/tools.md | 269 +++++++++++++++++++++++++++++++++ docs/ja/tracing.md | 102 +++++++++++++ docs/ja/visualization.md | 83 ++++++++++ docs/ja/voice/pipeline.md | 75 +++++++++ docs/ja/voice/quickstart.md | 194 ++++++++++++++++++++++++ docs/ja/voice/tracing.md | 14 ++ docs/scripts/translate_docs.py | 158 +++++++++++++++++++ mkdocs.yml | 188 ++++++++++++++--------- pyproject.toml | 2 + uv.lock | 16 ++ 25 files changed, 2321 insertions(+), 73 deletions(-) create mode 100644 docs/ja/agents.md create mode 100644 docs/ja/config.md create mode 100644 docs/ja/context.md create mode 100644 docs/ja/examples.md create mode 100644 docs/ja/guardrails.md create mode 100644 docs/ja/handoffs.md create mode 100644 docs/ja/index.md create mode 100644 docs/ja/mcp.md create mode 100644 docs/ja/models.md create mode 100644 docs/ja/multi_agent.md create mode 100644 docs/ja/quickstart.md create mode 100644 docs/ja/results.md create mode 100644 docs/ja/running_agents.md create mode 100644 docs/ja/streaming.md create mode 100644 docs/ja/tools.md create mode 100644 docs/ja/tracing.md create mode 100644 docs/ja/visualization.md create mode 100644 docs/ja/voice/pipeline.md create mode 100644 docs/ja/voice/quickstart.md create mode 100644 docs/ja/voice/tracing.md create mode 100644 docs/scripts/translate_docs.py diff --git a/Makefile b/Makefile index accf2d91..5c6aba42 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,11 @@ old_version_tests: build-docs: uv run mkdocs build +.PHONY: build-full-docs +build-full-docs: + uv run docs/scripts/translate_docs.py + uv run mkdocs build + .PHONY: serve-docs serve-docs: uv run mkdocs serve diff --git a/docs/ja/agents.md b/docs/ja/agents.md new file mode 100644 index 00000000..e9e303bc --- /dev/null +++ b/docs/ja/agents.md @@ -0,0 +1,147 @@ +# エージェント + +エージェント はアプリケーションの基本的な構成要素です。エージェント は、大規模言語モデル(LLM)であり、 instructions と tools を用いて設定されます。 + +## 基本設定 + +エージェント の設定でよく使われるプロパティは以下の通りです。 + +- `instructions` : 開発者メッセージまたはシステムプロンプトとも呼ばれます。 +- `model` : 使用する LLM を指定します。オプションで `model_settings` を指定し、temperature や top_p などのモデル調整パラメータを設定できます。 +- `tools` : エージェント がタスクを達成するために使用できるツールです。 + +```python +from agents import Agent, ModelSettings, function_tool + +@function_tool +def get_weather(city: str) -> str: + return f"The weather in {city} is sunny" + +agent = Agent( + name="Haiku agent", + instructions="Always respond in haiku form", + model="o3-mini", + tools=[get_weather], +) +``` + +## コンテキスト + +エージェント は、 `context` 型に対してジェネリックです。コンテキストは依存性注入のためのツールであり、作成したオブジェクトを `Runner.run()` に渡すことで、各エージェント、ツール、ハンドオフなどに渡されます。これはエージェントの実行に必要な依存関係や状態をまとめて保持するためのものです。任意の Python オブジェクトをコンテキストとして提供できます。 + +```python +@dataclass +class UserContext: + uid: str + is_pro_user: bool + + async def fetch_purchases() -> list[Purchase]: + return ... + +agent = Agent[UserContext]( + ..., +) +``` + +## 出力タイプ + +デフォルトでは、エージェント はプレーンテキスト(つまり `str` )を出力します。特定の型の出力を生成させたい場合は、 `output_type` パラメータを使用します。一般的には [Pydantic](https://docs.pydantic.dev/) オブジェクトを使用しますが、Pydantic の [TypeAdapter](https://docs.pydantic.dev/latest/api/type_adapter/) でラップ可能な型(データクラス、リスト、TypedDict など)であればどのような型でもサポートしています。 + +```python +from pydantic import BaseModel +from agents import Agent + + +class CalendarEvent(BaseModel): + name: str + date: str + participants: list[str] + +agent = Agent( + name="Calendar extractor", + instructions="Extract calendar events from text", + output_type=CalendarEvent, +) +``` + +!!! note + + `output_type` を指定すると、モデルは通常のプレーンテキストのレスポンスではなく、 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用します。 + +## ハンドオフ + +ハンドオフ は、エージェント が処理を委譲できるサブエージェントです。ハンドオフのリストを提供すると、エージェント は必要に応じてそれらに処理を委譲できます。これは、特定のタスクに特化したモジュール型のエージェントを組み合わせて調整するための強力なパターンです。詳細は [ハンドオフ](handoffs.md) のドキュメントを参照してください。 + +```python +from agents import Agent + +booking_agent = Agent(...) +refund_agent = Agent(...) + +triage_agent = Agent( + name="Triage agent", + instructions=( + "Help the user with their questions." + "If they ask about booking, handoff to the booking agent." + "If they ask about refunds, handoff to the refund agent." + ), + handoffs=[booking_agent, refund_agent], +) +``` + +## 動的な instructions + +多くの場合、エージェント 作成時に instructions を指定しますが、関数を通じて動的に instructions を提供することも可能です。この関数はエージェントとコンテキストを受け取り、プロンプトを返します。通常の関数と `async` 関数の両方が使用可能です。 + +```python +def dynamic_instructions( + context: RunContextWrapper[UserContext], agent: Agent[UserContext] +) -> str: + return f"The user's name is {context.context.name}. Help them with their questions." + + +agent = Agent[UserContext]( + name="Triage agent", + instructions=dynamic_instructions, +) +``` + +## ライフサイクルイベント(フック) + +エージェント のライフサイクルを監視したい場合があります。例えば、イベントをログに記録したり、特定のイベント発生時にデータを事前取得したりできます。エージェント のライフサイクルにフックするには、 `hooks` プロパティを使用します。[`AgentHooks`][agents.lifecycle.AgentHooks] クラスをサブクラス化し、関心のあるメソッドをオーバーライドします。 + +## ガードレール + +ガードレール を使用すると、エージェント の実行と並行してユーザー入力に対するチェックや検証を実行できます。例えば、ユーザー入力の関連性を検証できます。詳細は [ガードレール](guardrails.md) のドキュメントを参照してください。 + +## エージェント の複製(クローン) + +エージェント の `clone()` メソッドを使用すると、エージェント を複製し、必要に応じてプロパティを変更できます。 + +```python +pirate_agent = Agent( + name="Pirate", + instructions="Write like a pirate", + model="o3-mini", +) + +robot_agent = pirate_agent.clone( + name="Robot", + instructions="Write like a robot", +) +``` + +## ツール使用の強制 + +ツールのリストを提供しても、必ずしも LLM がツールを使用するとは限りません。ツールの使用を強制するには、 [`ModelSettings.tool_choice`][agents.model_settings.ModelSettings.tool_choice] を設定します。有効な値は以下の通りです。 + +1. `auto` : LLM がツールを使用するかどうかを決定します。 +2. `required` : LLM にツールの使用を強制します(ただし、どのツールを使用するかは LLM が判断します)。 +3. `none` : LLM にツールを使用しないことを強制します。 +4. 特定の文字列(例: `my_tool` )を設定すると、LLM はその特定のツールを使用することを強制されます。 + +!!! note + + 無限ループを防ぐため、フレームワークはツール呼び出し後に自動的に `tool_choice` を "auto" にリセットします。この動作は [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice] で設定可能です。無限ループは、ツールの実行結果が LLM に送信され、再びツール呼び出しが生成されるために発生します。 + + ツール呼び出し後に完全に停止させたい場合(自動モードで続行しない場合)は、 [`Agent.tool_use_behavior="stop_on_first_tool"`] を設定すると、ツールの出力を最終レスポンスとして直接使用し、それ以上の LLM 処理を行いません。 \ No newline at end of file diff --git a/docs/ja/config.md b/docs/ja/config.md new file mode 100644 index 00000000..6a764eb7 --- /dev/null +++ b/docs/ja/config.md @@ -0,0 +1,94 @@ +# SDK の設定 + +## API キーとクライアント + +デフォルトでは、SDK はインポート時に LLM リクエストおよびトレーシング用の環境変数 `OPENAI_API_KEY` を参照します。アプリケーションの起動前にこの環境変数を設定できない場合は、[set_default_openai_key()][agents.set_default_openai_key] 関数を使用してキーを設定できます。 + +```python +from agents import set_default_openai_key + +set_default_openai_key("sk-...") +``` + +また、使用する OpenAI クライアントを設定することも可能です。デフォルトでは、SDK は環境変数または上記で設定したデフォルトキーを使用して `AsyncOpenAI` インスタンスを作成します。これを変更するには、[set_default_openai_client()][agents.set_default_openai_client] 関数を使用します。 + +```python +from openai import AsyncOpenAI +from agents import set_default_openai_client + +custom_client = AsyncOpenAI(base_url="...", api_key="...") +set_default_openai_client(custom_client) +``` + +さらに、使用する OpenAI API をカスタマイズすることもできます。デフォルトでは OpenAI Responses API を使用しますが、[set_default_openai_api()][agents.set_default_openai_api] 関数を使用して Chat Completions API に変更できます。 + +```python +from agents import set_default_openai_api + +set_default_openai_api("chat_completions") +``` + +## トレーシング + +トレーシングはデフォルトで有効になっています。デフォルトでは、前述のセクションで設定した OpenAI API キー(環境変数またはデフォルトキー)を使用します。トレーシング専用の API キーを設定するには、[`set_tracing_export_api_key`][agents.set_tracing_export_api_key] 関数を使用します。 + +```python +from agents import set_tracing_export_api_key + +set_tracing_export_api_key("sk-...") +``` + +また、[`set_tracing_disabled()`][agents.set_tracing_disabled] 関数を使用してトレーシングを完全に無効化することもできます。 + +```python +from agents import set_tracing_disabled + +set_tracing_disabled(True) +``` + +## デバッグログ + +SDK はデフォルトでハンドラーが設定されていない 2 つの Python ロガーを持っています。デフォルトでは、警告およびエラーが `stdout` に送信され、それ以外のログは抑制されます。 + +詳細なログを有効にするには、[`enable_verbose_stdout_logging()`][agents.enable_verbose_stdout_logging] 関数を使用します。 + +```python +from agents import enable_verbose_stdout_logging + +enable_verbose_stdout_logging() +``` + +また、ハンドラー、フィルター、フォーマッターなどを追加してログをカスタマイズすることも可能です。詳細については、[Python logging ガイド](https://docs.python.org/3/howto/logging.html) を参照してください。 + +```python +import logging + +logger = logging.getLogger("openai.agents") # トレーシング用ロガーの場合は openai.agents.tracing + +# すべてのログを表示する場合 +logger.setLevel(logging.DEBUG) +# INFO 以上を表示する場合 +logger.setLevel(logging.INFO) +# WARNING 以上を表示する場合 +logger.setLevel(logging.WARNING) +# その他、必要に応じて設定 + +# 必要に応じてカスタマイズ可能ですが、デフォルトでは `stderr` に出力されます +logger.addHandler(logging.StreamHandler()) +``` + +### ログ内の機密データ + +一部のログには機密データ(ユーザーデータなど)が含まれる場合があります。このデータのログ記録を無効化したい場合は、以下の環境変数を設定してください。 + +LLM の入力および出力のログ記録を無効化する場合: + +```bash +export OPENAI_AGENTS_DONT_LOG_MODEL_DATA=1 +``` + +ツールの入力および出力のログ記録を無効化する場合: + +```bash +export OPENAI_AGENTS_DONT_LOG_TOOL_DATA=1 +``` \ No newline at end of file diff --git a/docs/ja/context.md b/docs/ja/context.md new file mode 100644 index 00000000..9ad6dc33 --- /dev/null +++ b/docs/ja/context.md @@ -0,0 +1,77 @@ +# コンテキスト管理 + +コンテキストという用語は多義的です。主に次の 2 種類のコンテキストがあります。 + +1. コード内でローカルに利用可能なコンテキスト:これは、関数ツールの実行時、`on_handoff` などのコールバック時、ライフサイクルフック時などに必要となるデータや依存関係です。 +2. LLM が利用可能なコンテキスト:これは、LLM が応答を生成する際に参照するデータです。 + +## ローカルコンテキスト + +これは [`RunContextWrapper`][agents.run_context.RunContextWrapper] クラスと、その内部の [`context`][agents.run_context.RunContextWrapper.context] プロパティを通じて表現されます。動作の仕組みは以下の通りです。 + +1. 任意の Python オブジェクトを作成します。一般的にはデータクラスや Pydantic オブジェクトを使用します。 +2. 作成したオブジェクトを各種の実行メソッドに渡します(例:`Runner.run(..., context=whatever)`)。 +3. すべての関数ツール呼び出しやライフサイクルフックなどには、ラッパーオブジェクト `RunContextWrapper[T]` が渡されます。ここで `T` はコンテキストオブジェクトの型を表し、`wrapper.context` を通じてアクセスできます。 + +**最も重要な点** は、特定のエージェント実行において、すべてのエージェント、関数ツール、ライフサイクルフックなどが同じコンテキストの _型_ を使用しなければならないということです。 + +コンテキストは以下のような用途で使用できます。 + +- 実行時のコンテキストデータ(ユーザー名や UID など、ユーザーに関する情報) +- 依存関係(ロガーオブジェクト、データ取得用オブジェクトなど) +- ヘルパー関数 + +!!! danger "注意" + + コンテキストオブジェクトは LLM には送信され **ません**。これは純粋にローカルなオブジェクトであり、読み取り、書き込み、メソッド呼び出しが可能です。 + +```python +import asyncio +from dataclasses import dataclass + +from agents import Agent, RunContextWrapper, Runner, function_tool + +@dataclass +class UserInfo: # (1)! + name: str + uid: int + +@function_tool +async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str: # (2)! + return f"User {wrapper.context.name} is 47 years old" + +async def main(): + user_info = UserInfo(name="John", uid=123) + + agent = Agent[UserInfo]( # (3)! + name="Assistant", + tools=[fetch_user_age], + ) + + result = await Runner.run( # (4)! + starting_agent=agent, + input="What is the age of the user?", + context=user_info, + ) + + print(result.final_output) # (5)! + # The user John is 47 years old. + +if __name__ == "__main__": + asyncio.run(main()) +``` + +1. これがコンテキストオブジェクトです。ここではデータクラスを使用していますが、任意の型を使用できます。 +2. これは関数ツールです。`RunContextWrapper[UserInfo]` を引数として受け取り、コンテキストからデータを読み取っています。 +3. エージェントにジェネリック型 `UserInfo` を指定することで、型チェッカーがエラーを検出できるようにしています(例えば、異なるコンテキスト型を取るツールを渡そうとした場合など)。 +4. コンテキストを `run` 関数に渡しています。 +5. エージェントが正しくツールを呼び出し、年齢を取得しています。 + +## エージェント / LLM コンテキスト + +LLM が呼び出される際、参照できるデータは会話履歴に含まれるもの **のみ** です。そのため、新しいデータを LLM に提供したい場合は、会話履歴に含まれるようにする必要があります。これを実現する方法はいくつかあります。 + +1. エージェントの `instructions` に追加する方法です。これは「システムプロンプト」または「開発者メッセージ」とも呼ばれます。システムプロンプトは静的な文字列でもよく、またはコンテキストを受け取って文字列を出力する動的な関数でも構いません。これは常に有用な情報(ユーザー名や現在の日付など)を提供する際によく使われる手法です。 +2. `Runner.run` 関数を呼び出す際の `input` に追加する方法です。これは `instructions` と似ていますが、[指揮系統(chain of command)](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command) の下位に位置するメッセージを設定できます。 +3. 関数ツールを通じて公開する方法です。これは _オンデマンド_ なコンテキストに有効で、LLM が必要なデータを判断し、そのデータを取得するためにツールを呼び出します。 +4. 検索(retrieval)やウェブ検索を使用する方法です。これらはファイルやデータベースから関連データを取得(検索)したり、ウェブからデータを取得(ウェブ検索)したりする特殊なツールです。これは関連するコンテキストデータに基づいて応答を「根拠付ける(grounding)」際に有効です。 \ No newline at end of file diff --git a/docs/ja/examples.md b/docs/ja/examples.md new file mode 100644 index 00000000..6d9252aa --- /dev/null +++ b/docs/ja/examples.md @@ -0,0 +1,40 @@ +# コード例 + +SDK のさまざまな実装サンプルについては、[リポジトリ](https://github.com/openai/openai-agents-python/tree/main/examples) のコード例セクションをご覧ください。コード例は、異なるパターンや機能を示す複数のカテゴリに分類されています。 + +## カテゴリ + +- **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** + このカテゴリのコード例では、一般的なエージェント設計パターンを示しています。例えば、 + + - 決定論的ワークフロー + - ツールとしてのエージェント + - エージェントの並列実行 + +- **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** + SDK の基本的な機能を示すコード例です。例えば、 + + - 動的なシステムプロンプト + - ストリーミング出力 + - ライフサイクルイベント + +- **[tool examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** + Web 検索やファイル検索などの OpenAI がホストするツールの実装方法と、それらをエージェントに統合する方法を学べます。 + +- **[model providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** + SDK で OpenAI 以外のモデルを使用する方法を確認できます。 + +- **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** + エージェントのハンドオフに関する実践的なコード例を参照できます。 + +- **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** + MCP を使用したエージェントの構築方法を学べます。 + +- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** および **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** + 実際のアプリケーションを示す、より具体的なコード例です。 + + - **customer_service**:航空会社向けのカスタマーサービスシステムのコード例。 + - **research_bot**:シンプルな詳細調査クローンのコード例。 + +- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** + TTS および STT モデルを使用した音声エージェントのコード例を参照できます。 \ No newline at end of file diff --git a/docs/ja/guardrails.md b/docs/ja/guardrails.md new file mode 100644 index 00000000..1c55b0e0 --- /dev/null +++ b/docs/ja/guardrails.md @@ -0,0 +1,154 @@ +# ガードレール + +ガードレールは、エージェント と _並行して_ 実行され、ユーザー入力のチェックや検証を可能にします。例えば、顧客のリクエストを処理するために非常に高性能(したがって遅く高価)なモデルを使用する エージェント があるとします。悪意のあるユーザーがそのモデルに数学の宿題を解かせるようなことは避けたいでしょう。そのため、高速で安価なモデルを使った ガードレール を実行できます。ガードレール が悪意のある使用を検知すると、即座にエラーを発生させ、高価なモデルの実行を停止し、時間とコストを節約できます。 + +ガードレール には 2 種類あります: + +1. 入力ガードレール:最初のユーザー入力に対して実行されます。 +2. 出力ガードレール:最終的な エージェント の出力に対して実行されます。 + +## 入力ガードレール + +入力ガードレール は次の 3 ステップで実行されます: + +1. 最初に、ガードレール は エージェント に渡されたものと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`InputGuardrailResult`][agents.guardrail.InputGuardrailResult] にラップされます。 +3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な応答や例外処理が可能になります。 + +!!! Note + + 入力ガードレール はユーザー入力に対して実行されるため、エージェント の ガードレール は、その エージェント が *最初の* エージェント である場合にのみ実行されます。なぜ `guardrails` プロパティが エージェント にあり、`Runner.run` に渡されないのか疑問に思うかもしれませんが、これは ガードレール が実際の エージェント に関連付けられる傾向があるためです。異なる エージェント には異なる ガードレール を実行するため、コードを同じ場所に配置することで可読性が向上します。 + +## 出力ガードレール + +出力ガードレール は次の 3 ステップで実行されます: + +1. 最初に、ガードレール は エージェント に渡されたものと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`OutputGuardrailResult`][agents.guardrail.OutputGuardrailResult] にラップされます。 +3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な応答や例外処理が可能になります。 + +!!! Note + + 出力ガードレール は最終的な エージェント の出力に対して実行されるため、エージェント の ガードレール は、その エージェント が *最後の* エージェント である場合にのみ実行されます。入力ガードレール と同様に、これは ガードレール が実際の エージェント に関連付けられる傾向があるためです。異なる エージェント には異なる ガードレール を実行するため、コードを同じ場所に配置することで可読性が向上します。 + +## トリップワイヤ(Tripwires) + +入力または出力が ガードレール を通過できない場合、ガードレール はトリップワイヤを使ってこれを通知します。トリップワイヤがトリガーされた ガードレール を検知すると、即座に `{Input,Output}GuardrailTripwireTriggered` 例外を発生させ、エージェント の実行を停止します。 + +## ガードレール の実装 + +入力を受け取り、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を返す関数を提供する必要があります。この例では、内部で エージェント を実行することでこれを実現します。 + +```python +from pydantic import BaseModel +from agents import ( + Agent, + GuardrailFunctionOutput, + InputGuardrailTripwireTriggered, + RunContextWrapper, + Runner, + TResponseInputItem, + input_guardrail, +) + +class MathHomeworkOutput(BaseModel): + is_math_homework: bool + reasoning: str + +guardrail_agent = Agent( # (1)! + name="Guardrail check", + instructions="Check if the user is asking you to do their math homework.", + output_type=MathHomeworkOutput, +) + + +@input_guardrail +async def math_guardrail( # (2)! + ctx: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem] +) -> GuardrailFunctionOutput: + result = await Runner.run(guardrail_agent, input, context=ctx.context) + + return GuardrailFunctionOutput( + output_info=result.final_output, # (3)! + tripwire_triggered=result.final_output.is_math_homework, + ) + + +agent = Agent( # (4)! + name="Customer support agent", + instructions="You are a customer support agent. You help customers with their questions.", + input_guardrails=[math_guardrail], +) + +async def main(): + # This should trip the guardrail + try: + await Runner.run(agent, "Hello, can you help me solve for x: 2x + 3 = 11?") + print("Guardrail didn't trip - this is unexpected") + + except InputGuardrailTripwireTriggered: + print("Math homework guardrail tripped") +``` + +1. この エージェント を ガードレール関数 内で使用します。 +2. これは エージェント の入力とコンテキストを受け取り、結果を返す ガードレール関数 です。 +3. ガードレール の結果に追加情報を含めることができます。 +4. これはワークフローを定義する実際の エージェント です。 + +出力ガードレール も同様です。 + +```python +from pydantic import BaseModel +from agents import ( + Agent, + GuardrailFunctionOutput, + OutputGuardrailTripwireTriggered, + RunContextWrapper, + Runner, + output_guardrail, +) +class MessageOutput(BaseModel): # (1)! + response: str + +class MathOutput(BaseModel): # (2)! + reasoning: str + is_math: bool + +guardrail_agent = Agent( + name="Guardrail check", + instructions="Check if the output includes any math.", + output_type=MathOutput, +) + +@output_guardrail +async def math_guardrail( # (3)! + ctx: RunContextWrapper, agent: Agent, output: MessageOutput +) -> GuardrailFunctionOutput: + result = await Runner.run(guardrail_agent, output.response, context=ctx.context) + + return GuardrailFunctionOutput( + output_info=result.final_output, + tripwire_triggered=result.final_output.is_math, + ) + +agent = Agent( # (4)! + name="Customer support agent", + instructions="You are a customer support agent. You help customers with their questions.", + output_guardrails=[math_guardrail], + output_type=MessageOutput, +) + +async def main(): + # This should trip the guardrail + try: + await Runner.run(agent, "Hello, can you help me solve for x: 2x + 3 = 11?") + print("Guardrail didn't trip - this is unexpected") + + except OutputGuardrailTripwireTriggered: + print("Math output guardrail tripped") +``` + +1. これは実際の エージェント の出力タイプです。 +2. これは ガードレール の出力タイプです。 +3. これは エージェント の出力を受け取り、結果を返す ガードレール関数 です。 +4. これはワークフローを定義する実際の エージェント です。 \ No newline at end of file diff --git a/docs/ja/handoffs.md b/docs/ja/handoffs.md new file mode 100644 index 00000000..2d5433f4 --- /dev/null +++ b/docs/ja/handoffs.md @@ -0,0 +1,113 @@ +# ハンドオフ + +ハンドオフを使用すると、あるエージェントが別のエージェントにタスクを委任できます。これは、異なるエージェントがそれぞれ特定の分野を専門としている場合に特に役立ちます。例えば、カスタマーサポートアプリでは、注文状況、返金、FAQ などのタスクをそれぞれ専門に処理するエージェントを用意できます。 + +ハンドオフは、LLM に対してツールとして表現されます。例えば、`Refund Agent` という名前のエージェントへのハンドオフがある場合、そのツールは `transfer_to_refund_agent` と呼ばれます。 + +## ハンドオフの作成 + +すべてのエージェントは [`handoffs`][agents.agent.Agent.handoffs] パラメータを持ち、これは直接 `Agent` を指定するか、またはカスタマイズされた `Handoff` オブジェクトを指定できます。 + +Agents SDK が提供する [`handoff()`][agents.handoffs.handoff] 関数を使用してハンドオフを作成できます。この関数を使用すると、ハンドオフ先のエージェントを指定し、オプションでオーバーライドや入力フィルターを設定できます。 + +### 基本的な使用方法 + +シンプルなハンドオフの作成方法は以下の通りです。 + +```python +from agents import Agent, handoff + +billing_agent = Agent(name="Billing agent") +refund_agent = Agent(name="Refund agent") + +# (1)! +triage_agent = Agent(name="Triage agent", handoffs=[billing_agent, handoff(refund_agent)]) +``` + +1. エージェントを直接指定する方法(`billing_agent` のように)と、`handoff()` 関数を使用する方法があります。 + +### `handoff()` 関数を使用したハンドオフのカスタマイズ + +[`handoff()`][agents.handoffs.handoff] 関数を使用すると、以下の項目をカスタマイズできます。 + +- `agent`:ハンドオフ先のエージェントを指定します。 +- `tool_name_override`:デフォルトでは `Handoff.default_tool_name()` 関数が使用され、`transfer_to_` という形式になりますが、これをオーバーライドできます。 +- `tool_description_override`:デフォルトのツール説明(`Handoff.default_tool_description()`)をオーバーライドします。 +- `on_handoff`:ハンドオフが呼び出された際に実行されるコールバック関数です。これは、ハンドオフが呼ばれた時点でデータ取得などの処理を開始する場合に便利です。この関数はエージェントのコンテキストを受け取り、オプションで LLM が生成した入力も受け取れます。入力データは `input_type` パラメータで制御されます。 +- `input_type`:ハンドオフが期待する入力の型を指定します(オプション)。 +- `input_filter`:次のエージェントが受け取る入力をフィルタリングできます。詳細は後述します。 + +```python +from agents import Agent, handoff, RunContextWrapper + +def on_handoff(ctx: RunContextWrapper[None]): + print("Handoff called") + +agent = Agent(name="My agent") + +handoff_obj = handoff( + agent=agent, + on_handoff=on_handoff, + tool_name_override="custom_handoff_tool", + tool_description_override="Custom description", +) +``` + +## ハンドオフの入力 + +特定の状況では、LLM がハンドオフを呼び出す際に何らかのデータを提供するようにしたい場合があります。例えば、「Escalation agent(エスカレーションエージェント)」へのハンドオフを考えると、ログ記録のために理由を提供してほしい場合があります。 + +```python +from pydantic import BaseModel + +from agents import Agent, handoff, RunContextWrapper + +class EscalationData(BaseModel): + reason: str + +async def on_handoff(ctx: RunContextWrapper[None], input_data: EscalationData): + print(f"Escalation agent called with reason: {input_data.reason}") + +agent = Agent(name="Escalation agent") + +handoff_obj = handoff( + agent=agent, + on_handoff=on_handoff, + input_type=EscalationData, +) +``` + +## 入力フィルター + +ハンドオフが発生すると、新しいエージェントが会話を引き継ぎ、それまでの会話履歴全体を参照できるようになります。これを変更したい場合は、[`input_filter`][agents.handoffs.Handoff.input_filter] を設定できます。入力フィルターは、既存の入力を [`HandoffInputData`][agents.handoffs.HandoffInputData] として受け取り、新しい `HandoffInputData` を返す関数です。 + +よくあるパターン(例えば履歴からすべてのツール呼び出しを削除するなど)は、[`agents.extensions.handoff_filters`][] にあらかじめ実装されています。 + +```python +from agents import Agent, handoff +from agents.extensions import handoff_filters + +agent = Agent(name="FAQ agent") + +handoff_obj = handoff( + agent=agent, + input_filter=handoff_filters.remove_all_tools, # (1)! +) +``` + +1. これにより、`FAQ agent` が呼び出された際に履歴からすべてのツールが自動的に削除されます。 + +## 推奨プロンプト + +LLM がハンドオフを適切に理解できるようにするため、エージェントのプロンプトにハンドオフに関する情報を含めることを推奨します。推奨されるプレフィックスは [`agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`][] に用意されています。または、[`agents.extensions.handoff_prompt.prompt_with_handoff_instructions`][] を呼び出して、推奨データをプロンプトに自動的に追加できます。 + +```python +from agents import Agent +from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX + +billing_agent = Agent( + name="Billing agent", + instructions=f"""{RECOMMENDED_PROMPT_PREFIX} + .""", +) +``` \ No newline at end of file diff --git a/docs/ja/index.md b/docs/ja/index.md new file mode 100644 index 00000000..87b419fd --- /dev/null +++ b/docs/ja/index.md @@ -0,0 +1,52 @@ +# OpenAI Agents SDK + +[OpenAI Agents SDK](https://github.com/openai/openai-agents-python) は、軽量で使いやすく、抽象化を最小限に抑えたエージェントベースの AI アプリケーションを構築できるようにします。これは、以前のエージェント実験である [Swarm](https://github.com/openai/swarm/tree/main) を本番環境向けにアップグレードしたものです。Agents SDK は非常に少数の基本コンポーネント(primitives)で構成されています。 + +- **エージェント** : instructions と tools を備えた LLM +- **ハンドオフ** : 特定のタスクを他のエージェントに委任する仕組み +- **ガードレール** : エージェントへの入力を検証する仕組み + +これらの基本コンポーネントを Python と組み合わせることで、ツールとエージェント間の複雑な関係を表現でき、急な学習曲線なしに実世界のアプリケーションを構築できます。さらに SDK には、エージェントのフローを視覚化・デバッグし、評価やモデルのファインチューニングまで可能にする組み込みの **トレーシング** 機能が備わっています。 + +## Agents SDK を使う理由 + +SDK は以下の 2 つの設計原則に基づいています。 + +1. 利用価値のある十分な機能を備えつつ、迅速に習得できるよう基本コンポーネントを最小限に抑える。 +2. そのままでも優れた動作をするが、必要に応じて細かくカスタマイズ可能。 + +SDK の主な機能は以下の通りです。 + +- **エージェントループ** : ツールの呼び出し、LLM への実行結果の送信、LLM が完了するまでのループ処理を組み込みで提供。 +- **Python ファースト** : 新しい抽象化を学ぶ必要なく、Python の言語機能を使ってエージェントの連携やチェーンを構築可能。 +- **ハンドオフ** : 複数のエージェント間での調整やタスク委任を可能にする強力な機能。 +- **ガードレール** : エージェントと並行して入力の検証やチェックを実行し、チェックが失敗した場合は早期に処理を中断。 +- **関数ツール** : 任意の Python 関数をツールに変換し、自動的なスキーマ生成と Pydantic による検証を提供。 +- **トレーシング** : ワークフローの視覚化、デバッグ、モニタリングを可能にする組み込みのトレーシング機能。OpenAI が提供する評価、ファインチューニング、蒸留ツールも利用可能。 + +## インストール + +```bash +pip install openai-agents +``` + +## Hello World の例 + +```python +from agents import Agent, Runner + +agent = Agent(name="Assistant", instructions="You are a helpful assistant") + +result = Runner.run_sync(agent, "Write a haiku about recursion in programming.") +print(result.final_output) + +# Code within the code, +# Functions calling themselves, +# Infinite loop's dance. +``` + +(_実行する場合は、環境変数 `OPENAI_API_KEY` を設定してください。_) + +```bash +export OPENAI_API_KEY=sk-... +``` \ No newline at end of file diff --git a/docs/ja/mcp.md b/docs/ja/mcp.md new file mode 100644 index 00000000..52ed1558 --- /dev/null +++ b/docs/ja/mcp.md @@ -0,0 +1,59 @@ +# Model context protocol (MCP) + +[Model context protocol](https://modelcontextprotocol.io/introduction)(別名 MCP)は、LLM にツールやコンテキストを提供するための方法です。MCP のドキュメントから引用します: + +> MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンなプロトコルです。MCP は AI アプリケーションにおける USB-C ポートのようなものです。USB-C がデバイスをさまざまな周辺機器やアクセサリに接続するための標準化された方法を提供するのと同様に、MCP は AI モデルをさまざまなデータソースやツールに接続するための標準化された方法を提供します。 + +Agents SDK は MCP をサポートしています。これにより、さまざまな MCP サーバーを使用して、エージェントにツールを提供できます。 + +## MCP サーバー + +現在、MCP の仕様では、使用するトランスポートメカニズムに基づいて 2 種類のサーバーが定義されています: + +1. **stdio** サーバーはアプリケーションのサブプロセスとして実行されます。これらは「ローカル」で実行されると考えることができます。 +2. **HTTP over SSE** サーバーはリモートで実行されます。これらには URL を介して接続します。 + +これらのサーバーに接続するには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] および [`MCPServerSse`][agents.mcp.server.MCPServerSse] クラスを使用できます。 + +例えば、[公式 MCP ファイルシステムサーバー](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem)を使用する場合は以下のようになります: + +```python +async with MCPServerStdio( + params={ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", samples_dir], + } +) as server: + tools = await server.list_tools() +``` + +## MCP サーバーの使用方法 + +MCP サーバーはエージェントに追加できます。Agents SDK はエージェントが実行されるたびに MCP サーバーの `list_tools()` を呼び出します。これにより、LLM は MCP サーバーのツールを認識します。LLM が MCP サーバーのツールを呼び出すと、SDK はそのサーバーの `call_tool()` を呼び出します。 + +```python +agent=Agent( + name="Assistant", + instructions="Use the tools to achieve the task", + mcp_servers=[mcp_server_1, mcp_server_2] +) +``` + +## キャッシュ + +エージェントが実行されるたびに、MCP サーバーの `list_tools()` が呼び出されます。特にサーバーがリモートの場合、これはレイテンシの原因となる可能性があります。ツールのリストを自動的にキャッシュするには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] の両方に `cache_tools_list=True` を渡します。これは、ツールのリストが変更されないことが確実な場合にのみ行ってください。 + +キャッシュを無効化したい場合は、サーバーの `invalidate_tools_cache()` を呼び出します。 + +## エンドツーエンドのコード例 + +完全な動作コード例については、[examples/mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp) を参照してください。 + +## トレーシング + +[トレーシング](./tracing.md) は、以下を含む MCP 操作を自動的にキャプチャします: + +1. MCP サーバーへのツール一覧取得の呼び出し +2. 関数呼び出しに関する MCP 関連情報 + +![MCP トレーシングのスクリーンショット](../assets/images/mcp-tracing.jpg) \ No newline at end of file diff --git a/docs/ja/models.md b/docs/ja/models.md new file mode 100644 index 00000000..b333666a --- /dev/null +++ b/docs/ja/models.md @@ -0,0 +1,93 @@ +# モデル + +Agents SDK は、OpenAI モデルを以下の 2 種類の形式で標準サポートしています。 + +- **推奨** : 新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使用して OpenAI API を呼び出す [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] +- [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使用して OpenAI API を呼び出す [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] + +## モデルの混在と組み合わせ + +単一のワークフロー内で、各エージェントごとに異なるモデルを使用したい場合があります。例えば、トリアージには小型で高速なモデルを使用し、複雑なタスクにはより大きく高性能なモデルを使用することができます。[`Agent`][agents.Agent] を設定する際、以下のいずれかの方法で特定のモデルを選択できます。 + +1. OpenAI モデルの名前を直接指定する。 +2. 任意のモデル名と、その名前をモデルインスタンスにマッピングできる [`ModelProvider`][agents.models.interface.ModelProvider] を指定する。 +3. [`Model`][agents.models.interface.Model] 実装を直接指定する。 + +!!! note + + SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形式をサポートしていますが、各ワークフローでは単一のモデル形式を使用することを推奨します。これは、2 つの形式が異なる機能やツールをサポートしているためです。ワークフローで複数のモデル形式を混在させる必要がある場合は、使用するすべての機能が両方の形式で利用可能であることを確認してください。 + +```python +from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel +import asyncio + +spanish_agent = Agent( + name="Spanish agent", + instructions="You only speak Spanish.", + model="o3-mini", # (1)! +) + +english_agent = Agent( + name="English agent", + instructions="You only speak English", + model=OpenAIChatCompletionsModel( # (2)! + model="gpt-4o", + openai_client=AsyncOpenAI() + ), +) + +triage_agent = Agent( + name="Triage agent", + instructions="Handoff to the appropriate agent based on the language of the request.", + handoffs=[spanish_agent, english_agent], + model="gpt-3.5-turbo", +) + +async def main(): + result = await Runner.run(triage_agent, input="Hola, ¿cómo estás?") + print(result.final_output) +``` + +1. OpenAI モデルの名前を直接指定しています。 +2. [`Model`][agents.models.interface.Model] 実装を指定しています。 + +## 他の LLM プロバイダーの使用 + +他の LLM プロバイダーを使用するには、以下の 3 つの方法があります(コード例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/))。 + +1. [`set_default_openai_client`][agents.set_default_openai_client] は、グローバルに `AsyncOpenAI` インスタンスを LLM クライアントとして使用したい場合に便利です。これは、LLM プロバイダーが OpenAI 互換の API エンドポイントを持ち、`base_url` と `api_key` を設定できる場合に使用します。設定可能なコード例は [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) を参照してください。 +2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルで指定します。これにより、「この実行におけるすべてのエージェントにカスタムモデルプロバイダーを使用する」と指定できます。設定可能なコード例は [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) を参照してください。 +3. [`Agent.model`][agents.agent.Agent.model] は特定のエージェントインスタンスにモデルを指定できます。これにより、異なるエージェントに異なるプロバイダーを組み合わせて使用できます。設定可能なコード例は [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) を参照してください。 + +`platform.openai.com` の API キーを持っていない場合は、`set_tracing_disabled()` を使用してトレーシングを無効化するか、[別のトレーシングプロセッサ](tracing.md) を設定することを推奨します。 + +!!! note + + これらのコード例では Chat Completions API / モデルを使用しています。これは、多くの LLM プロバイダーがまだ Responses API をサポートしていないためです。使用する LLM プロバイダーが Responses API をサポートしている場合は、Responses の使用を推奨します。 + +## 他の LLM プロバイダー使用時の一般的な問題 + +### トレーシングクライアントのエラー 401 + +トレーシングに関連するエラーが発生する場合、これはトレースが OpenAI サーバーにアップロードされるためであり、OpenAI API キーを持っていないことが原因です。解決方法は以下の 3 つです。 + +1. トレーシングを完全に無効化する : [`set_tracing_disabled(True)`][agents.set_tracing_disabled] +2. トレーシング用に OpenAI キーを設定する : [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースのアップロードのみに使用され、[platform.openai.com](https://platform.openai.com/) から取得する必要があります。 +3. OpenAI 以外のトレースプロセッサを使用する : 詳細は [トレーシングドキュメント](tracing.md#custom-tracing-processors) を参照してください。 + +### Responses API のサポート + +SDK はデフォルトで Responses API を使用しますが、多くの他の LLM プロバイダーはまだこれをサポートしていません。そのため、404 エラーなどが発生する場合があります。解決方法は以下の 2 つです。 + +1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出す。これは環境変数で `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に有効です。 +2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使用する。コード例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) を参照してください。 + +### structured outputs のサポート + +一部のモデルプロバイダーは [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。そのため、以下のようなエラーが発生する場合があります。 + +``` +BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}} +``` + +これは一部のモデルプロバイダーの制限であり、JSON 出力はサポートしていますが、出力に使用する `json_schema` を指定できないためです。この問題への対応を進めていますが、現時点では JSON スキーマ出力をサポートするプロバイダーを使用することを推奨します。そうでない場合、アプリケーションが不適切な JSON により頻繁に破損する可能性があります。 \ No newline at end of file diff --git a/docs/ja/multi_agent.md b/docs/ja/multi_agent.md new file mode 100644 index 00000000..4c8be96d --- /dev/null +++ b/docs/ja/multi_agent.md @@ -0,0 +1,37 @@ +# 複数のエージェントのオーケストレーション + +オーケストレーションとは、アプリ内でのエージェントの流れを指します。どのエージェントがどの順序で実行され、次に何を行うかをどのように決定するかを意味します。エージェントをオーケストレーションする主な方法は 2 つあります。 + +1. LLM に判断を任せる方法:LLM の知性を利用して計画、推論を行い、それに基づいて次のステップを決定します。 +2. コードによるオーケストレーション:コードを通じてエージェントの流れを決定します。 + +これらのパターンは組み合わせて使用することも可能です。それぞれのトレードオフについて以下で説明します。 + +## LLM によるオーケストレーション + +エージェントとは、LLM に instructions、tools、handoffs を与えたものです。これにより、自由度の高いタスクを与えられた場合でも、LLM は自律的にタスクの取り組み方を計画し、tools を使ってアクションを実行したりデータを取得したり、handoffs を使ってサブエージェントにタスクを委任したりできます。例えば、調査エージェントには以下のようなツールを装備できます。 + +- Web 検索:オンラインで情報を検索 +- ファイル検索と取得:独自データや接続先から情報を検索 +- コンピュータ操作:コンピュータ上でアクションを実行 +- コード実行:データ分析を実施 +- ハンドオフ:計画立案やレポート作成などに特化したエージェントにタスクを委任 + +このパターンは、自由度の高いタスクで LLM の知性に頼りたい場合に適しています。ここで重要な戦術は以下の通りです。 + +1. 良質なプロンプトに投資する。利用可能なツール、使用方法、動作パラメータを明確にする。 +2. アプリを監視し、繰り返し改善する。問題が発生した箇所を特定し、プロンプトを改善する。 +3. エージェントが自己内省し改善できるようにする。例えばループ内で実行し、自らを批評させたり、エラーメッセージを与えて改善させたりする。 +4. 汎用的なエージェントにあらゆるタスクを任せるのではなく、特定のタスクに秀でた専門エージェントを用意する。 +5. [evals](https://platform.openai.com/docs/guides/evals) に投資する。これによりエージェントを訓練し、タスクの遂行能力を向上させることができる。 + +## コードによるオーケストレーション + +LLM によるオーケストレーションは強力ですが、コードによるオーケストレーションは速度、コスト、パフォーマンスの観点でより決定論的で予測可能になります。ここでの一般的なパターンは以下の通りです。 + +- [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用して、コードで検査可能な適切な形式のデータを生成する。例えば、エージェントにタスクをいくつかのカテゴリに分類させ、そのカテゴリに基づいて次のエージェントを選択することができます。 +- 複数のエージェントを連鎖させ、一つのエージェントの出力を次のエージェントの入力に変換する。例えばブログ記事を書くというタスクを、調査、アウトライン作成、記事執筆、批評、改善という一連のステップに分解できます。 +- タスクを実行するエージェントを、評価とフィードバックを行うエージェントと共に `while` ループ内で実行し、評価エージェントが出力が一定の基準を満たすと判断するまで繰り返します。 +- 複数のエージェントを並列で実行する。例えば Python の基本コンポーネント (`asyncio.gather` など) を使用します。これは、互いに依存しない複数のタスクを高速に処理する場合に有効です。 + +[`examples/agent_patterns`](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) に多数のコード例があります。 \ No newline at end of file diff --git a/docs/ja/quickstart.md b/docs/ja/quickstart.md new file mode 100644 index 00000000..691761a2 --- /dev/null +++ b/docs/ja/quickstart.md @@ -0,0 +1,188 @@ +# クイックスタート + +## プロジェクトと仮想環境の作成 + +以下の操作は一度だけ行います。 + +```bash +mkdir my_project +cd my_project +python -m venv .venv +``` + +### 仮想環境の有効化 + +新しいターミナルセッションを開始するたびに、この操作を行います。 + +```bash +source .venv/bin/activate +``` + +### Agents SDK のインストール + +```bash +pip install openai-agents # または `uv add openai-agents` など +``` + +### OpenAI API キーの設定 + +API キーをお持ちでない場合は、[こちらの手順](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)に従って OpenAI API キーを作成してください。 + +```bash +export OPENAI_API_KEY=sk-... +``` + +## 最初のエージェントの作成 + +エージェント は、 instructions 、名前、およびオプションの設定( `model_config` など)で定義します。 + +```python +from agents import Agent + +agent = Agent( + name="Math Tutor", + instructions="You provide help with math problems. Explain your reasoning at each step and include examples", +) +``` + +## さらに複数のエージェントを追加する + +追加のエージェントも同様に定義できます。 `handoff_description` は、ハンドオフのルーティングを決定するための追加のコンテキストを提供します。 + +```python +from agents import Agent + +history_tutor_agent = Agent( + name="History Tutor", + handoff_description="Specialist agent for historical questions", + instructions="You provide assistance with historical queries. Explain important events and context clearly.", +) + +math_tutor_agent = Agent( + name="Math Tutor", + handoff_description="Specialist agent for math questions", + instructions="You provide help with math problems. Explain your reasoning at each step and include examples", +) +``` + +## ハンドオフの定義 + +各エージェントに対して、タスクを進める方法を決定するために選択可能なハンドオフのオプションを定義できます。 + +```python +triage_agent = Agent( + name="Triage Agent", + instructions="You determine which agent to use based on the user's homework question", + handoffs=[history_tutor_agent, math_tutor_agent] +) +``` + +## エージェントのオーケストレーションを実行する + +ワークフローが正しく動作し、トリアージエージェントが 2 つの専門エージェント間で適切にルーティングすることを確認しましょう。 + +```python +from agents import Runner + +async def main(): + result = await Runner.run(triage_agent, "What is the capital of France?") + print(result.final_output) +``` + +## ガードレールの追加 + +入力または出力に対して実行するカスタムのガードレールを定義できます。 + +```python +from agents import GuardrailFunctionOutput, Agent, Runner +from pydantic import BaseModel + +class HomeworkOutput(BaseModel): + is_homework: bool + reasoning: str + +guardrail_agent = Agent( + name="Guardrail check", + instructions="Check if the user is asking about homework.", + output_type=HomeworkOutput, +) + +async def homework_guardrail(ctx, agent, input_data): + result = await Runner.run(guardrail_agent, input_data, context=ctx.context) + final_output = result.final_output_as(HomeworkOutput) + return GuardrailFunctionOutput( + output_info=final_output, + tripwire_triggered=not final_output.is_homework, + ) +``` + +## すべてを統合する + +ハンドオフと入力ガードレールを使用して、ワークフロー全体を統合して実行しましょう。 + +```python +from agents import Agent, InputGuardrail, GuardrailFunctionOutput, Runner +from pydantic import BaseModel +import asyncio + +class HomeworkOutput(BaseModel): + is_homework: bool + reasoning: str + +guardrail_agent = Agent( + name="Guardrail check", + instructions="Check if the user is asking about homework.", + output_type=HomeworkOutput, +) + +math_tutor_agent = Agent( + name="Math Tutor", + handoff_description="Specialist agent for math questions", + instructions="You provide help with math problems. Explain your reasoning at each step and include examples", +) + +history_tutor_agent = Agent( + name="History Tutor", + handoff_description="Specialist agent for historical questions", + instructions="You provide assistance with historical queries. Explain important events and context clearly.", +) + +async def homework_guardrail(ctx, agent, input_data): + result = await Runner.run(guardrail_agent, input_data, context=ctx.context) + final_output = result.final_output_as(HomeworkOutput) + return GuardrailFunctionOutput( + output_info=final_output, + tripwire_triggered=not final_output.is_homework, + ) + +triage_agent = Agent( + name="Triage Agent", + instructions="You determine which agent to use based on the user's homework question", + handoffs=[history_tutor_agent, math_tutor_agent], + input_guardrails=[ + InputGuardrail(guardrail_function=homework_guardrail), + ], +) + +async def main(): + result = await Runner.run(triage_agent, "who was the first president of the united states?") + print(result.final_output) + + result = await Runner.run(triage_agent, "what is life") + print(result.final_output) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## トレースの確認 + +エージェントの実行中に何が起きたかを確認するには、[OpenAI ダッシュボードのトレースビューア](https://platform.openai.com/traces) にアクセスして、エージェント実行のトレースを確認してください。 + +## 次のステップ + +より複雑なエージェントフローの構築方法を学びましょう。 + +- [エージェント](agents.md) の設定方法について学ぶ。 +- [エージェントの実行方法](running_agents.md) について学ぶ。 +- [ツール](tools.md)、[ガードレール](guardrails.md)、[モデル](models.md) について学ぶ。 \ No newline at end of file diff --git a/docs/ja/results.md b/docs/ja/results.md new file mode 100644 index 00000000..13ff7cef --- /dev/null +++ b/docs/ja/results.md @@ -0,0 +1,52 @@ +# 実行結果 + +`Runner.run` メソッドを呼び出すと、以下のいずれかが返されます。 + +- `run` または `run_sync` を呼び出した場合は [`RunResult`][agents.result.RunResult] +- `run_streamed` を呼び出した場合は [`RunResultStreaming`][agents.result.RunResultStreaming] + +これらはいずれも [`RunResultBase`][agents.result.RunResultBase] を継承しており、ほとんどの有用な情報はここに含まれています。 + +## 最終出力 + +[`final_output`][agents.result.RunResultBase.final_output] プロパティには、最後に実行されたエージェントの最終出力が格納されています。これは以下のいずれかです。 + +- 最後のエージェントに `output_type` が定義されていない場合は `str` +- エージェントに `output_type` が定義されている場合は、その型(`last_agent.output_type`)のオブジェクト + +!!! note + + `final_output` は型としては `Any` です。これはハンドオフが発生する可能性があるため、静的に型付けできないためです。ハンドオフが発生すると、どのエージェントでも最後のエージェントになり得るため、可能な出力型の集合を静的に特定できません。 + +## 次のターンの入力 + +[`result.to_input_list()`][agents.result.RunResultBase.to_input_list] を使用すると、実行結果を入力リストに変換できます。これは、元々提供した入力とエージェントの実行中に生成された項目を連結したものです。これにより、あるエージェントの実行結果を別の実行に渡したり、ループ内で実行して毎回新しいユーザー入力を追加したりすることが容易になります。 + +## 最後のエージェント + +[`last_agent`][agents.result.RunResultBase.last_agent] プロパティには、最後に実行されたエージェントが格納されています。アプリケーションによっては、次回ユーザーが何かを入力する際に役立つことがあります。例えば、最前線のトリアージエージェントが言語特化型エージェントにハンドオフする場合、最後のエージェントを保存しておき、次回ユーザーがエージェントにメッセージを送る際に再利用できます。 + +## 新規項目 + +[`new_items`][agents.result.RunResultBase.new_items] プロパティには、実行中に生成された新しい項目が格納されています。これらの項目は [`RunItem`][agents.items.RunItem] です。RunItem は LLM によって生成された raw 項目をラップしています。 + +- [`MessageOutputItem`][agents.items.MessageOutputItem] は LLM からのメッセージを示します。raw 項目は生成されたメッセージです。 +- [`HandoffCallItem`][agents.items.HandoffCallItem] は LLM がハンドオフツールを呼び出したことを示します。raw 項目は LLM からのツール呼び出し項目です。 +- [`HandoffOutputItem`][agents.items.HandoffOutputItem] はハンドオフが発生したことを示します。raw 項目はハンドオフツール呼び出しに対するツールの応答です。また、この項目からソース/ターゲットエージェントにアクセスできます。 +- [`ToolCallItem`][agents.items.ToolCallItem] は LLM がツールを呼び出したことを示します。 +- [`ToolCallOutputItem`][agents.items.ToolCallOutputItem] はツールが呼び出されたことを示します。raw 項目はツールの応答です。また、この項目からツールの出力にアクセスできます。 +- [`ReasoningItem`][agents.items.ReasoningItem] は LLM による推論項目を示します。raw 項目は生成された推論内容です。 + +## その他の情報 + +### ガードレールの実行結果 + +[`input_guardrail_results`][agents.result.RunResultBase.input_guardrail_results] および [`output_guardrail_results`][agents.result.RunResultBase.output_guardrail_results] プロパティには、ガードレールの実行結果が格納されています(存在する場合)。ガードレールの実行結果にはログや保存したい有用な情報が含まれることがあるため、これらを利用可能にしています。 + +### raw レスポンス + +[`raw_responses`][agents.result.RunResultBase.raw_responses] プロパティには、LLM によって生成された [`ModelResponse`][agents.items.ModelResponse] が格納されています。 + +### 元の入力 + +[`input`][agents.result.RunResultBase.input] プロパティには、`run` メソッドに提供した元の入力が格納されています。ほとんどの場合、これは必要ありませんが、必要に応じて利用可能です。 \ No newline at end of file diff --git a/docs/ja/running_agents.md b/docs/ja/running_agents.md new file mode 100644 index 00000000..b02ee393 --- /dev/null +++ b/docs/ja/running_agents.md @@ -0,0 +1,95 @@ +# エージェントの実行 + +エージェントは [`Runner`][agents.run.Runner] クラスを通じて実行できます。以下の 3 つの方法があります。 + +1. [`Runner.run()`][agents.run.Runner.run]:非同期で実行され、[`RunResult`][agents.result.RunResult] を返します。 +2. [`Runner.run_sync()`][agents.run.Runner.run_sync]:同期メソッドで、内部的には `.run()` を呼び出します。 +3. [`Runner.run_streamed()`][agents.run.Runner.run_streamed]:非同期で実行され、[`RunResultStreaming`][agents.result.RunResultStreaming] を返します。LLM をストリーミングモードで呼び出し、受信したイベントを順次ストリームします。 + +```python +from agents import Agent, Runner + +async def main(): + agent = Agent(name="Assistant", instructions="You are a helpful assistant") + + result = await Runner.run(agent, "Write a haiku about recursion in programming.") + print(result.final_output) + # Code within the code, + # Functions calling themselves, + # Infinite loop's dance. +``` + +詳細は [results ガイド](results.md) を参照してください。 + +## エージェントの実行ループ + +`Runner` の run メソッドを使用する際、開始エージェントと入力を渡します。入力は文字列(ユーザーメッセージとして扱われる)または OpenAI Responses API の項目リストのいずれかです。 + +Runner は以下のループを実行します。 + +1. 現在のエージェントと入力を用いて LLM を呼び出します。 +2. LLM が出力を生成します。 + 1. LLM が `final_output` を返した場合、ループを終了し結果を返します。 + 2. LLM がハンドオフを行った場合、現在のエージェントと入力を更新し、再度ループを実行します。 + 3. LLM がツール呼び出しを生成した場合、それらのツール呼び出しを実行し、結果を追加して再度ループを実行します。 +3. 指定された `max_turns` を超えた場合、[`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] 例外を発生させます。 + +!!! note + + LLM の出力が「最終出力(final output)」とみなされる条件は、望ましいタイプのテキスト出力を生成し、かつツール呼び出しがない場合です。 + +## ストリーミング + +ストリーミングを使用すると、LLM の実行中にストリーミングイベントを受け取ることができます。ストリームが完了すると、[`RunResultStreaming`][agents.result.RunResultStreaming] に実行に関する完全な情報(生成されたすべての新しい出力を含む)が格納されます。ストリーミングイベントを取得するには `.stream_events()` を呼び出します。詳細は [streaming ガイド](streaming.md) を参照してください。 + +## 実行設定(Run config) + +`run_config` パラメータを使用すると、エージェント実行時のグローバル設定を構成できます。 + +- [`model`][agents.run.RunConfig.model]:各エージェントの `model` 設定に関係なく、グローバルな LLM モデルを指定します。 +- [`model_provider`][agents.run.RunConfig.model_provider]:モデル名を検索するためのモデルプロバイダーを指定します(デフォルトは OpenAI)。 +- [`model_settings`][agents.run.RunConfig.model_settings]:エージェント固有の設定を上書きします。例えば、グローバルな `temperature` や `top_p` を設定できます。 +- [`input_guardrails`][agents.run.RunConfig.input_guardrails]、[`output_guardrails`][agents.run.RunConfig.output_guardrails]:すべての実行に適用する入力または出力のガードレールのリストです。 +- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]:ハンドオフに既存の入力フィルターがない場合に適用されるグローバルな入力フィルターです。入力フィルターを使用すると、新しいエージェントに送信される入力を編集できます。詳細は [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] のドキュメントを参照してください。 +- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]:実行全体の [トレーシング](tracing.md) を無効にします。 +- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]:トレースに LLM やツール呼び出しの入出力など、機密性の高いデータを含めるかどうかを設定します。 +- [`workflow_name`][agents.run.RunConfig.workflow_name]、[`trace_id`][agents.run.RunConfig.trace_id]、[`group_id`][agents.run.RunConfig.group_id]:トレーシングのワークフロー名、トレース ID、トレースグループ ID を設定します。少なくとも `workflow_name` を設定することを推奨します。グループ ID はオプションで、複数の実行間でトレースを関連付けることができます。 +- [`trace_metadata`][agents.run.RunConfig.trace_metadata]:すべてのトレースに含めるメタデータです。 + +## 会話/チャットスレッド + +いずれかの run メソッドを呼び出すと、1 つ以上のエージェントが実行され(したがって 1 つ以上の LLM 呼び出しが発生)、チャット会話における単一の論理的ターンを表します。例えば: + +1. ユーザーのターン:ユーザーがテキストを入力 +2. Runner の実行:最初のエージェントが LLM を呼び出し、ツールを実行し、2 番目のエージェントにハンドオフし、2 番目のエージェントがさらにツールを実行して出力を生成 + +エージェントの実行終了時に、ユーザーに何を表示するかを選択できます。例えば、エージェントが生成したすべての新しい項目を表示するか、最終出力のみを表示するかを選択できます。いずれの場合も、ユーザーが追加の質問をした場合、再度 run メソッドを呼び出します。 + +次のターンの入力を取得するには、基本クラスの [`RunResultBase.to_input_list()`][agents.result.RunResultBase.to_input_list] メソッドを使用できます。 + +```python +async def main(): + agent = Agent(name="Assistant", instructions="Reply very concisely.") + + with trace(workflow_name="Conversation", group_id=thread_id): + # First turn + result = await Runner.run(agent, "What city is the Golden Gate Bridge in?") + print(result.final_output) + # San Francisco + + # Second turn + new_input = result.to_input_list() + [{"role": "user", "content": "What state is it in?"}] + result = await Runner.run(agent, new_input) + print(result.final_output) + # California +``` + +## 例外 + +SDK は特定の状況で例外を発生させます。完全なリストは [`agents.exceptions`][] にあります。概要は以下の通りです。 + +- [`AgentsException`][agents.exceptions.AgentsException]:SDK 内で発生するすべての例外の基底クラスです。 +- [`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded]:実行が指定された `max_turns` を超えた場合に発生します。 +- [`ModelBehaviorError`][agents.exceptions.ModelBehaviorError]:モデルが不正な出力(不正な JSON や存在しないツールの使用など)を生成した場合に発生します。 +- [`UserError`][agents.exceptions.UserError]:SDK を使用するコードに誤りがある場合に発生します。 +- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered]、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]:[ガードレール](guardrails.md) がトリガーされた場合に発生します。 \ No newline at end of file diff --git a/docs/ja/streaming.md b/docs/ja/streaming.md new file mode 100644 index 00000000..60719679 --- /dev/null +++ b/docs/ja/streaming.md @@ -0,0 +1,87 @@ +# ストリーミング + +ストリーミングを使用すると、エージェントの実行中にその進行状況の更新を購読できます。これは、エンドユーザーに進捗状況や部分的な応答を表示する際に役立ちます。 + +ストリーミングを行うには、[`Runner.run_streamed()`][agents.run.Runner.run_streamed] を呼び出します。これにより [`RunResultStreaming`][agents.result.RunResultStreaming] が返されます。`result.stream_events()` を呼び出すと、以下で説明する [`StreamEvent`][agents.stream_events.StreamEvent] オブジェクトの非同期ストリームが得られます。 + +## raw response events + +[`RawResponsesStreamEvent`][agents.stream_events.RawResponsesStreamEvent] は、LLM から直接渡される raw イベントです。これらは OpenAI Responses API の形式であり、各イベントはタイプ(`response.created` や `response.output_text.delta` など)とデータを持ちます。これらのイベントは、生成された応答メッセージを即座にユーザーにストリーミングしたい場合に役立ちます。 + +例えば、以下のコードは LLM が生成したテキストをトークンごとに出力します。 + +```python +import asyncio +from openai.types.responses import ResponseTextDeltaEvent +from agents import Agent, Runner + +async def main(): + agent = Agent( + name="Joker", + instructions="You are a helpful assistant.", + ) + + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + async for event in result.stream_events(): + if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): + print(event.data.delta, end="", flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Run item events と agent events + +[`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent] は、より高レベルのイベントです。これらはアイテムが完全に生成された際に通知されます。これにより、トークン単位ではなく、「メッセージが生成された」「ツールが実行された」などのレベルで進捗状況をユーザーに通知できます。同様に、[`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] は、現在のエージェントが変更された際(例えばハンドオフの結果として)に更新を通知します。 + +例えば、以下のコードは raw イベントを無視し、更新情報のみをユーザーにストリーミングします。 + +```python +import asyncio +import random +from agents import Agent, ItemHelpers, Runner, function_tool + +@function_tool +def how_many_jokes() -> int: + return random.randint(1, 10) + + +async def main(): + agent = Agent( + name="Joker", + instructions="First call the `how_many_jokes` tool, then tell that many jokes.", + tools=[how_many_jokes], + ) + + result = Runner.run_streamed( + agent, + input="Hello", + ) + print("=== Run starting ===") + + async for event in result.stream_events(): + # raw response イベントのデルタは無視します + if event.type == "raw_response_event": + continue + # エージェントが更新された場合、それを表示します + elif event.type == "agent_updated_stream_event": + print(f"Agent updated: {event.new_agent.name}") + continue + # アイテムが生成された場合、それを表示します + elif event.type == "run_item_stream_event": + if event.item.type == "tool_call_item": + print("-- Tool was called") + elif event.item.type == "tool_call_output_item": + print(f"-- Tool output: {event.item.output}") + elif event.item.type == "message_output_item": + print(f"-- Message output:\n {ItemHelpers.text_message_output(event.item)}") + else: + pass # 他のイベントタイプは無視します + + print("=== Run complete ===") + + +if __name__ == "__main__": + asyncio.run(main()) +``` \ No newline at end of file diff --git a/docs/ja/tools.md b/docs/ja/tools.md new file mode 100644 index 00000000..c100885b --- /dev/null +++ b/docs/ja/tools.md @@ -0,0 +1,269 @@ +# ツール + +ツールを使用すると、エージェントがデータ取得、コード実行、外部 API 呼び出し、さらにはコンピュータ操作などのアクションを実行できます。Agents SDK には以下の 3 種類のツールがあります。 + +- ホスト型ツール:AI モデルと共に LLM サーバー上で実行されます。OpenAI は、検索、ウェブ検索、コンピュータ操作をホスト型ツール(OpenAI がホストするツール)として提供しています。 +- 関数呼び出し:任意の Python 関数をツールとして使用できます。 +- エージェントをツールとして使用:エージェントをツールとして使用でき、エージェント間でハンドオフを行わずに他のエージェントを呼び出せます。 + +## ホスト型ツール + +[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] を使用する場合、OpenAI はいくつかの組み込みツールを提供しています。 + +- [`WebSearchTool`][agents.tool.WebSearchTool] は、エージェントがウェブ検索を行うためのツールです。 +- [`FileSearchTool`][agents.tool.FileSearchTool] は、OpenAI のベクトルストアから情報を取得するためのツールです。 +- [`ComputerTool`][agents.tool.ComputerTool] は、コンピュータ操作タスクを自動化するためのツールです。 + +```python +from agents import Agent, FileSearchTool, Runner, WebSearchTool + +agent = Agent( + name="Assistant", + tools=[ + WebSearchTool(), + FileSearchTool( + max_num_results=3, + vector_store_ids=["VECTOR_STORE_ID"], + ), + ], +) + +async def main(): + result = await Runner.run(agent, "Which coffee shop should I go to, taking into account my preferences and the weather today in SF?") + print(result.final_output) +``` + +## 関数ツール + +任意の Python 関数をツールとして使用できます。Agents SDK は自動的にツールを設定します。 + +- ツール名は Python 関数名が使用されます(または任意の名前を指定可能)。 +- ツールの説明は関数の docstring から取得されます(または任意の説明を指定可能)。 +- 関数の入力スキーマは関数の引数から自動的に作成されます。 +- 各入力の説明は、無効化されていない限り、関数の docstring から取得されます。 + +Python の `inspect` モジュールを使用して関数シグネチャを抽出し、[`griffe`](https://mkdocstrings.github.io/griffe/) を使用して docstring を解析し、`pydantic` を使用してスキーマを作成します。 + +```python +import json + +from typing_extensions import TypedDict, Any + +from agents import Agent, FunctionTool, RunContextWrapper, function_tool + + +class Location(TypedDict): + lat: float + long: float + +@function_tool # (1)! +async def fetch_weather(location: Location) -> str: + # (2)! + """Fetch the weather for a given location. + + Args: + location: The location to fetch the weather for. + """ + # In real life, we'd fetch the weather from a weather API + return "sunny" + + +@function_tool(name_override="fetch_data") # (3)! +def read_file(ctx: RunContextWrapper[Any], path: str, directory: str | None = None) -> str: + """Read the contents of a file. + + Args: + path: The path to the file to read. + directory: The directory to read the file from. + """ + # In real life, we'd read the file from the file system + return "" + + +agent = Agent( + name="Assistant", + tools=[fetch_weather, read_file], # (4)! +) + +for tool in agent.tools: + if isinstance(tool, FunctionTool): + print(tool.name) + print(tool.description) + print(json.dumps(tool.params_json_schema, indent=2)) + print() + +``` + +1. 任意の Python 型を関数の引数として使用可能で、関数は同期または非同期のどちらでも構いません。 +2. docstring が存在する場合、ツールおよび引数の説明として使用されます。 +3. 関数はオプションで `context` を引数として受け取れます(最初の引数である必要があります)。また、ツール名や説明、docstring のスタイルなどを上書き設定できます。 +4. デコレータを付けた関数をツールのリストに渡すことができます。 + +??? note "クリックして出力を表示" + + ``` + fetch_weather + Fetch the weather for a given location. + { + "$defs": { + "Location": { + "properties": { + "lat": { + "title": "Lat", + "type": "number" + }, + "long": { + "title": "Long", + "type": "number" + } + }, + "required": [ + "lat", + "long" + ], + "title": "Location", + "type": "object" + } + }, + "properties": { + "location": { + "$ref": "#/$defs/Location", + "description": "The location to fetch the weather for." + } + }, + "required": [ + "location" + ], + "title": "fetch_weather_args", + "type": "object" + } + + fetch_data + Read the contents of a file. + { + "properties": { + "path": { + "description": "The path to the file to read.", + "title": "Path", + "type": "string" + }, + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The directory to read the file from.", + "title": "Directory" + } + }, + "required": [ + "path" + ], + "title": "fetch_data_args", + "type": "object" + } + ``` +### カスタム関数ツール + +Python 関数をツールとして使用したくない場合もあります。その場合は、直接 [`FunctionTool`][agents.tool.FunctionTool] を作成できます。作成時には以下を指定する必要があります: + +- `name` +- `description` +- 引数の JSON スキーマを示す `params_json_schema` +- 非同期関数である `on_invoke_tool`(コンテキストと引数を JSON 文字列として受け取り、ツールの出力を文字列として返す必要があります) + +```python +from typing import Any + +from pydantic import BaseModel + +from agents import RunContextWrapper, FunctionTool + + + +def do_some_work(data: str) -> str: + return "done" + + +class FunctionArgs(BaseModel): + username: str + age: int + + +async def run_function(ctx: RunContextWrapper[Any], args: str) -> str: + parsed = FunctionArgs.model_validate_json(args) + return do_some_work(data=f"{parsed.username} is {parsed.age} years old") + + +tool = FunctionTool( + name="process_user", + description="Processes extracted user data", + params_json_schema=FunctionArgs.model_json_schema(), + on_invoke_tool=run_function, +) +``` + +### 引数とドックストリングの自動解析 + +前述の通り、関数のシグネチャを自動的に解析してツールのスキーマを抽出し、ドックストリングを解析してツールおよび各引数の説明を抽出します。これに関する注意点は以下の通りです: + +1. シグネチャの解析は `inspect` モジュールを使用して行われます。型アノテーションを用いて引数の型を把握し、動的に Pydantic モデルを構築して全体のスキーマを表現します。Python の基本型(basic components)、Pydantic モデル、TypedDict など、ほとんどの型をサポートしています。 +2. ドックストリングの解析には `griffe` を使用します。サポートされるドックストリング形式は `google`、`sphinx`、`numpy` です。ドックストリング形式は自動検出を試みますが、これはベストエフォートであり、`function_tool` 呼び出し時に明示的に指定することも可能です。また、`use_docstring_info` を `False` に設定することでドックストリングの解析を無効化できます。 + +スキーマ抽出のコードは [`agents.function_schema`][] にあります。 + +## ツールとしてのエージェント + +一部のワークフローでは、制御をハンドオフするのではなく、中央のエージェントが専門化されたエージェントのネットワークを調整することが望ましい場合があります。このような場合、エージェントをツールとしてモデル化できます。 + +```python +from agents import Agent, Runner +import asyncio + +spanish_agent = Agent( + name="Spanish agent", + instructions="You translate the user's message to Spanish", +) + +french_agent = Agent( + name="French agent", + instructions="You translate the user's message to French", +) + +orchestrator_agent = Agent( + name="orchestrator_agent", + instructions=( + "You are a translation agent. You use the tools given to you to translate." + "If asked for multiple translations, you call the relevant tools." + ), + tools=[ + spanish_agent.as_tool( + tool_name="translate_to_spanish", + tool_description="Translate the user's message to Spanish", + ), + french_agent.as_tool( + tool_name="translate_to_french", + tool_description="Translate the user's message to French", + ), + ], +) + +async def main(): + result = await Runner.run(orchestrator_agent, input="Say 'Hello, how are you?' in Spanish.") + print(result.final_output) +``` + +## 関数ツールにおけるエラー処理 + +`@function_tool` を使用して関数ツールを作成する際、`failure_error_function` を渡すことができます。これはツール呼び出しがクラッシュした場合に、LLM にエラー応答を提供する関数です。 + +- デフォルト(何も渡さない場合)では、エラーが発生したことを LLM に通知する `default_tool_error_function` が実行されます。 +- 独自のエラー関数を渡した場合は、それが代わりに実行され、その応答が LLM に送信されます。 +- 明示的に `None` を渡した場合、ツール呼び出しのエラーは再度発生(re-raise)し、自分で処理する必要があります。この場合、モデルが無効な JSON を生成した場合は `ModelBehaviorError`、コードがクラッシュした場合は `UserError` などが発生します。 + +手動で `FunctionTool` オブジェクトを作成する場合は、`on_invoke_tool` 関数内でエラーを処理する必要があります。 \ No newline at end of file diff --git a/docs/ja/tracing.md b/docs/ja/tracing.md new file mode 100644 index 00000000..2e0e80da --- /dev/null +++ b/docs/ja/tracing.md @@ -0,0 +1,102 @@ +# トレーシング + +Agents SDK には組み込みのトレーシング機能があり、エージェントの実行中に発生するイベント(LLM の生成、ツール呼び出し、ハンドオフ、ガードレール、さらにはカスタムイベントまで)を包括的に記録します。[Traces ダッシュボード](https://platform.openai.com/traces) を使用して、開発中および本番環境でワークフローのデバッグ、可視化、監視が可能です。 + +!!!note + + トレーシングはデフォルトで有効になっています。トレーシングを無効にする方法は 2 つあります。 + + 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定して、グローバルにトレーシングを無効化できます。 + 2. 個別の実行でトレーシングを無効化するには、[`agents.run.RunConfig.tracing_disabled`][] を `True` に設定します。 + +***OpenAI の API を使用してゼロデータ保持(ZDR)ポリシーで運用している組織の場合、トレーシングは利用できません。*** + +## トレースとスパン + +- **トレース(Traces)** は、ワークフローの単一のエンドツーエンド操作を表します。トレースは複数のスパン(Spans)で構成されます。トレースには以下のプロパティがあります。 + - `workflow_name`:論理的なワークフローまたはアプリ名。例:「Code generation」や「Customer service」。 + - `trace_id`:トレースの一意な ID。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 + - `group_id`:同じ会話からの複数のトレースを関連付けるためのオプションのグループ ID。例えば、チャットスレッド ID を使用できます。 + - `disabled`:True の場合、このトレースは記録されません。 + - `metadata`:トレースに関するオプションのメタデータ。 +- **スパン(Spans)** は、開始時刻と終了時刻を持つ操作を表します。スパンには以下が含まれます。 + - `started_at` と `ended_at` のタイムスタンプ。 + - 所属するトレースを示す `trace_id`。 + - 親スパンを指す `parent_id`(存在する場合)。 + - スパンに関する情報を含む `span_data`。例えば、`AgentSpanData` はエージェントに関する情報を、`GenerationSpanData` は LLM の生成に関する情報を含みます。 + +## デフォルトのトレーシング + +デフォルトでは、SDK は以下をトレースします。 + +- `Runner.{run, run_sync, run_streamed}()` 全体が `trace()` でラップされます。 +- エージェントが実行されるたびに `agent_span()` でラップされます。 +- LLM の生成は `generation_span()` でラップされます。 +- 関数ツールの呼び出しはそれぞれ `function_span()` でラップされます。 +- ガードレールは `guardrail_span()` でラップされます。 +- ハンドオフは `handoff_span()` でラップされます。 +- 音声入力(音声からテキスト)は `transcription_span()` でラップされます。 +- 音声出力(テキストから音声)は `speech_span()` でラップされます。 +- 関連する音声スパンは `speech_group_span()` の下にまとめられる場合があります。 + +デフォルトでは、トレース名は「Agent trace」です。`trace` を使用する場合、この名前を設定できます。また、[`RunConfig`][agents.run.RunConfig] を使用して名前やその他のプロパティを設定できます。 + +さらに、[カスタムトレースプロセッサ](#custom-tracing-processors) を設定して、トレースを他の宛先に送信することも可能です(置き換えまたは追加の宛先として)。 + +## 上位レベルのトレース + +複数回の `run()` 呼び出しを 1 つのトレースにまとめたい場合があります。その場合、コード全体を `trace()` でラップします。 + +```python +from agents import Agent, Runner, trace + +async def main(): + agent = Agent(name="Joke generator", instructions="Tell funny jokes.") + + with trace("Joke workflow"): # (1)! + first_result = await Runner.run(agent, "Tell me a joke") + second_result = await Runner.run(agent, f"Rate this joke: {first_result.final_output}") + print(f"Joke: {first_result.final_output}") + print(f"Rating: {second_result.final_output}") +``` + +1. 2 回の `Runner.run` 呼び出しが `with trace()` でラップされているため、個別の実行はそれぞれ別のトレースを作成するのではなく、全体のトレースの一部になります。 + +## トレースの作成 + +トレースを作成するには [`trace()`][agents.tracing.trace] 関数を使用します。トレースは開始と終了が必要です。以下の 2 つの方法があります。 + +1. **推奨**:コンテキストマネージャとして使用します(例:`with trace(...) as my_trace`)。これによりトレースの開始と終了が自動的に行われます。 +2. 手動で [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を呼び出すこともできます。 + +現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡されます。これにより、並行処理でも自動的に動作します。トレースを手動で開始・終了する場合、`start()` と `finish()` に `mark_as_current` と `reset_current` を渡して現在のトレースを更新する必要があります。 + +## スパンの作成 + +スパンを作成するには、各種の [`*_span()`][agents.tracing.create] メソッドを使用します。通常、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するために [`custom_span()`][agents.tracing.custom_span] 関数も利用可能です。 + +スパンは自動的に現在のトレースに属し、最も近い現在のスパンの下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡されます。 + +## 機密データ + +一部のスパンは機密データを含む可能性があります。 + +`generation_span()` は LLM 生成の入出力を、`function_span()` は関数呼び出しの入出力を保存します。これらには機密データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] を使用してデータの取得を無効化できます。 + +同様に、音声スパンはデフォルトで音声データを base64 エンコードされた PCM データとして含みます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定して音声データの取得を無効化できます。 + +## カスタムトレーシングプロセッサ + +トレーシングの高レベルアーキテクチャは以下の通りです。 + +- 初期化時にグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 +- `TraceProvider` はトレースを OpenAI バックエンドに送信する [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] と [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] で構成されます。 + +このデフォルト設定をカスタマイズするには、以下の 2 つの方法があります。 + +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] で追加のプロセッサを追加できます。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] でデフォルトのプロセッサを置き換えることができます。 + +## 外部トレーシングプロセッサ一覧 + +(省略:原文のまま) \ No newline at end of file diff --git a/docs/ja/visualization.md b/docs/ja/visualization.md new file mode 100644 index 00000000..a0e2b9ec --- /dev/null +++ b/docs/ja/visualization.md @@ -0,0 +1,83 @@ +# エージェントの可視化 + +エージェントの可視化を使用すると、**Graphviz** を用いてエージェントとその関係性を構造化したグラフィカルな表現として生成できます。これにより、アプリケーション内でエージェント、ツール、およびハンドオフがどのように相互作用するかを理解できます。 + +## インストール + +オプションの依存関係グループ `viz` をインストールします: + +```bash +pip install "openai-agents[viz]" +``` + +## グラフの生成 + +`draw_graph` 関数を使用してエージェントの可視化を生成できます。この関数は以下のような有向グラフを作成します: + +- **エージェント** は黄色の四角形で表されます。 +- **ツール** は緑色の楕円形で表されます。 +- **ハンドオフ** はあるエージェントから別のエージェントへの有向エッジで表されます。 + +### 使用例 + +```python +from agents import Agent, function_tool +from agents.extensions.visualization import draw_graph + +@function_tool +def get_weather(city: str) -> str: + return f"The weather in {city} is sunny." + +spanish_agent = Agent( + name="Spanish agent", + instructions="You only speak Spanish.", +) + +english_agent = Agent( + name="English agent", + instructions="You only speak English", +) + +triage_agent = Agent( + name="Triage agent", + instructions="Handoff to the appropriate agent based on the language of the request.", + handoffs=[spanish_agent, english_agent], + tools=[get_weather], +) + +draw_graph(triage_agent) +``` + +![Agent Graph](../assets/images/graph.png) + +これにより、**triage agent** とそのサブエージェントおよびツールとの接続構造を視覚的に表現したグラフが生成されます。 + +## 可視化の理解 + +生成されたグラフには以下が含まれます: + +- エントリーポイントを示す **開始ノード** (`__start__`)。 +- 黄色で塗りつぶされた四角形で表されるエージェント。 +- 緑色で塗りつぶされた楕円形で表されるツール。 +- 相互作用を示す有向エッジ: + - エージェント間のハンドオフは **実線矢印**。 + - ツールの呼び出しは **点線矢印**。 +- 実行終了地点を示す **終了ノード** (`__end__`)。 + +## グラフのカスタマイズ + +### グラフの表示 +デフォルトでは、`draw_graph` はグラフをインラインで表示します。グラフを別ウィンドウで表示するには、以下のように記述します: + +```python +draw_graph(triage_agent).view() +``` + +### グラフの保存 +デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存するには、ファイル名を指定します: + +```python +draw_graph(triage_agent, filename="agent_graph.png") +``` + +これにより、作業ディレクトリに `agent_graph.png` が生成されます。 \ No newline at end of file diff --git a/docs/ja/voice/pipeline.md b/docs/ja/voice/pipeline.md new file mode 100644 index 00000000..03e0c54e --- /dev/null +++ b/docs/ja/voice/pipeline.md @@ -0,0 +1,75 @@ +# パイプラインとワークフロー + +[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] クラスを使用すると、エージェントのワークフローを簡単に音声アプリに変換できます。実行するワークフローを渡すと、パイプラインが入力音声の文字起こし、音声終了の検出、適切なタイミングでのワークフロー呼び出し、ワークフロー出力の音声への変換を処理します。 + +```mermaid +graph LR + %% Input + A["🎤 Audio Input"] + + %% Voice Pipeline + subgraph Voice_Pipeline [Voice Pipeline] + direction TB + B["Transcribe (speech-to-text)"] + C["Your Code"]:::highlight + D["Text-to-speech"] + B --> C --> D + end + + %% Output + E["🎧 Audio Output"] + + %% Flow + A --> Voice_Pipeline + Voice_Pipeline --> E + + %% Custom styling + classDef highlight fill:#ffcc66,stroke:#333,stroke-width:1px,font-weight:700; + +``` + +## パイプラインの設定 + +パイプラインを作成する際、以下の項目を設定できます。 + +1. [`workflow`][agents.voice.workflow.VoiceWorkflowBase]:新しい音声が文字起こしされるたびに実行されるコードです。 +2. 使用する [`speech-to-text`][agents.voice.model.STTModel] および [`text-to-speech`][agents.voice.model.TTSModel] モデル +3. [`config`][agents.voice.pipeline_config.VoicePipelineConfig]:以下のような設定が可能です。 + - モデル名をモデルにマッピングするモデルプロバイダー + - トレーシング(トレーシングの無効化、音声ファイルのアップロード有無、ワークフロー名、トレース ID など) + - TTS および STT モデルの設定(プロンプト、言語、使用するデータ型など) + +## パイプラインの実行 + +パイプラインは [`run()`][agents.voice.pipeline.VoicePipeline.run] メソッドを使用して実行できます。このメソッドでは、以下の 2 種類の形式で音声入力を渡すことができます。 + +1. [`AudioInput`][agents.voice.input.AudioInput] は、完全な音声トランスクリプトがあり、それに対する結果を生成したい場合に使用します。これは、話者が話し終えたタイミングを検出する必要がない場合に便利です。例えば、事前録音された音声や、ユーザーが話し終えたタイミングが明確なプッシュトゥトーク型アプリなどです。 +2. [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] は、ユーザーが話し終えたタイミングを検出する必要がある場合に使用します。音声チャンクを検出時に順次送信でき、音声パイプラインは「アクティビティ検出」と呼ばれるプロセスを通じて、適切なタイミングでエージェントのワークフローを自動的に実行します。 + +## 実行結果 + +音声パイプラインの実行結果は [`StreamedAudioResult`][agents.voice.result.StreamedAudioResult] です。これは、発生したイベントをストリームとして取得できるオブジェクトです。いくつかの種類の [`VoiceStreamEvent`][agents.voice.events.VoiceStreamEvent] があり、以下を含みます。 + +1. [`VoiceStreamEventAudio`][agents.voice.events.VoiceStreamEventAudio]:音声チャンクを含むイベントです。 +2. [`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle]:ターンの開始や終了など、ライフサイクルイベントを通知します。 +3. [`VoiceStreamEventError`][agents.voice.events.VoiceStreamEventError]:エラーイベントです。 + +```python + +result = await pipeline.run(input) + +async for event in result.stream(): + if event.type == "voice_stream_event_audio": + # play audio + elif event.type == "voice_stream_event_lifecycle": + # lifecycle + elif event.type == "voice_stream_event_error" + # error + ... +``` + +## ベストプラクティス + +### 割り込み処理 + +Agents SDK は現在、[`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] に対する組み込みの割り込み処理をサポートしていません。その代わり、検出された各ターンごとにワークフローが個別に実行されます。アプリケーション内で割り込みを処理したい場合は、[`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] イベントを監視できます。`turn_started` は新しいターンが文字起こしされ、処理が開始されたことを示します。`turn_ended` は、該当するターンのすべての音声が送信された後にトリガーされます。これらのイベントを利用して、モデルがターンを開始した際に話者のマイクをミュートし、ターンに関連するすべての音声を送信した後にミュートを解除する、といった処理が可能です。 \ No newline at end of file diff --git a/docs/ja/voice/quickstart.md b/docs/ja/voice/quickstart.md new file mode 100644 index 00000000..0602efe6 --- /dev/null +++ b/docs/ja/voice/quickstart.md @@ -0,0 +1,194 @@ +# クイックスタート + +## 前提条件 + +まず、Agents SDK の基本的な[クイックスタート手順](../quickstart.md)に従い、仮想環境をセットアップしてください。その後、SDK のオプションである音声関連の依存関係をインストールします。 + +```bash +pip install 'openai-agents[voice]' +``` + +## コンセプト + +理解すべき主なコンセプトは [`VoicePipeline`][agents.voice.pipeline.VoicePipeline] です。これは以下の 3 ステップで構成されています。 + +1. 音声認識モデルを実行し、音声をテキストに変換します。 +2. 通常はエージェントを用いたワークフローであるコードを実行し、実行結果を生成します。 +3. テキスト読み上げモデルを実行し、実行結果のテキストを再び音声に変換します。 + +```mermaid +graph LR + %% Input + A["🎤 Audio Input"] + + %% Voice Pipeline + subgraph Voice_Pipeline [Voice Pipeline] + direction TB + B["Transcribe (speech-to-text)"] + C["Your Code"]:::highlight + D["Text-to-speech"] + B --> C --> D + end + + %% Output + E["🎧 Audio Output"] + + %% Flow + A --> Voice_Pipeline + Voice_Pipeline --> E + + %% Custom styling + classDef highlight fill:#ffcc66,stroke:#333,stroke-width:1px,font-weight:700; + +``` + +## エージェント + +まず、いくつかのエージェントを設定します。この SDK でエージェントを作成したことがあれば、馴染みのある内容です。ここでは、複数のエージェント、ハンドオフ、およびツールを設定します。 + +```python +import asyncio +import random + +from agents import ( + Agent, + function_tool, +) +from agents.extensions.handoff_prompt import prompt_with_handoff_instructions + + + +@function_tool +def get_weather(city: str) -> str: + """Get the weather for a given city.""" + print(f"[debug] get_weather called with city: {city}") + choices = ["sunny", "cloudy", "rainy", "snowy"] + return f"The weather in {city} is {random.choice(choices)}." + + +spanish_agent = Agent( + name="Spanish", + handoff_description="A spanish speaking agent.", + instructions=prompt_with_handoff_instructions( + "You're speaking to a human, so be polite and concise. Speak in Spanish.", + ), + model="gpt-4o-mini", +) + +agent = Agent( + name="Assistant", + instructions=prompt_with_handoff_instructions( + "You're speaking to a human, so be polite and concise. If the user speaks in Spanish, handoff to the spanish agent.", + ), + model="gpt-4o-mini", + handoffs=[spanish_agent], + tools=[get_weather], +) +``` + +## 音声パイプライン + +ここでは、ワークフローとして [`SingleAgentVoiceWorkflow`][agents.voice.workflow.SingleAgentVoiceWorkflow] を使用し、シンプルな音声パイプラインを設定します。 + +```python +from agents.voice import SingleAgentVoiceWorkflow, VoicePipeline +pipeline = VoicePipeline(workflow=SingleAgentVoiceWorkflow(agent)) +``` + +## パイプラインの実行 + +```python +import numpy as np +import sounddevice as sd +from agents.voice import AudioInput + +# 簡単のため、ここでは 3 秒間の無音を作成します +# 実際にはマイクからの音声データを使用します +buffer = np.zeros(24000 * 3, dtype=np.int16) +audio_input = AudioInput(buffer=buffer) + +result = await pipeline.run(audio_input) + +# `sounddevice` を使ってオーディオプレイヤーを作成します +player = sd.OutputStream(samplerate=24000, channels=1, dtype=np.int16) +player.start() + +# 音声ストリームをリアルタイムで再生します +async for event in result.stream(): + if event.type == "voice_stream_event_audio": + player.write(event.data) + +``` + +## すべてをまとめる + +```python +import asyncio +import random + +import numpy as np +import sounddevice as sd + +from agents import ( + Agent, + function_tool, + set_tracing_disabled, +) +from agents.voice import ( + AudioInput, + SingleAgentVoiceWorkflow, + VoicePipeline, +) +from agents.extensions.handoff_prompt import prompt_with_handoff_instructions + + +@function_tool +def get_weather(city: str) -> str: + """Get the weather for a given city.""" + print(f"[debug] get_weather called with city: {city}") + choices = ["sunny", "cloudy", "rainy", "snowy"] + return f"The weather in {city} is {random.choice(choices)}." + + +spanish_agent = Agent( + name="Spanish", + handoff_description="A spanish speaking agent.", + instructions=prompt_with_handoff_instructions( + "You're speaking to a human, so be polite and concise. Speak in Spanish.", + ), + model="gpt-4o-mini", +) + +agent = Agent( + name="Assistant", + instructions=prompt_with_handoff_instructions( + "You're speaking to a human, so be polite and concise. If the user speaks in Spanish, handoff to the spanish agent.", + ), + model="gpt-4o-mini", + handoffs=[spanish_agent], + tools=[get_weather], +) + + +async def main(): + pipeline = VoicePipeline(workflow=SingleAgentVoiceWorkflow(agent)) + buffer = np.zeros(24000 * 3, dtype=np.int16) + audio_input = AudioInput(buffer=buffer) + + result = await pipeline.run(audio_input) + + # `sounddevice` を使ってオーディオプレイヤーを作成します + player = sd.OutputStream(samplerate=24000, channels=1, dtype=np.int16) + player.start() + + # 音声ストリームをリアルタイムで再生します + async for event in result.stream(): + if event.type == "voice_stream_event_audio": + player.write(event.data) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +このコード例を実行すると、エージェントがあなたに話しかけます。実際にエージェントと会話できるデモについては、[examples/voice/static](https://github.com/openai/openai-agents-python/tree/main/examples/voice/static) のコード例を参照してください。 \ No newline at end of file diff --git a/docs/ja/voice/tracing.md b/docs/ja/voice/tracing.md new file mode 100644 index 00000000..8432d75d --- /dev/null +++ b/docs/ja/voice/tracing.md @@ -0,0 +1,14 @@ +# トレーシング + +[エージェントのトレーシング](../tracing.md) と同様に、音声パイプラインも自動的にトレーシングされます。 + +基本的なトレーシング情報については、上記のトレーシングドキュメントを参照してください。さらに、[`VoicePipelineConfig`][agents.voice.pipeline_config.VoicePipelineConfig] を使用してパイプラインのトレーシングを設定できます。 + +主なトレーシング関連フィールドは以下のとおりです。 + +- [`tracing_disabled`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレーシングを無効にするかどうかを制御します。デフォルトではトレーシングは有効です。 +- [`trace_include_sensitive_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_data]:音声文字起こしなど、機密性の高いデータをトレースに含めるかどうかを制御します。これは特に音声パイプライン向けであり、ワークフロー内部の処理には影響しません。 +- [`trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]:トレースに音声データを含めるかどうかを制御します。 +- [`workflow_name`][agents.voice.pipeline_config.VoicePipelineConfig.workflow_name]:トレースするワークフローの名前です。 +- [`group_id`][agents.voice.pipeline_config.VoicePipelineConfig.group_id]:複数のトレースを関連付けるためのトレースの `group_id` です。 +- [`trace_metadata`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレースに含める追加のメタデータです。 \ No newline at end of file diff --git a/docs/scripts/translate_docs.py b/docs/scripts/translate_docs.py new file mode 100644 index 00000000..c907754d --- /dev/null +++ b/docs/scripts/translate_docs.py @@ -0,0 +1,158 @@ +# ruff: noqa +import os +from openai import OpenAI +from concurrent.futures import ThreadPoolExecutor + + +# Define the source and target directories +source_dir = "docs" +languages = { + "ja": "Japanese", + # Add more languages here, e.g., "fr": "French" +} + +# Initialize OpenAI client +openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + +# Define dictionaries for translation control +do_not_translate = [ + "OpenAI", + "Agents SDK", + "Hello World", + "Model Context Protocol", + "structured outputs", + # Add more terms here +] + +eng_to_non_eng_mapping = { + "ja": { + "agents": "エージェント", + "computer use": "コンピュータ操作", + "OAI hosted tools": "OpenAI がホストするツール", + "well formed data": "適切な形式のデータ", + "guardrail": "ガードレール", + "handoffs": "ハンドオフ", + "function tools": "関数ツール", + "tracing": "トレーシング", + "code examples": "コード例", + "vector store": "ベクトルストア", + # Add more Japanese mappings here + }, + # Add more languages here +} +eng_to_non_eng_instructions = { + "ja": { + "The term 'result' in the Runner guide context must be translated like 'execution results'", + "The term 'raw' in 'raw response events' must be kept as is", + "The term 'examples' must be code examples when the page mentions the code examples in the repo, it can be translated as either 'code exmaples' or 'sample code'.", + "The term 'primitives' can be translated as basic components or building blocks.", + "When the terms 'instructions' and 'tools' are mentioned as API parameter names, they must be kept as is.", + # Add more Japanese mappings here + }, + # Add more languages here +} + + +def built_instructions(target_language: str, lang_code: str) -> str: + do_not_translate_terms = "\n".join(do_not_translate) + specific_terms = "\n".join( + [f"{k} -> {v}" for k, v in eng_to_non_eng_mapping.get(lang_code, {}).items()] + ) + specific_instructions = "\n".join(eng_to_non_eng_instructions.get(lang_code, {})) + return f"""You are a professional translator with extensive experience in translating technical documents. +You are assigned to translate markdown text written in English into {target_language}. +The tone and voice must be concise, consistent, and most importantly professional. +You must return only the generated markdown text. Don't include any additional comments. +When you're unable to complete full translation, return an error message indicating the reason instead of returning partial results. + +# Do not translate +{do_not_translate_terms} + +# Specific term mappings +When you convert these terms, do not append whitespaces before/after the terms. +{specific_terms} +{specific_instructions} + +# Other Rules +- When translating into Japanese, ensure there are spaces before and after alphanumeric terms and markdown special characters like italic and bold. +- When translating very uncommon technical terms, include both the translated term and the original term in parentheses. That said, the section titles should be as simple as possible. +- You must skip translating any parts of code snippets and code comments +- "./assets/*" needs to be converted to "../assets/*"; markdown files like ./tracing.md can be kept as is. +""" + + +# Function to translate and save files +def translate_file(file_path: str, target_path: str, lang_code: str) -> None: + print(f"Translating {file_path} into a different language: {lang_code}") + with open(file_path, encoding="utf-8") as f: + content = f.read() + + # Split content into lines + lines: list[str] = content.splitlines() + chunks: list[str] = [] + current_chunk: list[str] = [] + + # Split content into chunks of up to 120 lines, ensuring splits occur before section titles + for line in lines: + if len(current_chunk) >= 120 and line.startswith("#"): + chunks.append("\n".join(current_chunk)) + current_chunk = [] + current_chunk.append(line) + if current_chunk: + chunks.append("\n".join(current_chunk)) + + # Translate each chunk separately and combine results + translated_content: list[str] = [] + for chunk in chunks: + response = openai_client.responses.create( + model="gpt-4.5-preview", + temperature=0.0, + instructions=built_instructions(languages[lang_code], lang_code), + input=chunk, + ) + translated_content.append(response.output_text) + + # Save the combined translated content + with open(target_path, "w", encoding="utf-8") as f: + f.write("\n".join(translated_content)) + + +def translate_single_source_file(file_path: str) -> None: + relative_path = os.path.relpath(file_path, source_dir) + if "ref/" in relative_path or not file_path.endswith(".md"): + return + + for lang_code in languages: + target_dir = os.path.join(source_dir, lang_code) + target_path = os.path.join(target_dir, relative_path) + + # Ensure the target directory exists + os.makedirs(os.path.dirname(target_path), exist_ok=True) + + # Translate and save the file + translate_file(file_path, target_path, lang_code) + + +def main(): + # Traverse the source directory + for root, _, file_names in os.walk(source_dir): + # Skip the target directories + if any(lang in root for lang in languages): + continue + with ThreadPoolExecutor(max_workers=20) as executor: + futures = [ + executor.submit( + translate_single_source_file, + os.path.join(root, file_name), + ) + for file_name in file_names + ] + for future in futures: + future.result() + + print("Translation completed.") + + +if __name__ == "__main__": + # translate_single_source_file("docs/tools.md") + main() diff --git a/mkdocs.yml b/mkdocs.yml index 5e08e321..3eaf0f4b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,79 +18,6 @@ theme: primary: black logo: assets/logo.svg favicon: images/favicon-platform.svg -nav: - - Intro: index.md - - Quickstart: quickstart.md - - Examples: examples.md - - Documentation: - - agents.md - - running_agents.md - - results.md - - streaming.md - - tools.md - - mcp.md - - handoffs.md - - tracing.md - - context.md - - guardrails.md - - multi_agent.md - - models.md - - config.md - - visualization.md - - Voice agents: - - voice/quickstart.md - - voice/pipeline.md - - voice/tracing.md - - API Reference: - - Agents: - - ref/index.md - - ref/agent.md - - ref/run.md - - ref/tool.md - - ref/result.md - - ref/stream_events.md - - ref/handoffs.md - - ref/lifecycle.md - - ref/items.md - - ref/run_context.md - - ref/usage.md - - ref/exceptions.md - - ref/guardrail.md - - ref/model_settings.md - - ref/agent_output.md - - ref/function_schema.md - - ref/models/interface.md - - ref/models/openai_chatcompletions.md - - ref/models/openai_responses.md - - ref/mcp/server.md - - ref/mcp/util.md - - Tracing: - - ref/tracing/index.md - - ref/tracing/create.md - - ref/tracing/traces.md - - ref/tracing/spans.md - - ref/tracing/processor_interface.md - - ref/tracing/processors.md - - ref/tracing/scope.md - - ref/tracing/setup.md - - ref/tracing/span_data.md - - ref/tracing/util.md - - Voice: - - ref/voice/pipeline.md - - ref/voice/workflow.md - - ref/voice/input.md - - ref/voice/result.md - - ref/voice/pipeline_config.md - - ref/voice/events.md - - ref/voice/exceptions.md - - ref/voice/model.md - - ref/voice/utils.md - - ref/voice/models/openai_provider.md - - ref/voice/models/openai_stt.md - - ref/voice/models/openai_tts.md - - Extensions: - - ref/extensions/handoff_filters.md - - ref/extensions/handoff_prompt.md plugins: - search @@ -113,10 +40,125 @@ plugins: heading_level: 3 # Show inherited members inherited_members: true + - i18n: + docs_structure: folder + languages: + - locale: en + default: true + name: English + build: true + nav: + - Intro: index.md + - Quickstart: quickstart.md + - Examples: examples.md + - Documentation: + - agents.md + - running_agents.md + - results.md + - streaming.md + - tools.md + - mcp.md + - handoffs.md + - tracing.md + - context.md + - guardrails.md + - multi_agent.md + - models.md + - config.md + - visualization.md + - Voice agents: + - voice/quickstart.md + - voice/pipeline.md + - voice/tracing.md + - API Reference: + - Agents: + - ref/index.md + - ref/agent.md + - ref/run.md + - ref/tool.md + - ref/result.md + - ref/stream_events.md + - ref/handoffs.md + - ref/lifecycle.md + - ref/items.md + - ref/run_context.md + - ref/usage.md + - ref/exceptions.md + - ref/guardrail.md + - ref/model_settings.md + - ref/agent_output.md + - ref/function_schema.md + - ref/models/interface.md + - ref/models/openai_chatcompletions.md + - ref/models/openai_responses.md + - ref/mcp/server.md + - ref/mcp/util.md + - Tracing: + - ref/tracing/index.md + - ref/tracing/create.md + - ref/tracing/traces.md + - ref/tracing/spans.md + - ref/tracing/processor_interface.md + - ref/tracing/processors.md + - ref/tracing/scope.md + - ref/tracing/setup.md + - ref/tracing/span_data.md + - ref/tracing/util.md + - Voice: + - ref/voice/pipeline.md + - ref/voice/workflow.md + - ref/voice/input.md + - ref/voice/result.md + - ref/voice/pipeline_config.md + - ref/voice/events.md + - ref/voice/exceptions.md + - ref/voice/model.md + - ref/voice/utils.md + - ref/voice/models/openai_provider.md + - ref/voice/models/openai_stt.md + - ref/voice/models/openai_tts.md + - Extensions: + - ref/extensions/handoff_filters.md + - ref/extensions/handoff_prompt.md + + - locale: ja + name: 日本語 + build: true + nav: + - はじめに: index.md + - クイックスタート: quickstart.md + - サンプル例: examples.md + - ドキュメント: + - agents.md + - running_agents.md + - results.md + - streaming.md + - tools.md + - mcp.md + - handoffs.md + - tracing.md + - context.md + - guardrails.md + - multi_agent.md + - models.md + - config.md + - visualization.md + - 音声エージェント: + - voice/quickstart.md + - voice/pipeline.md + - voice/tracing.md extra: # Remove material generation message in footer generator: false + language: en + alternate: + - name: English + link: /openai-agents-python/ + lang: en + - name: 日本語 + link: /openai-agents-python/ja/ + lang: ja markdown_extensions: - pymdownx.superfences: diff --git a/pyproject.toml b/pyproject.toml index f850d629..ca398ba2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "requests>=2.0, <3", "types-requests>=2.0, <3", "mcp>=1.6.0, <2; python_version >= '3.10'", + "mkdocs-static-i18n>=1.3.0", ] classifiers = [ "Typing :: Typed", @@ -48,6 +49,7 @@ dev = [ "mkdocs>=1.6.0", "mkdocs-material>=9.6.0", "mkdocstrings[python]>=0.28.0", + "mkdocs-static-i18n", "coverage>=7.6.12", "playwright==1.50.0", "inline-snapshot>=0.20.7", diff --git a/uv.lock b/uv.lock index d1c27225..af7eef7e 100644 --- a/uv.lock +++ b/uv.lock @@ -851,6 +851,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, ] +[[package]] +name = "mkdocs-static-i18n" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/2b/59652a2550465fde25ae6a009cb6d74d0f7e724d272fc952685807b29ca1/mkdocs_static_i18n-1.3.0.tar.gz", hash = "sha256:65731e1e4ec6d719693e24fee9340f5516460b2b7244d2a89bed4ce3cfa6a173", size = 1370450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/f7/ef222a7a2f96ecf79c7c00bfc9dde3b22cd2cc1bd2b7472c7b204fc64225/mkdocs_static_i18n-1.3.0-py3-none-any.whl", hash = "sha256:7905d52fff71d2c108b6c344fd223e848ca7e39ddf319b70864dfa47dba85d6b", size = 21660 }, +] + [[package]] name = "mkdocstrings" version = "0.29.0" @@ -1092,6 +1104,7 @@ source = { editable = "." } dependencies = [ { name = "griffe" }, { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-static-i18n" }, { name = "openai" }, { name = "pydantic" }, { name = "requests" }, @@ -1115,6 +1128,7 @@ dev = [ { name = "inline-snapshot" }, { name = "mkdocs" }, { name = "mkdocs-material" }, + { name = "mkdocs-static-i18n" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, { name = "playwright" }, @@ -1135,6 +1149,7 @@ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, + { name = "mkdocs-static-i18n", specifier = ">=1.3.0" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.66.5" }, { name = "pydantic", specifier = ">=2.10,<3" }, @@ -1152,6 +1167,7 @@ dev = [ { name = "inline-snapshot", specifier = ">=0.20.7" }, { name = "mkdocs", specifier = ">=1.6.0" }, { name = "mkdocs-material", specifier = ">=9.6.0" }, + { name = "mkdocs-static-i18n" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.28.0" }, { name = "mypy" }, { name = "playwright", specifier = "==1.50.0" }, From 869c95b012b242ba1333ae01113ba8e1cc54a70b Mon Sep 17 00:00:00 2001 From: kanchi <17161397+KanchiShimono@users.noreply.github.com> Date: Wed, 9 Apr 2025 00:47:15 +0900 Subject: [PATCH 21/85] Update `computer-use` example to support simultaneous key presses (#452) ### Summary Updated the `computer-use` example to support simultaneous key presses. The current implementation presses and releases keys one at a time, which prevents combo inputs like copy (CTRL+C) from working correctly. ### Test plan N/A ### Issue number N/A ### Checks - [ ] I've added new tests (if relevant) - [ ] I've added/updated the relevant documentation - [x] I've run `make lint` and `make format` - [ ] I've made sure tests pass --- examples/tools/computer_use.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/tools/computer_use.py b/examples/tools/computer_use.py index 832255e8..0c17cf95 100644 --- a/examples/tools/computer_use.py +++ b/examples/tools/computer_use.py @@ -148,9 +148,11 @@ async def move(self, x: int, y: int) -> None: await self.page.mouse.move(x, y) async def keypress(self, keys: list[str]) -> None: - for key in keys: - mapped_key = CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) - await self.page.keyboard.press(mapped_key) + mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys] + for key in mapped_keys: + await self.page.keyboard.down(key) + for key in reversed(mapped_keys): + await self.page.keyboard.up(key) async def drag(self, path: list[tuple[int, int]]) -> None: if not path: From ccff74dcfabb6dd0f323e8489b15e78692ac5960 Mon Sep 17 00:00:00 2001 From: nikkie Date: Wed, 9 Apr 2025 00:47:23 +0900 Subject: [PATCH 22/85] docs: Make default model clear (#457) close https://github.com/openai/openai-agents-python/issues/440 --- src/agents/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index 4c6de245..a24456b0 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -108,7 +108,7 @@ class Agent(Generic[TContext]): """The model implementation to use when invoking the LLM. By default, if not set, the agent will use the default model configured in - `model_settings.DEFAULT_MODEL`. + `openai_provider.DEFAULT_MODEL` (currently "gpt-4o"). """ model_settings: ModelSettings = field(default_factory=ModelSettings) From d089886f01d46e3a4cd550d50a68d15a56cbba78 Mon Sep 17 00:00:00 2001 From: nikkie Date: Wed, 9 Apr 2025 03:01:09 +0900 Subject: [PATCH 23/85] examples(research_bot): Add space to INSTRUCTIONS (#439) Thanks to awesome example `research_bot` Fix andproduce and goodgrammar (and so on) ```python >>> from examples.research_bot.agents import search_agent >>> print(search_agent.INSTRUCTIONS) You are a research assistant. Given a search term, you search the web for that term andproduce a concise summary of the results. The summary must 2-3 paragraphs and less than 300words. Capture the main points. Write succinctly, no need to have complete sentences or goodgrammar. This will be consumed by someone synthesizing a report, so its vital you capture theessence and ignore any fluff. Do not include any additional commentary other than the summaryitself. ``` --- examples/research_bot/agents/search_agent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/research_bot/agents/search_agent.py b/examples/research_bot/agents/search_agent.py index f69cfda8..0212ce5b 100644 --- a/examples/research_bot/agents/search_agent.py +++ b/examples/research_bot/agents/search_agent.py @@ -2,11 +2,11 @@ from agents.model_settings import ModelSettings INSTRUCTIONS = ( - "You are a research assistant. Given a search term, you search the web for that term and" - "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300" - "words. Capture the main points. Write succinctly, no need to have complete sentences or good" - "grammar. This will be consumed by someone synthesizing a report, so its vital you capture the" - "essence and ignore any fluff. Do not include any additional commentary other than the summary" + "You are a research assistant. Given a search term, you search the web for that term and " + "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 " + "words. Capture the main points. Write succinctly, no need to have complete sentences or good " + "grammar. This will be consumed by someone synthesizing a report, so its vital you capture the " + "essence and ignore any fluff. Do not include any additional commentary other than the summary " "itself." ) From 84fb734ba042499d4c080410f6c337ffb50c138a Mon Sep 17 00:00:00 2001 From: Dmitry Pimenov Date: Wed, 9 Apr 2025 11:59:07 -0700 Subject: [PATCH 24/85] Adding scope and span_data doc strings (#463) Built docs and tested outputs locally --- src/agents/tracing/scope.py | 4 +++ src/agents/tracing/span_data.py | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/agents/tracing/scope.py b/src/agents/tracing/scope.py index 513ca8c0..1d31c1bd 100644 --- a/src/agents/tracing/scope.py +++ b/src/agents/tracing/scope.py @@ -18,6 +18,10 @@ class Scope: + """ + Manages the current span and trace in the context. + """ + @classmethod def get_current_span(cls) -> "Span[Any] | None": return _current_span.get() diff --git a/src/agents/tracing/span_data.py b/src/agents/tracing/span_data.py index fe2e0618..260e4c45 100644 --- a/src/agents/tracing/span_data.py +++ b/src/agents/tracing/span_data.py @@ -9,17 +9,28 @@ class SpanData(abc.ABC): + """ + Represents span data in the trace. + """ + @abc.abstractmethod def export(self) -> dict[str, Any]: + """Export the span data as a dictionary.""" pass @property @abc.abstractmethod def type(self) -> str: + """Return the type of the span.""" pass class AgentSpanData(SpanData): + """ + Represents an Agent Span in the trace. + Includes name, handoffs, tools, and output type. + """ + __slots__ = ("name", "handoffs", "tools", "output_type") def __init__( @@ -49,6 +60,11 @@ def export(self) -> dict[str, Any]: class FunctionSpanData(SpanData): + """ + Represents a Function Span in the trace. + Includes input, output and MCP data (if applicable). + """ + __slots__ = ("name", "input", "output", "mcp_data") def __init__( @@ -78,6 +94,11 @@ def export(self) -> dict[str, Any]: class GenerationSpanData(SpanData): + """ + Represents a Generation Span in the trace. + Includes input, output, model, model configuration, and usage. + """ + __slots__ = ( "input", "output", @@ -116,6 +137,11 @@ def export(self) -> dict[str, Any]: class ResponseSpanData(SpanData): + """ + Represents a Response Span in the trace. + Includes response and input. + """ + __slots__ = ("response", "input") def __init__( @@ -140,6 +166,11 @@ def export(self) -> dict[str, Any]: class HandoffSpanData(SpanData): + """ + Represents a Handoff Span in the trace. + Includes source and desitnation agents. + """ + __slots__ = ("from_agent", "to_agent") def __init__(self, from_agent: str | None, to_agent: str | None): @@ -159,6 +190,11 @@ def export(self) -> dict[str, Any]: class CustomSpanData(SpanData): + """ + Represents a Custom Span in the trace. + Includes name and data property bag. + """ + __slots__ = ("name", "data") def __init__(self, name: str, data: dict[str, Any]): @@ -178,6 +214,11 @@ def export(self) -> dict[str, Any]: class GuardrailSpanData(SpanData): + """ + Represents a Guardrail Span in the trace. + Includes name and triggered status. + """ + __slots__ = ("name", "triggered") def __init__(self, name: str, triggered: bool = False): @@ -197,6 +238,11 @@ def export(self) -> dict[str, Any]: class TranscriptionSpanData(SpanData): + """ + Represents a Transcription Span in the trace. + Includes input, output, model, and model configuration. + """ + __slots__ = ( "input", "output", @@ -236,6 +282,11 @@ def export(self) -> dict[str, Any]: class SpeechSpanData(SpanData): + """ + Represents a Speech Span in the trace. + Includes input, output, model, model configuration, and first content timestamp. + """ + __slots__ = ("input", "output", "model", "model_config", "first_content_at") def __init__( @@ -273,6 +324,10 @@ def export(self) -> dict[str, Any]: class SpeechGroupSpanData(SpanData): + """ + Represents a Speech Group Span in the trace. + """ + __slots__ = "input" def __init__( @@ -293,6 +348,11 @@ def export(self) -> dict[str, Any]: class MCPListToolsSpanData(SpanData): + """ + Represents an MCP List Tools Span in the trace. + Includes server and result. + """ + __slots__ = ( "server", "result", From 8ded8a9981abf21659ed0c8e0430f98ea0f1f7b4 Mon Sep 17 00:00:00 2001 From: Ddper Date: Fri, 11 Apr 2025 04:54:00 +0800 Subject: [PATCH 25/85] add overwrite mechanism for stream_options (#465) fix issue https://github.com/openai/openai-agents-python/issues/442 below is an example to overwrite include_usage ``` result = Runner.run_streamed( agent, "Write a haiku about recursion in programming.", run_config=RunConfig( model_provider=CUSTOM_MODEL_PROVIDER, model_settings=ModelSettings(include_usage=True) ), ) ``` --- src/agents/model_settings.py | 4 ++++ src/agents/models/openai_chatcompletions.py | 21 +++++++++++++++++++-- tests/test_openai_chatcompletions.py | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index bac71f58..f29cfa4a 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -54,6 +54,10 @@ class ModelSettings: """Whether to store the generated model response for later retrieval. Defaults to True if not provided.""" + include_usage: bool | None = None + """Whether to include usage chunk. + Defaults to True if not provided.""" + def resolve(self, override: ModelSettings | None) -> ModelSettings: """Produce a new ModelSettings by overlaying any non-None values from the override on top of this instance.""" diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index e0aafad0..807c6512 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -521,6 +521,8 @@ async def _fetch_response( reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None store = _Converter.get_store_param(self._get_client(), model_settings) + stream_options = _Converter.get_stream_options_param(self._get_client(), model_settings) + ret = await self._get_client().chat.completions.create( model=self.model, messages=converted_messages, @@ -534,7 +536,7 @@ async def _fetch_response( response_format=response_format, parallel_tool_calls=parallel_tool_calls, stream=stream, - stream_options={"include_usage": True} if stream else NOT_GIVEN, + stream_options=self._non_null_or_not_given(stream_options), store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), extra_headers=_HEADERS, @@ -568,12 +570,27 @@ def _get_client(self) -> AsyncOpenAI: class _Converter: + + @classmethod + def is_openai(cls, client: AsyncOpenAI): + return str(client.base_url).startswith("https://api.openai.com") + @classmethod def get_store_param(cls, client: AsyncOpenAI, model_settings: ModelSettings) -> bool | None: # Match the behavior of Responses where store is True when not given - default_store = True if str(client.base_url).startswith("https://api.openai.com") else None + default_store = True if cls.is_openai(client) else None return model_settings.store if model_settings.store is not None else default_store + @classmethod + def get_stream_options_param( + cls, client: AsyncOpenAI, model_settings: ModelSettings + ) -> dict[str, bool] | None: + default_include_usage = True if cls.is_openai(client) else None + include_usage = model_settings.include_usage if model_settings.include_usage is not None \ + else default_include_usage + stream_options = {"include_usage": include_usage} if include_usage is not None else None + return stream_options + @classmethod def convert_tool_choice( cls, tool_choice: Literal["auto", "required", "none"] | str | None diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index a3198d33..281d7b41 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -282,7 +282,7 @@ def __init__(self, completions: DummyCompletions) -> None: # Check OpenAI client was called for streaming assert completions.kwargs["stream"] is True assert completions.kwargs["store"] is NOT_GIVEN - assert completions.kwargs["stream_options"] == {"include_usage": True} + assert completions.kwargs["stream_options"] is NOT_GIVEN # Response is a proper openai Response assert isinstance(response, Response) assert response.id == FAKE_RESPONSES_ID From 68c725f9425ab371e8774e50319e18da61ef2e80 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 11 Apr 2025 05:54:05 +0900 Subject: [PATCH 26/85] Improve translation pipeline details (#475) This pull request improves the translation pipeline, which was introduced by #460. Now the document generation works pretty well with gpt-4o model. --- docs/agents.md | 8 +- docs/config.md | 2 +- docs/ja/agents.md | 52 ++++----- docs/ja/config.md | 32 +++--- docs/ja/context.md | 46 ++++---- docs/ja/examples.md | 42 ++++---- docs/ja/guardrails.md | 54 +++++----- docs/ja/handoffs.md | 40 +++---- docs/ja/index.md | 36 +++---- docs/ja/mcp.md | 37 +++---- docs/ja/models.md | 62 +++++------ docs/ja/multi_agent.md | 46 ++++---- docs/ja/quickstart.md | 47 ++++---- docs/ja/results.md | 48 ++++----- docs/ja/running_agents.md | 76 ++++++------- docs/ja/streaming.md | 24 ++--- docs/ja/tools.md | 73 ++++++------- docs/ja/tracing.md | 98 +++++++++-------- docs/ja/visualization.md | 32 +++--- docs/ja/voice/pipeline.md | 32 +++--- docs/ja/voice/quickstart.md | 30 +++--- docs/ja/voice/tracing.md | 18 ++-- docs/quickstart.md | 2 +- docs/scripts/translate_docs.py | 192 +++++++++++++++++++++++++-------- 24 files changed, 624 insertions(+), 505 deletions(-) diff --git a/docs/agents.md b/docs/agents.md index 23b18b64..39d4afd5 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -32,11 +32,11 @@ Agents are generic on their `context` type. Context is a dependency-injection to ```python @dataclass class UserContext: - uid: str - is_pro_user: bool + uid: str + is_pro_user: bool - async def fetch_purchases() -> list[Purchase]: - return ... + async def fetch_purchases() -> list[Purchase]: + return ... agent = Agent[UserContext]( ..., diff --git a/docs/config.md b/docs/config.md index 3cf83730..bfaf90e8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -63,7 +63,7 @@ Alternatively, you can customize the logs by adding handlers, filters, formatter ```python import logging -logger = logging.getLogger("openai.agents") # or openai.agents.tracing for the Tracing logger +logger = logging.getLogger("openai.agents") # or openai.agents.tracing for the Tracing logger # To make all logs show up logger.setLevel(logging.DEBUG) diff --git a/docs/ja/agents.md b/docs/ja/agents.md index e9e303bc..5d7a2dfe 100644 --- a/docs/ja/agents.md +++ b/docs/ja/agents.md @@ -1,14 +1,14 @@ # エージェント -エージェント はアプリケーションの基本的な構成要素です。エージェント は、大規模言語モデル(LLM)であり、 instructions と tools を用いて設定されます。 +エージェントはアプリのコア構成要素です。エージェントは、指示とツールで構成された大規模言語モデル (LLM) です。 ## 基本設定 -エージェント の設定でよく使われるプロパティは以下の通りです。 +エージェントで設定する最も一般的なプロパティは次のとおりです: -- `instructions` : 開発者メッセージまたはシステムプロンプトとも呼ばれます。 -- `model` : 使用する LLM を指定します。オプションで `model_settings` を指定し、temperature や top_p などのモデル調整パラメータを設定できます。 -- `tools` : エージェント がタスクを達成するために使用できるツールです。 +- `instructions`: 開発者メッセージまたはシステムプロンプトとも呼ばれます。 +- `model`: 使用する LLM と、temperature や top_p などのモデル調整パラメーターを設定するためのオプションの `model_settings`。 +- `tools`: エージェントがタスクを達成するために使用できるツール。 ```python from agents import Agent, ModelSettings, function_tool @@ -27,16 +27,16 @@ agent = Agent( ## コンテキスト -エージェント は、 `context` 型に対してジェネリックです。コンテキストは依存性注入のためのツールであり、作成したオブジェクトを `Runner.run()` に渡すことで、各エージェント、ツール、ハンドオフなどに渡されます。これはエージェントの実行に必要な依存関係や状態をまとめて保持するためのものです。任意の Python オブジェクトをコンテキストとして提供できます。 +エージェントは `context` タイプに対して汎用的です。コンテキストは依存性注入ツールであり、`Runner.run()` に渡すために作成するオブジェクトです。これはすべてのエージェント、ツール、ハンドオフなどに渡され、エージェントの実行に必要な依存関係や状態をまとめたものとして機能します。任意の Python オブジェクトをコンテキストとして提供できます。 ```python @dataclass class UserContext: - uid: str - is_pro_user: bool + uid: str + is_pro_user: bool - async def fetch_purchases() -> list[Purchase]: - return ... + async def fetch_purchases() -> list[Purchase]: + return ... agent = Agent[UserContext]( ..., @@ -45,7 +45,7 @@ agent = Agent[UserContext]( ## 出力タイプ -デフォルトでは、エージェント はプレーンテキスト(つまり `str` )を出力します。特定の型の出力を生成させたい場合は、 `output_type` パラメータを使用します。一般的には [Pydantic](https://docs.pydantic.dev/) オブジェクトを使用しますが、Pydantic の [TypeAdapter](https://docs.pydantic.dev/latest/api/type_adapter/) でラップ可能な型(データクラス、リスト、TypedDict など)であればどのような型でもサポートしています。 +デフォルトでは、エージェントはプレーンテキスト(つまり `str`)の出力を生成します。特定のタイプの出力をエージェントに生成させたい場合は、`output_type` パラメーターを使用できます。一般的な選択肢として [Pydantic](https://docs.pydantic.dev/) オブジェクトを使用しますが、Pydantic の [TypeAdapter](https://docs.pydantic.dev/latest/api/type_adapter/) でラップできる任意のタイプをサポートしています - データクラス、リスト、TypedDict など。 ```python from pydantic import BaseModel @@ -66,11 +66,11 @@ agent = Agent( !!! note - `output_type` を指定すると、モデルは通常のプレーンテキストのレスポンスではなく、 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用します。 + `output_type` を渡すと、モデルに通常のプレーンテキスト応答の代わりに [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用するよう指示します。 ## ハンドオフ -ハンドオフ は、エージェント が処理を委譲できるサブエージェントです。ハンドオフのリストを提供すると、エージェント は必要に応じてそれらに処理を委譲できます。これは、特定のタスクに特化したモジュール型のエージェントを組み合わせて調整するための強力なパターンです。詳細は [ハンドオフ](handoffs.md) のドキュメントを参照してください。 +ハンドオフは、エージェントが委任できるサブエージェントです。ハンドオフのリストを提供し、エージェントは関連する場合にそれらに委任することができます。これは、単一のタスクに優れたモジュール化された専門エージェントを編成する強力なパターンです。[handoffs](handoffs.md) ドキュメントで詳細をお読みください。 ```python from agents import Agent @@ -89,9 +89,9 @@ triage_agent = Agent( ) ``` -## 動的な instructions +## 動的指示 -多くの場合、エージェント 作成時に instructions を指定しますが、関数を通じて動的に instructions を提供することも可能です。この関数はエージェントとコンテキストを受け取り、プロンプトを返します。通常の関数と `async` 関数の両方が使用可能です。 +ほとんどの場合、エージェントを作成する際に指示を提供できますが、関数を介して動的な指示を提供することもできます。この関数はエージェントとコンテキストを受け取り、プロンプトを返す必要があります。通常の関数と `async` 関数の両方が受け入れられます。 ```python def dynamic_instructions( @@ -108,15 +108,15 @@ agent = Agent[UserContext]( ## ライフサイクルイベント(フック) -エージェント のライフサイクルを監視したい場合があります。例えば、イベントをログに記録したり、特定のイベント発生時にデータを事前取得したりできます。エージェント のライフサイクルにフックするには、 `hooks` プロパティを使用します。[`AgentHooks`][agents.lifecycle.AgentHooks] クラスをサブクラス化し、関心のあるメソッドをオーバーライドします。 +時には、エージェントのライフサイクルを観察したいことがあります。たとえば、イベントを記録したり、特定のイベントが発生したときにデータを事前取得したりすることができます。`hooks` プロパティを使用してエージェントのライフサイクルにフックすることができます。[`AgentHooks`][agents.lifecycle.AgentHooks] クラスをサブクラス化し、興味のあるメソッドをオーバーライドします。 ## ガードレール -ガードレール を使用すると、エージェント の実行と並行してユーザー入力に対するチェックや検証を実行できます。例えば、ユーザー入力の関連性を検証できます。詳細は [ガードレール](guardrails.md) のドキュメントを参照してください。 +ガードレールを使用すると、エージェントの実行と並行してユーザー入力のチェック/検証を行うことができます。たとえば、ユーザーの入力を関連性でスクリーニングすることができます。[guardrails](guardrails.md) ドキュメントで詳細をお読みください。 -## エージェント の複製(クローン) +## エージェントのクローン/コピー -エージェント の `clone()` メソッドを使用すると、エージェント を複製し、必要に応じてプロパティを変更できます。 +エージェントの `clone()` メソッドを使用すると、エージェントを複製し、任意のプロパティを変更することができます。 ```python pirate_agent = Agent( @@ -133,15 +133,15 @@ robot_agent = pirate_agent.clone( ## ツール使用の強制 -ツールのリストを提供しても、必ずしも LLM がツールを使用するとは限りません。ツールの使用を強制するには、 [`ModelSettings.tool_choice`][agents.model_settings.ModelSettings.tool_choice] を設定します。有効な値は以下の通りです。 +ツールのリストを提供しても、LLM がツールを使用するとは限りません。[`ModelSettings.tool_choice`][agents.model_settings.ModelSettings.tool_choice] を設定することでツール使用を強制できます。有効な値は次のとおりです: -1. `auto` : LLM がツールを使用するかどうかを決定します。 -2. `required` : LLM にツールの使用を強制します(ただし、どのツールを使用するかは LLM が判断します)。 -3. `none` : LLM にツールを使用しないことを強制します。 -4. 特定の文字列(例: `my_tool` )を設定すると、LLM はその特定のツールを使用することを強制されます。 +1. `auto`:LLM がツールを使用するかどうかを決定します。 +2. `required`:LLM がツールを使用する必要があります(ただし、どのツールを使用するかは賢く決定できます)。 +3. `none`:LLM がツールを使用しないことを要求します。 +4. 特定の文字列を設定する例:`my_tool`、特定のツールを使用することを要求します。 !!! note - 無限ループを防ぐため、フレームワークはツール呼び出し後に自動的に `tool_choice` を "auto" にリセットします。この動作は [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice] で設定可能です。無限ループは、ツールの実行結果が LLM に送信され、再びツール呼び出しが生成されるために発生します。 + 無限ループを防ぐために、フレームワークはツール呼び出し後に `tool_choice` を自動的に "auto" にリセットします。この動作は [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice] で設定可能です。無限ループは、ツールの結果が LLM に送信され、その後 `tool_choice` によって別のツール呼び出しが生成されるために発生します。 - ツール呼び出し後に完全に停止させたい場合(自動モードで続行しない場合)は、 [`Agent.tool_use_behavior="stop_on_first_tool"`] を設定すると、ツールの出力を最終レスポンスとして直接使用し、それ以上の LLM 処理を行いません。 \ No newline at end of file + ツール呼び出し後にエージェントが完全に停止するようにしたい場合(自動モードで続行するのではなく)、[`Agent.tool_use_behavior="stop_on_first_tool"`] を設定することで、ツールの出力を最終応答として直接使用し、さらなる LLM 処理を行わないようにできます。 \ No newline at end of file diff --git a/docs/ja/config.md b/docs/ja/config.md index 6a764eb7..f8586e8a 100644 --- a/docs/ja/config.md +++ b/docs/ja/config.md @@ -2,7 +2,7 @@ ## API キーとクライアント -デフォルトでは、SDK はインポート時に LLM リクエストおよびトレーシング用の環境変数 `OPENAI_API_KEY` を参照します。アプリケーションの起動前にこの環境変数を設定できない場合は、[set_default_openai_key()][agents.set_default_openai_key] 関数を使用してキーを設定できます。 +デフォルトでは、SDK はインポートされるとすぐに LLM リクエストとトレーシングのために `OPENAI_API_KEY` 環境変数を探します。アプリが起動する前にその環境変数を設定できない場合は、[set_default_openai_key()][agents.set_default_openai_key] 関数を使用してキーを設定できます。 ```python from agents import set_default_openai_key @@ -10,7 +10,7 @@ from agents import set_default_openai_key set_default_openai_key("sk-...") ``` -また、使用する OpenAI クライアントを設定することも可能です。デフォルトでは、SDK は環境変数または上記で設定したデフォルトキーを使用して `AsyncOpenAI` インスタンスを作成します。これを変更するには、[set_default_openai_client()][agents.set_default_openai_client] 関数を使用します。 +また、使用する OpenAI クライアントを設定することもできます。デフォルトでは、SDK は環境変数からの API キーまたは上記で設定したデフォルトキーを使用して `AsyncOpenAI` インスタンスを作成します。これを変更するには、[set_default_openai_client()][agents.set_default_openai_client] 関数を使用します。 ```python from openai import AsyncOpenAI @@ -20,7 +20,7 @@ custom_client = AsyncOpenAI(base_url="...", api_key="...") set_default_openai_client(custom_client) ``` -さらに、使用する OpenAI API をカスタマイズすることもできます。デフォルトでは OpenAI Responses API を使用しますが、[set_default_openai_api()][agents.set_default_openai_api] 関数を使用して Chat Completions API に変更できます。 +最後に、使用する OpenAI API をカスタマイズすることもできます。デフォルトでは、OpenAI Responses API を使用します。これを Chat Completions API に変更するには、[set_default_openai_api()][agents.set_default_openai_api] 関数を使用します。 ```python from agents import set_default_openai_api @@ -30,7 +30,7 @@ set_default_openai_api("chat_completions") ## トレーシング -トレーシングはデフォルトで有効になっています。デフォルトでは、前述のセクションで設定した OpenAI API キー(環境変数またはデフォルトキー)を使用します。トレーシング専用の API キーを設定するには、[`set_tracing_export_api_key`][agents.set_tracing_export_api_key] 関数を使用します。 +トレーシングはデフォルトで有効になっています。デフォルトでは、上記のセクションから OpenAI API キー(環境変数または設定したデフォルトキー)を使用します。トレーシングに使用する API キーを特定に設定するには、[`set_tracing_export_api_key`][agents.set_tracing_export_api_key] 関数を使用します。 ```python from agents import set_tracing_export_api_key @@ -38,7 +38,7 @@ from agents import set_tracing_export_api_key set_tracing_export_api_key("sk-...") ``` -また、[`set_tracing_disabled()`][agents.set_tracing_disabled] 関数を使用してトレーシングを完全に無効化することもできます。 +トレーシングを完全に無効にするには、[`set_tracing_disabled()`][agents.set_tracing_disabled] 関数を使用します。 ```python from agents import set_tracing_disabled @@ -48,7 +48,7 @@ set_tracing_disabled(True) ## デバッグログ -SDK はデフォルトでハンドラーが設定されていない 2 つの Python ロガーを持っています。デフォルトでは、警告およびエラーが `stdout` に送信され、それ以外のログは抑制されます。 +SDK にはハンドラーが設定されていない 2 つの Python ロガーがあります。デフォルトでは、警告とエラーが `stdout` に送信されますが、他のログは抑制されます。 詳細なログを有効にするには、[`enable_verbose_stdout_logging()`][agents.enable_verbose_stdout_logging] 関数を使用します。 @@ -58,36 +58,36 @@ from agents import enable_verbose_stdout_logging enable_verbose_stdout_logging() ``` -また、ハンドラー、フィルター、フォーマッターなどを追加してログをカスタマイズすることも可能です。詳細については、[Python logging ガイド](https://docs.python.org/3/howto/logging.html) を参照してください。 +また、ハンドラー、フィルター、フォーマッターなどを追加してログをカスタマイズすることもできます。詳細は [Python ロギングガイド](https://docs.python.org/3/howto/logging.html) を参照してください。 ```python import logging -logger = logging.getLogger("openai.agents") # トレーシング用ロガーの場合は openai.agents.tracing +logger = logging.getLogger("openai.agents") # or openai.agents.tracing for the Tracing logger -# すべてのログを表示する場合 +# To make all logs show up logger.setLevel(logging.DEBUG) -# INFO 以上を表示する場合 +# To make info and above show up logger.setLevel(logging.INFO) -# WARNING 以上を表示する場合 +# To make warning and above show up logger.setLevel(logging.WARNING) -# その他、必要に応じて設定 +# etc -# 必要に応じてカスタマイズ可能ですが、デフォルトでは `stderr` に出力されます +# You can customize this as needed, but this will output to `stderr` by default logger.addHandler(logging.StreamHandler()) ``` ### ログ内の機密データ -一部のログには機密データ(ユーザーデータなど)が含まれる場合があります。このデータのログ記録を無効化したい場合は、以下の環境変数を設定してください。 +特定のログには機密データ(たとえば、ユーザーデータ)が含まれる場合があります。このデータのログ記録を無効にしたい場合は、次の環境変数を設定します。 -LLM の入力および出力のログ記録を無効化する場合: +LLM の入力と出力のログ記録を無効にするには: ```bash export OPENAI_AGENTS_DONT_LOG_MODEL_DATA=1 ``` -ツールの入力および出力のログ記録を無効化する場合: +ツールの入力と出力のログ記録を無効にするには: ```bash export OPENAI_AGENTS_DONT_LOG_TOOL_DATA=1 diff --git a/docs/ja/context.md b/docs/ja/context.md index 9ad6dc33..3d3c426e 100644 --- a/docs/ja/context.md +++ b/docs/ja/context.md @@ -1,29 +1,29 @@ # コンテキスト管理 -コンテキストという用語は多義的です。主に次の 2 種類のコンテキストがあります。 +コンテキストは多義的な用語です。考慮すべき主なコンテキストのクラスは2つあります。 -1. コード内でローカルに利用可能なコンテキスト:これは、関数ツールの実行時、`on_handoff` などのコールバック時、ライフサイクルフック時などに必要となるデータや依存関係です。 -2. LLM が利用可能なコンテキスト:これは、LLM が応答を生成する際に参照するデータです。 +1. コードにローカルで利用可能なコンテキスト: これは、ツール関数が実行されるときや、`on_handoff` のようなコールバック、ライフサイクルフックなどで必要になるデータや依存関係です。 +2. LLM に利用可能なコンテキスト: これは、LLM が応答を生成するときに参照するデータです。 ## ローカルコンテキスト -これは [`RunContextWrapper`][agents.run_context.RunContextWrapper] クラスと、その内部の [`context`][agents.run_context.RunContextWrapper.context] プロパティを通じて表現されます。動作の仕組みは以下の通りです。 +これは [`RunContextWrapper`][agents.run_context.RunContextWrapper] クラスとその中の [`context`][agents.run_context.RunContextWrapper.context] プロパティを通じて表現されます。動作の仕組みは次の通りです。 -1. 任意の Python オブジェクトを作成します。一般的にはデータクラスや Pydantic オブジェクトを使用します。 -2. 作成したオブジェクトを各種の実行メソッドに渡します(例:`Runner.run(..., context=whatever)`)。 -3. すべての関数ツール呼び出しやライフサイクルフックなどには、ラッパーオブジェクト `RunContextWrapper[T]` が渡されます。ここで `T` はコンテキストオブジェクトの型を表し、`wrapper.context` を通じてアクセスできます。 +1. 任意の Python オブジェクトを作成します。一般的なパターンは、データクラスや Pydantic オブジェクトを使用することです。 +2. そのオブジェクトを様々な実行メソッドに渡します(例: `Runner.run(..., **context=whatever**)`)。 +3. すべてのツール呼び出し、ライフサイクルフックなどにラッパーオブジェクト `RunContextWrapper[T]` が渡されます。ここで `T` はコンテキストオブジェクトの型を表し、`wrapper.context` を通じてアクセスできます。 -**最も重要な点** は、特定のエージェント実行において、すべてのエージェント、関数ツール、ライフサイクルフックなどが同じコンテキストの _型_ を使用しなければならないということです。 +**最も重要な**点は、特定のエージェント実行において、すべてのエージェント、ツール関数、ライフサイクルなどが同じ _型_ のコンテキストを使用しなければならないことです。 -コンテキストは以下のような用途で使用できます。 +コンテキストは以下のような用途に使用できます。 -- 実行時のコンテキストデータ(ユーザー名や UID など、ユーザーに関する情報) -- 依存関係(ロガーオブジェクト、データ取得用オブジェクトなど) +- 実行のためのコンテキストデータ(例: ユーザー名/uid やその他のユーザー情報) +- 依存関係(例: ロガーオブジェクト、データフェッチャーなど) - ヘルパー関数 !!! danger "注意" - コンテキストオブジェクトは LLM には送信され **ません**。これは純粋にローカルなオブジェクトであり、読み取り、書き込み、メソッド呼び出しが可能です。 + コンテキストオブジェクトは **LLM に送信されません**。これは純粋にローカルなオブジェクトであり、読み書きやメソッドの呼び出しが可能です。 ```python import asyncio @@ -61,17 +61,17 @@ if __name__ == "__main__": asyncio.run(main()) ``` -1. これがコンテキストオブジェクトです。ここではデータクラスを使用していますが、任意の型を使用できます。 -2. これは関数ツールです。`RunContextWrapper[UserInfo]` を引数として受け取り、コンテキストからデータを読み取っています。 -3. エージェントにジェネリック型 `UserInfo` を指定することで、型チェッカーがエラーを検出できるようにしています(例えば、異なるコンテキスト型を取るツールを渡そうとした場合など)。 -4. コンテキストを `run` 関数に渡しています。 -5. エージェントが正しくツールを呼び出し、年齢を取得しています。 +1. これはコンテキストオブジェクトです。ここではデータクラスを使用していますが、任意の型を使用できます。 +2. これはツールです。`RunContextWrapper[UserInfo]` を受け取ることがわかります。ツールの実装はコンテキストから読み取ります。 +3. エージェントをジェネリック `UserInfo` でマークし、型チェッカーがエラーを検出できるようにします(例えば、異なるコンテキスト型を取るツールを渡そうとした場合)。 +4. コンテキストは `run` 関数に渡されます。 +5. エージェントは正しくツールを呼び出し、年齢を取得します。 -## エージェント / LLM コンテキスト +## エージェント/LLM コンテキスト -LLM が呼び出される際、参照できるデータは会話履歴に含まれるもの **のみ** です。そのため、新しいデータを LLM に提供したい場合は、会話履歴に含まれるようにする必要があります。これを実現する方法はいくつかあります。 +LLM が呼び出されるとき、**唯一** 参照できるデータは会話履歴からのものです。新しいデータを LLM に利用可能にしたい場合、それを履歴に含める方法で行う必要があります。これにはいくつかの方法があります。 -1. エージェントの `instructions` に追加する方法です。これは「システムプロンプト」または「開発者メッセージ」とも呼ばれます。システムプロンプトは静的な文字列でもよく、またはコンテキストを受け取って文字列を出力する動的な関数でも構いません。これは常に有用な情報(ユーザー名や現在の日付など)を提供する際によく使われる手法です。 -2. `Runner.run` 関数を呼び出す際の `input` に追加する方法です。これは `instructions` と似ていますが、[指揮系統(chain of command)](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command) の下位に位置するメッセージを設定できます。 -3. 関数ツールを通じて公開する方法です。これは _オンデマンド_ なコンテキストに有効で、LLM が必要なデータを判断し、そのデータを取得するためにツールを呼び出します。 -4. 検索(retrieval)やウェブ検索を使用する方法です。これらはファイルやデータベースから関連データを取得(検索)したり、ウェブからデータを取得(ウェブ検索)したりする特殊なツールです。これは関連するコンテキストデータに基づいて応答を「根拠付ける(grounding)」際に有効です。 \ No newline at end of file +1. エージェントの `instructions` に追加します。これは「システムプロンプト」または「開発者メッセージ」とも呼ばれます。システムプロンプトは静的な文字列でも、コンテキストを受け取って文字列を出力する動的な関数でもかまいません。常に有用な情報(例: ユーザーの名前や現在の日付)に対して一般的な戦術です。 +2. `Runner.run` 関数を呼び出す際に `input` に追加します。これは `instructions` 戦術に似ていますが、[chain of command](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command) の下位にメッセージを持つことができます。 +3. 関数ツールを通じて公開します。これはオンデマンドのコンテキストに有用です。LLM がデータを必要とするときにツールを呼び出してそのデータを取得できます。 +4. リトリーバルや Web 検索を使用します。これらはファイルやデータベース(リトリーバル)、または Web(Web 検索)から関連データを取得できる特別なツールです。関連するコンテキストデータで応答を「グラウンド」するのに有用です。 \ No newline at end of file diff --git a/docs/ja/examples.md b/docs/ja/examples.md index 6d9252aa..21a00f67 100644 --- a/docs/ja/examples.md +++ b/docs/ja/examples.md @@ -1,40 +1,40 @@ -# コード例 +# サンプルコード -SDK のさまざまな実装サンプルについては、[リポジトリ](https://github.com/openai/openai-agents-python/tree/main/examples) のコード例セクションをご覧ください。コード例は、異なるパターンや機能を示す複数のカテゴリに分類されています。 +SDK のさまざまなサンプル実装は、[リポジトリ](https://github.com/openai/openai-agents-python/tree/main/examples)のサンプルコードセクションで確認できます。これらのサンプルコードは、異なるパターンや機能を示すいくつかのカテゴリーに整理されています。 -## カテゴリ +## カテゴリー -- **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - このカテゴリのコード例では、一般的なエージェント設計パターンを示しています。例えば、 +- **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** + このカテゴリーの例は、一般的なエージェント設計パターンを示しています。例えば、 - 決定論的ワークフロー - ツールとしてのエージェント - エージェントの並列実行 -- **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** - SDK の基本的な機能を示すコード例です。例えば、 +- **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** + これらの例は、SDK の基本的な機能を紹介しています。例えば、 - 動的なシステムプロンプト - ストリーミング出力 - ライフサイクルイベント -- **[tool examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** - Web 検索やファイル検索などの OpenAI がホストするツールの実装方法と、それらをエージェントに統合する方法を学べます。 +- **[tool examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** + Web 検索やファイル検索などの OpenAI がホストするツールを実装し、それらをエージェントに統合する方法を学びます。 -- **[model providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - SDK で OpenAI 以外のモデルを使用する方法を確認できます。 +- **[model providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** + OpenAI 以外のモデルを SDK で使用する方法を探ります。 -- **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - エージェントのハンドオフに関する実践的なコード例を参照できます。 +- **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** + エージェントのハンドオフの実用的な例を見てみましょう。 -- **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - MCP を使用したエージェントの構築方法を学べます。 +- **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** + MCP を使用してエージェントを構築する方法を学びます。 -- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** および **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 実際のアプリケーションを示す、より具体的なコード例です。 +- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** と **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** + 実際のアプリケーションを示す、さらに詳細な2つの例 - - **customer_service**:航空会社向けのカスタマーサービスシステムのコード例。 - - **research_bot**:シンプルな詳細調査クローンのコード例。 + - **customer_service**: 航空会社向けのカスタマーサービスシステムの例。 + - **research_bot**: シンプルなディープリサーチクローン。 -- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** - TTS および STT モデルを使用した音声エージェントのコード例を参照できます。 \ No newline at end of file +- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** + TTS と STT モデルを使用した音声エージェントの例を見てみましょう。 \ No newline at end of file diff --git a/docs/ja/guardrails.md b/docs/ja/guardrails.md index 1c55b0e0..da38d432 100644 --- a/docs/ja/guardrails.md +++ b/docs/ja/guardrails.md @@ -1,43 +1,43 @@ # ガードレール -ガードレールは、エージェント と _並行して_ 実行され、ユーザー入力のチェックや検証を可能にします。例えば、顧客のリクエストを処理するために非常に高性能(したがって遅く高価)なモデルを使用する エージェント があるとします。悪意のあるユーザーがそのモデルに数学の宿題を解かせるようなことは避けたいでしょう。そのため、高速で安価なモデルを使った ガードレール を実行できます。ガードレール が悪意のある使用を検知すると、即座にエラーを発生させ、高価なモデルの実行を停止し、時間とコストを節約できます。 +ガードレールはエージェントと _並行して_ 実行され、ユーザー入力のチェックと検証を可能にします。例えば、非常に賢い(したがって遅く/高価な)モデルを使用して顧客のリクエストを支援するエージェントがあるとします。悪意のあるユーザーがモデルに数学の宿題を手伝わせるようなことは避けたいでしょう。そこで、速く/安価なモデルでガードレールを実行できます。ガードレールが悪意のある使用を検出した場合、即座にエラーを発生させ、高価なモデルの実行を停止し、時間とお金を節約できます。 -ガードレール には 2 種類あります: +ガードレールには2種類あります: -1. 入力ガードレール:最初のユーザー入力に対して実行されます。 -2. 出力ガードレール:最終的な エージェント の出力に対して実行されます。 +1. 入力ガードレールは初期のユーザー入力に対して実行されます +2. 出力ガードレールは最終的なエージェントの出力に対して実行されます ## 入力ガードレール -入力ガードレール は次の 3 ステップで実行されます: +入力ガードレールは3つのステップで実行されます: -1. 最初に、ガードレール は エージェント に渡されたものと同じ入力を受け取ります。 -2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`InputGuardrailResult`][agents.guardrail.InputGuardrailResult] にラップされます。 -3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な応答や例外処理が可能になります。 +1. まず、ガードレールはエージェントに渡されたのと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput]を生成し、それが[`InputGuardrailResult`][agents.guardrail.InputGuardrailResult]でラップされます。 +3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered]が true かどうかを確認します。true の場合、[`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered]例外が発生し、ユーザーに適切に応答したり、例外を処理したりできます。 !!! Note - 入力ガードレール はユーザー入力に対して実行されるため、エージェント の ガードレール は、その エージェント が *最初の* エージェント である場合にのみ実行されます。なぜ `guardrails` プロパティが エージェント にあり、`Runner.run` に渡されないのか疑問に思うかもしれませんが、これは ガードレール が実際の エージェント に関連付けられる傾向があるためです。異なる エージェント には異なる ガードレール を実行するため、コードを同じ場所に配置することで可読性が向上します。 + 入力ガードレールはユーザー入力に対して実行されることを意図しているため、エージェントのガードレールはエージェントが*最初の*エージェントである場合にのみ実行されます。なぜ `guardrails` プロパティがエージェントにあり、`Runner.run` に渡されないのか疑問に思うかもしれません。それは、ガードレールが実際のエージェントに関連している傾向があるためです。異なるエージェントには異なるガードレールを実行するので、コードを一緒に配置することは可読性に役立ちます。 ## 出力ガードレール -出力ガードレール は次の 3 ステップで実行されます: +出力ガードレールは3つのステップで実行されます: -1. 最初に、ガードレール は エージェント に渡されたものと同じ入力を受け取ります。 -2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`OutputGuardrailResult`][agents.guardrail.OutputGuardrailResult] にラップされます。 -3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な応答や例外処理が可能になります。 +1. まず、ガードレールはエージェントに渡されたのと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput]を生成し、それが[`OutputGuardrailResult`][agents.guardrail.OutputGuardrailResult]でラップされます。 +3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered]が true かどうかを確認します。true の場合、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]例外が発生し、ユーザーに適切に応答したり、例外を処理したりできます。 !!! Note - 出力ガードレール は最終的な エージェント の出力に対して実行されるため、エージェント の ガードレール は、その エージェント が *最後の* エージェント である場合にのみ実行されます。入力ガードレール と同様に、これは ガードレール が実際の エージェント に関連付けられる傾向があるためです。異なる エージェント には異なる ガードレール を実行するため、コードを同じ場所に配置することで可読性が向上します。 + 出力ガードレールは最終的なエージェントの出力に対して実行されることを意図しているため、エージェントのガードレールはエージェントが*最後の*エージェントである場合にのみ実行されます。入力ガードレールと同様に、ガードレールが実際のエージェントに関連している傾向があるため、コードを一緒に配置することは可読性に役立ちます。 -## トリップワイヤ(Tripwires) +## トリップワイヤー -入力または出力が ガードレール を通過できない場合、ガードレール はトリップワイヤを使ってこれを通知します。トリップワイヤがトリガーされた ガードレール を検知すると、即座に `{Input,Output}GuardrailTripwireTriggered` 例外を発生させ、エージェント の実行を停止します。 +入力または出力がガードレールに失敗した場合、ガードレールはトリップワイヤーでこれを知らせることができます。トリップワイヤーがトリガーされたガードレールを確認するとすぐに、`{Input,Output}GuardrailTripwireTriggered`例外を即座に発生させ、エージェントの実行を停止します。 -## ガードレール の実装 +## ガードレールの実装 -入力を受け取り、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を返す関数を提供する必要があります。この例では、内部で エージェント を実行することでこれを実現します。 +入力を受け取り、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput]を返す関数を提供する必要があります。この例では、エージェントを内部で実行することでこれを行います。 ```python from pydantic import BaseModel @@ -90,12 +90,12 @@ async def main(): print("Math homework guardrail tripped") ``` -1. この エージェント を ガードレール関数 内で使用します。 -2. これは エージェント の入力とコンテキストを受け取り、結果を返す ガードレール関数 です。 -3. ガードレール の結果に追加情報を含めることができます。 -4. これはワークフローを定義する実際の エージェント です。 +1. このエージェントをガードレール関数で使用します。 +2. これはエージェントの入力/コンテキストを受け取り、結果を返すガードレール関数です。 +3. ガードレールの結果に追加情報を含めることができます。 +4. これはワークフローを定義する実際のエージェントです。 -出力ガードレール も同様です。 +出力ガードレールも同様です。 ```python from pydantic import BaseModel @@ -148,7 +148,7 @@ async def main(): print("Math output guardrail tripped") ``` -1. これは実際の エージェント の出力タイプです。 -2. これは ガードレール の出力タイプです。 -3. これは エージェント の出力を受け取り、結果を返す ガードレール関数 です。 -4. これはワークフローを定義する実際の エージェント です。 \ No newline at end of file +1. これは実際のエージェントの出力タイプです。 +2. これはガードレールの出力タイプです。 +3. これはエージェントの出力を受け取り、結果を返すガードレール関数です。 +4. これはワークフローを定義する実際のエージェントです。 \ No newline at end of file diff --git a/docs/ja/handoffs.md b/docs/ja/handoffs.md index 2d5433f4..575195b1 100644 --- a/docs/ja/handoffs.md +++ b/docs/ja/handoffs.md @@ -1,18 +1,18 @@ # ハンドオフ -ハンドオフを使用すると、あるエージェントが別のエージェントにタスクを委任できます。これは、異なるエージェントがそれぞれ特定の分野を専門としている場合に特に役立ちます。例えば、カスタマーサポートアプリでは、注文状況、返金、FAQ などのタスクをそれぞれ専門に処理するエージェントを用意できます。 +ハンドオフは、エージェントがタスクを他のエージェントに委任することを可能にします。これは、異なるエージェントが異なる分野に特化しているシナリオで特に有用です。例えば、カスタマーサポートアプリでは、注文状況、返金、FAQ などのタスクをそれぞれ専門に扱うエージェントがいるかもしれません。 -ハンドオフは、LLM に対してツールとして表現されます。例えば、`Refund Agent` という名前のエージェントへのハンドオフがある場合、そのツールは `transfer_to_refund_agent` と呼ばれます。 +ハンドオフは LLM にとってツールとして表現されます。したがって、`Refund Agent` という名前のエージェントへのハンドオフがある場合、ツールは `transfer_to_refund_agent` と呼ばれます。 ## ハンドオフの作成 -すべてのエージェントは [`handoffs`][agents.agent.Agent.handoffs] パラメータを持ち、これは直接 `Agent` を指定するか、またはカスタマイズされた `Handoff` オブジェクトを指定できます。 +すべてのエージェントには [`handoffs`][agents.agent.Agent.handoffs] パラメーターがあり、これは `Agent` を直接取るか、ハンドオフをカスタマイズする `Handoff` オブジェクトを取ることができます。 Agents SDK が提供する [`handoff()`][agents.handoffs.handoff] 関数を使用してハンドオフを作成できます。この関数を使用すると、ハンドオフ先のエージェントを指定し、オプションでオーバーライドや入力フィルターを設定できます。 -### 基本的な使用方法 +### 基本的な使い方 -シンプルなハンドオフの作成方法は以下の通りです。 +簡単なハンドオフを作成する方法は次のとおりです。 ```python from agents import Agent, handoff @@ -24,18 +24,18 @@ refund_agent = Agent(name="Refund agent") triage_agent = Agent(name="Triage agent", handoffs=[billing_agent, handoff(refund_agent)]) ``` -1. エージェントを直接指定する方法(`billing_agent` のように)と、`handoff()` 関数を使用する方法があります。 +1. エージェントを直接使用することも(`billing_agent` のように)、`handoff()` 関数を使用することもできます。 -### `handoff()` 関数を使用したハンドオフのカスタマイズ +### `handoff()` 関数によるハンドオフのカスタマイズ -[`handoff()`][agents.handoffs.handoff] 関数を使用すると、以下の項目をカスタマイズできます。 +[`handoff()`][agents.handoffs.handoff] 関数を使用すると、さまざまなカスタマイズが可能です。 -- `agent`:ハンドオフ先のエージェントを指定します。 -- `tool_name_override`:デフォルトでは `Handoff.default_tool_name()` 関数が使用され、`transfer_to_` という形式になりますが、これをオーバーライドできます。 -- `tool_description_override`:デフォルトのツール説明(`Handoff.default_tool_description()`)をオーバーライドします。 -- `on_handoff`:ハンドオフが呼び出された際に実行されるコールバック関数です。これは、ハンドオフが呼ばれた時点でデータ取得などの処理を開始する場合に便利です。この関数はエージェントのコンテキストを受け取り、オプションで LLM が生成した入力も受け取れます。入力データは `input_type` パラメータで制御されます。 -- `input_type`:ハンドオフが期待する入力の型を指定します(オプション)。 -- `input_filter`:次のエージェントが受け取る入力をフィルタリングできます。詳細は後述します。 +- `agent`: ハンドオフ先のエージェントです。 +- `tool_name_override`: デフォルトでは `Handoff.default_tool_name()` 関数が使用され、`transfer_to_` に解決されます。これをオーバーライドできます。 +- `tool_description_override`: `Handoff.default_tool_description()` からのデフォルトのツール説明をオーバーライドします。 +- `on_handoff`: ハンドオフが呼び出されたときに実行されるコールバック関数です。ハンドオフが呼び出されるとすぐにデータ取得を開始するなどに役立ちます。この関数はエージェントコンテキストを受け取り、オプションで LLM が生成した入力も受け取ることができます。入力データは `input_type` パラメーターで制御されます。 +- `input_type`: ハンドオフが期待する入力のタイプ(オプション)。 +- `input_filter`: 次のエージェントが受け取る入力をフィルタリングできます。詳細は以下を参照してください。 ```python from agents import Agent, handoff, RunContextWrapper @@ -53,9 +53,9 @@ handoff_obj = handoff( ) ``` -## ハンドオフの入力 +## ハンドオフ入力 -特定の状況では、LLM がハンドオフを呼び出す際に何らかのデータを提供するようにしたい場合があります。例えば、「Escalation agent(エスカレーションエージェント)」へのハンドオフを考えると、ログ記録のために理由を提供してほしい場合があります。 +特定の状況では、LLM がハンドオフを呼び出す際にデータを提供することを望む場合があります。例えば、「エスカレーションエージェント」へのハンドオフを想像してください。理由を提供してログに記録したいかもしれません。 ```python from pydantic import BaseModel @@ -79,9 +79,9 @@ handoff_obj = handoff( ## 入力フィルター -ハンドオフが発生すると、新しいエージェントが会話を引き継ぎ、それまでの会話履歴全体を参照できるようになります。これを変更したい場合は、[`input_filter`][agents.handoffs.Handoff.input_filter] を設定できます。入力フィルターは、既存の入力を [`HandoffInputData`][agents.handoffs.HandoffInputData] として受け取り、新しい `HandoffInputData` を返す関数です。 +ハンドオフが発生すると、新しいエージェントが会話を引き継ぎ、以前の会話履歴全体を見ることができます。これを変更したい場合は、[`input_filter`][agents.handoffs.Handoff.input_filter] を設定できます。入力フィルターは、既存の入力を [`HandoffInputData`][agents.handoffs.HandoffInputData] 経由で受け取り、新しい `HandoffInputData` を返す必要がある関数です。 -よくあるパターン(例えば履歴からすべてのツール呼び出しを削除するなど)は、[`agents.extensions.handoff_filters`][] にあらかじめ実装されています。 +一般的なパターン(例えば、履歴からすべてのツール呼び出しを削除する)が [`agents.extensions.handoff_filters`][] に実装されています。 ```python from agents import Agent, handoff @@ -95,11 +95,11 @@ handoff_obj = handoff( ) ``` -1. これにより、`FAQ agent` が呼び出された際に履歴からすべてのツールが自動的に削除されます。 +1. これにより、`FAQ agent` が呼び出されたときに履歴からすべてのツールが自動的に削除されます。 ## 推奨プロンプト -LLM がハンドオフを適切に理解できるようにするため、エージェントのプロンプトにハンドオフに関する情報を含めることを推奨します。推奨されるプレフィックスは [`agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`][] に用意されています。または、[`agents.extensions.handoff_prompt.prompt_with_handoff_instructions`][] を呼び出して、推奨データをプロンプトに自動的に追加できます。 +LLM がハンドオフを正しく理解するために、エージェントにハンドオフに関する情報を含めることをお勧めします。[`agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`][] に推奨されるプレフィックスがあります。または、[`agents.extensions.handoff_prompt.prompt_with_handoff_instructions`][] を呼び出して、プロンプトに推奨データを自動的に追加できます。 ```python from agents import Agent diff --git a/docs/ja/index.md b/docs/ja/index.md index 87b419fd..f36de0ac 100644 --- a/docs/ja/index.md +++ b/docs/ja/index.md @@ -1,28 +1,28 @@ -# OpenAI Agents SDK +# OpenAI エージェント SDK -[OpenAI Agents SDK](https://github.com/openai/openai-agents-python) は、軽量で使いやすく、抽象化を最小限に抑えたエージェントベースの AI アプリケーションを構築できるようにします。これは、以前のエージェント実験である [Swarm](https://github.com/openai/swarm/tree/main) を本番環境向けにアップグレードしたものです。Agents SDK は非常に少数の基本コンポーネント(primitives)で構成されています。 +[OpenAI エージェント SDK](https://github.com/openai/openai-agents-python) は、軽量で使いやすいパッケージでエージェント AI アプリを構築することを可能にします。これは、以前のエージェントの実験である [Swarm](https://github.com/openai/swarm/tree/main) のプロダクション対応のアップグレード版です。エージェント SDK には、非常に少ない基本コンポーネントがあります。 -- **エージェント** : instructions と tools を備えた LLM -- **ハンドオフ** : 特定のタスクを他のエージェントに委任する仕組み -- **ガードレール** : エージェントへの入力を検証する仕組み +- **エージェント**: instructions と tools を備えた LLM +- **ハンドオフ**: エージェントが特定のタスクを他のエージェントに委任することを可能にします +- **ガードレール**: エージェントへの入力を検証することを可能にします -これらの基本コンポーネントを Python と組み合わせることで、ツールとエージェント間の複雑な関係を表現でき、急な学習曲線なしに実世界のアプリケーションを構築できます。さらに SDK には、エージェントのフローを視覚化・デバッグし、評価やモデルのファインチューニングまで可能にする組み込みの **トレーシング** 機能が備わっています。 +これらの基本コンポーネントは Python と組み合わせることで、ツールとエージェント間の複雑な関係を表現するのに十分な強力さを持ち、急な学習曲線なしで実際のアプリケーションを構築することができます。さらに、SDK には組み込みの **トレーシング** があり、エージェントのフローを視覚化してデバッグし、評価し、さらにはアプリケーション用にモデルを微調整することができます。 -## Agents SDK を使う理由 +## なぜエージェント SDK を使うのか -SDK は以下の 2 つの設計原則に基づいています。 +SDK には 2 つの設計原則があります。 -1. 利用価値のある十分な機能を備えつつ、迅速に習得できるよう基本コンポーネントを最小限に抑える。 -2. そのままでも優れた動作をするが、必要に応じて細かくカスタマイズ可能。 +1. 使用する価値があるだけの機能を持ち、学習が迅速になるように基本コンポーネントを少なくする。 +2. すぐに使えるが、何が起こるかを正確にカスタマイズできる。 -SDK の主な機能は以下の通りです。 +SDK の主な機能は次のとおりです。 -- **エージェントループ** : ツールの呼び出し、LLM への実行結果の送信、LLM が完了するまでのループ処理を組み込みで提供。 -- **Python ファースト** : 新しい抽象化を学ぶ必要なく、Python の言語機能を使ってエージェントの連携やチェーンを構築可能。 -- **ハンドオフ** : 複数のエージェント間での調整やタスク委任を可能にする強力な機能。 -- **ガードレール** : エージェントと並行して入力の検証やチェックを実行し、チェックが失敗した場合は早期に処理を中断。 -- **関数ツール** : 任意の Python 関数をツールに変換し、自動的なスキーマ生成と Pydantic による検証を提供。 -- **トレーシング** : ワークフローの視覚化、デバッグ、モニタリングを可能にする組み込みのトレーシング機能。OpenAI が提供する評価、ファインチューニング、蒸留ツールも利用可能。 +- エージェントループ: ツールの呼び出し、LLM への結果の送信、LLM が完了するまでのループを処理する組み込みのエージェントループ。 +- Python ファースト: 新しい抽象化を学ぶ必要なく、組み込みの言語機能を使用してエージェントを編成し、連鎖させる。 +- ハンドオフ: 複数のエージェント間で調整し、委任するための強力な機能。 +- ガードレール: エージェントと並行して入力検証とチェックを実行し、チェックが失敗した場合は早期に中断。 +- 関数ツール: 任意の Python 関数をツールに変換し、自動スキーマ生成と Pydantic による検証を行う。 +- トレーシング: ワークフローを視覚化、デバッグ、監視するための組み込みのトレーシング、および OpenAI の評価、微調整、蒸留ツールを使用。 ## インストール @@ -45,7 +45,7 @@ print(result.final_output) # Infinite loop's dance. ``` -(_実行する場合は、環境変数 `OPENAI_API_KEY` を設定してください。_) +(_これを実行する場合は、`OPENAI_API_KEY` 環境変数を設定してください_) ```bash export OPENAI_API_KEY=sk-... diff --git a/docs/ja/mcp.md b/docs/ja/mcp.md index 52ed1558..057fe188 100644 --- a/docs/ja/mcp.md +++ b/docs/ja/mcp.md @@ -1,21 +1,21 @@ # Model context protocol (MCP) -[Model context protocol](https://modelcontextprotocol.io/introduction)(別名 MCP)は、LLM にツールやコンテキストを提供するための方法です。MCP のドキュメントから引用します: +[Model context protocol](https://modelcontextprotocol.io/introduction) (別名 MCP) は、LLM にツールとコンテキストを提供する方法です。MCP ドキュメントからの引用: -> MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンなプロトコルです。MCP は AI アプリケーションにおける USB-C ポートのようなものです。USB-C がデバイスをさまざまな周辺機器やアクセサリに接続するための標準化された方法を提供するのと同様に、MCP は AI モデルをさまざまなデータソースやツールに接続するための標準化された方法を提供します。 +> MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンプロトコルです。MCP を AI アプリケーションのための USB-C ポートのように考えてください。USB-C がデバイスをさまざまな周辺機器やアクセサリーに接続する標準化された方法を提供するのと同様に、MCP は AI モデルをさまざまなデータソースやツールに接続する標準化された方法を提供します。 -Agents SDK は MCP をサポートしています。これにより、さまざまな MCP サーバーを使用して、エージェントにツールを提供できます。 +エージェント SDK は MCP をサポートしています。これにより、エージェントにツールを提供するために幅広い MCP サーバーを使用できます。 ## MCP サーバー -現在、MCP の仕様では、使用するトランスポートメカニズムに基づいて 2 種類のサーバーが定義されています: +現在、MCP 仕様は使用するトランスポートメカニズムに基づいて2種類のサーバーを定義しています: -1. **stdio** サーバーはアプリケーションのサブプロセスとして実行されます。これらは「ローカル」で実行されると考えることができます。 -2. **HTTP over SSE** サーバーはリモートで実行されます。これらには URL を介して接続します。 +1. **stdio** サーバーは、アプリケーションのサブプロセスとして実行されます。これらは「ローカルで」実行されると考えることができます。 +2. **HTTP over SSE** サーバーはリモートで実行されます。URL を介して接続します。 -これらのサーバーに接続するには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] および [`MCPServerSse`][agents.mcp.server.MCPServerSse] クラスを使用できます。 +これらのサーバーに接続するには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] クラスを使用できます。 -例えば、[公式 MCP ファイルシステムサーバー](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem)を使用する場合は以下のようになります: +例えば、[公式 MCP ファイルシステムサーバー](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem) を使用する方法は次のとおりです。 ```python async with MCPServerStdio( @@ -27,11 +27,12 @@ async with MCPServerStdio( tools = await server.list_tools() ``` -## MCP サーバーの使用方法 +## MCP サーバーの使用 -MCP サーバーはエージェントに追加できます。Agents SDK はエージェントが実行されるたびに MCP サーバーの `list_tools()` を呼び出します。これにより、LLM は MCP サーバーのツールを認識します。LLM が MCP サーバーのツールを呼び出すと、SDK はそのサーバーの `call_tool()` を呼び出します。 +MCP サーバーはエージェントに追加できます。エージェント SDK は、エージェントが実行されるたびに MCP サーバーで `list_tools()` を呼び出します。これにより、LLM は MCP サーバーのツールを認識します。LLM が MCP サーバーのツールを呼び出すと、SDK はそのサーバーで `call_tool()` を呼び出します。 ```python + agent=Agent( name="Assistant", instructions="Use the tools to achieve the task", @@ -39,21 +40,21 @@ agent=Agent( ) ``` -## キャッシュ +## キャッシング -エージェントが実行されるたびに、MCP サーバーの `list_tools()` が呼び出されます。特にサーバーがリモートの場合、これはレイテンシの原因となる可能性があります。ツールのリストを自動的にキャッシュするには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] の両方に `cache_tools_list=True` を渡します。これは、ツールのリストが変更されないことが確実な場合にのみ行ってください。 +エージェントが実行されるたびに、MCP サーバーで `list_tools()` を呼び出します。これは、特にサーバーがリモートサーバーの場合、レイテンシーの影響を受ける可能性があります。ツールのリストを自動的にキャッシュするには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] に `cache_tools_list=True` を渡すことができます。ツールリストが変更されないことが確実な場合にのみこれを行うべきです。 -キャッシュを無効化したい場合は、サーバーの `invalidate_tools_cache()` を呼び出します。 +キャッシュを無効にしたい場合は、サーバーで `invalidate_tools_cache()` を呼び出すことができます。 -## エンドツーエンドのコード例 +## エンドツーエンドの例 -完全な動作コード例については、[examples/mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp) を参照してください。 +[examples/mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp) で完全な動作例を確認してください。 ## トレーシング -[トレーシング](./tracing.md) は、以下を含む MCP 操作を自動的にキャプチャします: +[トレーシング](./tracing.md) は、MCP 操作を自動的にキャプチャします。これには以下が含まれます: -1. MCP サーバーへのツール一覧取得の呼び出し +1. ツールをリストするための MCP サーバーへの呼び出し 2. 関数呼び出しに関する MCP 関連情報 -![MCP トレーシングのスクリーンショット](../assets/images/mcp-tracing.jpg) \ No newline at end of file +![MCP Tracing Screenshot](../assets/images/mcp-tracing.jpg) \ No newline at end of file diff --git a/docs/ja/models.md b/docs/ja/models.md index b333666a..a71c9fef 100644 --- a/docs/ja/models.md +++ b/docs/ja/models.md @@ -1,21 +1,21 @@ # モデル -Agents SDK は、OpenAI モデルを以下の 2 種類の形式で標準サポートしています。 +エージェント SDK は、次の 2 種類の OpenAI モデルをサポートしています。 -- **推奨** : 新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使用して OpenAI API を呼び出す [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] -- [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使用して OpenAI API を呼び出す [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] +- **推奨**: 新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使用して OpenAI API を呼び出す [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]。 +- [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使用して OpenAI API を呼び出す [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。 -## モデルの混在と組み合わせ +## モデルの組み合わせ -単一のワークフロー内で、各エージェントごとに異なるモデルを使用したい場合があります。例えば、トリアージには小型で高速なモデルを使用し、複雑なタスクにはより大きく高性能なモデルを使用することができます。[`Agent`][agents.Agent] を設定する際、以下のいずれかの方法で特定のモデルを選択できます。 +1 つのワークフロー内で、各エージェントに異なるモデルを使用したい場合があります。たとえば、トリアージには小さくて高速なモデルを使用し、複雑なタスクにはより大きくて高機能なモデルを使用することができます。[`Agent`][agents.Agent] を設定する際に、次の方法で特定のモデルを選択できます。 -1. OpenAI モデルの名前を直接指定する。 -2. 任意のモデル名と、その名前をモデルインスタンスにマッピングできる [`ModelProvider`][agents.models.interface.ModelProvider] を指定する。 -3. [`Model`][agents.models.interface.Model] 実装を直接指定する。 +1. OpenAI モデルの名前を渡す。 +2. モデル名とその名前をモデルインスタンスにマッピングできる [`ModelProvider`][agents.models.interface.ModelProvider] を渡す。 +3. 直接 [`Model`][agents.models.interface.Model] 実装を提供する。 -!!! note +!!!note - SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形式をサポートしていますが、各ワークフローでは単一のモデル形式を使用することを推奨します。これは、2 つの形式が異なる機能やツールをサポートしているためです。ワークフローで複数のモデル形式を混在させる必要がある場合は、使用するすべての機能が両方の形式で利用可能であることを確認してください。 + SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形状をサポートしていますが、各ワークフローに対して単一のモデル形状を使用することをお勧めします。2 つの形状は異なる機能とツールをサポートしているためです。ワークフローでモデル形状を組み合わせる必要がある場合は、使用しているすべての機能が両方で利用可能であることを確認してください。 ```python from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel @@ -48,46 +48,46 @@ async def main(): print(result.final_output) ``` -1. OpenAI モデルの名前を直接指定しています。 -2. [`Model`][agents.models.interface.Model] 実装を指定しています。 +1. OpenAI モデルの名前を直接設定します。 +2. [`Model`][agents.models.interface.Model] 実装を提供します。 ## 他の LLM プロバイダーの使用 -他の LLM プロバイダーを使用するには、以下の 3 つの方法があります(コード例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/))。 +他の LLM プロバイダーを 3 つの方法で使用できます(例は[こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/))。 -1. [`set_default_openai_client`][agents.set_default_openai_client] は、グローバルに `AsyncOpenAI` インスタンスを LLM クライアントとして使用したい場合に便利です。これは、LLM プロバイダーが OpenAI 互換の API エンドポイントを持ち、`base_url` と `api_key` を設定できる場合に使用します。設定可能なコード例は [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) を参照してください。 -2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルで指定します。これにより、「この実行におけるすべてのエージェントにカスタムモデルプロバイダーを使用する」と指定できます。設定可能なコード例は [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) を参照してください。 -3. [`Agent.model`][agents.agent.Agent.model] は特定のエージェントインスタンスにモデルを指定できます。これにより、異なるエージェントに異なるプロバイダーを組み合わせて使用できます。設定可能なコード例は [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) を参照してください。 +1. [`set_default_openai_client`][agents.set_default_openai_client] は、`AsyncOpenAI` のインスタンスを LLM クライアントとしてグローバルに使用したい場合に便利です。これは、LLM プロバイダーが OpenAI 互換の API エンドポイントを持っている場合で、`base_url` と `api_key` を設定できます。[examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) に設定可能な例があります。 +2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルで使用します。これにより、「この実行のすべてのエージェントにカスタムモデルプロバイダーを使用する」と指定できます。[examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) に設定可能な例があります。 +3. [`Agent.model`][agents.agent.Agent.model] では、特定のエージェントインスタンスにモデルを指定できます。これにより、異なるエージェントに対して異なるプロバイダーを組み合わせて使用することができます。[examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) に設定可能な例があります。 -`platform.openai.com` の API キーを持っていない場合は、`set_tracing_disabled()` を使用してトレーシングを無効化するか、[別のトレーシングプロセッサ](tracing.md) を設定することを推奨します。 +`platform.openai.com` からの API キーがない場合は、`set_tracing_disabled()` を使用してトレーシングを無効にするか、[異なるトレーシングプロセッサー](tracing.md)を設定することをお勧めします。 !!! note - これらのコード例では Chat Completions API / モデルを使用しています。これは、多くの LLM プロバイダーがまだ Responses API をサポートしていないためです。使用する LLM プロバイダーが Responses API をサポートしている場合は、Responses の使用を推奨します。 + これらの例では、Chat Completions API/モデルを使用しています。なぜなら、ほとんどの LLM プロバイダーはまだ Responses API をサポートしていないからです。LLM プロバイダーがそれをサポートしている場合は、Responses を使用することをお勧めします。 -## 他の LLM プロバイダー使用時の一般的な問題 +## 他の LLM プロバイダーを使用する際の一般的な問題 -### トレーシングクライアントのエラー 401 +### トレーシングクライアントエラー 401 -トレーシングに関連するエラーが発生する場合、これはトレースが OpenAI サーバーにアップロードされるためであり、OpenAI API キーを持っていないことが原因です。解決方法は以下の 3 つです。 +トレーシングに関連するエラーが発生した場合、これはトレースが OpenAI サーバーにアップロードされ、OpenAI API キーを持っていないためです。これを解決するには、次の 3 つのオプションがあります。 -1. トレーシングを完全に無効化する : [`set_tracing_disabled(True)`][agents.set_tracing_disabled] -2. トレーシング用に OpenAI キーを設定する : [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースのアップロードのみに使用され、[platform.openai.com](https://platform.openai.com/) から取得する必要があります。 -3. OpenAI 以外のトレースプロセッサを使用する : 詳細は [トレーシングドキュメント](tracing.md#custom-tracing-processors) を参照してください。 +1. トレーシングを完全に無効にする: [`set_tracing_disabled(True)`][agents.set_tracing_disabled]。 +2. トレーシング用の OpenAI キーを設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースのアップロードにのみ使用され、[platform.openai.com](https://platform.openai.com/) からのものでなければなりません。 +3. 非 OpenAI トレースプロセッサーを使用する。[トレーシングドキュメント](tracing.md#custom-tracing-processors)を参照してください。 -### Responses API のサポート +### Responses API サポート -SDK はデフォルトで Responses API を使用しますが、多くの他の LLM プロバイダーはまだこれをサポートしていません。そのため、404 エラーなどが発生する場合があります。解決方法は以下の 2 つです。 +SDK はデフォルトで Responses API を使用しますが、ほとんどの他の LLM プロバイダーはまだサポートしていません。その結果、404 エラーや類似の問題が発生することがあります。これを解決するには、次の 2 つのオプションがあります。 -1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出す。これは環境変数で `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に有効です。 -2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使用する。コード例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) を参照してください。 +1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出します。これは、環境変数を介して `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に機能します。 +2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使用します。例は[こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)にあります。 -### structured outputs のサポート +### 適切な形式のデータのサポート -一部のモデルプロバイダーは [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。そのため、以下のようなエラーが発生する場合があります。 +一部のモデルプロバイダーは[適切な形式のデータ](https://platform.openai.com/docs/guides/structured-outputs)をサポートしていません。これにより、次のようなエラーが発生することがあります。 ``` BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}} ``` -これは一部のモデルプロバイダーの制限であり、JSON 出力はサポートしていますが、出力に使用する `json_schema` を指定できないためです。この問題への対応を進めていますが、現時点では JSON スキーマ出力をサポートするプロバイダーを使用することを推奨します。そうでない場合、アプリケーションが不適切な JSON により頻繁に破損する可能性があります。 \ No newline at end of file +これは一部のモデルプロバイダーの欠点で、JSON 出力をサポートしていますが、出力に使用する `json_schema` を指定することはできません。この問題の修正に取り組んでいますが、JSON スキーマ出力をサポートしているプロバイダーに依存することをお勧めします。さもないと、アプリが不正な JSON のために頻繁に壊れることになります。 \ No newline at end of file diff --git a/docs/ja/multi_agent.md b/docs/ja/multi_agent.md index 4c8be96d..af635776 100644 --- a/docs/ja/multi_agent.md +++ b/docs/ja/multi_agent.md @@ -1,37 +1,37 @@ -# 複数のエージェントのオーケストレーション +# 複数エージェントのオーケストレーション -オーケストレーションとは、アプリ内でのエージェントの流れを指します。どのエージェントがどの順序で実行され、次に何を行うかをどのように決定するかを意味します。エージェントをオーケストレーションする主な方法は 2 つあります。 +オーケストレーションとは、アプリ内でのエージェントの流れを指します。どのエージェントが実行され、どの順序で、次に何が起こるかをどのように決定するかです。エージェントをオーケストレーションする主な方法は2つあります。 -1. LLM に判断を任せる方法:LLM の知性を利用して計画、推論を行い、それに基づいて次のステップを決定します。 -2. コードによるオーケストレーション:コードを通じてエージェントの流れを決定します。 +1. LLM に決定を任せる: これは LLM の知能を利用して、計画、推論し、それに基づいてどのステップを取るかを決定します。 +2. コードによるオーケストレーション: コードを通じてエージェントの流れを決定します。 -これらのパターンは組み合わせて使用することも可能です。それぞれのトレードオフについて以下で説明します。 +これらのパターンを組み合わせて使用することができます。それぞれに独自のトレードオフがあり、以下で説明します。 ## LLM によるオーケストレーション -エージェントとは、LLM に instructions、tools、handoffs を与えたものです。これにより、自由度の高いタスクを与えられた場合でも、LLM は自律的にタスクの取り組み方を計画し、tools を使ってアクションを実行したりデータを取得したり、handoffs を使ってサブエージェントにタスクを委任したりできます。例えば、調査エージェントには以下のようなツールを装備できます。 +エージェントは、指示、ツール、ハンドオフを備えた LLM です。これにより、オープンエンドなタスクが与えられた場合、LLM は自律的にタスクに取り組む方法を計画し、ツールを使用してアクションを実行し、データを取得し、ハンドオフを使用してサブエージェントにタスクを委任できます。例えば、リサーチエージェントは次のようなツールを備えることができます。 -- Web 検索:オンラインで情報を検索 -- ファイル検索と取得:独自データや接続先から情報を検索 -- コンピュータ操作:コンピュータ上でアクションを実行 -- コード実行:データ分析を実施 -- ハンドオフ:計画立案やレポート作成などに特化したエージェントにタスクを委任 +- Web 検索でオンライン情報を見つける +- ファイル検索と取得で独自データや接続を検索する +- コンピュータ操作でコンピュータ上でアクションを実行する +- コード実行でデータ分析を行う +- 計画やレポート作成に優れた専門エージェントへのハンドオフ -このパターンは、自由度の高いタスクで LLM の知性に頼りたい場合に適しています。ここで重要な戦術は以下の通りです。 +このパターンは、タスクがオープンエンドであり、LLM の知能に依存したい場合に最適です。ここでの最も重要な戦術は次のとおりです。 -1. 良質なプロンプトに投資する。利用可能なツール、使用方法、動作パラメータを明確にする。 -2. アプリを監視し、繰り返し改善する。問題が発生した箇所を特定し、プロンプトを改善する。 -3. エージェントが自己内省し改善できるようにする。例えばループ内で実行し、自らを批評させたり、エラーメッセージを与えて改善させたりする。 -4. 汎用的なエージェントにあらゆるタスクを任せるのではなく、特定のタスクに秀でた専門エージェントを用意する。 -5. [evals](https://platform.openai.com/docs/guides/evals) に投資する。これによりエージェントを訓練し、タスクの遂行能力を向上させることができる。 +1. 良いプロンプトに投資する。利用可能なツール、使用方法、および操作するパラメーターを明確にします。 +2. アプリを監視し、反復する。問題が発生する箇所を確認し、プロンプトを改善します。 +3. エージェントに内省と改善を許可する。例えば、ループで実行し、自己批評させるか、エラーメッセージを提供して改善させます。 +4. 一つのタスクに優れた専門エージェントを持つこと。何でも得意とされる汎用エージェントを持つのではなく。 +5. [evals](https://platform.openai.com/docs/guides/evals) に投資する。これにより、エージェントを訓練してタスクの改善と向上を図ることができます。 ## コードによるオーケストレーション -LLM によるオーケストレーションは強力ですが、コードによるオーケストレーションは速度、コスト、パフォーマンスの観点でより決定論的で予測可能になります。ここでの一般的なパターンは以下の通りです。 +LLM によるオーケストレーションは強力ですが、コードによるオーケストレーションは、速度、コスト、パフォーマンスの面でタスクをより決定的で予測可能にします。ここでの一般的なパターンは次のとおりです。 -- [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用して、コードで検査可能な適切な形式のデータを生成する。例えば、エージェントにタスクをいくつかのカテゴリに分類させ、そのカテゴリに基づいて次のエージェントを選択することができます。 -- 複数のエージェントを連鎖させ、一つのエージェントの出力を次のエージェントの入力に変換する。例えばブログ記事を書くというタスクを、調査、アウトライン作成、記事執筆、批評、改善という一連のステップに分解できます。 -- タスクを実行するエージェントを、評価とフィードバックを行うエージェントと共に `while` ループ内で実行し、評価エージェントが出力が一定の基準を満たすと判断するまで繰り返します。 -- 複数のエージェントを並列で実行する。例えば Python の基本コンポーネント (`asyncio.gather` など) を使用します。これは、互いに依存しない複数のタスクを高速に処理する場合に有効です。 +- [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用して、コードで検査できる適切な形式のデータを生成します。例えば、エージェントにタスクをいくつかのカテゴリーに分類させ、そのカテゴリーに基づいて次のエージェントを選択することができます。 +- 複数のエージェントをチェーンして、一つの出力を次の入力に変換します。ブログ記事を書くタスクを、リサーチ、アウトライン作成、記事執筆、批評、改善という一連のステップに分解できます。 +- `while` ループでタスクを実行するエージェントと、評価とフィードバックを提供するエージェントを実行し、評価者が出力が特定の基準を満たすと判断するまで続けます。 +- 複数のエージェントを並行して実行します。例えば、Python の基本コンポーネントである `asyncio.gather` を使用します。これは、互いに依存しない複数のタスクがある場合に速度のために役立ちます。 -[`examples/agent_patterns`](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) に多数のコード例があります。 \ No newline at end of file +[`examples/agent_patterns`](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) には多くのコード例があります。 \ No newline at end of file diff --git a/docs/ja/quickstart.md b/docs/ja/quickstart.md index 691761a2..9e4846ef 100644 --- a/docs/ja/quickstart.md +++ b/docs/ja/quickstart.md @@ -2,7 +2,7 @@ ## プロジェクトと仮想環境の作成 -以下の操作は一度だけ行います。 +これは一度だけ行えば大丈夫です。 ```bash mkdir my_project @@ -10,9 +10,9 @@ cd my_project python -m venv .venv ``` -### 仮想環境の有効化 +### 仮想環境をアクティブにする -新しいターミナルセッションを開始するたびに、この操作を行います。 +新しいターミナルセッションを開始するたびに行ってください。 ```bash source .venv/bin/activate @@ -21,20 +21,20 @@ source .venv/bin/activate ### Agents SDK のインストール ```bash -pip install openai-agents # または `uv add openai-agents` など +pip install openai-agents # or `uv add openai-agents`, etc ``` ### OpenAI API キーの設定 -API キーをお持ちでない場合は、[こちらの手順](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)に従って OpenAI API キーを作成してください。 +お持ちでない場合は、[こちらの手順](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)に従って OpenAI API キーを作成してください。 ```bash export OPENAI_API_KEY=sk-... ``` -## 最初のエージェントの作成 +## 最初のエージェントを作成する -エージェント は、 instructions 、名前、およびオプションの設定( `model_config` など)で定義します。 +エージェントは、instructions、名前、オプションの設定(`model_config` など)で定義されます。 ```python from agents import Agent @@ -45,9 +45,9 @@ agent = Agent( ) ``` -## さらに複数のエージェントを追加する +## さらにエージェントを追加する -追加のエージェントも同様に定義できます。 `handoff_description` は、ハンドオフのルーティングを決定するための追加のコンテキストを提供します。 +追加のエージェントも同様に定義できます。`handoff_descriptions` は、ハンドオフルーティングを決定するための追加のコンテキストを提供します。 ```python from agents import Agent @@ -65,9 +65,9 @@ math_tutor_agent = Agent( ) ``` -## ハンドオフの定義 +## ハンドオフを定義する -各エージェントに対して、タスクを進める方法を決定するために選択可能なハンドオフのオプションを定義できます。 +各エージェントで、タスクを進める方法を決定するための送信ハンドオフオプションのインベントリを定義できます。 ```python triage_agent = Agent( @@ -77,9 +77,9 @@ triage_agent = Agent( ) ``` -## エージェントのオーケストレーションを実行する +## エージェントオーケストレーションを実行する -ワークフローが正しく動作し、トリアージエージェントが 2 つの専門エージェント間で適切にルーティングすることを確認しましょう。 +ワークフローが実行され、トリアージエージェントが2つの専門エージェント間を正しくルーティングするか確認しましょう。 ```python from agents import Runner @@ -89,9 +89,9 @@ async def main(): print(result.final_output) ``` -## ガードレールの追加 +## ガードレールを追加する -入力または出力に対して実行するカスタムのガードレールを定義できます。 +入力または出力に対してカスタムガードレールを定義できます。 ```python from agents import GuardrailFunctionOutput, Agent, Runner @@ -116,9 +116,9 @@ async def homework_guardrail(ctx, agent, input_data): ) ``` -## すべてを統合する +## すべてをまとめる -ハンドオフと入力ガードレールを使用して、ワークフロー全体を統合して実行しましょう。 +すべてをまとめて、ハンドオフと入力ガードレールを使用して全体のワークフローを実行しましょう。 ```python from agents import Agent, InputGuardrail, GuardrailFunctionOutput, Runner @@ -147,6 +147,7 @@ history_tutor_agent = Agent( instructions="You provide assistance with historical queries. Explain important events and context clearly.", ) + async def homework_guardrail(ctx, agent, input_data): result = await Runner.run(guardrail_agent, input_data, context=ctx.context) final_output = result.final_output_as(HomeworkOutput) @@ -175,14 +176,14 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## トレースの確認 +## トレースを表示する -エージェントの実行中に何が起きたかを確認するには、[OpenAI ダッシュボードのトレースビューア](https://platform.openai.com/traces) にアクセスして、エージェント実行のトレースを確認してください。 +エージェントの実行中に何が起こったかを確認するには、[OpenAI ダッシュボードのトレースビューア](https://platform.openai.com/traces)に移動して、エージェント実行のトレースを表示してください。 ## 次のステップ -より複雑なエージェントフローの構築方法を学びましょう。 +より複雑なエージェントフローの構築方法を学びましょう: -- [エージェント](agents.md) の設定方法について学ぶ。 -- [エージェントの実行方法](running_agents.md) について学ぶ。 -- [ツール](tools.md)、[ガードレール](guardrails.md)、[モデル](models.md) について学ぶ。 \ No newline at end of file +- [エージェント](agents.md)の設定方法を学ぶ。 +- [エージェントの実行](running_agents.md)について学ぶ。 +- [ツール](tools.md)、[ガードレール](guardrails.md)、[モデル](models.md)について学ぶ。 \ No newline at end of file diff --git a/docs/ja/results.md b/docs/ja/results.md index 13ff7cef..765bba5c 100644 --- a/docs/ja/results.md +++ b/docs/ja/results.md @@ -1,52 +1,52 @@ -# 実行結果 +# 結果 -`Runner.run` メソッドを呼び出すと、以下のいずれかが返されます。 +`Runner.run` メソッドを呼び出すと、次のいずれかが得られます: -- `run` または `run_sync` を呼び出した場合は [`RunResult`][agents.result.RunResult] -- `run_streamed` を呼び出した場合は [`RunResultStreaming`][agents.result.RunResultStreaming] +- `run` または `run_sync` を呼び出すと [`RunResult`][agents.result.RunResult] +- `run_streamed` を呼び出すと [`RunResultStreaming`][agents.result.RunResultStreaming] -これらはいずれも [`RunResultBase`][agents.result.RunResultBase] を継承しており、ほとんどの有用な情報はここに含まれています。 +これらはどちらも [`RunResultBase`][agents.result.RunResultBase] を継承しており、ほとんどの有用な情報はここにあります。 ## 最終出力 -[`final_output`][agents.result.RunResultBase.final_output] プロパティには、最後に実行されたエージェントの最終出力が格納されています。これは以下のいずれかです。 +[`final_output`][agents.result.RunResultBase.final_output] プロパティには、最後に実行されたエージェントの最終出力が含まれています。これは次のいずれかです: -- 最後のエージェントに `output_type` が定義されていない場合は `str` -- エージェントに `output_type` が定義されている場合は、その型(`last_agent.output_type`)のオブジェクト +- `output_type` が定義されていない場合は `str` +- エージェントに出力タイプが定義されている場合は `last_agent.output_type` のオブジェクト !!! note - `final_output` は型としては `Any` です。これはハンドオフが発生する可能性があるため、静的に型付けできないためです。ハンドオフが発生すると、どのエージェントでも最後のエージェントになり得るため、可能な出力型の集合を静的に特定できません。 + `final_output` は `Any` 型です。ハンドオフのため、静的に型を指定することはできません。ハンドオフが発生すると、どのエージェントが最後になるか分からないため、可能な出力タイプのセットを静的に知ることはできません。 ## 次のターンの入力 -[`result.to_input_list()`][agents.result.RunResultBase.to_input_list] を使用すると、実行結果を入力リストに変換できます。これは、元々提供した入力とエージェントの実行中に生成された項目を連結したものです。これにより、あるエージェントの実行結果を別の実行に渡したり、ループ内で実行して毎回新しいユーザー入力を追加したりすることが容易になります。 +[`result.to_input_list()`][agents.result.RunResultBase.to_input_list] を使用して、結果を入力リストに変換できます。これにより、提供した元の入力をエージェント実行中に生成されたアイテムに連結します。これにより、1つのエージェント実行の出力を別の実行に渡したり、ループで実行して毎回新しいユーザー入力を追加したりするのが便利になります。 ## 最後のエージェント -[`last_agent`][agents.result.RunResultBase.last_agent] プロパティには、最後に実行されたエージェントが格納されています。アプリケーションによっては、次回ユーザーが何かを入力する際に役立つことがあります。例えば、最前線のトリアージエージェントが言語特化型エージェントにハンドオフする場合、最後のエージェントを保存しておき、次回ユーザーがエージェントにメッセージを送る際に再利用できます。 +[`last_agent`][agents.result.RunResultBase.last_agent] プロパティには、最後に実行されたエージェントが含まれています。アプリケーションによっては、次回ユーザーが何かを入力する際に役立つことがよくあります。たとえば、フロントラインのトリアージエージェントが言語特定のエージェントにハンドオフする場合、最後のエージェントを保存し、次回ユーザーがエージェントにメッセージを送信する際に再利用できます。 -## 新規項目 +## 新しいアイテム -[`new_items`][agents.result.RunResultBase.new_items] プロパティには、実行中に生成された新しい項目が格納されています。これらの項目は [`RunItem`][agents.items.RunItem] です。RunItem は LLM によって生成された raw 項目をラップしています。 +[`new_items`][agents.result.RunResultBase.new_items] プロパティには、実行中に生成された新しいアイテムが含まれています。アイテムは [`RunItem`][agents.items.RunItem] です。実行アイテムは LLM によって生成された raw アイテムをラップします。 -- [`MessageOutputItem`][agents.items.MessageOutputItem] は LLM からのメッセージを示します。raw 項目は生成されたメッセージです。 -- [`HandoffCallItem`][agents.items.HandoffCallItem] は LLM がハンドオフツールを呼び出したことを示します。raw 項目は LLM からのツール呼び出し項目です。 -- [`HandoffOutputItem`][agents.items.HandoffOutputItem] はハンドオフが発生したことを示します。raw 項目はハンドオフツール呼び出しに対するツールの応答です。また、この項目からソース/ターゲットエージェントにアクセスできます。 -- [`ToolCallItem`][agents.items.ToolCallItem] は LLM がツールを呼び出したことを示します。 -- [`ToolCallOutputItem`][agents.items.ToolCallOutputItem] はツールが呼び出されたことを示します。raw 項目はツールの応答です。また、この項目からツールの出力にアクセスできます。 -- [`ReasoningItem`][agents.items.ReasoningItem] は LLM による推論項目を示します。raw 項目は生成された推論内容です。 +- [`MessageOutputItem`][agents.items.MessageOutputItem] は LLM からのメッセージを示します。raw アイテムは生成されたメッセージです。 +- [`HandoffCallItem`][agents.items.HandoffCallItem] は LLM がハンドオフツールを呼び出したことを示します。raw アイテムは LLM からのツール呼び出しアイテムです。 +- [`HandoffOutputItem`][agents.items.HandoffOutputItem] はハンドオフが発生したことを示します。raw アイテムはハンドオフツール呼び出しへのツール応答です。アイテムからソース/ターゲットエージェントにもアクセスできます。 +- [`ToolCallItem`][agents.items.ToolCallItem] は LLM がツールを呼び出したことを示します。 +- [`ToolCallOutputItem`][agents.items.ToolCallOutputItem] はツールが呼び出されたことを示します。raw アイテムはツール応答です。アイテムからツール出力にもアクセスできます。 +- [`ReasoningItem`][agents.items.ReasoningItem] は LLM からの推論アイテムを示します。raw アイテムは生成された推論です。 ## その他の情報 -### ガードレールの実行結果 +### ガードレール結果 -[`input_guardrail_results`][agents.result.RunResultBase.input_guardrail_results] および [`output_guardrail_results`][agents.result.RunResultBase.output_guardrail_results] プロパティには、ガードレールの実行結果が格納されています(存在する場合)。ガードレールの実行結果にはログや保存したい有用な情報が含まれることがあるため、これらを利用可能にしています。 +[`input_guardrail_results`][agents.result.RunResultBase.input_guardrail_results] と [`output_guardrail_results`][agents.result.RunResultBase.output_guardrail_results] プロパティには、ガードレールの結果が含まれています。ガードレール結果には、ログや保存に役立つ情報が含まれることがあるため、これらを利用可能にしています。 -### raw レスポンス +### Raw 応答 -[`raw_responses`][agents.result.RunResultBase.raw_responses] プロパティには、LLM によって生成された [`ModelResponse`][agents.items.ModelResponse] が格納されています。 +[`raw_responses`][agents.result.RunResultBase.raw_responses] プロパティには、LLM によって生成された [`ModelResponse`][agents.items.ModelResponse] が含まれています。 ### 元の入力 -[`input`][agents.result.RunResultBase.input] プロパティには、`run` メソッドに提供した元の入力が格納されています。ほとんどの場合、これは必要ありませんが、必要に応じて利用可能です。 \ No newline at end of file +[`input`][agents.result.RunResultBase.input] プロパティには、`run` メソッドに提供した元の入力が含まれています。ほとんどの場合、これを必要としませんが、必要な場合に備えて利用可能です。 \ No newline at end of file diff --git a/docs/ja/running_agents.md b/docs/ja/running_agents.md index b02ee393..c2071c65 100644 --- a/docs/ja/running_agents.md +++ b/docs/ja/running_agents.md @@ -1,10 +1,10 @@ # エージェントの実行 -エージェントは [`Runner`][agents.run.Runner] クラスを通じて実行できます。以下の 3 つの方法があります。 +エージェントは [`Runner`][agents.run.Runner] クラスを通じて実行できます。3つのオプションがあります: -1. [`Runner.run()`][agents.run.Runner.run]:非同期で実行され、[`RunResult`][agents.result.RunResult] を返します。 -2. [`Runner.run_sync()`][agents.run.Runner.run_sync]:同期メソッドで、内部的には `.run()` を呼び出します。 -3. [`Runner.run_streamed()`][agents.run.Runner.run_streamed]:非同期で実行され、[`RunResultStreaming`][agents.result.RunResultStreaming] を返します。LLM をストリーミングモードで呼び出し、受信したイベントを順次ストリームします。 +1. 非同期で実行し、[`RunResult`][agents.result.RunResult] を返す [`Runner.run()`][agents.run.Runner.run]。 +2. 同期メソッドで、内部で `.run()` を実行する [`Runner.run_sync()`][agents.run.Runner.run_sync]。 +3. 非同期で実行し、[`RunResultStreaming`][agents.result.RunResultStreaming] を返す [`Runner.run_streamed()`][agents.run.Runner.run_streamed]。ストリーミングモードで LLM を呼び出し、受信したイベントをストリームします。 ```python from agents import Agent, Runner @@ -19,53 +19,53 @@ async def main(): # Infinite loop's dance. ``` -詳細は [results ガイド](results.md) を参照してください。 +詳細は[結果ガイド](results.md)をご覧ください。 -## エージェントの実行ループ +## エージェントループ -`Runner` の run メソッドを使用する際、開始エージェントと入力を渡します。入力は文字列(ユーザーメッセージとして扱われる)または OpenAI Responses API の項目リストのいずれかです。 +`Runner` の実行メソッドを使用する際、開始エージェントと入力を渡します。入力は文字列(ユーザーメッセージと見なされる)または OpenAI Responses API のアイテムリストのいずれかです。 -Runner は以下のループを実行します。 +ランナーは次のループを実行します: -1. 現在のエージェントと入力を用いて LLM を呼び出します。 +1. 現在のエージェントと入力で LLM を呼び出します。 2. LLM が出力を生成します。 - 1. LLM が `final_output` を返した場合、ループを終了し結果を返します。 - 2. LLM がハンドオフを行った場合、現在のエージェントと入力を更新し、再度ループを実行します。 - 3. LLM がツール呼び出しを生成した場合、それらのツール呼び出しを実行し、結果を追加して再度ループを実行します。 -3. 指定された `max_turns` を超えた場合、[`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] 例外を発生させます。 + 1. LLM が `final_output` を返すと、ループは終了し、結果を返します。 + 2. LLM がハンドオフを行う場合、現在のエージェントと入力を更新し、ループを再実行します。 + 3. LLM がツール呼び出しを生成する場合、それらを実行し、結果を追加し、ループを再実行します。 +3. 渡された `max_turns` を超えた場合、[`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] 例外を発生させます。 !!! note - LLM の出力が「最終出力(final output)」とみなされる条件は、望ましいタイプのテキスト出力を生成し、かつツール呼び出しがない場合です。 + LLM の出力が「最終出力」と見なされるかどうかのルールは、望ましいタイプのテキスト出力を生成し、ツール呼び出しがないことです。 ## ストリーミング -ストリーミングを使用すると、LLM の実行中にストリーミングイベントを受け取ることができます。ストリームが完了すると、[`RunResultStreaming`][agents.result.RunResultStreaming] に実行に関する完全な情報(生成されたすべての新しい出力を含む)が格納されます。ストリーミングイベントを取得するには `.stream_events()` を呼び出します。詳細は [streaming ガイド](streaming.md) を参照してください。 +ストリーミングを使用すると、LLM が実行される際にストリーミングイベントを受け取ることができます。ストリームが完了すると、[`RunResultStreaming`][agents.result.RunResultStreaming] は実行に関する完全な情報を含み、生成されたすべての新しい出力を含みます。ストリーミングイベントには `.stream_events()` を呼び出せます。[ストリーミングガイド](streaming.md)で詳細を確認してください。 -## 実行設定(Run config) +## 実行設定 -`run_config` パラメータを使用すると、エージェント実行時のグローバル設定を構成できます。 +`run_config` パラメーターを使用して、エージェント実行のグローバル設定を構成できます: -- [`model`][agents.run.RunConfig.model]:各エージェントの `model` 設定に関係なく、グローバルな LLM モデルを指定します。 -- [`model_provider`][agents.run.RunConfig.model_provider]:モデル名を検索するためのモデルプロバイダーを指定します(デフォルトは OpenAI)。 -- [`model_settings`][agents.run.RunConfig.model_settings]:エージェント固有の設定を上書きします。例えば、グローバルな `temperature` や `top_p` を設定できます。 -- [`input_guardrails`][agents.run.RunConfig.input_guardrails]、[`output_guardrails`][agents.run.RunConfig.output_guardrails]:すべての実行に適用する入力または出力のガードレールのリストです。 -- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]:ハンドオフに既存の入力フィルターがない場合に適用されるグローバルな入力フィルターです。入力フィルターを使用すると、新しいエージェントに送信される入力を編集できます。詳細は [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] のドキュメントを参照してください。 -- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]:実行全体の [トレーシング](tracing.md) を無効にします。 -- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]:トレースに LLM やツール呼び出しの入出力など、機密性の高いデータを含めるかどうかを設定します。 -- [`workflow_name`][agents.run.RunConfig.workflow_name]、[`trace_id`][agents.run.RunConfig.trace_id]、[`group_id`][agents.run.RunConfig.group_id]:トレーシングのワークフロー名、トレース ID、トレースグループ ID を設定します。少なくとも `workflow_name` を設定することを推奨します。グループ ID はオプションで、複数の実行間でトレースを関連付けることができます。 -- [`trace_metadata`][agents.run.RunConfig.trace_metadata]:すべてのトレースに含めるメタデータです。 +- [`model`][agents.run.RunConfig.model]: 各エージェントの `model` に関係なく、使用するグローバル LLM モデルを設定できます。 +- [`model_provider`][agents.run.RunConfig.model_provider]: モデル名を検索するためのモデルプロバイダーで、デフォルトは OpenAI です。 +- [`model_settings`][agents.run.RunConfig.model_settings]: エージェント固有の設定を上書きします。たとえば、グローバルな `temperature` や `top_p` を設定できます。 +- [`input_guardrails`][agents.run.RunConfig.input_guardrails], [`output_guardrails`][agents.run.RunConfig.output_guardrails]: すべての実行に含める入力または出力ガードレールのリスト。 +- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]: すべてのハンドオフに適用するグローバル入力フィルター。ハンドオフに既にフィルターがない場合に適用されます。入力フィルターを使用して、新しいエージェントに送信される入力を編集できます。詳細は [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] のドキュメントを参照してください。 +- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]: 実行全体の[トレーシング](tracing.md)を無効にできます。 +- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]: トレースに LLM やツール呼び出しの入出力など、潜在的に機密性のあるデータを含めるかどうかを設定します。 +- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]: 実行のトレーシングワークフロー名、トレース ID、トレースグループ ID を設定します。少なくとも `workflow_name` を設定することをお勧めします。グループ ID は、複数の実行にわたってトレースをリンクするためのオプションフィールドです。 +- [`trace_metadata`][agents.run.RunConfig.trace_metadata]: すべてのトレースに含めるメタデータ。 -## 会話/チャットスレッド +## 会話/チャットスレッド -いずれかの run メソッドを呼び出すと、1 つ以上のエージェントが実行され(したがって 1 つ以上の LLM 呼び出しが発生)、チャット会話における単一の論理的ターンを表します。例えば: +いずれかの実行メソッドを呼び出すと、1つ以上のエージェントが実行される可能性があります(したがって、1つ以上の LLM 呼び出しが行われます)が、チャット会話の単一の論理ターンを表します。例えば: -1. ユーザーのターン:ユーザーがテキストを入力 -2. Runner の実行:最初のエージェントが LLM を呼び出し、ツールを実行し、2 番目のエージェントにハンドオフし、2 番目のエージェントがさらにツールを実行して出力を生成 +1. ユーザーターン:ユーザーがテキストを入力 +2. ランナー実行:最初のエージェントが LLM を呼び出し、ツールを実行し、2番目のエージェントにハンドオフし、2番目のエージェントがさらにツールを実行し、出力を生成します。 -エージェントの実行終了時に、ユーザーに何を表示するかを選択できます。例えば、エージェントが生成したすべての新しい項目を表示するか、最終出力のみを表示するかを選択できます。いずれの場合も、ユーザーが追加の質問をした場合、再度 run メソッドを呼び出します。 +エージェント実行の終了時に、ユーザーに何を表示するかを選択できます。例えば、エージェントによって生成されたすべての新しいアイテムをユーザーに表示するか、最終出力のみを表示するかです。いずれにせよ、ユーザーがフォローアップの質問をする可能性があり、その場合は再度実行メソッドを呼び出すことができます。 -次のターンの入力を取得するには、基本クラスの [`RunResultBase.to_input_list()`][agents.result.RunResultBase.to_input_list] メソッドを使用できます。 +次のターンの入力を取得するには、基本の [`RunResultBase.to_input_list()`][agents.result.RunResultBase.to_input_list] メソッドを使用できます。 ```python async def main(): @@ -86,10 +86,10 @@ async def main(): ## 例外 -SDK は特定の状況で例外を発生させます。完全なリストは [`agents.exceptions`][] にあります。概要は以下の通りです。 +SDK は特定のケースで例外を発生させます。完全なリストは [`agents.exceptions`][] にあります。概要は以下の通りです: -- [`AgentsException`][agents.exceptions.AgentsException]:SDK 内で発生するすべての例外の基底クラスです。 -- [`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded]:実行が指定された `max_turns` を超えた場合に発生します。 -- [`ModelBehaviorError`][agents.exceptions.ModelBehaviorError]:モデルが不正な出力(不正な JSON や存在しないツールの使用など)を生成した場合に発生します。 -- [`UserError`][agents.exceptions.UserError]:SDK を使用するコードに誤りがある場合に発生します。 -- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered]、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]:[ガードレール](guardrails.md) がトリガーされた場合に発生します。 \ No newline at end of file +- [`AgentsException`][agents.exceptions.AgentsException] は、SDK で発生するすべての例外の基本クラスです。 +- [`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] は、実行が渡された `max_turns` を超えた場合に発生します。 +- [`ModelBehaviorError`][agents.exceptions.ModelBehaviorError] は、モデルが無効な出力を生成した場合に発生します。例:不正な形式の JSON や存在しないツールの使用。 +- [`UserError`][agents.exceptions.UserError] は、SDK を使用する際にエラーを犯した場合に発生します。 +- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered], [`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered] は、[ガードレール](guardrails.md)が作動した場合に発生します。 \ No newline at end of file diff --git a/docs/ja/streaming.md b/docs/ja/streaming.md index 60719679..2f7a8dd2 100644 --- a/docs/ja/streaming.md +++ b/docs/ja/streaming.md @@ -1,14 +1,14 @@ # ストリーミング -ストリーミングを使用すると、エージェントの実行中にその進行状況の更新を購読できます。これは、エンドユーザーに進捗状況や部分的な応答を表示する際に役立ちます。 +ストリーミングを使用すると、エージェントの実行が進行する際の更新を購読できます。これは、エンドユーザーに進捗状況の更新や部分的な応答を表示するのに役立ちます。 -ストリーミングを行うには、[`Runner.run_streamed()`][agents.run.Runner.run_streamed] を呼び出します。これにより [`RunResultStreaming`][agents.result.RunResultStreaming] が返されます。`result.stream_events()` を呼び出すと、以下で説明する [`StreamEvent`][agents.stream_events.StreamEvent] オブジェクトの非同期ストリームが得られます。 +ストリーミングを行うには、[`Runner.run_streamed()`][agents.run.Runner.run_streamed] を呼び出します。これにより、[`RunResultStreaming`][agents.result.RunResultStreaming] が得られます。`result.stream_events()` を呼び出すと、以下で説明する [`StreamEvent`][agents.stream_events.StreamEvent] オブジェクトの非同期ストリームが得られます。 -## raw response events +## Raw response events -[`RawResponsesStreamEvent`][agents.stream_events.RawResponsesStreamEvent] は、LLM から直接渡される raw イベントです。これらは OpenAI Responses API の形式であり、各イベントはタイプ(`response.created` や `response.output_text.delta` など)とデータを持ちます。これらのイベントは、生成された応答メッセージを即座にユーザーにストリーミングしたい場合に役立ちます。 +[`RawResponsesStreamEvent`][agents.stream_events.RawResponsesStreamEvent] は、LLM から直接渡される raw イベントです。これらは OpenAI Responses API フォーマットであり、各イベントにはタイプ(`response.created`、`response.output_text.delta` など)とデータがあります。これらのイベントは、生成され次第、ユーザーに応答メッセージをストリーミングしたい場合に便利です。 -例えば、以下のコードは LLM が生成したテキストをトークンごとに出力します。 +例えば、これは LLM によってトークンごとに生成されたテキストを出力します。 ```python import asyncio @@ -31,11 +31,11 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## Run item events と agent events +## 実行アイテムイベントとエージェントイベント -[`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent] は、より高レベルのイベントです。これらはアイテムが完全に生成された際に通知されます。これにより、トークン単位ではなく、「メッセージが生成された」「ツールが実行された」などのレベルで進捗状況をユーザーに通知できます。同様に、[`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] は、現在のエージェントが変更された際(例えばハンドオフの結果として)に更新を通知します。 +[`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent] は、より高レベルのイベントです。アイテムが完全に生成されたときに通知します。これにより、「メッセージが生成された」、「ツールが実行された」などのレベルで進捗状況を更新することができます。同様に、[`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] は、現在のエージェントが変更されたとき(例えば、ハンドオフの結果として)に更新を提供します。 -例えば、以下のコードは raw イベントを無視し、更新情報のみをユーザーにストリーミングします。 +例えば、これは raw イベントを無視し、ユーザーに更新をストリーミングします。 ```python import asyncio @@ -61,14 +61,14 @@ async def main(): print("=== Run starting ===") async for event in result.stream_events(): - # raw response イベントのデルタは無視します + # We'll ignore the raw responses event deltas if event.type == "raw_response_event": continue - # エージェントが更新された場合、それを表示します + # When the agent updates, print that elif event.type == "agent_updated_stream_event": print(f"Agent updated: {event.new_agent.name}") continue - # アイテムが生成された場合、それを表示します + # When items are generated, print them elif event.type == "run_item_stream_event": if event.item.type == "tool_call_item": print("-- Tool was called") @@ -77,7 +77,7 @@ async def main(): elif event.item.type == "message_output_item": print(f"-- Message output:\n {ItemHelpers.text_message_output(event.item)}") else: - pass # 他のイベントタイプは無視します + pass # Ignore other event types print("=== Run complete ===") diff --git a/docs/ja/tools.md b/docs/ja/tools.md index c100885b..cd747663 100644 --- a/docs/ja/tools.md +++ b/docs/ja/tools.md @@ -1,18 +1,18 @@ # ツール -ツールを使用すると、エージェントがデータ取得、コード実行、外部 API 呼び出し、さらにはコンピュータ操作などのアクションを実行できます。Agents SDK には以下の 3 種類のツールがあります。 +ツールはエージェントがアクションを実行するためのものです。データの取得、コードの実行、外部 API の呼び出し、さらにはコンピュータの使用などが含まれます。Agent SDK には3つのクラスのツールがあります。 -- ホスト型ツール:AI モデルと共に LLM サーバー上で実行されます。OpenAI は、検索、ウェブ検索、コンピュータ操作をホスト型ツール(OpenAI がホストするツール)として提供しています。 -- 関数呼び出し:任意の Python 関数をツールとして使用できます。 -- エージェントをツールとして使用:エージェントをツールとして使用でき、エージェント間でハンドオフを行わずに他のエージェントを呼び出せます。 +- ホストされたツール: これらは LLM サーバー上で AI モデルと一緒に実行されます。OpenAI は、リトリーバル、Web 検索、コンピュータ操作をホストされたツールとして提供しています。 +- 関数呼び出し: 任意の Python 関数をツールとして使用できます。 +- エージェントをツールとして使用: エージェントをツールとして使用し、エージェントが他のエージェントをハンドオフせずに呼び出すことができます。 -## ホスト型ツール +## ホストされたツール -[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] を使用する場合、OpenAI はいくつかの組み込みツールを提供しています。 +OpenAI は、[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] を使用する際にいくつかの組み込みツールを提供しています。 -- [`WebSearchTool`][agents.tool.WebSearchTool] は、エージェントがウェブ検索を行うためのツールです。 -- [`FileSearchTool`][agents.tool.FileSearchTool] は、OpenAI のベクトルストアから情報を取得するためのツールです。 -- [`ComputerTool`][agents.tool.ComputerTool] は、コンピュータ操作タスクを自動化するためのツールです。 +- [`WebSearchTool`][agents.tool.WebSearchTool] はエージェントが Web 検索を行うことを可能にします。 +- [`FileSearchTool`][agents.tool.FileSearchTool] は OpenAI ベクトルストアから情報を取得することを可能にします。 +- [`ComputerTool`][agents.tool.ComputerTool] はコンピュータ操作タスクを自動化することを可能にします。 ```python from agents import Agent, FileSearchTool, Runner, WebSearchTool @@ -35,12 +35,12 @@ async def main(): ## 関数ツール -任意の Python 関数をツールとして使用できます。Agents SDK は自動的にツールを設定します。 +任意の Python 関数をツールとして使用できます。Agents SDK はツールを自動的にセットアップします。 -- ツール名は Python 関数名が使用されます(または任意の名前を指定可能)。 -- ツールの説明は関数の docstring から取得されます(または任意の説明を指定可能)。 -- 関数の入力スキーマは関数の引数から自動的に作成されます。 -- 各入力の説明は、無効化されていない限り、関数の docstring から取得されます。 +- ツールの名前は Python 関数の名前になります(または名前を指定できます) +- ツールの説明は関数の docstring から取得されます(または説明を指定できます) +- 関数入力のスキーマは関数の引数から自動的に作成されます +- 各入力の説明は、無効にしない限り、関数の docstring から取得されます Python の `inspect` モジュールを使用して関数シグネチャを抽出し、[`griffe`](https://mkdocstrings.github.io/griffe/) を使用して docstring を解析し、`pydantic` を使用してスキーマを作成します。 @@ -94,12 +94,12 @@ for tool in agent.tools: ``` -1. 任意の Python 型を関数の引数として使用可能で、関数は同期または非同期のどちらでも構いません。 -2. docstring が存在する場合、ツールおよび引数の説明として使用されます。 -3. 関数はオプションで `context` を引数として受け取れます(最初の引数である必要があります)。また、ツール名や説明、docstring のスタイルなどを上書き設定できます。 -4. デコレータを付けた関数をツールのリストに渡すことができます。 +1. 任意の Python 型を関数の引数として使用でき、関数は同期または非同期であることができます。 +2. Docstring が存在する場合、説明や引数の説明を取得するために使用されます。 +3. 関数はオプションで `context` を取ることができ(最初の引数である必要があります)、ツールの名前、説明、使用する docstring スタイルなどのオーバーライドを設定できます。 +4. デコレートされた関数をツールのリストに渡すことができます。 -??? note "クリックして出力を表示" +??? note "出力を表示するには展開してください" ``` fetch_weather @@ -168,14 +168,15 @@ for tool in agent.tools: "type": "object" } ``` + ### カスタム関数ツール -Python 関数をツールとして使用したくない場合もあります。その場合は、直接 [`FunctionTool`][agents.tool.FunctionTool] を作成できます。作成時には以下を指定する必要があります: +時には、Python 関数をツールとして使用したくない場合もあります。その場合、直接 [`FunctionTool`][agents.tool.FunctionTool] を作成できます。以下を提供する必要があります。 -- `name` -- `description` -- 引数の JSON スキーマを示す `params_json_schema` -- 非同期関数である `on_invoke_tool`(コンテキストと引数を JSON 文字列として受け取り、ツールの出力を文字列として返す必要があります) +- `name` +- `description` +- `params_json_schema`(引数の JSON スキーマ) +- `on_invoke_tool`(コンテキストと引数を JSON 文字列として受け取り、ツールの出力を文字列として返す非同期関数) ```python from typing import Any @@ -208,18 +209,18 @@ tool = FunctionTool( ) ``` -### 引数とドックストリングの自動解析 +### 引数と docstring の自動解析 -前述の通り、関数のシグネチャを自動的に解析してツールのスキーマを抽出し、ドックストリングを解析してツールおよび各引数の説明を抽出します。これに関する注意点は以下の通りです: +前述のように、ツールのスキーマを抽出するために関数シグネチャを自動的に解析し、ツールおよび個々の引数の説明を抽出するために docstring を解析します。以下の点に注意してください。 -1. シグネチャの解析は `inspect` モジュールを使用して行われます。型アノテーションを用いて引数の型を把握し、動的に Pydantic モデルを構築して全体のスキーマを表現します。Python の基本型(basic components)、Pydantic モデル、TypedDict など、ほとんどの型をサポートしています。 -2. ドックストリングの解析には `griffe` を使用します。サポートされるドックストリング形式は `google`、`sphinx`、`numpy` です。ドックストリング形式は自動検出を試みますが、これはベストエフォートであり、`function_tool` 呼び出し時に明示的に指定することも可能です。また、`use_docstring_info` を `False` に設定することでドックストリングの解析を無効化できます。 +1. シグネチャの解析は `inspect` モジュールを通じて行われます。引数の型を理解するために型注釈を使用し、全体のスキーマを表す Pydantic モデルを動的に構築します。Python の基本コンポーネント、Pydantic モデル、TypedDict など、ほとんどの型をサポートしています。 +2. `griffe` を使用して docstring を解析します。サポートされている docstring フォーマットは `google`、`sphinx`、`numpy` です。docstring フォーマットを自動的に検出しようとしますが、これはベストエフォートであり、`function_tool` を呼び出す際に明示的に設定できます。`use_docstring_info` を `False` に設定することで docstring 解析を無効にすることもできます。 スキーマ抽出のコードは [`agents.function_schema`][] にあります。 -## ツールとしてのエージェント +## エージェントをツールとして使用 -一部のワークフローでは、制御をハンドオフするのではなく、中央のエージェントが専門化されたエージェントのネットワークを調整することが望ましい場合があります。このような場合、エージェントをツールとしてモデル化できます。 +一部のワークフローでは、中央のエージェントが専門エージェントのネットワークをオーケストレーションすることを望むかもしれません。これを行うには、エージェントをツールとしてモデル化します。 ```python from agents import Agent, Runner @@ -258,12 +259,12 @@ async def main(): print(result.final_output) ``` -## 関数ツールにおけるエラー処理 +## 関数ツールでのエラー処理 -`@function_tool` を使用して関数ツールを作成する際、`failure_error_function` を渡すことができます。これはツール呼び出しがクラッシュした場合に、LLM にエラー応答を提供する関数です。 +`@function_tool` を使用して関数ツールを作成する際、`failure_error_function` を渡すことができます。これは、ツール呼び出しがクラッシュした場合に LLM にエラーレスポンスを提供する関数です。 -- デフォルト(何も渡さない場合)では、エラーが発生したことを LLM に通知する `default_tool_error_function` が実行されます。 -- 独自のエラー関数を渡した場合は、それが代わりに実行され、その応答が LLM に送信されます。 -- 明示的に `None` を渡した場合、ツール呼び出しのエラーは再度発生(re-raise)し、自分で処理する必要があります。この場合、モデルが無効な JSON を生成した場合は `ModelBehaviorError`、コードがクラッシュした場合は `UserError` などが発生します。 +- デフォルトでは(何も渡さない場合)、`default_tool_error_function` が実行され、LLM にエラーが発生したことを伝えます。 +- 独自のエラー関数を渡した場合、それが実行され、LLM にレスポンスが送信されます。 +- 明示的に `None` を渡した場合、ツール呼び出しエラーは再度発生し、処理する必要があります。モデルが無効な JSON を生成した場合は `ModelBehaviorError`、コードがクラッシュした場合は `UserError` などが考えられます。 -手動で `FunctionTool` オブジェクトを作成する場合は、`on_invoke_tool` 関数内でエラーを処理する必要があります。 \ No newline at end of file +`FunctionTool` オブジェクトを手動で作成する場合、`on_invoke_tool` 関数内でエラーを処理する必要があります。 \ No newline at end of file diff --git a/docs/ja/tracing.md b/docs/ja/tracing.md index 2e0e80da..0f573eb8 100644 --- a/docs/ja/tracing.md +++ b/docs/ja/tracing.md @@ -1,51 +1,51 @@ # トレーシング -Agents SDK には組み込みのトレーシング機能があり、エージェントの実行中に発生するイベント(LLM の生成、ツール呼び出し、ハンドオフ、ガードレール、さらにはカスタムイベントまで)を包括的に記録します。[Traces ダッシュボード](https://platform.openai.com/traces) を使用して、開発中および本番環境でワークフローのデバッグ、可視化、監視が可能です。 +Agents SDK には組み込みのトレーシング機能があり、エージェント実行中のイベント(LLM の生成、ツール呼び出し、ハンドオフ、ガードレール、カスタムイベントなど)の包括的な記録を収集します。[Traces ダッシュボード](https://platform.openai.com/traces)を使用して、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 !!!note - トレーシングはデフォルトで有効になっています。トレーシングを無効にする方法は 2 つあります。 + トレーシングはデフォルトで有効です。トレーシングを無効にする方法は2つあります: - 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定して、グローバルにトレーシングを無効化できます。 - 2. 個別の実行でトレーシングを無効化するには、[`agents.run.RunConfig.tracing_disabled`][] を `True` に設定します。 + 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定して、グローバルにトレーシングを無効にすることができます。 + 2. [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定して、単一の実行でトレーシングを無効にすることができます。 -***OpenAI の API を使用してゼロデータ保持(ZDR)ポリシーで運用している組織の場合、トレーシングは利用できません。*** +***OpenAI の API を使用している Zero Data Retention (ZDR) ポリシーの下で運営されている組織では、トレーシングは利用できません。*** ## トレースとスパン -- **トレース(Traces)** は、ワークフローの単一のエンドツーエンド操作を表します。トレースは複数のスパン(Spans)で構成されます。トレースには以下のプロパティがあります。 - - `workflow_name`:論理的なワークフローまたはアプリ名。例:「Code generation」や「Customer service」。 - - `trace_id`:トレースの一意な ID。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 - - `group_id`:同じ会話からの複数のトレースを関連付けるためのオプションのグループ ID。例えば、チャットスレッド ID を使用できます。 - - `disabled`:True の場合、このトレースは記録されません。 - - `metadata`:トレースに関するオプションのメタデータ。 -- **スパン(Spans)** は、開始時刻と終了時刻を持つ操作を表します。スパンには以下が含まれます。 - - `started_at` と `ended_at` のタイムスタンプ。 - - 所属するトレースを示す `trace_id`。 - - 親スパンを指す `parent_id`(存在する場合)。 - - スパンに関する情報を含む `span_data`。例えば、`AgentSpanData` はエージェントに関する情報を、`GenerationSpanData` は LLM の生成に関する情報を含みます。 +- **トレース** は「ワークフロー」の単一のエンドツーエンドの操作を表します。スパンで構成されています。トレースには以下のプロパティがあります: + - `workflow_name`: 論理的なワークフローまたはアプリです。例: "Code generation" や "Customer service"。 + - `trace_id`: トレースの一意の ID。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` でなければなりません。 + - `group_id`: 同じ会話からの複数のトレースをリンクするためのオプションのグループ ID。例: チャットスレッド ID。 + - `disabled`: True の場合、トレースは記録されません。 + - `metadata`: トレースのオプションのメタデータ。 +- **スパン** は開始時間と終了時間を持つ操作を表します。スパンには以下があります: + - `started_at` と `ended_at` のタイムスタンプ。 + - 所属するトレースを表す `trace_id` + - 親スパンを指す `parent_id`(ある場合) + - スパンに関する情報を含む `span_data`。例: `AgentSpanData` はエージェントに関する情報を含み、`GenerationSpanData` は LLM の生成に関する情報を含みます。 ## デフォルトのトレーシング -デフォルトでは、SDK は以下をトレースします。 +デフォルトで、SDK は以下をトレースします: - `Runner.{run, run_sync, run_streamed}()` 全体が `trace()` でラップされます。 -- エージェントが実行されるたびに `agent_span()` でラップされます。 +- エージェントが実行されるたびに、`agent_span()` でラップされます。 - LLM の生成は `generation_span()` でラップされます。 - 関数ツールの呼び出しはそれぞれ `function_span()` でラップされます。 - ガードレールは `guardrail_span()` でラップされます。 - ハンドオフは `handoff_span()` でラップされます。 - 音声入力(音声からテキスト)は `transcription_span()` でラップされます。 - 音声出力(テキストから音声)は `speech_span()` でラップされます。 -- 関連する音声スパンは `speech_group_span()` の下にまとめられる場合があります。 +- 関連する音声スパンは `speech_group_span()` の下に配置されることがあります。 -デフォルトでは、トレース名は「Agent trace」です。`trace` を使用する場合、この名前を設定できます。また、[`RunConfig`][agents.run.RunConfig] を使用して名前やその他のプロパティを設定できます。 +デフォルトでは、トレースは "Agent trace" と名付けられています。`trace` を使用する場合、この名前を設定することができ、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定できます。 -さらに、[カスタムトレースプロセッサ](#custom-tracing-processors) を設定して、トレースを他の宛先に送信することも可能です(置き換えまたは追加の宛先として)。 +さらに、[カスタムトレースプロセッサー](#custom-tracing-processors)を設定して、トレースを他の宛先に送信することもできます(置き換えまたは二次的な宛先として)。 -## 上位レベルのトレース +## 高レベルのトレース -複数回の `run()` 呼び出しを 1 つのトレースにまとめたい場合があります。その場合、コード全体を `trace()` でラップします。 +時には、複数の `run()` 呼び出しを単一のトレースの一部にしたいことがあります。これを行うには、コード全体を `trace()` でラップします。 ```python from agents import Agent, Runner, trace @@ -60,43 +60,57 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. 2 回の `Runner.run` 呼び出しが `with trace()` でラップされているため、個別の実行はそれぞれ別のトレースを作成するのではなく、全体のトレースの一部になります。 +1. `Runner.run` への2つの呼び出しが `with trace()` でラップされているため、個々の実行は2つのトレースを作成するのではなく、全体のトレースの一部になります。 ## トレースの作成 -トレースを作成するには [`trace()`][agents.tracing.trace] 関数を使用します。トレースは開始と終了が必要です。以下の 2 つの方法があります。 +[`trace()`][agents.tracing.trace] 関数を使用してトレースを作成できます。トレースは開始と終了が必要です。以下の2つの方法があります: -1. **推奨**:コンテキストマネージャとして使用します(例:`with trace(...) as my_trace`)。これによりトレースの開始と終了が自動的に行われます。 +1. **推奨**: トレースをコンテキストマネージャーとして使用します。つまり、`with trace(...) as my_trace` とします。これにより、トレースが自動的に適切なタイミングで開始および終了します。 2. 手動で [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を呼び出すこともできます。 -現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡されます。これにより、並行処理でも自動的に動作します。トレースを手動で開始・終了する場合、`start()` と `finish()` に `mark_as_current` と `reset_current` を渡して現在のトレースを更新する必要があります。 +現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡されます。これにより、並行処理でも自動的に機能します。トレースを手動で開始/終了する場合は、`start()`/`finish()` に `mark_as_current` と `reset_current` を渡して現在のトレースを更新する必要があります。 ## スパンの作成 -スパンを作成するには、各種の [`*_span()`][agents.tracing.create] メソッドを使用します。通常、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するために [`custom_span()`][agents.tracing.custom_span] 関数も利用可能です。 +さまざまな [`*_span()`][agents.tracing.create] メソッドを使用してスパンを作成できます。一般的に、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数が利用可能です。 -スパンは自動的に現在のトレースに属し、最も近い現在のスパンの下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡されます。 +スパンは自動的に現在のトレースの一部となり、Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡される最も近い現在のスパンの下にネストされます。 -## 機密データ +## センシティブデータ -一部のスパンは機密データを含む可能性があります。 +特定のスパンは潜在的にセンシティブなデータをキャプチャすることがあります。 -`generation_span()` は LLM 生成の入出力を、`function_span()` は関数呼び出しの入出力を保存します。これらには機密データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] を使用してデータの取得を無効化できます。 +`generation_span()` は LLM 生成の入力/出力を保存し、`function_span()` は関数呼び出しの入力/出力を保存します。これらにはセンシティブなデータが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] を使用してそのデータのキャプチャを無効にすることができます。 -同様に、音声スパンはデフォルトで音声データを base64 エンコードされた PCM データとして含みます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定して音声データの取得を無効化できます。 +同様に、音声スパンにはデフォルトで入力および出力音声の base64 エンコードされた PCM データが含まれます。この音声データのキャプチャを無効にするには、[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定します。 -## カスタムトレーシングプロセッサ +## カスタムトレーシングプロセッサー -トレーシングの高レベルアーキテクチャは以下の通りです。 +トレーシングの高レベルアーキテクチャは次のとおりです: -- 初期化時にグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 -- `TraceProvider` はトレースを OpenAI バックエンドに送信する [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] と [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] で構成されます。 +- 初期化時に、トレースを作成する責任を持つグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 +- `TraceProvider` を [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] で構成し、トレース/スパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信し、OpenAI バックエンドにバッチでスパンとトレースをエクスポートします。 -このデフォルト設定をカスタマイズするには、以下の 2 つの方法があります。 +このデフォルトのセットアップをカスタマイズして、トレースを代替または追加のバックエンドに送信したり、エクスポーターの動作を変更したりするには、次の2つのオプションがあります: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] で追加のプロセッサを追加できます。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] でデフォルトのプロセッサを置き換えることができます。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] を使用して、トレースとスパンが準備できたときに受け取る**追加の**トレースプロセッサーを追加できます。これにより、OpenAI のバックエンドにトレースを送信することに加えて、独自の処理を行うことができます。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] を使用して、デフォルトのプロセッサーを独自のトレースプロセッサーに**置き換える**ことができます。これにより、`TracingProcessor` を含めない限り、トレースは OpenAI バックエンドに送信されません。 -## 外部トレーシングプロセッサ一覧 +## 外部トレーシングプロセッサーリスト -(省略:原文のまま) \ No newline at end of file +- [Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents) +- [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) +- [MLflow (self-hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) +- [MLflow (Databricks hosted](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) +- [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) +- [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents) +- [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk) +- [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration) +- [Keywords AI](https://docs.keywordsai.co/integration/development-frameworks/openai-agent) +- [LangSmith](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_openai_agents_sdk) +- [Maxim AI](https://www.getmaxim.ai/docs/observe/integrations/openai-agents-sdk) +- [Comet Opik](https://www.comet.com/docs/opik/tracing/integrations/openai_agents) +- [Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents) +- [Langtrace](https://docs.langtrace.ai/supported-integrations/llm-frameworks/openai-agents-sdk) +- [Okahu-Monocle](https://github.com/monocle2ai/monocle) \ No newline at end of file diff --git a/docs/ja/visualization.md b/docs/ja/visualization.md index a0e2b9ec..1054dd12 100644 --- a/docs/ja/visualization.md +++ b/docs/ja/visualization.md @@ -1,10 +1,10 @@ # エージェントの可視化 -エージェントの可視化を使用すると、**Graphviz** を用いてエージェントとその関係性を構造化したグラフィカルな表現として生成できます。これにより、アプリケーション内でエージェント、ツール、およびハンドオフがどのように相互作用するかを理解できます。 +エージェントの可視化では、**Graphviz** を使用してエージェントとその関係の構造化されたグラフィカル表現を生成できます。これは、アプリケーション内でエージェント、ツール、ハンドオフがどのように相互作用するかを理解するのに役立ちます。 ## インストール -オプションの依存関係グループ `viz` をインストールします: +オプションの `viz` 依存関係グループをインストールします: ```bash pip install "openai-agents[viz]" @@ -12,11 +12,11 @@ pip install "openai-agents[viz]" ## グラフの生成 -`draw_graph` 関数を使用してエージェントの可視化を生成できます。この関数は以下のような有向グラフを作成します: +`draw_graph` 関数を使用してエージェントの可視化を生成できます。この関数は、以下のような有向グラフを作成します: -- **エージェント** は黄色の四角形で表されます。 -- **ツール** は緑色の楕円形で表されます。 -- **ハンドオフ** はあるエージェントから別のエージェントへの有向エッジで表されます。 +- **エージェント** は黄色のボックスで表されます。 +- **ツール** は緑色の楕円で表されます。 +- **ハンドオフ** はあるエージェントから別のエージェントへの有向エッジです。 ### 使用例 @@ -50,31 +50,31 @@ draw_graph(triage_agent) ![Agent Graph](../assets/images/graph.png) -これにより、**triage agent** とそのサブエージェントおよびツールとの接続構造を視覚的に表現したグラフが生成されます。 +これにより、**トリアージエージェント** の構造とサブエージェントおよびツールへの接続を視覚的に表現するグラフが生成されます。 ## 可視化の理解 -生成されたグラフには以下が含まれます: +生成されたグラフには以下が含まれます: - エントリーポイントを示す **開始ノード** (`__start__`)。 -- 黄色で塗りつぶされた四角形で表されるエージェント。 -- 緑色で塗りつぶされた楕円形で表されるツール。 -- 相互作用を示す有向エッジ: - - エージェント間のハンドオフは **実線矢印**。 - - ツールの呼び出しは **点線矢印**。 -- 実行終了地点を示す **終了ノード** (`__end__`)。 +- 黄色で塗りつぶされた **長方形** で表されるエージェント。 +- 緑色で塗りつぶされた **楕円** で表されるツール。 +- 相互作用を示す有向エッジ: + - エージェント間のハンドオフには **実線の矢印**。 + - ツール呼び出しには **点線の矢印**。 +- 実行が終了する場所を示す **終了ノード** (`__end__`)。 ## グラフのカスタマイズ ### グラフの表示 -デフォルトでは、`draw_graph` はグラフをインラインで表示します。グラフを別ウィンドウで表示するには、以下のように記述します: +デフォルトでは、`draw_graph` はグラフをインラインで表示します。別ウィンドウでグラフを表示するには、次のように記述します: ```python draw_graph(triage_agent).view() ``` ### グラフの保存 -デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存するには、ファイル名を指定します: +デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存するには、ファイル名を指定します: ```python draw_graph(triage_agent, filename="agent_graph.png") diff --git a/docs/ja/voice/pipeline.md b/docs/ja/voice/pipeline.md index 03e0c54e..6df7dd04 100644 --- a/docs/ja/voice/pipeline.md +++ b/docs/ja/voice/pipeline.md @@ -1,6 +1,6 @@ # パイプラインとワークフロー -[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] クラスを使用すると、エージェントのワークフローを簡単に音声アプリに変換できます。実行するワークフローを渡すと、パイプラインが入力音声の文字起こし、音声終了の検出、適切なタイミングでのワークフロー呼び出し、ワークフロー出力の音声への変換を処理します。 +[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] は、エージェントのワークフローを音声アプリに変換するのを簡単にするクラスです。実行するワークフローを渡すと、パイプラインが入力音声の文字起こし、音声の終了検出、適切なタイミングでのワークフロー呼び出し、ワークフロー出力の音声化を行います。 ```mermaid graph LR @@ -30,28 +30,28 @@ graph LR ## パイプラインの設定 -パイプラインを作成する際、以下の項目を設定できます。 +パイプラインを作成する際に、以下のことを設定できます: -1. [`workflow`][agents.voice.workflow.VoiceWorkflowBase]:新しい音声が文字起こしされるたびに実行されるコードです。 -2. 使用する [`speech-to-text`][agents.voice.model.STTModel] および [`text-to-speech`][agents.voice.model.TTSModel] モデル +1. [`workflow`][agents.voice.workflow.VoiceWorkflowBase]:新しい音声が文字起こしされるたびに実行されるコード。 +2. 使用する [`speech-to-text`][agents.voice.model.STTModel] および [`text-to-speech`][agents.voice.model.TTSModel] モデル。 3. [`config`][agents.voice.pipeline_config.VoicePipelineConfig]:以下のような設定が可能です。 - - モデル名をモデルにマッピングするモデルプロバイダー - - トレーシング(トレーシングの無効化、音声ファイルのアップロード有無、ワークフロー名、トレース ID など) - - TTS および STT モデルの設定(プロンプト、言語、使用するデータ型など) + - モデルプロバイダー:モデル名をモデルにマッピングできます。 + - トレーシング:トレーシングを無効にするかどうか、音声ファイルのアップロード、ワークフロー名、トレース ID など。 + - TTS および STT モデルの設定:プロンプト、言語、使用するデータ型など。 ## パイプラインの実行 -パイプラインは [`run()`][agents.voice.pipeline.VoicePipeline.run] メソッドを使用して実行できます。このメソッドでは、以下の 2 種類の形式で音声入力を渡すことができます。 +パイプラインは [`run()`][agents.voice.pipeline.VoicePipeline.run] メソッドを通じて実行できます。これにより、2 つの形式で音声入力を渡すことができます: -1. [`AudioInput`][agents.voice.input.AudioInput] は、完全な音声トランスクリプトがあり、それに対する結果を生成したい場合に使用します。これは、話者が話し終えたタイミングを検出する必要がない場合に便利です。例えば、事前録音された音声や、ユーザーが話し終えたタイミングが明確なプッシュトゥトーク型アプリなどです。 -2. [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] は、ユーザーが話し終えたタイミングを検出する必要がある場合に使用します。音声チャンクを検出時に順次送信でき、音声パイプラインは「アクティビティ検出」と呼ばれるプロセスを通じて、適切なタイミングでエージェントのワークフローを自動的に実行します。 +1. [`AudioInput`][agents.voice.input.AudioInput]:完全な音声トランスクリプトがある場合に使用し、その結果を生成するだけです。これは、話者が話し終わったときの検出が不要な場合に便利です。例えば、事前録音された音声や、ユーザーが話し終わったことが明確なプッシュ・トゥ・トークアプリで使用します。 +2. [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput]:ユーザーが話し終わったときの検出が必要な場合に使用します。音声チャンクを検出時にプッシュでき、音声パイプラインが自動的にエージェントワークフローを適切なタイミングで実行します。これは「アクティビティ検出」と呼ばれるプロセスを通じて行われます。 -## 実行結果 +## 結果 -音声パイプラインの実行結果は [`StreamedAudioResult`][agents.voice.result.StreamedAudioResult] です。これは、発生したイベントをストリームとして取得できるオブジェクトです。いくつかの種類の [`VoiceStreamEvent`][agents.voice.events.VoiceStreamEvent] があり、以下を含みます。 +音声パイプライン実行の結果は [`StreamedAudioResult`][agents.voice.result.StreamedAudioResult] です。これは、イベントが発生するたびにストリーミングできるオブジェクトです。いくつかの種類の [`VoiceStreamEvent`][agents.voice.events.VoiceStreamEvent] があります: -1. [`VoiceStreamEventAudio`][agents.voice.events.VoiceStreamEventAudio]:音声チャンクを含むイベントです。 -2. [`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle]:ターンの開始や終了など、ライフサイクルイベントを通知します。 +1. [`VoiceStreamEventAudio`][agents.voice.events.VoiceStreamEventAudio]:音声チャンクを含むイベント。 +2. [`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle]:ターンの開始や終了などのライフサイクルイベントを通知します。 3. [`VoiceStreamEventError`][agents.voice.events.VoiceStreamEventError]:エラーイベントです。 ```python @@ -70,6 +70,6 @@ async for event in result.stream(): ## ベストプラクティス -### 割り込み処理 +### 中断 -Agents SDK は現在、[`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] に対する組み込みの割り込み処理をサポートしていません。その代わり、検出された各ターンごとにワークフローが個別に実行されます。アプリケーション内で割り込みを処理したい場合は、[`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] イベントを監視できます。`turn_started` は新しいターンが文字起こしされ、処理が開始されたことを示します。`turn_ended` は、該当するターンのすべての音声が送信された後にトリガーされます。これらのイベントを利用して、モデルがターンを開始した際に話者のマイクをミュートし、ターンに関連するすべての音声を送信した後にミュートを解除する、といった処理が可能です。 \ No newline at end of file +Agents SDK は現在、[`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] に対する組み込みの中断サポートを提供していません。代わりに、検出されたターンごとにワークフローの別の実行をトリガーします。アプリケーション内で中断を処理したい場合は、[`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] イベントを監視できます。`turn_started` は新しいターンが文字起こしされ、処理が始まったことを示します。`turn_ended` は、関連するすべての音声が送信された後にトリガーされます。これらのイベントを使用して、モデルがターンを開始したときにスピーカーのマイクをミュートし、関連する音声をすべてフラッシュした後にアンミュートすることができます。 \ No newline at end of file diff --git a/docs/ja/voice/quickstart.md b/docs/ja/voice/quickstart.md index 0602efe6..77e5ef27 100644 --- a/docs/ja/voice/quickstart.md +++ b/docs/ja/voice/quickstart.md @@ -2,7 +2,7 @@ ## 前提条件 -まず、Agents SDK の基本的な[クイックスタート手順](../quickstart.md)に従い、仮想環境をセットアップしてください。その後、SDK のオプションである音声関連の依存関係をインストールします。 +Agents SDK の基本的な[クイックスタート手順](../quickstart.md)に従い、仮想環境を設定してください。その後、SDK からオプションの音声依存関係をインストールします。 ```bash pip install 'openai-agents[voice]' @@ -10,11 +10,11 @@ pip install 'openai-agents[voice]' ## コンセプト -理解すべき主なコンセプトは [`VoicePipeline`][agents.voice.pipeline.VoicePipeline] です。これは以下の 3 ステップで構成されています。 +知っておくべき主なコンセプトは、[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] です。これは3ステップのプロセスです。 -1. 音声認識モデルを実行し、音声をテキストに変換します。 -2. 通常はエージェントを用いたワークフローであるコードを実行し、実行結果を生成します。 -3. テキスト読み上げモデルを実行し、実行結果のテキストを再び音声に変換します。 +1. 音声をテキストに変換する音声認識モデルを実行します。 +2. 通常はエージェントワークフローであるコードを実行して、結果を生成します。 +3. 結果のテキストを音声に戻すテキスト読み上げモデルを実行します。 ```mermaid graph LR @@ -44,7 +44,7 @@ graph LR ## エージェント -まず、いくつかのエージェントを設定します。この SDK でエージェントを作成したことがあれば、馴染みのある内容です。ここでは、複数のエージェント、ハンドオフ、およびツールを設定します。 +まず、いくつかのエージェントを設定しましょう。この SDK でエージェントを構築したことがある場合、これは馴染みがあるはずです。エージェント、ハンドオフ、ツールを用意します。 ```python import asyncio @@ -88,32 +88,32 @@ agent = Agent( ## 音声パイプライン -ここでは、ワークフローとして [`SingleAgentVoiceWorkflow`][agents.voice.workflow.SingleAgentVoiceWorkflow] を使用し、シンプルな音声パイプラインを設定します。 +[`SingleAgentVoiceWorkflow`][agents.voice.workflow.SingleAgentVoiceWorkflow] をワークフローとして使用して、シンプルな音声パイプラインを設定します。 ```python from agents.voice import SingleAgentVoiceWorkflow, VoicePipeline pipeline = VoicePipeline(workflow=SingleAgentVoiceWorkflow(agent)) ``` -## パイプラインの実行 +## パイプラインを実行する ```python import numpy as np import sounddevice as sd from agents.voice import AudioInput -# 簡単のため、ここでは 3 秒間の無音を作成します -# 実際にはマイクからの音声データを使用します +# For simplicity, we'll just create 3 seconds of silence +# In reality, you'd get microphone data buffer = np.zeros(24000 * 3, dtype=np.int16) audio_input = AudioInput(buffer=buffer) result = await pipeline.run(audio_input) -# `sounddevice` を使ってオーディオプレイヤーを作成します +# Create an audio player using `sounddevice` player = sd.OutputStream(samplerate=24000, channels=1, dtype=np.int16) player.start() -# 音声ストリームをリアルタイムで再生します +# Play the audio stream as it comes in async for event in result.stream(): if event.type == "voice_stream_event_audio": player.write(event.data) @@ -177,11 +177,11 @@ async def main(): result = await pipeline.run(audio_input) - # `sounddevice` を使ってオーディオプレイヤーを作成します + # Create an audio player using `sounddevice` player = sd.OutputStream(samplerate=24000, channels=1, dtype=np.int16) player.start() - # 音声ストリームをリアルタイムで再生します + # Play the audio stream as it comes in async for event in result.stream(): if event.type == "voice_stream_event_audio": player.write(event.data) @@ -191,4 +191,4 @@ if __name__ == "__main__": asyncio.run(main()) ``` -このコード例を実行すると、エージェントがあなたに話しかけます。実際にエージェントと会話できるデモについては、[examples/voice/static](https://github.com/openai/openai-agents-python/tree/main/examples/voice/static) のコード例を参照してください。 \ No newline at end of file +この例を実行すると、エージェントがあなたに話しかけます! [examples/voice/static](https://github.com/openai/openai-agents-python/tree/main/examples/voice/static) の例をチェックして、自分でエージェントと話すデモを見てください。 \ No newline at end of file diff --git a/docs/ja/voice/tracing.md b/docs/ja/voice/tracing.md index 8432d75d..1a1df278 100644 --- a/docs/ja/voice/tracing.md +++ b/docs/ja/voice/tracing.md @@ -1,14 +1,14 @@ # トレーシング -[エージェントのトレーシング](../tracing.md) と同様に、音声パイプラインも自動的にトレーシングされます。 +[エージェントがトレースされる](../tracing.md)のと同様に、音声パイプラインも自動的にトレースされます。 -基本的なトレーシング情報については、上記のトレーシングドキュメントを参照してください。さらに、[`VoicePipelineConfig`][agents.voice.pipeline_config.VoicePipelineConfig] を使用してパイプラインのトレーシングを設定できます。 +基本的なトレーシング情報については上記のトレーシングドキュメントを参照できますが、パイプラインのトレーシングは [`VoicePipelineConfig`][agents.voice.pipeline_config.VoicePipelineConfig] を通じて追加で設定できます。 -主なトレーシング関連フィールドは以下のとおりです。 +トレーシングに関連する主なフィールドは次のとおりです: -- [`tracing_disabled`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレーシングを無効にするかどうかを制御します。デフォルトではトレーシングは有効です。 -- [`trace_include_sensitive_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_data]:音声文字起こしなど、機密性の高いデータをトレースに含めるかどうかを制御します。これは特に音声パイプライン向けであり、ワークフロー内部の処理には影響しません。 -- [`trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]:トレースに音声データを含めるかどうかを制御します。 -- [`workflow_name`][agents.voice.pipeline_config.VoicePipelineConfig.workflow_name]:トレースするワークフローの名前です。 -- [`group_id`][agents.voice.pipeline_config.VoicePipelineConfig.group_id]:複数のトレースを関連付けるためのトレースの `group_id` です。 -- [`trace_metadata`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレースに含める追加のメタデータです。 \ No newline at end of file +- [`tracing_disabled`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]: トレーシングを無効にするかどうかを制御します。デフォルトでは、トレーシングは有効です。 +- [`trace_include_sensitive_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_data]: トレースに音声トランスクリプトのような潜在的に機密性のあるデータを含めるかどうかを制御します。これは特に音声パイプライン用であり、ワークフロー内で行われることには関係ありません。 +- [`trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]: トレースに音声データを含めるかどうかを制御します。 +- [`workflow_name`][agents.voice.pipeline_config.VoicePipelineConfig.workflow_name]: トレースワークフローの名前です。 +- [`group_id`][agents.voice.pipeline_config.VoicePipelineConfig.group_id]: 複数のトレースをリンクするための `group_id` です。 +- [`trace_metadata`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]: トレースに含める追加のメタデータです。 \ No newline at end of file diff --git a/docs/quickstart.md b/docs/quickstart.md index f8eca5ca..9513fdb8 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -121,7 +121,7 @@ async def homework_guardrail(ctx, agent, input_data): Let's put it all together and run the entire workflow, using handoffs and the input guardrail. ```python -from agents import Agent, InputGuardrail,GuardrailFunctionOutput, Runner +from agents import Agent, InputGuardrail, GuardrailFunctionOutput, Runner from pydantic import BaseModel import asyncio diff --git a/docs/scripts/translate_docs.py b/docs/scripts/translate_docs.py index c907754d..7294e6a9 100644 --- a/docs/scripts/translate_docs.py +++ b/docs/scripts/translate_docs.py @@ -3,6 +3,11 @@ from openai import OpenAI from concurrent.futures import ThreadPoolExecutor +# import logging +# logging.basicConfig(level=logging.INFO) +# logging.getLogger("openai").setLevel(logging.DEBUG) + +OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o") # Define the source and target directories source_dir = "docs" @@ -19,8 +24,18 @@ "OpenAI", "Agents SDK", "Hello World", - "Model Context Protocol", + "Model context protocol", "structured outputs", + "Chain-of-Thought", + "Chat Completions", + "Computer-Using Agent", + "Code Interpreter", + "Function Calling", + "LLM", + "Operator", + "Playground", + "Realtime API", + "Sora", # Add more terms here ] @@ -36,19 +51,34 @@ "tracing": "トレーシング", "code examples": "コード例", "vector store": "ベクトルストア", + "deep research": "ディープリサーチ", + "category": "カテゴリー", + "user": "ユーザー", + "parameter": "パラメーター", + "processor": "プロセッサー", + "server": "サーバー", + "web search": "Web 検索", + "file search": "ファイル検索", + "streaming": "ストリーミング", + "system prompt": "システムプロンプト", + "Python first": "Python ファースト", # Add more Japanese mappings here }, # Add more languages here } eng_to_non_eng_instructions = { - "ja": { - "The term 'result' in the Runner guide context must be translated like 'execution results'", - "The term 'raw' in 'raw response events' must be kept as is", - "The term 'examples' must be code examples when the page mentions the code examples in the repo, it can be translated as either 'code exmaples' or 'sample code'.", - "The term 'primitives' can be translated as basic components or building blocks.", - "When the terms 'instructions' and 'tools' are mentioned as API parameter names, they must be kept as is.", + "common": [ + "* The term 'examples' must be code examples when the page mentions the code examples in the repo, it can be translated as either 'code exmaples' or 'sample code'.", + "* The term 'primitives' can be translated as basic components.", + "* When the terms 'instructions' and 'tools' are mentioned as API parameter names, they must be kept as is.", + "* The terms 'temperature', 'top_p', 'max_tokens', 'presence_penalty', 'frequency_penalty' as parameter names must be kept as is.", + ], + "ja": [ + "* The term 'result' in the Runner guide context must be translated like 'execution results'", + "* The term 'raw' in 'raw response events' must be kept as is", + "* You must consistently use polite wording such as です/ます rather than である/なのだ.", # Add more Japanese mappings here - }, + ], # Add more languages here } @@ -56,28 +86,72 @@ def built_instructions(target_language: str, lang_code: str) -> str: do_not_translate_terms = "\n".join(do_not_translate) specific_terms = "\n".join( - [f"{k} -> {v}" for k, v in eng_to_non_eng_mapping.get(lang_code, {}).items()] + [f"* {k} -> {v}" for k, v in eng_to_non_eng_mapping.get(lang_code, {}).items()] + ) + specific_instructions = "\n".join( + eng_to_non_eng_instructions.get("common", []) + + eng_to_non_eng_instructions.get(lang_code, []) ) - specific_instructions = "\n".join(eng_to_non_eng_instructions.get(lang_code, {})) - return f"""You are a professional translator with extensive experience in translating technical documents. -You are assigned to translate markdown text written in English into {target_language}. -The tone and voice must be concise, consistent, and most importantly professional. -You must return only the generated markdown text. Don't include any additional comments. -When you're unable to complete full translation, return an error message indicating the reason instead of returning partial results. - -# Do not translate + return f"""You are an expert technical translator. + +Your task: translate the markdown passed as a user input from English into {target_language}. + +############################ +## OUTPUT REQUIREMENTS ## +############################ +- Return **only** the translated markdown, with the original markdown structure preserved. +- Do **not** add explanations, comments, or metadata. + +######################### +## GENERAL RULES ## +######################### +- The output quality must be great enough to be used for public documentation. +- Be professional and polite. +- Keep the tone **natural** and concise. +- Do not omit any content. If a segment should stay in English, copy it verbatim. +- Do not change the markdown data structure, including the indentations. +- Keep all placeholders such as `CODE_BLOCK_*` and `CODE_LINE_PREFIX` unchanged. +- Convert asset paths: `./assets/…` → `../assets/…`. + *Example:* `![img](./assets/pic.png)` → `![img](../assets/pic.png)` +- Treat the **Do‑Not‑Translate list** and **Term‑Specific list** as case‑insensitive; preserve the original casing you see. +- Skip translation for: + - Inline code surrounded by single back‑ticks ( `like_this` ). + - Fenced code blocks delimited by ``` or ~~~, including all comments inside them. + - Link URLs inside `[label](URL)` – translate the label, never the URL. + +######################### +## LANGUAGE‑SPECIFIC ## +######################### +*(applies only when {target_language} = Japanese)* +- Insert a half‑width space before and after all alphanumeric terms. +- Add a half‑width space just outside markdown emphasis markers: ` **太字** ` (good) vs `** 太字 **` (bad). + +######################### +## DO NOT TRANSLATE ## +######################### +When replacing the following terms, do not have extra spaces before/after them: {do_not_translate_terms} -# Specific term mappings -When you convert these terms, do not append whitespaces before/after the terms. +######################### +## TERM‑SPECIFIC ## +######################### +Translate these terms exactly as provided (no extra spaces): {specific_terms} + +######################### +## EXTRA GUIDELINES ## +######################### {specific_instructions} -# Other Rules -- When translating into Japanese, ensure there are spaces before and after alphanumeric terms and markdown special characters like italic and bold. -- When translating very uncommon technical terms, include both the translated term and the original term in parentheses. That said, the section titles should be as simple as possible. -- You must skip translating any parts of code snippets and code comments -- "./assets/*" needs to be converted to "../assets/*"; markdown files like ./tracing.md can be kept as is. +######################### +## IF UNSURE ## +######################### +If you are uncertain about a term, leave the original English term in parentheses after your translation. + +######################### +## FINAL REMINDER ## +######################### +Return **only** the translated markdown text. No extra commentary. """ @@ -93,28 +167,55 @@ def translate_file(file_path: str, target_path: str, lang_code: str) -> None: current_chunk: list[str] = [] # Split content into chunks of up to 120 lines, ensuring splits occur before section titles + in_code_block = False + code_blocks: list[str] = [] + code_block_chunks: list[str] = [] for line in lines: - if len(current_chunk) >= 120 and line.startswith("#"): + if len(current_chunk) >= 120 and not in_code_block and line.startswith("#"): chunks.append("\n".join(current_chunk)) current_chunk = [] - current_chunk.append(line) + if line.strip().startswith("```"): + code_block_chunks.append(line) + if in_code_block is True: + code_blocks.append("\n".join(code_block_chunks)) + current_chunk.append(f"CODE_BLOCK_{(len(code_blocks) - 1):02}") + code_block_chunks.clear() + in_code_block = not in_code_block + continue + if in_code_block is True: + code_block_chunks.append(line) + else: + current_chunk.append(line) if current_chunk: chunks.append("\n".join(current_chunk)) # Translate each chunk separately and combine results translated_content: list[str] = [] for chunk in chunks: - response = openai_client.responses.create( - model="gpt-4.5-preview", - temperature=0.0, - instructions=built_instructions(languages[lang_code], lang_code), - input=chunk, - ) - translated_content.append(response.output_text) + instructions = built_instructions(languages[lang_code], lang_code) + if OPENAI_MODEL.startswith("o"): + response = openai_client.responses.create( + model=OPENAI_MODEL, + instructions=instructions, + input=chunk, + ) + translated_content.append(response.output_text) + else: + response = openai_client.responses.create( + model=OPENAI_MODEL, + instructions=instructions, + input=chunk, + temperature=0.0, + ) + translated_content.append(response.output_text) + + translated_text = "\n".join(translated_content) + for idx, code_block in enumerate(code_blocks): + translated_text = translated_text.replace(f"CODE_BLOCK_{idx:02}", code_block) # Save the combined translated content with open(target_path, "w", encoding="utf-8") as f: - f.write("\n".join(translated_content)) + f.write(translated_text) def translate_single_source_file(file_path: str) -> None: @@ -139,20 +240,21 @@ def main(): # Skip the target directories if any(lang in root for lang in languages): continue - with ThreadPoolExecutor(max_workers=20) as executor: - futures = [ - executor.submit( - translate_single_source_file, - os.path.join(root, file_name), - ) - for file_name in file_names - ] - for future in futures: - future.result() + # Increasing this will make the translation faster; you can decide considering the model's capacity + concurrency = 6 + with ThreadPoolExecutor(max_workers=concurrency) as executor: + futures = [] + for file_name in file_names: + filepath = os.path.join(root, file_name) + futures.append(executor.submit(translate_single_source_file, filepath)) + if len(futures) >= concurrency: + for future in futures: + future.result() + futures.clear() print("Translation completed.") if __name__ == "__main__": - # translate_single_source_file("docs/tools.md") + # translate_single_source_file("docs/index.md") main() From e115d5a4599a4421dbca694b85d95bcc47602839 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 12 Apr 2025 02:27:01 +0900 Subject: [PATCH 27/85] Add model_settings guide to models document page (#484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a pretty minor improvement to the docs: `model_settings` parameter is only mentioned on the agent doc page, but first-time visitors may want to know it’s also available on the models page. --- docs/ja/models.md | 21 +++++++++++++++++---- docs/models.md | 13 +++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/ja/models.md b/docs/ja/models.md index a71c9fef..3d216ac3 100644 --- a/docs/ja/models.md +++ b/docs/ja/models.md @@ -1,6 +1,6 @@ # モデル -エージェント SDK は、次の 2 種類の OpenAI モデルをサポートしています。 +Agents SDK は、OpenAI モデルを次の 2 つの形式でサポートしています。 - **推奨**: 新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使用して OpenAI API を呼び出す [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]。 - [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使用して OpenAI API を呼び出す [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。 @@ -51,11 +51,24 @@ async def main(): 1. OpenAI モデルの名前を直接設定します。 2. [`Model`][agents.models.interface.Model] 実装を提供します。 +エージェントに使用するモデルをさらに設定したい場合は、`temperature` などのオプションのモデル設定パラメーターを提供する [`ModelSettings`][agents.models.interface.ModelSettings] を渡すことができます。 + +```python +from agents import Agent, ModelSettings + +english_agent = Agent( + name="English agent", + instructions="You only speak English", + model="gpt-4o", + model_settings=ModelSettings(temperature=0.1), +) +``` + ## 他の LLM プロバイダーの使用 他の LLM プロバイダーを 3 つの方法で使用できます(例は[こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/))。 -1. [`set_default_openai_client`][agents.set_default_openai_client] は、`AsyncOpenAI` のインスタンスを LLM クライアントとしてグローバルに使用したい場合に便利です。これは、LLM プロバイダーが OpenAI 互換の API エンドポイントを持っている場合で、`base_url` と `api_key` を設定できます。[examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) に設定可能な例があります。 +1. [`set_default_openai_client`][agents.set_default_openai_client] は、`AsyncOpenAI` のインスタンスを LLM クライアントとしてグローバルに使用したい場合に便利です。LLM プロバイダーが OpenAI 互換の API エンドポイントを持っている場合に、`base_url` と `api_key` を設定できます。[examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) に設定可能な例があります。 2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルで使用します。これにより、「この実行のすべてのエージェントにカスタムモデルプロバイダーを使用する」と指定できます。[examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) に設定可能な例があります。 3. [`Agent.model`][agents.agent.Agent.model] では、特定のエージェントインスタンスにモデルを指定できます。これにより、異なるエージェントに対して異なるプロバイダーを組み合わせて使用することができます。[examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) に設定可能な例があります。 @@ -84,10 +97,10 @@ SDK はデフォルトで Responses API を使用しますが、ほとんどの ### 適切な形式のデータのサポート -一部のモデルプロバイダーは[適切な形式のデータ](https://platform.openai.com/docs/guides/structured-outputs)をサポートしていません。これにより、次のようなエラーが発生することがあります。 +一部のモデルプロバイダーは、[適切な形式のデータ](https://platform.openai.com/docs/guides/structured-outputs)をサポートしていません。これにより、次のようなエラーが発生することがあります。 ``` BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}} ``` -これは一部のモデルプロバイダーの欠点で、JSON 出力をサポートしていますが、出力に使用する `json_schema` を指定することはできません。この問題の修正に取り組んでいますが、JSON スキーマ出力をサポートしているプロバイダーに依存することをお勧めします。さもないと、アプリが不正な JSON のために頻繁に壊れることになります。 \ No newline at end of file +これは一部のモデルプロバイダーの欠点であり、JSON 出力をサポートしていますが、出力に使用する `json_schema` を指定することはできません。これに対する修正に取り組んでいますが、JSON スキーマ出力をサポートしているプロバイダーに依存することをお勧めします。さもないと、アプリが不正な JSON のために頻繁に壊れることになります。 \ No newline at end of file diff --git a/docs/models.md b/docs/models.md index ab4cefb8..1ce6e8f2 100644 --- a/docs/models.md +++ b/docs/models.md @@ -51,6 +51,19 @@ async def main(): 1. Sets the name of an OpenAI model directly. 2. Provides a [`Model`][agents.models.interface.Model] implementation. +When you want to further configure the model used for an agent, you can pass [`ModelSettings`][agents.models.interface.ModelSettings], which provides optional model configuration parameters such as temperature. + +```python +from agents import Agent, ModelSettings + +english_agent = Agent( + name="English agent", + instructions="You only speak English", + model="gpt-4o", + model_settings=ModelSettings(temperature=0.1), +) +``` + ## Using other LLM providers You can use other LLM providers in 3 ways (examples [here](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)): From d0693a192e92057bfae7cc9d621a6a68e76a111b Mon Sep 17 00:00:00 2001 From: LouisShark Date: Sat, 12 Apr 2025 06:20:59 +0800 Subject: [PATCH 28/85] fix: ensure metadata is non-null in response handling (#483) ensure metadata is non-null in response handling --- src/agents/models/openai_responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 6fda4bb3..06828884 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -248,7 +248,7 @@ async def _fetch_response( text=response_format, store=self._non_null_or_not_given(model_settings.store), reasoning=self._non_null_or_not_given(model_settings.reasoning), - metadata=model_settings.metadata, + metadata=self._non_null_or_not_given(model_settings.metadata) ) def _get_client(self) -> AsyncOpenAI: From e04d87cb1fbd72f8d8c48ba18730408cdd321766 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:37:00 +0200 Subject: [PATCH 29/85] Add extra request parameters `extra_query` and `extra_body` (#500) Added the possibility to pass `extra_query` and `extra_body` parameters when sending a request. In this implementation I added the attributes to `ModelSettings` as suggested by @rm-openai in #487 . I'll be happy to add some tests if you have any suggestions. --- src/agents/model_settings.py | 9 +++++++++ src/agents/models/openai_chatcompletions.py | 2 ++ src/agents/models/openai_responses.py | 2 ++ 3 files changed, 13 insertions(+) diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index f29cfa4a..ed9a0131 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, fields, replace from typing import Literal +from openai._types import Body, Query from openai.types.shared import Reasoning @@ -58,6 +59,14 @@ class ModelSettings: """Whether to include usage chunk. Defaults to True if not provided.""" + extra_query: Query | None = None + """Additional query fields to provide with the request. + Defaults to None if not provided.""" + + extra_body: Body | None = None + """Additional body fields to provide with the request. + Defaults to None if not provided.""" + def resolve(self, override: ModelSettings | None) -> ModelSettings: """Produce a new ModelSettings by overlaying any non-None values from the override on top of this instance.""" diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 807c6512..267efcaf 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -540,6 +540,8 @@ async def _fetch_response( store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), extra_headers=_HEADERS, + extra_query=model_settings.extra_query, + extra_body=model_settings.extra_body, metadata=self._non_null_or_not_given(model_settings.metadata), ) diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 06828884..cb5a603f 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -245,6 +245,8 @@ async def _fetch_response( parallel_tool_calls=parallel_tool_calls, stream=stream, extra_headers=_HEADERS, + extra_query=model_settings.extra_query, + extra_body=model_settings.extra_body, text=response_format, store=self._non_null_or_not_given(model_settings.store), reasoning=self._non_null_or_not_given(model_settings.reasoning), From 25f97f979b32810eb9b455d2809f5ef5f14bdb54 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Mon, 14 Apr 2025 23:37:13 +0900 Subject: [PATCH 30/85] Fix typos and misspellings (#486) Detected typos using typos-cli (https://crates.io/crates/typos-cli). It detected "occured" in a string constant "handoff_occured" too, but I didn't change the part this time because it could be a minor breaking change. Full outputs: ``` % typos . error: `Supresses` should be `Suppresses` --> ./src/agents/function_schema.py:134:7 | 134 | # Supresses warnings about missing annotations for params | ^^^^^^^^^ | error: `typ` should be `typo`, `type` --> ./src/agents/strict_schema.py:51:5 | 51 | typ = json_schema.get("type") | ^^^ | error: `typ` should be `typo`, `type` --> ./src/agents/strict_schema.py:52:8 | 52 | if typ == "object" and "additionalProperties" not in json_schema: | ^^^ | error: `typ` should be `typo`, `type` --> ./src/agents/strict_schema.py:55:9 | 55 | typ == "object" | ^^^ | error: `occured` should be `occurred` --> ./src/agents/stream_events.py:34:18 | 34 | "handoff_occured", | ^^^^^^^ | error: `occured` should be `occurred` --> ./src/agents/_run_impl.py:723:69 | 723 | event = RunItemStreamEvent(item=item, name="handoff_occured") | ^^^^^^^ | error: `desitnation` should be `destination` --> ./src/agents/tracing/span_data.py:171:25 | 171 | Includes source and desitnation agents. | ^^^^^^^^^^^ | error: `exmaples` should be `examples` --> ./docs/scripts/translate_docs.py:71:145 | 71 | "* The term 'examples' must be code examples when the page mentions the code examples in the repo, it can be translated as either 'code exmaples' or 'sample code'.", | ^^^^^^^^ | error: `structed` should be `structured` --> ./tests/test_agent_hooks.py:227:16 | 227 | async def test_structed_output_non_streamed_agent_hooks(): | ^^^^^^^^ | error: `structed` should be `structured` --> ./tests/test_agent_hooks.py:298:16 | 298 | async def test_structed_output_streamed_agent_hooks(): | ^^^^^^^^ | ``` --- docs/scripts/translate_docs.py | 2 +- src/agents/function_schema.py | 2 +- src/agents/tracing/span_data.py | 2 +- tests/test_agent_hooks.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/scripts/translate_docs.py b/docs/scripts/translate_docs.py index 7294e6a9..e62a2c09 100644 --- a/docs/scripts/translate_docs.py +++ b/docs/scripts/translate_docs.py @@ -68,7 +68,7 @@ } eng_to_non_eng_instructions = { "common": [ - "* The term 'examples' must be code examples when the page mentions the code examples in the repo, it can be translated as either 'code exmaples' or 'sample code'.", + "* The term 'examples' must be code examples when the page mentions the code examples in the repo, it can be translated as either 'code examples' or 'sample code'.", "* The term 'primitives' can be translated as basic components.", "* When the terms 'instructions' and 'tools' are mentioned as API parameter names, they must be kept as is.", "* The terms 'temperature', 'top_p', 'max_tokens', 'presence_penalty', 'frequency_penalty' as parameter names must be kept as is.", diff --git a/src/agents/function_schema.py b/src/agents/function_schema.py index 681affce..0e586896 100644 --- a/src/agents/function_schema.py +++ b/src/agents/function_schema.py @@ -131,7 +131,7 @@ def _detect_docstring_style(doc: str) -> DocstringStyle: @contextlib.contextmanager def _suppress_griffe_logging(): - # Supresses warnings about missing annotations for params + # Suppresses warnings about missing annotations for params logger = logging.getLogger("griffe") previous_level = logger.getEffectiveLevel() logger.setLevel(logging.ERROR) diff --git a/src/agents/tracing/span_data.py b/src/agents/tracing/span_data.py index 260e4c45..1a450280 100644 --- a/src/agents/tracing/span_data.py +++ b/src/agents/tracing/span_data.py @@ -168,7 +168,7 @@ def export(self) -> dict[str, Any]: class HandoffSpanData(SpanData): """ Represents a Handoff Span in the trace. - Includes source and desitnation agents. + Includes source and destination agents. """ __slots__ = ("from_agent", "to_agent") diff --git a/tests/test_agent_hooks.py b/tests/test_agent_hooks.py index 33107cba..a6c302dc 100644 --- a/tests/test_agent_hooks.py +++ b/tests/test_agent_hooks.py @@ -224,7 +224,7 @@ class Foo(TypedDict): @pytest.mark.asyncio -async def test_structed_output_non_streamed_agent_hooks(): +async def test_structured_output_non_streamed_agent_hooks(): hooks = AgentHooksForTests() model = FakeModel() agent_1 = Agent(name="test_1", model=model) @@ -295,7 +295,7 @@ async def test_structed_output_non_streamed_agent_hooks(): @pytest.mark.asyncio -async def test_structed_output_streamed_agent_hooks(): +async def test_structured_output_streamed_agent_hooks(): hooks = AgentHooksForTests() model = FakeModel() agent_1 = Agent(name="test_1", model=model) From 5183f528f441cadcb55599c654192a31c390723f Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Apr 2025 12:40:32 -0400 Subject: [PATCH 31/85] Add docs for customizng agent-as-tool (#504) Co-authored-by: pakrym-oai --- docs/tools.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/tools.md b/docs/tools.md index f7a88691..5fe2eced 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -259,6 +259,27 @@ async def main(): print(result.final_output) ``` +### Customizing tool-agents + +The `agent.as_tool` function is a convenience method to make it easy to turn an agent into a tool. It doesn't support all configuration though; for example, you can't set `max_turns`. For advanced use cases, use `Runner.run` directly in your tool implementation: + +```python +@function_tool +async def run_my_agent() -> str: + """A tool that runs the agent with custom configs". + + agent = Agent(name="My agent", instructions="...") + + result = await Runner.run( + agent, + input="...", + max_turns=5, + run_config=... + ) + + return str(result.final_output) +``` + ## Handling errors in function tools When you create a function tool via `@function_tool`, you can pass a `failure_error_function`. This is a function that provides an error response to the LLM in case the tool call crashes. From 5727a1c73a45a7e2681f90b6cdc0cd372434bd3f Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Apr 2025 12:40:41 -0400 Subject: [PATCH 32/85] Example for streaming guardrails (#505) An example for the question in the issue attached - how to run guardrails during streaming. Towards #495. --- .../agent_patterns/streaming_guardrails.py | 93 +++++++++++++++++++ src/agents/models/openai_chatcompletions.py | 8 +- src/agents/models/openai_responses.py | 2 +- 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 examples/agent_patterns/streaming_guardrails.py diff --git a/examples/agent_patterns/streaming_guardrails.py b/examples/agent_patterns/streaming_guardrails.py new file mode 100644 index 00000000..f4db2869 --- /dev/null +++ b/examples/agent_patterns/streaming_guardrails.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import asyncio + +from openai.types.responses import ResponseTextDeltaEvent +from pydantic import BaseModel, Field + +from agents import Agent, Runner + +""" +This example shows how to use guardrails as the model is streaming. Output guardrails run after the +final output has been generated; this example runs guardails every N tokens, allowing for early +termination if bad output is detected. + +The expected output is that you'll see a bunch of tokens stream in, then the guardrail will trigger +and stop the streaming. +""" + + +agent = Agent( + name="Assistant", + instructions=( + "You are a helpful assistant. You ALWAYS write long responses, making sure to be verbose " + "and detailed." + ), +) + + +class GuardrailOutput(BaseModel): + reasoning: str = Field( + description="Reasoning about whether the response could be understood by a ten year old." + ) + is_readable_by_ten_year_old: bool = Field( + description="Whether the response is understandable by a ten year old." + ) + + +guardrail_agent = Agent( + name="Checker", + instructions=( + "You will be given a question and a response. Your goal is to judge whether the response " + "is simple enough to be understood by a ten year old." + ), + output_type=GuardrailOutput, + model="gpt-4o-mini", +) + + +async def check_guardrail(text: str) -> GuardrailOutput: + result = await Runner.run(guardrail_agent, text) + return result.final_output_as(GuardrailOutput) + + +async def main(): + question = "What is a black hole, and how does it behave?" + result = Runner.run_streamed(agent, question) + current_text = "" + + # We will check the guardrail every N characters + next_guardrail_check_len = 300 + guardrail_task = None + + async for event in result.stream_events(): + if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): + print(event.data.delta, end="", flush=True) + current_text += event.data.delta + + # Check if it's time to run the guardrail check + # Note that we don't run the guardrail check if there's already a task running. An + # alternate implementation is to have N guardrails running, or cancel the previous + # one. + if len(current_text) >= next_guardrail_check_len and not guardrail_task: + print("Running guardrail check") + guardrail_task = asyncio.create_task(check_guardrail(current_text)) + next_guardrail_check_len += 300 + + # Every iteration of the loop, check if the guardrail has been triggered + if guardrail_task and guardrail_task.done(): + guardrail_result = guardrail_task.result() + if not guardrail_result.is_readable_by_ten_year_old: + print("\n\n================\n\n") + print(f"Guardrail triggered. Reasoning:\n{guardrail_result.reasoning}") + break + + # Do one final check on the final output + guardrail_result = await check_guardrail(current_text) + if not guardrail_result.is_readable_by_ten_year_old: + print("\n\n================\n\n") + print(f"Guardrail triggered. Reasoning:\n{guardrail_result.reasoning}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 267efcaf..6978ee30 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -572,7 +572,6 @@ def _get_client(self) -> AsyncOpenAI: class _Converter: - @classmethod def is_openai(cls, client: AsyncOpenAI): return str(client.base_url).startswith("https://api.openai.com") @@ -585,11 +584,14 @@ def get_store_param(cls, client: AsyncOpenAI, model_settings: ModelSettings) -> @classmethod def get_stream_options_param( - cls, client: AsyncOpenAI, model_settings: ModelSettings + cls, client: AsyncOpenAI, model_settings: ModelSettings ) -> dict[str, bool] | None: default_include_usage = True if cls.is_openai(client) else None - include_usage = model_settings.include_usage if model_settings.include_usage is not None \ + include_usage = ( + model_settings.include_usage + if model_settings.include_usage is not None else default_include_usage + ) stream_options = {"include_usage": include_usage} if include_usage is not None else None return stream_options diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index cb5a603f..055ab79b 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -250,7 +250,7 @@ async def _fetch_response( text=response_format, store=self._non_null_or_not_given(model_settings.store), reasoning=self._non_null_or_not_given(model_settings.reasoning), - metadata=self._non_null_or_not_given(model_settings.metadata) + metadata=self._non_null_or_not_given(model_settings.metadata), ) def _get_client(self) -> AsyncOpenAI: From 36f5b72449cb1bb20b0b464a671a24af11df8013 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Apr 2025 12:46:00 -0400 Subject: [PATCH 33/85] Don't run tracing shutdown behavior if disabled (#503) Towards #502 --- src/agents/tracing/setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/agents/tracing/setup.py b/src/agents/tracing/setup.py index 3a7c6ade..9e27d210 100644 --- a/src/agents/tracing/setup.py +++ b/src/agents/tracing/setup.py @@ -201,6 +201,9 @@ def create_span( ) def shutdown(self) -> None: + if self._disabled: + return + try: logger.debug("Shutting down trace provider") self._multi_processor.shutdown() From e8f14da473775ca6a5f3d5208e21759bdeaf5040 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:28:43 +0200 Subject: [PATCH 34/85] Fix type annotations examples (#496) I fixed the type annotations errors for pydantic objects in some examples as noted in #490 . When running `make lint` the following error occurs "UP007 Use `X | Y` for type annotations". If you know how to ignore it I will be happy to edit the pull request, thanks. --- pyproject.toml | 1 + uv.lock | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ca398ba2..fa3da072 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "types-requests>=2.0, <3", "mcp>=1.6.0, <2; python_version >= '3.10'", "mkdocs-static-i18n>=1.3.0", + "eval-type-backport>=0.2.2", ] classifiers = [ "Typing :: Typed", diff --git a/uv.lock b/uv.lock index af7eef7e..a762c723 100644 --- a/uv.lock +++ b/uv.lock @@ -313,6 +313,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] +[[package]] +name = "eval-type-backport" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830 }, +] + [[package]] name = "evdev" version = "1.9.1" @@ -1102,6 +1111,7 @@ name = "openai-agents" version = "0.0.9" source = { editable = "." } dependencies = [ + { name = "eval-type-backport" }, { name = "griffe" }, { name = "mcp", marker = "python_full_version >= '3.10'" }, { name = "mkdocs-static-i18n" }, @@ -1146,6 +1156,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "eval-type-backport", specifier = ">=0.2.2" }, { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, From d6f5190d53dd1923f380356abd21848b53059e7f Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Apr 2025 21:37:18 -0400 Subject: [PATCH 35/85] Replace referencable_id with response_id (#508) Minor change - naming. So that it doesn't pollute the next PR. --- [//]: # (BEGIN SAPLING FOOTER) * #509 * __->__ #508 --- src/agents/items.py | 2 +- src/agents/models/openai_chatcompletions.py | 2 +- src/agents/models/openai_responses.py | 2 +- src/agents/run.py | 2 +- tests/fake_model.py | 2 +- tests/test_items_helpers.py | 12 ++++---- tests/test_openai_chatcompletions.py | 2 +- tests/test_run_step_execution.py | 20 ++++++------- tests/test_run_step_processing.py | 32 ++++++++++----------- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/agents/items.py b/src/agents/items.py index c2af0dfc..d72701ab 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -166,7 +166,7 @@ class ModelResponse: usage: Usage """The usage information for the response.""" - referenceable_id: str | None + response_id: str | None """An ID for the response which can be used to refer to the response in subsequent calls to the model. Not supported by all model providers. """ diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 6978ee30..b60d5d6a 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -156,7 +156,7 @@ async def get_response( return ModelResponse( output=items, usage=usage, - referenceable_id=None, + response_id=None, ) async def stream_response( diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 055ab79b..e509d6f8 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -120,7 +120,7 @@ async def get_response( return ModelResponse( output=response.output, usage=usage, - referenceable_id=response.id, + response_id=response.id, ) async def stream_response( diff --git a/src/agents/run.py b/src/agents/run.py index 0159822a..93e6490c 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -677,7 +677,7 @@ async def _run_single_turn_streamed( final_response = ModelResponse( output=event.response.output, usage=usage, - referenceable_id=event.response.id, + response_id=event.response.id, ) streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event)) diff --git a/tests/fake_model.py b/tests/fake_model.py index ecbb7583..61fb5951 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -81,7 +81,7 @@ async def get_response( return ModelResponse( output=output, usage=Usage(), - referenceable_id=None, + response_id=None, ) async def stream_response( diff --git a/tests/test_items_helpers.py b/tests/test_items_helpers.py index 90fe6475..5dba21d8 100644 --- a/tests/test_items_helpers.py +++ b/tests/test_items_helpers.py @@ -168,7 +168,7 @@ def test_to_input_items_for_message() -> None: message = ResponseOutputMessage( id="m1", content=[content], role="assistant", status="completed", type="message" ) - resp = ModelResponse(output=[message], usage=Usage(), referenceable_id=None) + resp = ModelResponse(output=[message], usage=Usage(), response_id=None) input_items = resp.to_input_items() assert isinstance(input_items, list) and len(input_items) == 1 # The dict should contain exactly the primitive values of the message @@ -193,7 +193,7 @@ def test_to_input_items_for_function_call() -> None: tool_call = ResponseFunctionToolCall( id="f1", arguments="{}", call_id="c1", name="func", type="function_call" ) - resp = ModelResponse(output=[tool_call], usage=Usage(), referenceable_id=None) + resp = ModelResponse(output=[tool_call], usage=Usage(), response_id=None) input_items = resp.to_input_items() assert isinstance(input_items, list) and len(input_items) == 1 expected: ResponseFunctionToolCallParam = { @@ -211,7 +211,7 @@ def test_to_input_items_for_file_search_call() -> None: fs_call = ResponseFileSearchToolCall( id="fs1", queries=["query"], status="completed", type="file_search_call" ) - resp = ModelResponse(output=[fs_call], usage=Usage(), referenceable_id=None) + resp = ModelResponse(output=[fs_call], usage=Usage(), response_id=None) input_items = resp.to_input_items() assert isinstance(input_items, list) and len(input_items) == 1 expected: ResponseFileSearchToolCallParam = { @@ -226,7 +226,7 @@ def test_to_input_items_for_file_search_call() -> None: def test_to_input_items_for_web_search_call() -> None: """A web search tool call output should produce the same dict as a web search input.""" ws_call = ResponseFunctionWebSearch(id="w1", status="completed", type="web_search_call") - resp = ModelResponse(output=[ws_call], usage=Usage(), referenceable_id=None) + resp = ModelResponse(output=[ws_call], usage=Usage(), response_id=None) input_items = resp.to_input_items() assert isinstance(input_items, list) and len(input_items) == 1 expected: ResponseFunctionWebSearchParam = { @@ -248,7 +248,7 @@ def test_to_input_items_for_computer_call_click() -> None: pending_safety_checks=[], status="completed", ) - resp = ModelResponse(output=[comp_call], usage=Usage(), referenceable_id=None) + resp = ModelResponse(output=[comp_call], usage=Usage(), response_id=None) input_items = resp.to_input_items() assert isinstance(input_items, list) and len(input_items) == 1 converted_dict = input_items[0] @@ -268,7 +268,7 @@ def test_to_input_items_for_reasoning() -> None: """A reasoning output should produce the same dict as a reasoning input item.""" rc = Summary(text="why", type="summary_text") reasoning = ResponseReasoningItem(id="rid1", summary=[rc], type="reasoning") - resp = ModelResponse(output=[reasoning], usage=Usage(), referenceable_id=None) + resp = ModelResponse(output=[reasoning], usage=Usage(), response_id=None) input_items = resp.to_input_items() assert isinstance(input_items, list) and len(input_items) == 1 converted_dict = input_items[0] diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index 281d7b41..3608fc57 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -80,7 +80,7 @@ async def patched_fetch_response(self, *args, **kwargs): assert resp.usage.input_tokens == 7 assert resp.usage.output_tokens == 5 assert resp.usage.total_tokens == 12 - assert resp.referenceable_id is None + assert resp.response_id is None @pytest.mark.allow_call_model_methods diff --git a/tests/test_run_step_execution.py b/tests/test_run_step_execution.py index 16c62c84..6ae25fbd 100644 --- a/tests/test_run_step_execution.py +++ b/tests/test_run_step_execution.py @@ -43,7 +43,7 @@ async def test_empty_response_is_final_output(): response = ModelResponse( output=[], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent, response) @@ -59,7 +59,7 @@ async def test_plaintext_agent_no_tool_calls_is_final_output(): response = ModelResponse( output=[get_text_message("hello_world")], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent, response) @@ -79,7 +79,7 @@ async def test_plaintext_agent_no_tool_calls_multiple_messages_is_final_output() get_text_message("bye"), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result( agent, @@ -105,7 +105,7 @@ async def test_plaintext_agent_with_tool_call_is_run_again(): response = ModelResponse( output=[get_text_message("hello_world"), get_function_tool_call("test", "")], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent, response) @@ -140,7 +140,7 @@ async def test_multiple_tool_calls(): get_function_tool_call("test_2"), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent, response) @@ -166,7 +166,7 @@ async def test_handoff_output_leads_to_handoff_next_step(): response = ModelResponse( output=[get_text_message("Hello, world!"), get_handoff_tool_call(agent_1)], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent_3, response) @@ -186,7 +186,7 @@ async def test_final_output_without_tool_runs_again(): response = ModelResponse( output=[get_function_tool_call("tool_1")], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent, response) @@ -203,7 +203,7 @@ async def test_final_output_leads_to_final_output_next_step(): get_final_output_message(Foo(bar="123").model_dump_json()), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent, response) @@ -222,7 +222,7 @@ async def test_handoff_and_final_output_leads_to_handoff_next_step(): get_handoff_tool_call(agent_1), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent_3, response) @@ -241,7 +241,7 @@ async def test_multiple_final_output_leads_to_final_output_next_step(): get_final_output_message(Foo(bar="456").model_dump_json()), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = await get_execute_result(agent_3, response) diff --git a/tests/test_run_step_processing.py b/tests/test_run_step_processing.py index 2a6634ac..2ea98f06 100644 --- a/tests/test_run_step_processing.py +++ b/tests/test_run_step_processing.py @@ -39,7 +39,7 @@ def test_empty_response(): response = ModelResponse( output=[], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( @@ -58,7 +58,7 @@ def test_no_tool_calls(): response = ModelResponse( output=[get_text_message("Hello, world!")], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent, response=response, output_schema=None, handoffs=[], all_tools=[] @@ -76,7 +76,7 @@ async def test_single_tool_call(): get_function_tool_call("test", ""), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent, @@ -102,7 +102,7 @@ async def test_missing_tool_call_raises_error(): get_function_tool_call("missing", ""), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) with pytest.raises(ModelBehaviorError): @@ -132,7 +132,7 @@ async def test_multiple_tool_calls(): get_function_tool_call("test_2", "xyz"), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( @@ -162,7 +162,7 @@ async def test_handoffs_parsed_correctly(): response = ModelResponse( output=[get_text_message("Hello, world!")], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent_3, @@ -176,7 +176,7 @@ async def test_handoffs_parsed_correctly(): response = ModelResponse( output=[get_text_message("Hello, world!"), get_handoff_tool_call(agent_1)], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent_3, @@ -205,7 +205,7 @@ async def test_missing_handoff_fails(): response = ModelResponse( output=[get_text_message("Hello, world!"), get_handoff_tool_call(agent_2)], usage=Usage(), - referenceable_id=None, + response_id=None, ) with pytest.raises(ModelBehaviorError): RunImpl.process_model_response( @@ -229,7 +229,7 @@ async def test_multiple_handoffs_doesnt_error(): get_handoff_tool_call(agent_2), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent_3, @@ -254,7 +254,7 @@ async def test_final_output_parsed_correctly(): get_final_output_message(Foo(bar="123").model_dump_json()), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) RunImpl.process_model_response( @@ -281,7 +281,7 @@ async def test_file_search_tool_call_parsed_correctly(): response = ModelResponse( output=[get_text_message("hello"), file_search_call], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent, @@ -306,7 +306,7 @@ async def test_function_web_search_tool_call_parsed_correctly(): response = ModelResponse( output=[get_text_message("hello"), web_search_call], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent, @@ -333,7 +333,7 @@ async def test_reasoning_item_parsed_correctly(): response = ModelResponse( output=[reasoning], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=Agent(name="test"), @@ -401,7 +401,7 @@ async def test_computer_tool_call_without_computer_tool_raises_error(): response = ModelResponse( output=[computer_call], usage=Usage(), - referenceable_id=None, + response_id=None, ) with pytest.raises(ModelBehaviorError): RunImpl.process_model_response( @@ -430,7 +430,7 @@ async def test_computer_tool_call_with_computer_tool_parsed_correctly(): response = ModelResponse( output=[computer_call], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( agent=agent, @@ -460,7 +460,7 @@ async def test_tool_and_handoff_parsed_correctly(): get_handoff_tool_call(agent_1), ], usage=Usage(), - referenceable_id=None, + response_id=None, ) result = RunImpl.process_model_response( From 86ad99d7982ebe92259d656e2929d300290d96ae Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Apr 2025 21:59:41 -0400 Subject: [PATCH 36/85] Move dev-only deps to dev section (#506) These two deps are only used in development/examples --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fa3da072..251a6ab5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,8 +14,6 @@ dependencies = [ "requests>=2.0, <3", "types-requests>=2.0, <3", "mcp>=1.6.0, <2; python_version >= '3.10'", - "mkdocs-static-i18n>=1.3.0", - "eval-type-backport>=0.2.2", ] classifiers = [ "Typing :: Typed", @@ -60,6 +58,8 @@ dev = [ "textual", "websockets", "graphviz", + "mkdocs-static-i18n>=1.3.0", + "eval-type-backport>=0.2.2", ] [tool.uv.workspace] From 92d6e3e66cc9672895dac15c3ebbb3f4ee6475c4 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Apr 2025 22:02:47 -0400 Subject: [PATCH 37/85] Previous response id (#509) Allows passing in the previous_response_id to reduce sending the same data again and again. Test plan: Examples. Adding tests in next PR shortly. --- [//]: # (BEGIN SAPLING FOOTER) * __->__ #509 * #508 --- src/agents/items.py | 2 + src/agents/models/interface.py | 8 ++ src/agents/models/openai_chatcompletions.py | 3 + src/agents/models/openai_responses.py | 9 ++ src/agents/result.py | 8 ++ src/agents/run.py | 22 +++- tests/fake_model.py | 4 + tests/test_openai_chatcompletions.py | 3 + tests/test_openai_chatcompletions_stream.py | 3 + tests/test_responses_tracing.py | 108 +++++++++++++++++--- tests/voice/test_workflow.py | 4 + 11 files changed, 161 insertions(+), 13 deletions(-) diff --git a/src/agents/items.py b/src/agents/items.py index d72701ab..8fb2b52a 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -169,6 +169,8 @@ class ModelResponse: response_id: str | None """An ID for the response which can be used to refer to the response in subsequent calls to the model. Not supported by all model providers. + If using OpenAI models via the Responses API, this is the `response_id` parameter, and it can + be passed to `Runner.run`. """ def to_input_items(self) -> list[TResponseInputItem]: diff --git a/src/agents/models/interface.py b/src/agents/models/interface.py index e9a8700c..bcf2c1a6 100644 --- a/src/agents/models/interface.py +++ b/src/agents/models/interface.py @@ -44,6 +44,8 @@ async def get_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + *, + previous_response_id: str | None, ) -> ModelResponse: """Get a response from the model. @@ -55,6 +57,8 @@ async def get_response( output_schema: The output schema to use. handoffs: The handoffs available to the model. tracing: Tracing configuration. + previous_response_id: the ID of the previous response. Generally not used by the model, + except for the OpenAI Responses API. Returns: The full model response. @@ -71,6 +75,8 @@ def stream_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + *, + previous_response_id: str | None, ) -> AsyncIterator[TResponseStreamEvent]: """Stream a response from the model. @@ -82,6 +88,8 @@ def stream_response( output_schema: The output schema to use. handoffs: The handoffs available to the model. tracing: Tracing configuration. + previous_response_id: the ID of the previous response. Generally not used by the model, + except for the OpenAI Responses API. Returns: An iterator of response stream events, in OpenAI Responses format. diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index b60d5d6a..c12fe68a 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -108,6 +108,7 @@ async def get_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + previous_response_id: str | None, ) -> ModelResponse: with generation_span( model=str(self.model), @@ -168,6 +169,8 @@ async def stream_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + *, + previous_response_id: str | None, ) -> AsyncIterator[TResponseStreamEvent]: """ Yields a partial message as it is generated, as well as the usage information. diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index e509d6f8..ab4617d4 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -69,6 +69,7 @@ async def get_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + previous_response_id: str | None, ) -> ModelResponse: with response_span(disabled=tracing.is_disabled()) as span_response: try: @@ -79,6 +80,7 @@ async def get_response( tools, output_schema, handoffs, + previous_response_id, stream=False, ) @@ -132,6 +134,7 @@ async def stream_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + previous_response_id: str | None, ) -> AsyncIterator[ResponseStreamEvent]: """ Yields a partial message as it is generated, as well as the usage information. @@ -145,6 +148,7 @@ async def stream_response( tools, output_schema, handoffs, + previous_response_id, stream=True, ) @@ -180,6 +184,7 @@ async def _fetch_response( tools: list[Tool], output_schema: AgentOutputSchema | None, handoffs: list[Handoff], + previous_response_id: str | None, stream: Literal[True], ) -> AsyncStream[ResponseStreamEvent]: ... @@ -192,6 +197,7 @@ async def _fetch_response( tools: list[Tool], output_schema: AgentOutputSchema | None, handoffs: list[Handoff], + previous_response_id: str | None, stream: Literal[False], ) -> Response: ... @@ -203,6 +209,7 @@ async def _fetch_response( tools: list[Tool], output_schema: AgentOutputSchema | None, handoffs: list[Handoff], + previous_response_id: str | None, stream: Literal[True] | Literal[False] = False, ) -> Response | AsyncStream[ResponseStreamEvent]: list_input = ItemHelpers.input_to_new_input_list(input) @@ -229,9 +236,11 @@ async def _fetch_response( f"Stream: {stream}\n" f"Tool choice: {tool_choice}\n" f"Response format: {response_format}\n" + f"Previous response id: {previous_response_id}\n" ) return await self._client.responses.create( + previous_response_id=self._non_null_or_not_given(previous_response_id), instructions=self._non_null_or_not_given(system_instructions), model=self.model, input=list_input, diff --git a/src/agents/result.py b/src/agents/result.py index 40a64806..a2a6cc4a 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -80,6 +80,14 @@ def to_input_list(self) -> list[TResponseInputItem]: return original_items + new_items + @property + def last_response_id(self) -> str | None: + """Convenience method to get the response ID of the last model response.""" + if not self.raw_responses: + return None + + return self.raw_responses[-1].response_id + @dataclass class RunResult(RunResultBase): diff --git a/src/agents/run.py b/src/agents/run.py index 93e6490c..e2b0dbce 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -117,6 +117,7 @@ async def run( max_turns: int = DEFAULT_MAX_TURNS, hooks: RunHooks[TContext] | None = None, run_config: RunConfig | None = None, + previous_response_id: str | None = None, ) -> RunResult: """Run a workflow starting at the given agent. The agent will run in a loop until a final output is generated. The loop runs like so: @@ -141,6 +142,8 @@ async def run( AI invocation (including any tool calls that might occur). hooks: An object that receives callbacks on various lifecycle events. run_config: Global settings for the entire agent run. + previous_response_id: The ID of the previous response, if using OpenAI models via the + Responses API, this allows you to skip passing in input from the previous turn. Returns: A run result containing all the inputs, guardrail results and the output of the last @@ -230,6 +233,7 @@ async def run( run_config=run_config, should_run_agent_start_hooks=should_run_agent_start_hooks, tool_use_tracker=tool_use_tracker, + previous_response_id=previous_response_id, ), ) else: @@ -243,6 +247,7 @@ async def run( run_config=run_config, should_run_agent_start_hooks=should_run_agent_start_hooks, tool_use_tracker=tool_use_tracker, + previous_response_id=previous_response_id, ) should_run_agent_start_hooks = False @@ -291,6 +296,7 @@ def run_sync( max_turns: int = DEFAULT_MAX_TURNS, hooks: RunHooks[TContext] | None = None, run_config: RunConfig | None = None, + previous_response_id: str | None = None, ) -> RunResult: """Run a workflow synchronously, starting at the given agent. Note that this just wraps the `run` method, so it will not work if there's already an event loop (e.g. inside an async @@ -319,6 +325,8 @@ def run_sync( AI invocation (including any tool calls that might occur). hooks: An object that receives callbacks on various lifecycle events. run_config: Global settings for the entire agent run. + previous_response_id: The ID of the previous response, if using OpenAI models via the + Responses API, this allows you to skip passing in input from the previous turn. Returns: A run result containing all the inputs, guardrail results and the output of the last @@ -332,6 +340,7 @@ def run_sync( max_turns=max_turns, hooks=hooks, run_config=run_config, + previous_response_id=previous_response_id, ) ) @@ -344,6 +353,7 @@ def run_streamed( max_turns: int = DEFAULT_MAX_TURNS, hooks: RunHooks[TContext] | None = None, run_config: RunConfig | None = None, + previous_response_id: str | None = None, ) -> RunResultStreaming: """Run a workflow starting at the given agent in streaming mode. The returned result object contains a method you can use to stream semantic events as they are generated. @@ -370,7 +380,8 @@ def run_streamed( AI invocation (including any tool calls that might occur). hooks: An object that receives callbacks on various lifecycle events. run_config: Global settings for the entire agent run. - + previous_response_id: The ID of the previous response, if using OpenAI models via the + Responses API, this allows you to skip passing in input from the previous turn. Returns: A result object that contains data about the run, as well as a method to stream events. """ @@ -428,6 +439,7 @@ def run_streamed( hooks=hooks, context_wrapper=context_wrapper, run_config=run_config, + previous_response_id=previous_response_id, ) ) return streamed_result @@ -485,6 +497,7 @@ async def _run_streamed_impl( hooks: RunHooks[TContext], context_wrapper: RunContextWrapper[TContext], run_config: RunConfig, + previous_response_id: str | None, ): current_span: Span[AgentSpanData] | None = None current_agent = starting_agent @@ -554,6 +567,7 @@ async def _run_streamed_impl( should_run_agent_start_hooks, tool_use_tracker, all_tools, + previous_response_id, ) should_run_agent_start_hooks = False @@ -623,6 +637,7 @@ async def _run_single_turn_streamed( should_run_agent_start_hooks: bool, tool_use_tracker: AgentToolUseTracker, all_tools: list[Tool], + previous_response_id: str | None, ) -> SingleStepResult: if should_run_agent_start_hooks: await asyncio.gather( @@ -662,6 +677,7 @@ async def _run_single_turn_streamed( get_model_tracing_impl( run_config.tracing_disabled, run_config.trace_include_sensitive_data ), + previous_response_id=previous_response_id, ): if isinstance(event, ResponseCompletedEvent): usage = ( @@ -717,6 +733,7 @@ async def _run_single_turn( run_config: RunConfig, should_run_agent_start_hooks: bool, tool_use_tracker: AgentToolUseTracker, + previous_response_id: str | None, ) -> SingleStepResult: # Ensure we run the hooks before anything else if should_run_agent_start_hooks: @@ -746,6 +763,7 @@ async def _run_single_turn( context_wrapper, run_config, tool_use_tracker, + previous_response_id, ) return await cls._get_single_step_result_from_response( @@ -888,6 +906,7 @@ async def _get_new_response( context_wrapper: RunContextWrapper[TContext], run_config: RunConfig, tool_use_tracker: AgentToolUseTracker, + previous_response_id: str | None, ) -> ModelResponse: model = cls._get_model(agent, run_config) model_settings = agent.model_settings.resolve(run_config.model_settings) @@ -903,6 +922,7 @@ async def _get_new_response( tracing=get_model_tracing_impl( run_config.tracing_disabled, run_config.trace_include_sensitive_data ), + previous_response_id=previous_response_id, ) context_wrapper.usage.add(new_response.usage) diff --git a/tests/fake_model.py b/tests/fake_model.py index 61fb5951..203479d0 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -54,6 +54,8 @@ async def get_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + *, + previous_response_id: str | None, ) -> ModelResponse: self.last_turn_args = { "system_instructions": system_instructions, @@ -93,6 +95,8 @@ async def stream_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + *, + previous_response_id: str | None, ) -> AsyncIterator[TResponseStreamEvent]: with generation_span(disabled=not self.tracing_enabled) as span: output = self.get_next_output() diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index 3608fc57..92d65fda 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -67,6 +67,7 @@ async def patched_fetch_response(self, *args, **kwargs): output_schema=None, handoffs=[], tracing=ModelTracing.DISABLED, + previous_response_id=None, ) # Should have produced exactly one output message with one text part assert isinstance(resp, ModelResponse) @@ -115,6 +116,7 @@ async def patched_fetch_response(self, *args, **kwargs): output_schema=None, handoffs=[], tracing=ModelTracing.DISABLED, + previous_response_id=None, ) assert len(resp.output) == 1 assert isinstance(resp.output[0], ResponseOutputMessage) @@ -164,6 +166,7 @@ async def patched_fetch_response(self, *args, **kwargs): output_schema=None, handoffs=[], tracing=ModelTracing.DISABLED, + previous_response_id=None, ) # Expect a message item followed by a function tool call item. assert len(resp.output) == 2 diff --git a/tests/test_openai_chatcompletions_stream.py b/tests/test_openai_chatcompletions_stream.py index 7add92a6..b82f2430 100644 --- a/tests/test_openai_chatcompletions_stream.py +++ b/tests/test_openai_chatcompletions_stream.py @@ -79,6 +79,7 @@ async def patched_fetch_response(self, *args, **kwargs): output_schema=None, handoffs=[], tracing=ModelTracing.DISABLED, + previous_response_id=None, ): output_events.append(event) # We expect a response.created, then a response.output_item.added, content part added, @@ -168,6 +169,7 @@ async def patched_fetch_response(self, *args, **kwargs): output_schema=None, handoffs=[], tracing=ModelTracing.DISABLED, + previous_response_id=None, ): output_events.append(event) # Expect sequence similar to text: created, output_item.added, content part added, @@ -255,6 +257,7 @@ async def patched_fetch_response(self, *args, **kwargs): output_schema=None, handoffs=[], tracing=ModelTracing.DISABLED, + previous_response_id=None, ): output_events.append(event) # Sequence should be: response.created, then after loop we expect function call-related events: diff --git a/tests/test_responses_tracing.py b/tests/test_responses_tracing.py index 40bdfafb..0bc97a95 100644 --- a/tests/test_responses_tracing.py +++ b/tests/test_responses_tracing.py @@ -44,7 +44,14 @@ async def test_get_response_creates_trace(monkeypatch): # Mock _fetch_response to return a dummy response with a known id async def dummy_fetch_response( - system_instructions, input, model_settings, tools, output_schema, handoffs, stream + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + prev_response_id, + stream, ): return DummyResponse() @@ -52,7 +59,14 @@ async def dummy_fetch_response( # Call get_response await model.get_response( - "instr", "input", ModelSettings(), [], None, [], ModelTracing.ENABLED + "instr", + "input", + ModelSettings(), + [], + None, + [], + ModelTracing.ENABLED, + previous_response_id=None, ) assert fetch_normalized_spans() == snapshot( @@ -74,7 +88,14 @@ async def test_non_data_tracing_doesnt_set_response_id(monkeypatch): # Mock _fetch_response to return a dummy response with a known id async def dummy_fetch_response( - system_instructions, input, model_settings, tools, output_schema, handoffs, stream + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + prev_response_id, + stream, ): return DummyResponse() @@ -82,7 +103,14 @@ async def dummy_fetch_response( # Call get_response await model.get_response( - "instr", "input", ModelSettings(), [], None, [], ModelTracing.ENABLED_WITHOUT_DATA + "instr", + "input", + ModelSettings(), + [], + None, + [], + ModelTracing.ENABLED_WITHOUT_DATA, + previous_response_id=None, ) assert fetch_normalized_spans() == snapshot( @@ -102,7 +130,14 @@ async def test_disable_tracing_does_not_create_span(monkeypatch): # Mock _fetch_response to return a dummy response with a known id async def dummy_fetch_response( - system_instructions, input, model_settings, tools, output_schema, handoffs, stream + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + prev_response_id, + stream, ): return DummyResponse() @@ -110,7 +145,14 @@ async def dummy_fetch_response( # Call get_response await model.get_response( - "instr", "input", ModelSettings(), [], None, [], ModelTracing.DISABLED + "instr", + "input", + ModelSettings(), + [], + None, + [], + ModelTracing.DISABLED, + previous_response_id=None, ) assert fetch_normalized_spans() == snapshot([{"workflow_name": "test"}]) @@ -127,7 +169,14 @@ async def test_stream_response_creates_trace(monkeypatch): # Define a dummy fetch function that returns an async stream with a dummy response async def dummy_fetch_response( - system_instructions, input, model_settings, tools, output_schema, handoffs, stream + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + prev_response_id, + stream, ): class DummyStream: async def __aiter__(self): @@ -142,7 +191,14 @@ async def __aiter__(self): # Consume the stream to trigger processing of the final response async for _ in model.stream_response( - "instr", "input", ModelSettings(), [], None, [], ModelTracing.ENABLED + "instr", + "input", + ModelSettings(), + [], + None, + [], + ModelTracing.ENABLED, + previous_response_id=None, ): pass @@ -165,7 +221,14 @@ async def test_stream_non_data_tracing_doesnt_set_response_id(monkeypatch): # Define a dummy fetch function that returns an async stream with a dummy response async def dummy_fetch_response( - system_instructions, input, model_settings, tools, output_schema, handoffs, stream + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + prev_response_id, + stream, ): class DummyStream: async def __aiter__(self): @@ -180,7 +243,14 @@ async def __aiter__(self): # Consume the stream to trigger processing of the final response async for _ in model.stream_response( - "instr", "input", ModelSettings(), [], None, [], ModelTracing.ENABLED_WITHOUT_DATA + "instr", + "input", + ModelSettings(), + [], + None, + [], + ModelTracing.ENABLED_WITHOUT_DATA, + previous_response_id=None, ): pass @@ -202,7 +272,14 @@ async def test_stream_disabled_tracing_doesnt_create_span(monkeypatch): # Define a dummy fetch function that returns an async stream with a dummy response async def dummy_fetch_response( - system_instructions, input, model_settings, tools, output_schema, handoffs, stream + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + prev_response_id, + stream, ): class DummyStream: async def __aiter__(self): @@ -217,7 +294,14 @@ async def __aiter__(self): # Consume the stream to trigger processing of the final response async for _ in model.stream_response( - "instr", "input", ModelSettings(), [], None, [], ModelTracing.DISABLED + "instr", + "input", + ModelSettings(), + [], + None, + [], + ModelTracing.DISABLED, + previous_response_id=None, ): pass diff --git a/tests/voice/test_workflow.py b/tests/voice/test_workflow.py index 3f18c049..72a3370d 100644 --- a/tests/voice/test_workflow.py +++ b/tests/voice/test_workflow.py @@ -51,6 +51,8 @@ async def get_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + *, + previous_response_id: str | None, ) -> ModelResponse: raise NotImplementedError("Not implemented") @@ -63,6 +65,8 @@ async def stream_response( output_schema: AgentOutputSchema | None, handoffs: list[Handoff], tracing: ModelTracing, + *, + previous_response_id: str | None, ) -> AsyncIterator[TResponseStreamEvent]: output = self.get_next_output() for item in output: From 360f173b73233bbe82ceb6586809c016d8584f0b Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 15 Apr 2025 11:04:07 +0900 Subject: [PATCH 38/85] Evolve the doc translation workflow by using gpt-4.1 (#507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request enhances the document translation workflow by switching to the new GPT-4.1 model. The generator script’s prompt now includes a “workflow” section that guides the model to iterate self-reviews on its outputs to autonomously achieve the highest quality. This addition has noticeably improved the naturalness and consistency of the wording in the translated outputs. --- docs/ja/agents.md | 44 ++++++------ docs/ja/config.md | 24 +++---- docs/ja/context.md | 44 ++++++------ docs/ja/examples.md | 40 +++++------ docs/ja/guardrails.md | 40 +++++------ docs/ja/handoffs.md | 32 ++++----- docs/ja/index.md | 36 +++++----- docs/ja/mcp.md | 34 ++++----- docs/ja/models.md | 60 ++++++++-------- docs/ja/multi_agent.md | 44 ++++++------ docs/ja/quickstart.md | 32 ++++----- docs/ja/results.md | 38 +++++----- docs/ja/running_agents.md | 70 +++++++++--------- docs/ja/streaming.md | 16 ++--- docs/ja/tools.md | 70 +++++++++--------- docs/ja/tracing.md | 128 ++++++++++++++++----------------- docs/ja/visualization.md | 27 +++---- docs/ja/voice/pipeline.md | 30 ++++---- docs/ja/voice/quickstart.md | 18 ++--- docs/ja/voice/tracing.md | 18 ++--- docs/scripts/translate_docs.py | 21 ++++-- mkdocs.yml | 2 +- 22 files changed, 438 insertions(+), 430 deletions(-) diff --git a/docs/ja/agents.md b/docs/ja/agents.md index 5d7a2dfe..f4b108e8 100644 --- a/docs/ja/agents.md +++ b/docs/ja/agents.md @@ -1,14 +1,14 @@ # エージェント -エージェントはアプリのコア構成要素です。エージェントは、指示とツールで構成された大規模言語モデル (LLM) です。 +エージェントは、アプリケーションの中核となる基本コンポーネントです。エージェントとは、instructions とツールで構成された大規模言語モデル(LLM)のことです。 ## 基本設定 -エージェントで設定する最も一般的なプロパティは次のとおりです: +エージェントで最も一般的に設定するプロパティは以下の通りです。 -- `instructions`: 開発者メッセージまたはシステムプロンプトとも呼ばれます。 -- `model`: 使用する LLM と、temperature や top_p などのモデル調整パラメーターを設定するためのオプションの `model_settings`。 -- `tools`: エージェントがタスクを達成するために使用できるツール。 +- `instructions`:developer message や システムプロンプト(system prompt)とも呼ばれます。 +- `model`:どの LLM を使用するか、また `model_settings` で temperature や top_p などのモデル調整パラメーターを設定できます。 +- `tools`:エージェントがタスクを達成するために使用できるツールです。 ```python from agents import Agent, ModelSettings, function_tool @@ -27,7 +27,7 @@ agent = Agent( ## コンテキスト -エージェントは `context` タイプに対して汎用的です。コンテキストは依存性注入ツールであり、`Runner.run()` に渡すために作成するオブジェクトです。これはすべてのエージェント、ツール、ハンドオフなどに渡され、エージェントの実行に必要な依存関係や状態をまとめたものとして機能します。任意の Python オブジェクトをコンテキストとして提供できます。 +エージェントは `context` 型に対して汎用的です。コンテキストは依存性注入ツールであり、`Runner.run()` に渡すオブジェクトです。これはすべてのエージェント、ツール、ハンドオフなどに渡され、エージェント実行時の依存関係や状態をまとめて管理します。任意の Python オブジェクトを context として指定できます。 ```python @dataclass @@ -45,7 +45,7 @@ agent = Agent[UserContext]( ## 出力タイプ -デフォルトでは、エージェントはプレーンテキスト(つまり `str`)の出力を生成します。特定のタイプの出力をエージェントに生成させたい場合は、`output_type` パラメーターを使用できます。一般的な選択肢として [Pydantic](https://docs.pydantic.dev/) オブジェクトを使用しますが、Pydantic の [TypeAdapter](https://docs.pydantic.dev/latest/api/type_adapter/) でラップできる任意のタイプをサポートしています - データクラス、リスト、TypedDict など。 +デフォルトでは、エージェントはプレーンテキスト(つまり `str`)出力を生成します。特定の型の出力をエージェントに生成させたい場合は、`output_type` パラメーターを使用できます。一般的な選択肢として [Pydantic](https://docs.pydantic.dev/) オブジェクトがありますが、Pydantic の [TypeAdapter](https://docs.pydantic.dev/latest/api/type_adapter/) でラップできる型(dataclasses、リスト、TypedDict など)であればサポートしています。 ```python from pydantic import BaseModel @@ -66,11 +66,11 @@ agent = Agent( !!! note - `output_type` を渡すと、モデルに通常のプレーンテキスト応答の代わりに [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用するよう指示します。 + `output_type` を指定すると、モデルは通常のプレーンテキスト応答の代わりに [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用するよう指示されます。 ## ハンドオフ -ハンドオフは、エージェントが委任できるサブエージェントです。ハンドオフのリストを提供し、エージェントは関連する場合にそれらに委任することができます。これは、単一のタスクに優れたモジュール化された専門エージェントを編成する強力なパターンです。[handoffs](handoffs.md) ドキュメントで詳細をお読みください。 +ハンドオフは、エージェントが委任できるサブエージェントです。ハンドオフのリストを指定すると、エージェントは必要に応じてそれらに処理を委任できます。これは、単一タスクに特化したモジュール型のエージェントをオーケストレーションする強力なパターンです。詳細は [handoffs](handoffs.md) ドキュメントをご覧ください。 ```python from agents import Agent @@ -89,9 +89,9 @@ triage_agent = Agent( ) ``` -## 動的指示 +## 動的 instructions -ほとんどの場合、エージェントを作成する際に指示を提供できますが、関数を介して動的な指示を提供することもできます。この関数はエージェントとコンテキストを受け取り、プロンプトを返す必要があります。通常の関数と `async` 関数の両方が受け入れられます。 +多くの場合、エージェント作成時に instructions を指定できますが、関数を使って動的に instructions を提供することも可能です。この関数はエージェントと context を受け取り、プロンプトを返す必要があります。通常の関数と `async` 関数の両方が利用可能です。 ```python def dynamic_instructions( @@ -108,15 +108,15 @@ agent = Agent[UserContext]( ## ライフサイクルイベント(フック) -時には、エージェントのライフサイクルを観察したいことがあります。たとえば、イベントを記録したり、特定のイベントが発生したときにデータを事前取得したりすることができます。`hooks` プロパティを使用してエージェントのライフサイクルにフックすることができます。[`AgentHooks`][agents.lifecycle.AgentHooks] クラスをサブクラス化し、興味のあるメソッドをオーバーライドします。 +エージェントのライフサイクルを監視したい場合があります。たとえば、イベントを記録したり、特定のイベント発生時にデータを事前取得したりしたい場合です。`hooks` プロパティを使ってエージェントのライフサイクルにフックできます。[`AgentHooks`][agents.lifecycle.AgentHooks] クラスをサブクラス化し、関心のあるメソッドをオーバーライドしてください。 ## ガードレール -ガードレールを使用すると、エージェントの実行と並行してユーザー入力のチェック/検証を行うことができます。たとえば、ユーザーの入力を関連性でスクリーニングすることができます。[guardrails](guardrails.md) ドキュメントで詳細をお読みください。 +ガードレールを使うと、エージェントの実行と並行して user 入力のチェックやバリデーションを行えます。たとえば、user の入力が関連性のある内容かどうかをスクリーニングできます。詳細は [guardrails](guardrails.md) ドキュメントをご覧ください。 -## エージェントのクローン/コピー +## エージェントのクローン/コピー -エージェントの `clone()` メソッドを使用すると、エージェントを複製し、任意のプロパティを変更することができます。 +エージェントの `clone()` メソッドを使うことで、エージェントを複製し、任意のプロパティを変更できます。 ```python pirate_agent = Agent( @@ -133,15 +133,15 @@ robot_agent = pirate_agent.clone( ## ツール使用の強制 -ツールのリストを提供しても、LLM がツールを使用するとは限りません。[`ModelSettings.tool_choice`][agents.model_settings.ModelSettings.tool_choice] を設定することでツール使用を強制できます。有効な値は次のとおりです: +ツールのリストを指定しても、必ずしも LLM がツールを使用するとは限りません。[`ModelSettings.tool_choice`][agents.model_settings.ModelSettings.tool_choice] を設定することでツールの使用を強制できます。有効な値は以下の通りです。 -1. `auto`:LLM がツールを使用するかどうかを決定します。 -2. `required`:LLM がツールを使用する必要があります(ただし、どのツールを使用するかは賢く決定できます)。 -3. `none`:LLM がツールを使用しないことを要求します。 -4. 特定の文字列を設定する例:`my_tool`、特定のツールを使用することを要求します。 +1. `auto`:LLM がツールを使うかどうかを自動で判断します。 +2. `required`:LLM にツールの使用を必須とします(どのツールを使うかは賢く選択されます)。 +3. `none`:LLM にツールを _使わない_ ことを要求します。 +4. 特定の文字列(例:`my_tool`)を指定すると、その特定のツールの使用を必須とします。 !!! note - 無限ループを防ぐために、フレームワークはツール呼び出し後に `tool_choice` を自動的に "auto" にリセットします。この動作は [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice] で設定可能です。無限ループは、ツールの結果が LLM に送信され、その後 `tool_choice` によって別のツール呼び出しが生成されるために発生します。 + 無限ループを防ぐため、フレームワークはツール呼び出し後に自動的に `tool_choice` を "auto" にリセットします。この挙動は [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice] で設定可能です。無限ループは、ツールの execution results が LLM に送信され、`tool_choice` のために再度ツール呼び出しが発生し、これが繰り返されることで発生します。 - ツール呼び出し後にエージェントが完全に停止するようにしたい場合(自動モードで続行するのではなく)、[`Agent.tool_use_behavior="stop_on_first_tool"`] を設定することで、ツールの出力を最終応答として直接使用し、さらなる LLM 処理を行わないようにできます。 \ No newline at end of file + ツール呼び出し後にエージェントを完全に停止させたい場合(auto モードで継続させたくない場合)は、[`Agent.tool_use_behavior="stop_on_first_tool"`] を設定できます。これにより、ツールの出力がそのまま最終応答として使用され、以降の LLM 処理は行われません。 \ No newline at end of file diff --git a/docs/ja/config.md b/docs/ja/config.md index f8586e8a..c547a793 100644 --- a/docs/ja/config.md +++ b/docs/ja/config.md @@ -2,7 +2,7 @@ ## API キーとクライアント -デフォルトでは、SDK はインポートされるとすぐに LLM リクエストとトレーシングのために `OPENAI_API_KEY` 環境変数を探します。アプリが起動する前にその環境変数を設定できない場合は、[set_default_openai_key()][agents.set_default_openai_key] 関数を使用してキーを設定できます。 +デフォルトでは、SDK はインポート時に LLM リクエストやトレーシングのために `OPENAI_API_KEY` 環境変数を探します。アプリの起動前にこの環境変数を設定できない場合は、[set_default_openai_key()][agents.set_default_openai_key] 関数を使ってキーを設定できます。 ```python from agents import set_default_openai_key @@ -10,7 +10,7 @@ from agents import set_default_openai_key set_default_openai_key("sk-...") ``` -また、使用する OpenAI クライアントを設定することもできます。デフォルトでは、SDK は環境変数からの API キーまたは上記で設定したデフォルトキーを使用して `AsyncOpenAI` インスタンスを作成します。これを変更するには、[set_default_openai_client()][agents.set_default_openai_client] 関数を使用します。 +また、使用する OpenAI クライアントを設定することも可能です。デフォルトでは、SDK は環境変数または上記で設定したデフォルトキーを使って `AsyncOpenAI` インスタンスを作成します。これを変更したい場合は、[set_default_openai_client()][agents.set_default_openai_client] 関数を利用してください。 ```python from openai import AsyncOpenAI @@ -20,7 +20,7 @@ custom_client = AsyncOpenAI(base_url="...", api_key="...") set_default_openai_client(custom_client) ``` -最後に、使用する OpenAI API をカスタマイズすることもできます。デフォルトでは、OpenAI Responses API を使用します。これを Chat Completions API に変更するには、[set_default_openai_api()][agents.set_default_openai_api] 関数を使用します。 +さらに、使用する OpenAI API をカスタマイズすることもできます。デフォルトでは OpenAI Responses API を使用していますが、[set_default_openai_api()][agents.set_default_openai_api] 関数を使って Chat Completions API を利用するように上書きできます。 ```python from agents import set_default_openai_api @@ -30,7 +30,7 @@ set_default_openai_api("chat_completions") ## トレーシング -トレーシングはデフォルトで有効になっています。デフォルトでは、上記のセクションから OpenAI API キー(環境変数または設定したデフォルトキー)を使用します。トレーシングに使用する API キーを特定に設定するには、[`set_tracing_export_api_key`][agents.set_tracing_export_api_key] 関数を使用します。 +トレーシングはデフォルトで有効になっています。デフォルトでは、上記のセクションで説明した OpenAI API キー(環境変数または設定したデフォルトキー)を使用します。トレーシング専用の API キーを設定したい場合は、[`set_tracing_export_api_key`][agents.set_tracing_export_api_key] 関数を利用してください。 ```python from agents import set_tracing_export_api_key @@ -38,7 +38,7 @@ from agents import set_tracing_export_api_key set_tracing_export_api_key("sk-...") ``` -トレーシングを完全に無効にするには、[`set_tracing_disabled()`][agents.set_tracing_disabled] 関数を使用します。 +また、[`set_tracing_disabled()`][agents.set_tracing_disabled] 関数を使ってトレーシングを完全に無効化することもできます。 ```python from agents import set_tracing_disabled @@ -48,9 +48,9 @@ set_tracing_disabled(True) ## デバッグログ -SDK にはハンドラーが設定されていない 2 つの Python ロガーがあります。デフォルトでは、警告とエラーが `stdout` に送信されますが、他のログは抑制されます。 +SDK には、ハンドラーが設定されていない 2 つの Python ロガーがあります。デフォルトでは、警告やエラーは `stdout` に送信されますが、それ以外のログは抑制されます。 -詳細なログを有効にするには、[`enable_verbose_stdout_logging()`][agents.enable_verbose_stdout_logging] 関数を使用します。 +詳細なログ出力を有効にするには、[`enable_verbose_stdout_logging()`][agents.enable_verbose_stdout_logging] 関数を使用してください。 ```python from agents import enable_verbose_stdout_logging @@ -58,7 +58,7 @@ from agents import enable_verbose_stdout_logging enable_verbose_stdout_logging() ``` -また、ハンドラー、フィルター、フォーマッターなどを追加してログをカスタマイズすることもできます。詳細は [Python ロギングガイド](https://docs.python.org/3/howto/logging.html) を参照してください。 +また、ハンドラーやフィルター、フォーマッターなどを追加してログをカスタマイズすることも可能です。詳細は [Python ロギングガイド](https://docs.python.org/3/howto/logging.html) をご覧ください。 ```python import logging @@ -77,17 +77,17 @@ logger.setLevel(logging.WARNING) logger.addHandler(logging.StreamHandler()) ``` -### ログ内の機密データ +### ログ内の機微なデータ -特定のログには機密データ(たとえば、ユーザーデータ)が含まれる場合があります。このデータのログ記録を無効にしたい場合は、次の環境変数を設定します。 +一部のログには機微なデータ(たとえば ユーザー データ)が含まれる場合があります。これらのデータのログ出力を無効にしたい場合は、以下の環境変数を設定してください。 -LLM の入力と出力のログ記録を無効にするには: +LLM の入力および出力のログ出力を無効にするには: ```bash export OPENAI_AGENTS_DONT_LOG_MODEL_DATA=1 ``` -ツールの入力と出力のログ記録を無効にするには: +ツールの入力および出力のログ出力を無効にするには: ```bash export OPENAI_AGENTS_DONT_LOG_TOOL_DATA=1 diff --git a/docs/ja/context.md b/docs/ja/context.md index 3d3c426e..4240ceee 100644 --- a/docs/ja/context.md +++ b/docs/ja/context.md @@ -1,29 +1,29 @@ # コンテキスト管理 -コンテキストは多義的な用語です。考慮すべき主なコンテキストのクラスは2つあります。 +コンテキストは多義的な用語です。主に関心を持つべきコンテキストには、次の 2 つの大きなクラスがあります。 -1. コードにローカルで利用可能なコンテキスト: これは、ツール関数が実行されるときや、`on_handoff` のようなコールバック、ライフサイクルフックなどで必要になるデータや依存関係です。 -2. LLM に利用可能なコンテキスト: これは、LLM が応答を生成するときに参照するデータです。 +1. コード内でローカルに利用可能なコンテキスト:これは、ツール関数の実行時や `on_handoff` のようなコールバック、ライフサイクルフックなどで必要となるデータや依存関係です。 +2. LLM に利用可能なコンテキスト:これは、LLM がレスポンスを生成する際に参照できるデータです。 ## ローカルコンテキスト -これは [`RunContextWrapper`][agents.run_context.RunContextWrapper] クラスとその中の [`context`][agents.run_context.RunContextWrapper.context] プロパティを通じて表現されます。動作の仕組みは次の通りです。 +これは [`RunContextWrapper`][agents.run_context.RunContextWrapper] クラスおよびその中の [`context`][agents.run_context.RunContextWrapper.context] プロパティによって表現されます。仕組みは以下の通りです。 -1. 任意の Python オブジェクトを作成します。一般的なパターンは、データクラスや Pydantic オブジェクトを使用することです。 -2. そのオブジェクトを様々な実行メソッドに渡します(例: `Runner.run(..., **context=whatever**)`)。 -3. すべてのツール呼び出し、ライフサイクルフックなどにラッパーオブジェクト `RunContextWrapper[T]` が渡されます。ここで `T` はコンテキストオブジェクトの型を表し、`wrapper.context` を通じてアクセスできます。 +1. 任意の Python オブジェクトを作成します。一般的なパターンとしては、dataclass や Pydantic オブジェクトを使います。 +2. そのオブジェクトを各種 run メソッド(例:`Runner.run(..., **context=whatever**))`)に渡します。 +3. すべてのツール呼び出しやライフサイクルフックなどには、ラッパーオブジェクト `RunContextWrapper[T]` が渡されます。ここで `T` はコンテキストオブジェクトの型を表し、`wrapper.context` からアクセスできます。 -**最も重要な**点は、特定のエージェント実行において、すべてのエージェント、ツール関数、ライフサイクルなどが同じ _型_ のコンテキストを使用しなければならないことです。 +**最も重要**な注意点:特定のエージェント実行において、すべてのエージェント、ツール関数、ライフサイクルなどは、同じ _型_ のコンテキストを使用する必要があります。 -コンテキストは以下のような用途に使用できます。 +コンテキストは以下のような用途で利用できます。 -- 実行のためのコンテキストデータ(例: ユーザー名/uid やその他のユーザー情報) -- 依存関係(例: ロガーオブジェクト、データフェッチャーなど) -- ヘルパー関数 +- 実行時のコンテキストデータ(例:ユーザー名/uid やユーザーに関するその他の情報など) +- 依存関係(例:ロガーオブジェクト、データフェッチャーなど) +- ヘルパー関数 !!! danger "注意" - コンテキストオブジェクトは **LLM に送信されません**。これは純粋にローカルなオブジェクトであり、読み書きやメソッドの呼び出しが可能です。 + コンテキストオブジェクトは **LLM には送信されません**。これは純粋にローカルなオブジェクトであり、読み書きやメソッド呼び出しが可能です。 ```python import asyncio @@ -61,17 +61,17 @@ if __name__ == "__main__": asyncio.run(main()) ``` -1. これはコンテキストオブジェクトです。ここではデータクラスを使用していますが、任意の型を使用できます。 -2. これはツールです。`RunContextWrapper[UserInfo]` を受け取ることがわかります。ツールの実装はコンテキストから読み取ります。 -3. エージェントをジェネリック `UserInfo` でマークし、型チェッカーがエラーを検出できるようにします(例えば、異なるコンテキスト型を取るツールを渡そうとした場合)。 +1. これはコンテキストオブジェクトです。ここでは dataclass を使用していますが、任意の型を利用できます。 +2. これはツールです。`RunContextWrapper[UserInfo]` を受け取っていることが分かります。ツールの実装はコンテキストから値を読み取ります。 +3. エージェントにはジェネリック型 `UserInfo` を指定しています。これにより、型チェッカーがエラーを検出できます(例えば、異なるコンテキスト型を受け取るツールを渡そうとした場合など)。 4. コンテキストは `run` 関数に渡されます。 5. エージェントは正しくツールを呼び出し、年齢を取得します。 -## エージェント/LLM コンテキスト +## エージェント/LLM コンテキスト -LLM が呼び出されるとき、**唯一** 参照できるデータは会話履歴からのものです。新しいデータを LLM に利用可能にしたい場合、それを履歴に含める方法で行う必要があります。これにはいくつかの方法があります。 +LLM が呼び出される際、**唯一** 参照できるデータは会話履歴からのものです。つまり、LLM に新しいデータを利用させたい場合は、そのデータを履歴に含める必要があります。これを実現する方法はいくつかあります。 -1. エージェントの `instructions` に追加します。これは「システムプロンプト」または「開発者メッセージ」とも呼ばれます。システムプロンプトは静的な文字列でも、コンテキストを受け取って文字列を出力する動的な関数でもかまいません。常に有用な情報(例: ユーザーの名前や現在の日付)に対して一般的な戦術です。 -2. `Runner.run` 関数を呼び出す際に `input` に追加します。これは `instructions` 戦術に似ていますが、[chain of command](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command) の下位にメッセージを持つことができます。 -3. 関数ツールを通じて公開します。これはオンデマンドのコンテキストに有用です。LLM がデータを必要とするときにツールを呼び出してそのデータを取得できます。 -4. リトリーバルや Web 検索を使用します。これらはファイルやデータベース(リトリーバル)、または Web(Web 検索)から関連データを取得できる特別なツールです。関連するコンテキストデータで応答を「グラウンド」するのに有用です。 \ No newline at end of file +1. エージェントの `instructions` に追加する。この方法は「システムプロンプト」や「開発者メッセージ」とも呼ばれます。システムプロンプトは静的な文字列でも、コンテキストを受け取って文字列を出力する動的な関数でも構いません。たとえば、ユーザー名や現在の日付など、常に有用な情報に適しています。 +2. `Runner.run` 関数を呼び出す際に `input` に追加する。この方法は `instructions` と似ていますが、[chain of command](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command) の下位メッセージとして追加できます。 +3. 関数ツールを通じて公開する。この方法は _オンデマンド_ のコンテキストに適しています。LLM が必要なタイミングでツールを呼び出し、データを取得できます。 +4. リトリーバルや Web 検索を利用する。これらはファイルやデータベース(リトリーバル)、または Web(Web 検索)から関連データを取得できる特別なツールです。関連するコンテキストデータに基づいたレスポンスを「グラウンディング」するのに役立ちます。 \ No newline at end of file diff --git a/docs/ja/examples.md b/docs/ja/examples.md index 21a00f67..c2393c8e 100644 --- a/docs/ja/examples.md +++ b/docs/ja/examples.md @@ -1,40 +1,40 @@ -# サンプルコード +# コード例 -SDK のさまざまなサンプル実装は、[リポジトリ](https://github.com/openai/openai-agents-python/tree/main/examples)のサンプルコードセクションで確認できます。これらのサンプルコードは、異なるパターンや機能を示すいくつかのカテゴリーに整理されています。 +SDK のさまざまなサンプル実装については、[リポジトリ](https://github.com/openai/openai-agents-python/tree/main/examples) のコード例セクションをご覧ください。これらのコード例は、異なるパターンや機能を示すいくつかのカテゴリーに整理されています。 ## カテゴリー -- **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - このカテゴリーの例は、一般的なエージェント設計パターンを示しています。例えば、 +- **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** + このカテゴリーのコード例では、よく使われるエージェント設計パターンを紹介しています。 - - 決定論的ワークフロー + - 決定論的なワークフロー - ツールとしてのエージェント - エージェントの並列実行 -- **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** - これらの例は、SDK の基本的な機能を紹介しています。例えば、 +- **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** + これらのコード例では、SDK の基本的な機能を紹介しています。 - 動的なシステムプロンプト - ストリーミング出力 - ライフサイクルイベント -- **[tool examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** - Web 検索やファイル検索などの OpenAI がホストするツールを実装し、それらをエージェントに統合する方法を学びます。 +- **[tool examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** + OpenAI がホストするツール(Web 検索やファイル検索など)の実装方法や、それらをエージェントに統合する方法を学べます。 -- **[model providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - OpenAI 以外のモデルを SDK で使用する方法を探ります。 +- **[model providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** + OpenAI 以外のモデルを SDK で利用する方法を紹介しています。 -- **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - エージェントのハンドオフの実用的な例を見てみましょう。 +- **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** + エージェントのハンドオフの実践的なコード例をご覧いただけます。 -- **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - MCP を使用してエージェントを構築する方法を学びます。 +- **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** + Model context protocol (MCP) を使ったエージェントの構築方法を学べます。 -- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** と **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 実際のアプリケーションを示す、さらに詳細な2つの例 +- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** および **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** + 実際のユースケースを示す、より発展的な 2 つのコード例です。 - - **customer_service**: 航空会社向けのカスタマーサービスシステムの例。 + - **customer_service**: 航空会社向けカスタマーサービスシステムの例。 - **research_bot**: シンプルなディープリサーチクローン。 -- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** - TTS と STT モデルを使用した音声エージェントの例を見てみましょう。 \ No newline at end of file +- **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** + TTS および STT モデルを利用した音声エージェントのコード例をご覧いただけます。 \ No newline at end of file diff --git a/docs/ja/guardrails.md b/docs/ja/guardrails.md index da38d432..2525f8c2 100644 --- a/docs/ja/guardrails.md +++ b/docs/ja/guardrails.md @@ -1,43 +1,43 @@ # ガードレール -ガードレールはエージェントと _並行して_ 実行され、ユーザー入力のチェックと検証を可能にします。例えば、非常に賢い(したがって遅く/高価な)モデルを使用して顧客のリクエストを支援するエージェントがあるとします。悪意のあるユーザーがモデルに数学の宿題を手伝わせるようなことは避けたいでしょう。そこで、速く/安価なモデルでガードレールを実行できます。ガードレールが悪意のある使用を検出した場合、即座にエラーを発生させ、高価なモデルの実行を停止し、時間とお金を節約できます。 +ガードレールは、エージェントと _並行して_ 実行され、ユーザー入力のチェックやバリデーションを行うことができます。例えば、非常に賢い(そのため遅くて高価な)モデルを使ってカスタマーリクエストに対応するエージェントがあるとします。悪意のあるユーザーがモデルに数学の宿題を手伝わせるようなリクエストを送ることは避けたいでしょう。そこで、ガードレールを高速かつ安価なモデルで実行できます。ガードレールが悪意のある利用を検知した場合、即座にエラーを発生させ、高価なモデルの実行を止めて時間やコストを節約できます。 -ガードレールには2種類あります: +ガードレールには 2 種類あります: -1. 入力ガードレールは初期のユーザー入力に対して実行されます -2. 出力ガードレールは最終的なエージェントの出力に対して実行されます +1. 入力ガードレール:最初のユーザー入力に対して実行されます +2. 出力ガードレール:最終的なエージェント出力に対して実行されます ## 入力ガードレール -入力ガードレールは3つのステップで実行されます: +入力ガードレールは 3 ステップで実行されます: -1. まず、ガードレールはエージェントに渡されたのと同じ入力を受け取ります。 -2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput]を生成し、それが[`InputGuardrailResult`][agents.guardrail.InputGuardrailResult]でラップされます。 -3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered]が true かどうかを確認します。true の場合、[`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered]例外が発生し、ユーザーに適切に応答したり、例外を処理したりできます。 +1. まず、ガードレールはエージェントに渡されたものと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`InputGuardrailResult`][agents.guardrail.InputGuardrailResult] でラップされます。 +3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な対応や例外処理が可能です。 !!! Note - 入力ガードレールはユーザー入力に対して実行されることを意図しているため、エージェントのガードレールはエージェントが*最初の*エージェントである場合にのみ実行されます。なぜ `guardrails` プロパティがエージェントにあり、`Runner.run` に渡されないのか疑問に思うかもしれません。それは、ガードレールが実際のエージェントに関連している傾向があるためです。異なるエージェントには異なるガードレールを実行するので、コードを一緒に配置することは可読性に役立ちます。 + 入力ガードレールはユーザー入力に対して実行されることを想定しているため、エージェントのガードレールは *最初* のエージェントでのみ実行されます。「なぜ `guardrails` プロパティがエージェントにあり、`Runner.run` に渡さないのか?」と疑問に思うかもしれません。これは、ガードレールが実際のエージェントに関連することが多いためです。異なるエージェントごとに異なるガードレールを実行するため、コードを同じ場所にまとめておくと可読性が向上します。 ## 出力ガードレール -出力ガードレールは3つのステップで実行されます: +出力ガードレールも 3 ステップで実行されます: -1. まず、ガードレールはエージェントに渡されたのと同じ入力を受け取ります。 -2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput]を生成し、それが[`OutputGuardrailResult`][agents.guardrail.OutputGuardrailResult]でラップされます。 -3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered]が true かどうかを確認します。true の場合、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]例外が発生し、ユーザーに適切に応答したり、例外を処理したりできます。 +1. まず、ガードレールはエージェントに渡されたものと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`OutputGuardrailResult`][agents.guardrail.OutputGuardrailResult] でラップされます。 +3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な対応や例外処理が可能です。 !!! Note - 出力ガードレールは最終的なエージェントの出力に対して実行されることを意図しているため、エージェントのガードレールはエージェントが*最後の*エージェントである場合にのみ実行されます。入力ガードレールと同様に、ガードレールが実際のエージェントに関連している傾向があるため、コードを一緒に配置することは可読性に役立ちます。 + 出力ガードレールは最終的なエージェント出力に対して実行されることを想定しているため、エージェントのガードレールは *最後* のエージェントでのみ実行されます。入力ガードレールと同様に、ガードレールが実際のエージェントに関連することが多いため、コードを同じ場所にまとめておくと可読性が向上します。 ## トリップワイヤー -入力または出力がガードレールに失敗した場合、ガードレールはトリップワイヤーでこれを知らせることができます。トリップワイヤーがトリガーされたガードレールを確認するとすぐに、`{Input,Output}GuardrailTripwireTriggered`例外を即座に発生させ、エージェントの実行を停止します。 +入力または出力がガードレールに失敗した場合、ガードレールはトリップワイヤーでこれを通知できます。トリップワイヤーが発動したガードレールを検知した時点で、即座に `{Input,Output}GuardrailTripwireTriggered` 例外を発生させ、エージェントの実行を停止します。 ## ガードレールの実装 -入力を受け取り、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput]を返す関数を提供する必要があります。この例では、エージェントを内部で実行することでこれを行います。 +入力を受け取り、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を返す関数を用意する必要があります。この例では、内部でエージェントを実行することでこれを実現します。 ```python from pydantic import BaseModel @@ -90,8 +90,8 @@ async def main(): print("Math homework guardrail tripped") ``` -1. このエージェントをガードレール関数で使用します。 -2. これはエージェントの入力/コンテキストを受け取り、結果を返すガードレール関数です。 +1. このエージェントをガードレール関数内で使用します。 +2. これはエージェントの入力やコンテキストを受け取り、結果を返すガードレール関数です。 3. ガードレールの結果に追加情報を含めることができます。 4. これはワークフローを定義する実際のエージェントです。 @@ -148,7 +148,7 @@ async def main(): print("Math output guardrail tripped") ``` -1. これは実際のエージェントの出力タイプです。 -2. これはガードレールの出力タイプです。 +1. これは実際のエージェントの出力型です。 +2. これはガードレールの出力型です。 3. これはエージェントの出力を受け取り、結果を返すガードレール関数です。 4. これはワークフローを定義する実際のエージェントです。 \ No newline at end of file diff --git a/docs/ja/handoffs.md b/docs/ja/handoffs.md index 575195b1..ae3b8b12 100644 --- a/docs/ja/handoffs.md +++ b/docs/ja/handoffs.md @@ -1,18 +1,18 @@ # ハンドオフ -ハンドオフは、エージェントがタスクを他のエージェントに委任することを可能にします。これは、異なるエージェントが異なる分野に特化しているシナリオで特に有用です。例えば、カスタマーサポートアプリでは、注文状況、返金、FAQ などのタスクをそれぞれ専門に扱うエージェントがいるかもしれません。 +ハンドオフは、エージェントが他のエージェントにタスクを委任できる仕組みです。これは、異なるエージェントがそれぞれ異なる分野に特化しているシナリオで特に有用です。たとえば、カスタマーサポートアプリでは、注文状況、返金、FAQ などのタスクをそれぞれ専門に扱うエージェントが存在する場合があります。 -ハンドオフは LLM にとってツールとして表現されます。したがって、`Refund Agent` という名前のエージェントへのハンドオフがある場合、ツールは `transfer_to_refund_agent` と呼ばれます。 +ハンドオフは LLM からはツールとして認識されます。たとえば、`Refund Agent` という名前のエージェントへのハンドオフがある場合、そのツール名は `transfer_to_refund_agent` となります。 ## ハンドオフの作成 -すべてのエージェントには [`handoffs`][agents.agent.Agent.handoffs] パラメーターがあり、これは `Agent` を直接取るか、ハンドオフをカスタマイズする `Handoff` オブジェクトを取ることができます。 +すべてのエージェントは [`handoffs`][agents.agent.Agent.handoffs] パラメーターを持っており、これは直接 `Agent` を指定することも、ハンドオフをカスタマイズする `Handoff` オブジェクトを指定することもできます。 -Agents SDK が提供する [`handoff()`][agents.handoffs.handoff] 関数を使用してハンドオフを作成できます。この関数を使用すると、ハンドオフ先のエージェントを指定し、オプションでオーバーライドや入力フィルターを設定できます。 +Agents SDK で提供されている [`handoff()`][agents.handoffs.handoff] 関数を使ってハンドオフを作成できます。この関数では、ハンドオフ先のエージェントや、オプションのオーバーライドや入力フィルターを指定できます。 ### 基本的な使い方 -簡単なハンドオフを作成する方法は次のとおりです。 +シンプルなハンドオフの作成方法は以下の通りです。 ```python from agents import Agent, handoff @@ -24,18 +24,18 @@ refund_agent = Agent(name="Refund agent") triage_agent = Agent(name="Triage agent", handoffs=[billing_agent, handoff(refund_agent)]) ``` -1. エージェントを直接使用することも(`billing_agent` のように)、`handoff()` 関数を使用することもできます。 +1. エージェントを直接指定する(例:`billing_agent`)ことも、`handoff()` 関数を使うこともできます。 ### `handoff()` 関数によるハンドオフのカスタマイズ -[`handoff()`][agents.handoffs.handoff] 関数を使用すると、さまざまなカスタマイズが可能です。 +[`handoff()`][agents.handoffs.handoff] 関数では、さまざまなカスタマイズが可能です。 - `agent`: ハンドオフ先のエージェントです。 -- `tool_name_override`: デフォルトでは `Handoff.default_tool_name()` 関数が使用され、`transfer_to_` に解決されます。これをオーバーライドできます。 -- `tool_description_override`: `Handoff.default_tool_description()` からのデフォルトのツール説明をオーバーライドします。 -- `on_handoff`: ハンドオフが呼び出されたときに実行されるコールバック関数です。ハンドオフが呼び出されるとすぐにデータ取得を開始するなどに役立ちます。この関数はエージェントコンテキストを受け取り、オプションで LLM が生成した入力も受け取ることができます。入力データは `input_type` パラメーターで制御されます。 -- `input_type`: ハンドオフが期待する入力のタイプ(オプション)。 -- `input_filter`: 次のエージェントが受け取る入力をフィルタリングできます。詳細は以下を参照してください。 +- `tool_name_override`: デフォルトでは `Handoff.default_tool_name()` 関数が使われ、`transfer_to_` となります。これを上書きできます。 +- `tool_description_override`: デフォルトのツール説明(`Handoff.default_tool_description()`)を上書きできます。 +- `on_handoff`: ハンドオフが呼び出されたときに実行されるコールバック関数です。たとえば、ハンドオフが呼び出されたタイミングでデータ取得を開始するなどに便利です。この関数はエージェントコンテキストを受け取り、オプションで LLM が生成した入力も受け取れます。入力データは `input_type` パラメーターで制御します。 +- `input_type`: ハンドオフで期待される入力の型(オプション)です。 +- `input_filter`: 次のエージェントが受け取る入力をフィルタリングできます。詳細は下記をご覧ください。 ```python from agents import Agent, handoff, RunContextWrapper @@ -55,7 +55,7 @@ handoff_obj = handoff( ## ハンドオフ入力 -特定の状況では、LLM がハンドオフを呼び出す際にデータを提供することを望む場合があります。例えば、「エスカレーションエージェント」へのハンドオフを想像してください。理由を提供してログに記録したいかもしれません。 +状況によっては、LLM にハンドオフ時に何らかのデータを提供してほしい場合があります。たとえば、「エスカレーションエージェント」へのハンドオフを考えてみましょう。この場合、理由を提供して記録できるようにしたいことがあります。 ```python from pydantic import BaseModel @@ -79,9 +79,9 @@ handoff_obj = handoff( ## 入力フィルター -ハンドオフが発生すると、新しいエージェントが会話を引き継ぎ、以前の会話履歴全体を見ることができます。これを変更したい場合は、[`input_filter`][agents.handoffs.Handoff.input_filter] を設定できます。入力フィルターは、既存の入力を [`HandoffInputData`][agents.handoffs.HandoffInputData] 経由で受け取り、新しい `HandoffInputData` を返す必要がある関数です。 +ハンドオフが発生すると、新しいエージェントが会話を引き継ぎ、これまでの会話履歴全体を見ることができます。これを変更したい場合は、[`input_filter`][agents.handoffs.Handoff.input_filter] を設定できます。入力フィルターは、[`HandoffInputData`][agents.handoffs.HandoffInputData] を受け取り、新しい `HandoffInputData` を返す関数です。 -一般的なパターン(例えば、履歴からすべてのツール呼び出しを削除する)が [`agents.extensions.handoff_filters`][] に実装されています。 +よくあるパターン(たとえば履歴からすべてのツール呼び出しを削除するなど)は、[`agents.extensions.handoff_filters`][] で実装されています。 ```python from agents import Agent, handoff @@ -99,7 +99,7 @@ handoff_obj = handoff( ## 推奨プロンプト -LLM がハンドオフを正しく理解するために、エージェントにハンドオフに関する情報を含めることをお勧めします。[`agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`][] に推奨されるプレフィックスがあります。または、[`agents.extensions.handoff_prompt.prompt_with_handoff_instructions`][] を呼び出して、プロンプトに推奨データを自動的に追加できます。 +LLM がハンドオフを正しく理解できるようにするため、エージェントにハンドオフに関する情報を含めることを推奨します。[`agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`][] で推奨されるプレフィックスを利用するか、[`agents.extensions.handoff_prompt.prompt_with_handoff_instructions`][] を呼び出して、推奨データをプロンプトに自動追加できます。 ```python from agents import Agent diff --git a/docs/ja/index.md b/docs/ja/index.md index f36de0ac..3b306464 100644 --- a/docs/ja/index.md +++ b/docs/ja/index.md @@ -1,28 +1,28 @@ -# OpenAI エージェント SDK +# OpenAI Agents SDK -[OpenAI エージェント SDK](https://github.com/openai/openai-agents-python) は、軽量で使いやすいパッケージでエージェント AI アプリを構築することを可能にします。これは、以前のエージェントの実験である [Swarm](https://github.com/openai/swarm/tree/main) のプロダクション対応のアップグレード版です。エージェント SDK には、非常に少ない基本コンポーネントがあります。 +[OpenAI Agents SDK](https://github.com/openai/openai-agents-python) は、非常に少ない抽象化で、軽量かつ使いやすいパッケージで エージェント 型 AI アプリを構築できる SDK です。これは、以前のエージェント向け実験プロジェクト [Swarm](https://github.com/openai/swarm/tree/main) の本番運用向けアップグレード版です。Agents SDK には、非常に少数の基本コンポーネントが含まれています。 -- **エージェント**: instructions と tools を備えた LLM -- **ハンドオフ**: エージェントが特定のタスクを他のエージェントに委任することを可能にします -- **ガードレール**: エージェントへの入力を検証することを可能にします +- **エージェント**: instructions と tools を備えた LLM +- **ハンドオフ**:特定のタスクを他のエージェントに委任できる仕組み +- **ガードレール**:エージェントへの入力を検証できる仕組み -これらの基本コンポーネントは Python と組み合わせることで、ツールとエージェント間の複雑な関係を表現するのに十分な強力さを持ち、急な学習曲線なしで実際のアプリケーションを構築することができます。さらに、SDK には組み込みの **トレーシング** があり、エージェントのフローを視覚化してデバッグし、評価し、さらにはアプリケーション用にモデルを微調整することができます。 +Python と組み合わせることで、これらの基本コンポーネントは tools と エージェント 間の複雑な関係を表現でき、急な学習コストなしに実用的なアプリケーションを構築できます。さらに、SDK には組み込みの **トレーシング** 機能があり、エージェント フローの可視化やデバッグ、評価、さらにはアプリケーション向けモデルのファインチューニングも可能です。 -## なぜエージェント SDK を使うのか +## なぜ Agents SDK を使うのか SDK には 2 つの設計原則があります。 -1. 使用する価値があるだけの機能を持ち、学習が迅速になるように基本コンポーネントを少なくする。 -2. すぐに使えるが、何が起こるかを正確にカスタマイズできる。 +1. 使う価値のある十分な機能を持ちつつ、学習が容易なほど少ない基本コンポーネントで構成されていること。 +2. すぐに使い始められるが、動作を細かくカスタマイズできること。 -SDK の主な機能は次のとおりです。 +SDK の主な特徴は以下の通りです。 -- エージェントループ: ツールの呼び出し、LLM への結果の送信、LLM が完了するまでのループを処理する組み込みのエージェントループ。 -- Python ファースト: 新しい抽象化を学ぶ必要なく、組み込みの言語機能を使用してエージェントを編成し、連鎖させる。 -- ハンドオフ: 複数のエージェント間で調整し、委任するための強力な機能。 -- ガードレール: エージェントと並行して入力検証とチェックを実行し、チェックが失敗した場合は早期に中断。 -- 関数ツール: 任意の Python 関数をツールに変換し、自動スキーマ生成と Pydantic による検証を行う。 -- トレーシング: ワークフローを視覚化、デバッグ、監視するための組み込みのトレーシング、および OpenAI の評価、微調整、蒸留ツールを使用。 +- エージェントループ: tools の呼び出し、LLM への実行結果の送信、LLM が完了するまでのループ処理を自動で行います。 +- Python ファースト:新しい抽象化を学ぶ必要なく、Python の言語機能でエージェントのオーケストレーションや連携が可能です。 +- ハンドオフ:複数のエージェント間での調整や委任を実現する強力な機能です。 +- ガードレール:エージェントと並行して入力検証やチェックを実行し、チェックに失敗した場合は早期に処理を中断します。 +- 関数ツール:任意の Python 関数を自動でスキーマ生成・Pydantic ベースのバリデーション付きツールに変換できます。 +- トレーシング:組み込みのトレーシングでワークフローの可視化・デバッグ・モニタリングができ、OpenAI の評価・ファインチューニング・蒸留ツールも利用可能です。 ## インストール @@ -30,7 +30,7 @@ SDK の主な機能は次のとおりです。 pip install openai-agents ``` -## Hello World の例 +## Hello World サンプル ```python from agents import Agent, Runner @@ -45,7 +45,7 @@ print(result.final_output) # Infinite loop's dance. ``` -(_これを実行する場合は、`OPENAI_API_KEY` 環境変数を設定してください_) +(_実行する場合は、`OPENAI_API_KEY` 環境変数を設定してください_) ```bash export OPENAI_API_KEY=sk-... diff --git a/docs/ja/mcp.md b/docs/ja/mcp.md index 057fe188..d93252fb 100644 --- a/docs/ja/mcp.md +++ b/docs/ja/mcp.md @@ -1,21 +1,21 @@ # Model context protocol (MCP) -[Model context protocol](https://modelcontextprotocol.io/introduction) (別名 MCP) は、LLM にツールとコンテキストを提供する方法です。MCP ドキュメントからの引用: +[Model context protocol](https://modelcontextprotocol.io/introduction)(通称 MCP)は、 LLM にツールやコンテキストを提供するための方法です。MCP ドキュメントからの引用です: -> MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンプロトコルです。MCP を AI アプリケーションのための USB-C ポートのように考えてください。USB-C がデバイスをさまざまな周辺機器やアクセサリーに接続する標準化された方法を提供するのと同様に、MCP は AI モデルをさまざまなデータソースやツールに接続する標準化された方法を提供します。 +> MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンプロトコルです。MCP を AI アプリケーションのための USB-C ポートのようなものと考えてください。USB-C がさまざまな周辺機器やアクセサリにデバイスを接続する標準的な方法を提供するのと同様に、MCP は AI モデルをさまざまなデータソースやツールに接続する標準的な方法を提供します。 -エージェント SDK は MCP をサポートしています。これにより、エージェントにツールを提供するために幅広い MCP サーバーを使用できます。 +Agents SDK は MCP をサポートしています。これにより、幅広い MCP サーバーを利用して、エージェントにツールを提供することができます。 ## MCP サーバー -現在、MCP 仕様は使用するトランスポートメカニズムに基づいて2種類のサーバーを定義しています: +現在、MCP 仕様では、使用するトランスポートメカニズムに基づいて 2 種類のサーバーが定義されています: -1. **stdio** サーバーは、アプリケーションのサブプロセスとして実行されます。これらは「ローカルで」実行されると考えることができます。 +1. **stdio** サーバーは、アプリケーションのサブプロセスとして実行されます。ローカルで実行されるものと考えることができます。 2. **HTTP over SSE** サーバーはリモートで実行されます。URL を介して接続します。 -これらのサーバーに接続するには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] クラスを使用できます。 +これらのサーバーに接続するには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] および [`MCPServerSse`][agents.mcp.server.MCPServerSse] クラスを使用できます。 -例えば、[公式 MCP ファイルシステムサーバー](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem) を使用する方法は次のとおりです。 +例えば、[公式 MCP ファイルシステムサーバー](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem) を使用する場合は、次のようになります。 ```python async with MCPServerStdio( @@ -27,9 +27,9 @@ async with MCPServerStdio( tools = await server.list_tools() ``` -## MCP サーバーの使用 +## MCP サーバーの利用 -MCP サーバーはエージェントに追加できます。エージェント SDK は、エージェントが実行されるたびに MCP サーバーで `list_tools()` を呼び出します。これにより、LLM は MCP サーバーのツールを認識します。LLM が MCP サーバーのツールを呼び出すと、SDK はそのサーバーで `call_tool()` を呼び出します。 +MCP サーバーはエージェントに追加できます。Agents SDK は、エージェントが実行されるたびに MCP サーバーの `list_tools()` を呼び出します。これにより、LLM は MCP サーバーのツールを認識できるようになります。LLM が MCP サーバーのツールを呼び出すと、SDK はそのサーバーの `call_tool()` を呼び出します。 ```python @@ -40,21 +40,21 @@ agent=Agent( ) ``` -## キャッシング +## キャッシュ -エージェントが実行されるたびに、MCP サーバーで `list_tools()` を呼び出します。これは、特にサーバーがリモートサーバーの場合、レイテンシーの影響を受ける可能性があります。ツールのリストを自動的にキャッシュするには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] に `cache_tools_list=True` を渡すことができます。ツールリストが変更されないことが確実な場合にのみこれを行うべきです。 +エージェントが実行されるたびに、MCP サーバーの `list_tools()` が呼び出されます。特にサーバーがリモートの場合、これはレイテンシの原因となることがあります。ツールリストを自動的にキャッシュするには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] および [`MCPServerSse`][agents.mcp.server.MCPServerSse] の両方に `cache_tools_list=True` を渡すことができます。ツールリストが変更されないことが確実な場合のみ、この設定を行ってください。 -キャッシュを無効にしたい場合は、サーバーで `invalidate_tools_cache()` を呼び出すことができます。 +キャッシュを無効化したい場合は、サーバーで `invalidate_tools_cache()` を呼び出すことができます。 -## エンドツーエンドの例 +## エンドツーエンドの code examples -[examples/mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp) で完全な動作例を確認してください。 +[examples/mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp) で、完全な動作 code examples をご覧いただけます。 ## トレーシング -[トレーシング](./tracing.md) は、MCP 操作を自動的にキャプチャします。これには以下が含まれます: +[トレーシング](../tracing.md) は、以下を含む MCP の操作を自動的に記録します: -1. ツールをリストするための MCP サーバーへの呼び出し +1. MCP サーバーへのツールリスト取得の呼び出し 2. 関数呼び出しに関する MCP 関連情報 -![MCP Tracing Screenshot](../assets/images/mcp-tracing.jpg) \ No newline at end of file +![MCP トレーシングのスクリーンショット](../assets/images/mcp-tracing.jpg) \ No newline at end of file diff --git a/docs/ja/models.md b/docs/ja/models.md index 3d216ac3..5a76d60e 100644 --- a/docs/ja/models.md +++ b/docs/ja/models.md @@ -1,21 +1,21 @@ # モデル -Agents SDK は、OpenAI モデルを次の 2 つの形式でサポートしています。 +Agents SDK には、OpenAI モデルの 2 種類のサポートが標準で用意されています。 -- **推奨**: 新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使用して OpenAI API を呼び出す [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]。 -- [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使用して OpenAI API を呼び出す [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。 +- **推奨**: [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] は、新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使って OpenAI API を呼び出します。 +- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] は、[Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使って OpenAI API を呼び出します。 ## モデルの組み合わせ -1 つのワークフロー内で、各エージェントに異なるモデルを使用したい場合があります。たとえば、トリアージには小さくて高速なモデルを使用し、複雑なタスクにはより大きくて高機能なモデルを使用することができます。[`Agent`][agents.Agent] を設定する際に、次の方法で特定のモデルを選択できます。 +1 つのワークフロー内で、各エージェントごとに異なるモデルを使いたい場合があります。たとえば、トリアージには小型で高速なモデルを使い、複雑なタスクにはより大きく高性能なモデルを使うことができます。[`Agent`][agents.Agent] を設定する際、以下のいずれかの方法で特定のモデルを選択できます。 -1. OpenAI モデルの名前を渡す。 -2. モデル名とその名前をモデルインスタンスにマッピングできる [`ModelProvider`][agents.models.interface.ModelProvider] を渡す。 -3. 直接 [`Model`][agents.models.interface.Model] 実装を提供する。 +1. OpenAI モデル名を直接渡す。 +2. 任意のモデル名と、その名前を Model インスタンスにマッピングできる [`ModelProvider`][agents.models.interface.ModelProvider] を渡す。 +3. [`Model`][agents.models.interface.Model] 実装を直接指定する。 !!!note - SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形状をサポートしていますが、各ワークフローに対して単一のモデル形状を使用することをお勧めします。2 つの形状は異なる機能とツールをサポートしているためです。ワークフローでモデル形状を組み合わせる必要がある場合は、使用しているすべての機能が両方で利用可能であることを確認してください。 + SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形状をサポートしていますが、各ワークフローで 1 つのモデル形状のみを使うことを推奨します。なぜなら、2 つの形状はサポートする機能やツールが異なるためです。ワークフローでモデル形状を組み合わせて使う場合は、利用するすべての機能が両方で利用可能かご確認ください。 ```python from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel @@ -48,10 +48,10 @@ async def main(): print(result.final_output) ``` -1. OpenAI モデルの名前を直接設定します。 -2. [`Model`][agents.models.interface.Model] 実装を提供します。 +1. OpenAI モデル名を直接設定します。 +2. [`Model`][agents.models.interface.Model] 実装を指定します。 -エージェントに使用するモデルをさらに設定したい場合は、`temperature` などのオプションのモデル設定パラメーターを提供する [`ModelSettings`][agents.models.interface.ModelSettings] を渡すことができます。 +エージェントで使用するモデルをさらに細かく設定したい場合は、[`ModelSettings`][agents.models.interface.ModelSettings] を渡すことができます。これにより、temperature などのオプションのモデル設定パラメーターを指定できます。 ```python from agents import Agent, ModelSettings @@ -64,43 +64,43 @@ english_agent = Agent( ) ``` -## 他の LLM プロバイダーの使用 +## 他の LLM プロバイダーの利用 -他の LLM プロバイダーを 3 つの方法で使用できます(例は[こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/))。 +他の LLM プロバイダーは、3 つの方法で利用できます([こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) に code examples があります)。 -1. [`set_default_openai_client`][agents.set_default_openai_client] は、`AsyncOpenAI` のインスタンスを LLM クライアントとしてグローバルに使用したい場合に便利です。LLM プロバイダーが OpenAI 互換の API エンドポイントを持っている場合に、`base_url` と `api_key` を設定できます。[examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) に設定可能な例があります。 -2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルで使用します。これにより、「この実行のすべてのエージェントにカスタムモデルプロバイダーを使用する」と指定できます。[examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) に設定可能な例があります。 -3. [`Agent.model`][agents.agent.Agent.model] では、特定のエージェントインスタンスにモデルを指定できます。これにより、異なるエージェントに対して異なるプロバイダーを組み合わせて使用することができます。[examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) に設定可能な例があります。 +1. [`set_default_openai_client`][agents.set_default_openai_client] は、`AsyncOpenAI` のインスタンスを LLM クライアントとしてグローバルに利用したい場合に便利です。これは、LLM プロバイダーが OpenAI 互換の API エンドポイントを持ち、`base_url` と `api_key` を設定できる場合に使います。[examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) に設定例があります。 +2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルで利用します。これにより、「この実行のすべてのエージェントでカスタムモデルプロバイダーを使う」と指定できます。[examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) に設定例があります。 +3. [`Agent.model`][agents.agent.Agent.model] で、特定のエージェントインスタンスにモデルを指定できます。これにより、エージェントごとに異なるプロバイダーを組み合わせて使うことができます。[examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) に設定例があります。 -`platform.openai.com` からの API キーがない場合は、`set_tracing_disabled()` を使用してトレーシングを無効にするか、[異なるトレーシングプロセッサー](tracing.md)を設定することをお勧めします。 +`platform.openai.com` の API キーがない場合は、`set_tracing_disabled()` でトレーシングを無効にするか、[別のトレーシングプロセッサー](tracing.md) を設定することを推奨します。 !!! note - これらの例では、Chat Completions API/モデルを使用しています。なぜなら、ほとんどの LLM プロバイダーはまだ Responses API をサポートしていないからです。LLM プロバイダーがそれをサポートしている場合は、Responses を使用することをお勧めします。 + これらの code examples では Chat Completions API/モデルを使っています。なぜなら、ほとんどの LLM プロバイダーはまだ Responses API をサポートしていないためです。もし LLM プロバイダーが Responses API をサポートしている場合は、Responses の利用を推奨します。 -## 他の LLM プロバイダーを使用する際の一般的な問題 +## 他の LLM プロバイダー利用時のよくある問題 -### トレーシングクライアントエラー 401 +### Tracing クライアントの 401 エラー -トレーシングに関連するエラーが発生した場合、これはトレースが OpenAI サーバーにアップロードされ、OpenAI API キーを持っていないためです。これを解決するには、次の 3 つのオプションがあります。 +トレーシングに関連するエラーが発生した場合、これはトレースが OpenAI サーバーにアップロードされるため、OpenAI API キーがないことが原因です。解決方法は 3 つあります。 -1. トレーシングを完全に無効にする: [`set_tracing_disabled(True)`][agents.set_tracing_disabled]。 -2. トレーシング用の OpenAI キーを設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースのアップロードにのみ使用され、[platform.openai.com](https://platform.openai.com/) からのものでなければなりません。 -3. 非 OpenAI トレースプロセッサーを使用する。[トレーシングドキュメント](tracing.md#custom-tracing-processors)を参照してください。 +1. トレーシングを完全に無効化する: [`set_tracing_disabled(True)`][agents.set_tracing_disabled]。 +2. トレーシング用の OpenAI キーを設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースのアップロードのみに使われ、[platform.openai.com](https://platform.openai.com/) のものが必要です。 +3. OpenAI 以外のトレースプロセッサーを使う。[トレーシングのドキュメント](tracing.md#custom-tracing-processors) をご覧ください。 ### Responses API サポート -SDK はデフォルトで Responses API を使用しますが、ほとんどの他の LLM プロバイダーはまだサポートしていません。その結果、404 エラーや類似の問題が発生することがあります。これを解決するには、次の 2 つのオプションがあります。 +SDK はデフォルトで Responses API を使いますが、ほとんどの他の LLM プロバイダーはまだ対応していません。そのため、404 エラーなどが発生する場合があります。解決方法は 2 つあります。 -1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出します。これは、環境変数を介して `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に機能します。 -2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使用します。例は[こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)にあります。 +1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出します。これは、環境変数で `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に有効です。 +2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使います。[こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) に code examples があります。 -### 適切な形式のデータのサポート +### structured outputs サポート -一部のモデルプロバイダーは、[適切な形式のデータ](https://platform.openai.com/docs/guides/structured-outputs)をサポートしていません。これにより、次のようなエラーが発生することがあります。 +一部のモデルプロバイダーは [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。その場合、次のようなエラーが発生することがあります。 ``` BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}} ``` -これは一部のモデルプロバイダーの欠点であり、JSON 出力をサポートしていますが、出力に使用する `json_schema` を指定することはできません。これに対する修正に取り組んでいますが、JSON スキーマ出力をサポートしているプロバイダーに依存することをお勧めします。さもないと、アプリが不正な JSON のために頻繁に壊れることになります。 \ No newline at end of file +これは一部のモデルプロバイダーの制限で、JSON 出力には対応していても、出力に使う `json_schema` を指定できない場合があります。現在この問題の修正に取り組んでいますが、JSON schema 出力をサポートしているプロバイダーの利用を推奨します。そうでない場合、不正な JSON によりアプリが頻繁に動作しなくなる可能性があります。 \ No newline at end of file diff --git a/docs/ja/multi_agent.md b/docs/ja/multi_agent.md index af635776..e5604f6f 100644 --- a/docs/ja/multi_agent.md +++ b/docs/ja/multi_agent.md @@ -1,37 +1,37 @@ -# 複数エージェントのオーケストレーション +# 複数のエージェントのオーケストレーション -オーケストレーションとは、アプリ内でのエージェントの流れを指します。どのエージェントが実行され、どの順序で、次に何が起こるかをどのように決定するかです。エージェントをオーケストレーションする主な方法は2つあります。 +オーケストレーションとは、アプリ内でのエージェントの流れを指します。どのエージェントが、どの順番で実行され、次に何が起こるかをどのように決定するかということです。エージェントをオーケストレーションする主な方法は 2 つあります。 -1. LLM に決定を任せる: これは LLM の知能を利用して、計画、推論し、それに基づいてどのステップを取るかを決定します。 -2. コードによるオーケストレーション: コードを通じてエージェントの流れを決定します。 +1. LLM に意思決定を任せる方法:これは LLM の知能を活用し、計画・推論・意思決定を行わせるものです。 +2. コードによるオーケストレーション:コードでエージェントの流れを制御する方法です。 -これらのパターンを組み合わせて使用することができます。それぞれに独自のトレードオフがあり、以下で説明します。 +これらのパターンは組み合わせて使うこともできます。それぞれにメリット・デメリットがあり、以下で説明します。 ## LLM によるオーケストレーション -エージェントは、指示、ツール、ハンドオフを備えた LLM です。これにより、オープンエンドなタスクが与えられた場合、LLM は自律的にタスクに取り組む方法を計画し、ツールを使用してアクションを実行し、データを取得し、ハンドオフを使用してサブエージェントにタスクを委任できます。例えば、リサーチエージェントは次のようなツールを備えることができます。 +エージェントとは、instructions、tools、handoffs を備えた LLM です。つまり、オープンエンドなタスクが与えられた場合、LLM は自律的にタスクへの取り組み方を計画し、tools を使ってアクションを実行・データを取得し、handoffs を使ってサブエージェントにタスクを委任できます。たとえば、リサーチエージェントには以下のような tools を持たせることができます。 -- Web 検索でオンライン情報を見つける -- ファイル検索と取得で独自データや接続を検索する -- コンピュータ操作でコンピュータ上でアクションを実行する +- Web 検索でオンライン情報を探す +- ファイル検索やリトリーバルで独自データや接続先を検索する +- コンピュータ操作でコンピュータ上のアクションを実行する - コード実行でデータ分析を行う -- 計画やレポート作成に優れた専門エージェントへのハンドオフ +- 計画やレポート作成などに特化したエージェントへの handoffs -このパターンは、タスクがオープンエンドであり、LLM の知能に依存したい場合に最適です。ここでの最も重要な戦術は次のとおりです。 +このパターンは、タスクがオープンエンドで LLM の知能に頼りたい場合に最適です。ここで重要なポイントは次のとおりです。 -1. 良いプロンプトに投資する。利用可能なツール、使用方法、および操作するパラメーターを明確にします。 -2. アプリを監視し、反復する。問題が発生する箇所を確認し、プロンプトを改善します。 -3. エージェントに内省と改善を許可する。例えば、ループで実行し、自己批評させるか、エラーメッセージを提供して改善させます。 -4. 一つのタスクに優れた専門エージェントを持つこと。何でも得意とされる汎用エージェントを持つのではなく。 -5. [evals](https://platform.openai.com/docs/guides/evals) に投資する。これにより、エージェントを訓練してタスクの改善と向上を図ることができます。 +1. 良いプロンプトに投資しましょう。利用可能な tools、使い方、守るべきパラメーターを明確に伝えます。 +2. アプリをモニタリングし、改善を繰り返しましょう。問題が発生した箇所を確認し、プロンプトを改善します。 +3. エージェントに内省と改善を促しましょう。たとえばループで実行し、自己批評させたり、エラーメッセージを与えて改善させたりします。 +4. 何でもできる汎用エージェントではなく、1 つのタスクに特化したエージェントを用意しましょう。 +5. [evals](https://platform.openai.com/docs/guides/evals) に投資しましょう。これによりエージェントを訓練し、タスクの精度を向上させることができます。 ## コードによるオーケストレーション -LLM によるオーケストレーションは強力ですが、コードによるオーケストレーションは、速度、コスト、パフォーマンスの面でタスクをより決定的で予測可能にします。ここでの一般的なパターンは次のとおりです。 +LLM によるオーケストレーションは強力ですが、コードによるオーケストレーションは、速度・コスト・パフォーマンスの面でより決定的かつ予測可能になります。よく使われるパターンは次のとおりです。 -- [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用して、コードで検査できる適切な形式のデータを生成します。例えば、エージェントにタスクをいくつかのカテゴリーに分類させ、そのカテゴリーに基づいて次のエージェントを選択することができます。 -- 複数のエージェントをチェーンして、一つの出力を次の入力に変換します。ブログ記事を書くタスクを、リサーチ、アウトライン作成、記事執筆、批評、改善という一連のステップに分解できます。 -- `while` ループでタスクを実行するエージェントと、評価とフィードバックを提供するエージェントを実行し、評価者が出力が特定の基準を満たすと判断するまで続けます。 -- 複数のエージェントを並行して実行します。例えば、Python の基本コンポーネントである `asyncio.gather` を使用します。これは、互いに依存しない複数のタスクがある場合に速度のために役立ちます。 +- [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使い、コードで検査できる適切な形式のデータを生成する。たとえば、エージェントにタスクをいくつかのカテゴリーに分類させ、そのカテゴリーに応じて次のエージェントを選択することができます。 +- 複数のエージェントを連鎖させ、一方の出力を次の入力に変換する。たとえば、ブログ記事の作成タスクを「リサーチ→アウトライン作成→記事執筆→批評→改善」といった一連のステップに分解できます。 +- タスクを実行するエージェントと、評価・フィードバックを行うエージェントを `while` ループで回し、評価者が基準を満たしたと判断するまで繰り返します。 +- 複数のエージェントを並列で実行する(例:Python の基本コンポーネントである `asyncio.gather` を利用)。これは、互いに依存しない複数のタスクを高速に処理したい場合に有効です。 -[`examples/agent_patterns`](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) には多くのコード例があります。 \ No newline at end of file +[`examples/agent_patterns`](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) には、さまざまな code examples をご用意しています。 \ No newline at end of file diff --git a/docs/ja/quickstart.md b/docs/ja/quickstart.md index 9e4846ef..1a4913c7 100644 --- a/docs/ja/quickstart.md +++ b/docs/ja/quickstart.md @@ -2,7 +2,7 @@ ## プロジェクトと仮想環境の作成 -これは一度だけ行えば大丈夫です。 +この作業は一度だけ行えば十分です。 ```bash mkdir my_project @@ -10,9 +10,9 @@ cd my_project python -m venv .venv ``` -### 仮想環境をアクティブにする +### 仮想環境の有効化 -新しいターミナルセッションを開始するたびに行ってください。 +新しいターミナルセッションを開始するたびに、これを実行してください。 ```bash source .venv/bin/activate @@ -26,7 +26,7 @@ pip install openai-agents # or `uv add openai-agents`, etc ### OpenAI API キーの設定 -お持ちでない場合は、[こちらの手順](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)に従って OpenAI API キーを作成してください。 +まだお持ちでない場合は、[こちらの手順](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)に従って OpenAI API キーを作成してください。 ```bash export OPENAI_API_KEY=sk-... @@ -34,7 +34,7 @@ export OPENAI_API_KEY=sk-... ## 最初のエージェントを作成する -エージェントは、instructions、名前、オプションの設定(`model_config` など)で定義されます。 +エージェントは instructions、名前、およびオプションの設定(例: `model_config` )で定義します。 ```python from agents import Agent @@ -47,7 +47,7 @@ agent = Agent( ## さらにエージェントを追加する -追加のエージェントも同様に定義できます。`handoff_descriptions` は、ハンドオフルーティングを決定するための追加のコンテキストを提供します。 +追加のエージェントも同様の方法で定義できます。 `handoff_descriptions` は、ハンドオフのルーティングを判断するための追加コンテキストを提供します。 ```python from agents import Agent @@ -67,7 +67,7 @@ math_tutor_agent = Agent( ## ハンドオフを定義する -各エージェントで、タスクを進める方法を決定するための送信ハンドオフオプションのインベントリを定義できます。 +各エージェントで、タスクを進めるために選択できる送信先ハンドオフオプションのインベントリを定義できます。 ```python triage_agent = Agent( @@ -77,9 +77,9 @@ triage_agent = Agent( ) ``` -## エージェントオーケストレーションを実行する +## エージェントのオーケストレーションを実行する -ワークフローが実行され、トリアージエージェントが2つの専門エージェント間を正しくルーティングするか確認しましょう。 +ワークフローが正しく動作し、トリアージエージェントが 2 つの専門エージェント間で正しくルーティングするか確認しましょう。 ```python from agents import Runner @@ -91,7 +91,7 @@ async def main(): ## ガードレールを追加する -入力または出力に対してカスタムガードレールを定義できます。 +入力または出力に対して実行するカスタムガードレールを定義できます。 ```python from agents import GuardrailFunctionOutput, Agent, Runner @@ -118,7 +118,7 @@ async def homework_guardrail(ctx, agent, input_data): ## すべてをまとめる -すべてをまとめて、ハンドオフと入力ガードレールを使用して全体のワークフローを実行しましょう。 +すべてを組み合わせて、ハンドオフと入力ガードレールを使ったワークフロー全体を実行してみましょう。 ```python from agents import Agent, InputGuardrail, GuardrailFunctionOutput, Runner @@ -176,14 +176,14 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## トレースを表示する +## トレースを確認する -エージェントの実行中に何が起こったかを確認するには、[OpenAI ダッシュボードのトレースビューア](https://platform.openai.com/traces)に移動して、エージェント実行のトレースを表示してください。 +エージェントの実行中に何が起こったかを確認するには、[OpenAI ダッシュボードの Trace viewer](https://platform.openai.com/traces) にアクセスして、エージェント実行のトレースを表示してください。 ## 次のステップ より複雑なエージェントフローの構築方法を学びましょう: -- [エージェント](agents.md)の設定方法を学ぶ。 -- [エージェントの実行](running_agents.md)について学ぶ。 -- [ツール](tools.md)、[ガードレール](guardrails.md)、[モデル](models.md)について学ぶ。 \ No newline at end of file +- [エージェント](agents.md) の設定方法について学ぶ +- [エージェントの実行](running_agents.md) について学ぶ +- [tools](tools.md)、[guardrails](guardrails.md)、[models](models.md) について学ぶ \ No newline at end of file diff --git a/docs/ja/results.md b/docs/ja/results.md index 765bba5c..6b2c0ccc 100644 --- a/docs/ja/results.md +++ b/docs/ja/results.md @@ -1,52 +1,52 @@ # 結果 -`Runner.run` メソッドを呼び出すと、次のいずれかが得られます: +`Runner.run` メソッドを呼び出すと、以下のいずれかが返されます: -- `run` または `run_sync` を呼び出すと [`RunResult`][agents.result.RunResult] -- `run_streamed` を呼び出すと [`RunResultStreaming`][agents.result.RunResultStreaming] +- `run` または `run_sync` を呼び出した場合は、[`RunResult`][agents.result.RunResult] +- `run_streamed` を呼び出した場合は、[`RunResultStreaming`][agents.result.RunResultStreaming] -これらはどちらも [`RunResultBase`][agents.result.RunResultBase] を継承しており、ほとんどの有用な情報はここにあります。 +これらはどちらも [`RunResultBase`][agents.result.RunResultBase] を継承しており、ほとんどの有用な情報はここに含まれています。 ## 最終出力 -[`final_output`][agents.result.RunResultBase.final_output] プロパティには、最後に実行されたエージェントの最終出力が含まれています。これは次のいずれかです: +[`final_output`][agents.result.RunResultBase.final_output] プロパティには、最後に実行されたエージェントの最終出力が格納されます。これは以下のいずれかです: -- `output_type` が定義されていない場合は `str` -- エージェントに出力タイプが定義されている場合は `last_agent.output_type` のオブジェクト +- 最後のエージェントに `output_type` が定義されていない場合は `str` +- エージェントに `output_type` が定義されている場合は、`last_agent.output_type` 型のオブジェクト !!! note - `final_output` は `Any` 型です。ハンドオフのため、静的に型を指定することはできません。ハンドオフが発生すると、どのエージェントが最後になるか分からないため、可能な出力タイプのセットを静的に知ることはできません。 + `final_output` の型は `Any` です。ハンドオフのため、静的に型を決定することはできません。ハンドオフが発生した場合、どのエージェントが最後になるか分からないため、出力型の集合を静的に知ることができません。 -## 次のターンの入力 +## 次のターンへの入力 -[`result.to_input_list()`][agents.result.RunResultBase.to_input_list] を使用して、結果を入力リストに変換できます。これにより、提供した元の入力をエージェント実行中に生成されたアイテムに連結します。これにより、1つのエージェント実行の出力を別の実行に渡したり、ループで実行して毎回新しいユーザー入力を追加したりするのが便利になります。 +[`result.to_input_list()`][agents.result.RunResultBase.to_input_list] を使うことで、実行結果を入力リストに変換できます。これは、あなたが提供した元の入力と、エージェント実行中に生成されたアイテムを連結したものです。これにより、あるエージェント実行の出力を別の実行に渡したり、ループで実行して毎回新しいユーザー入力を追加したりするのが簡単になります。 ## 最後のエージェント -[`last_agent`][agents.result.RunResultBase.last_agent] プロパティには、最後に実行されたエージェントが含まれています。アプリケーションによっては、次回ユーザーが何かを入力する際に役立つことがよくあります。たとえば、フロントラインのトリアージエージェントが言語特定のエージェントにハンドオフする場合、最後のエージェントを保存し、次回ユーザーがエージェントにメッセージを送信する際に再利用できます。 +[`last_agent`][agents.result.RunResultBase.last_agent] プロパティには、最後に実行されたエージェントが格納されます。アプリケーションによっては、次回ユーザーが何か入力した際にこれが役立つことがよくあります。たとえば、フロントラインのトリアージエージェントが言語特化エージェントにハンドオフする場合、最後のエージェントを保存しておき、次回ユーザーがエージェントにメッセージを送る際に再利用できます。 ## 新しいアイテム -[`new_items`][agents.result.RunResultBase.new_items] プロパティには、実行中に生成された新しいアイテムが含まれています。アイテムは [`RunItem`][agents.items.RunItem] です。実行アイテムは LLM によって生成された raw アイテムをラップします。 +[`new_items`][agents.result.RunResultBase.new_items] プロパティには、実行中に生成された新しいアイテムが格納されます。これらのアイテムは [`RunItem`][agents.items.RunItem] です。RunItem は LLM によって生成された raw アイテムをラップします。 - [`MessageOutputItem`][agents.items.MessageOutputItem] は LLM からのメッセージを示します。raw アイテムは生成されたメッセージです。 -- [`HandoffCallItem`][agents.items.HandoffCallItem] は LLM がハンドオフツールを呼び出したことを示します。raw アイテムは LLM からのツール呼び出しアイテムです。 -- [`HandoffOutputItem`][agents.items.HandoffOutputItem] はハンドオフが発生したことを示します。raw アイテムはハンドオフツール呼び出しへのツール応答です。アイテムからソース/ターゲットエージェントにもアクセスできます。 +- [`HandoffCallItem`][agents.items.HandoffCallItem] は LLM がハンドオフツールを呼び出したことを示します。raw アイテムは LLM からのツールコールアイテムです。 +- [`HandoffOutputItem`][agents.items.HandoffOutputItem] はハンドオフが発生したことを示します。raw アイテムはハンドオフツールコールへのツールレスポンスです。また、アイテムからソース/ターゲットエージェントにもアクセスできます。 - [`ToolCallItem`][agents.items.ToolCallItem] は LLM がツールを呼び出したことを示します。 -- [`ToolCallOutputItem`][agents.items.ToolCallOutputItem] はツールが呼び出されたことを示します。raw アイテムはツール応答です。アイテムからツール出力にもアクセスできます。 +- [`ToolCallOutputItem`][agents.items.ToolCallOutputItem] はツールが呼び出されたことを示します。raw アイテムはツールレスポンスです。また、アイテムからツール出力にもアクセスできます。 - [`ReasoningItem`][agents.items.ReasoningItem] は LLM からの推論アイテムを示します。raw アイテムは生成された推論です。 ## その他の情報 ### ガードレール結果 -[`input_guardrail_results`][agents.result.RunResultBase.input_guardrail_results] と [`output_guardrail_results`][agents.result.RunResultBase.output_guardrail_results] プロパティには、ガードレールの結果が含まれています。ガードレール結果には、ログや保存に役立つ情報が含まれることがあるため、これらを利用可能にしています。 +[`input_guardrail_results`][agents.result.RunResultBase.input_guardrail_results] および [`output_guardrail_results`][agents.result.RunResultBase.output_guardrail_results] プロパティには、ガードレールの結果(存在する場合)が格納されます。ガードレール結果には、ログや保存に役立つ有用な情報が含まれることがあるため、これらを利用できるようにしています。 -### Raw 応答 +### raw レスポンス -[`raw_responses`][agents.result.RunResultBase.raw_responses] プロパティには、LLM によって生成された [`ModelResponse`][agents.items.ModelResponse] が含まれています。 +[`raw_responses`][agents.result.RunResultBase.raw_responses] プロパティには、LLM によって生成された [`ModelResponse`][agents.items.ModelResponse] が格納されます。 ### 元の入力 -[`input`][agents.result.RunResultBase.input] プロパティには、`run` メソッドに提供した元の入力が含まれています。ほとんどの場合、これを必要としませんが、必要な場合に備えて利用可能です。 \ No newline at end of file +[`input`][agents.result.RunResultBase.input] プロパティには、`run` メソッドに提供した元の入力が格納されます。ほとんどの場合これは必要ありませんが、必要な場合のために利用可能です。 \ No newline at end of file diff --git a/docs/ja/running_agents.md b/docs/ja/running_agents.md index c2071c65..1a56fcc7 100644 --- a/docs/ja/running_agents.md +++ b/docs/ja/running_agents.md @@ -1,10 +1,10 @@ # エージェントの実行 -エージェントは [`Runner`][agents.run.Runner] クラスを通じて実行できます。3つのオプションがあります: +エージェントは [`Runner`][agents.run.Runner] クラスを使って実行できます。3 つのオプションがあります。 -1. 非同期で実行し、[`RunResult`][agents.result.RunResult] を返す [`Runner.run()`][agents.run.Runner.run]。 -2. 同期メソッドで、内部で `.run()` を実行する [`Runner.run_sync()`][agents.run.Runner.run_sync]。 -3. 非同期で実行し、[`RunResultStreaming`][agents.result.RunResultStreaming] を返す [`Runner.run_streamed()`][agents.run.Runner.run_streamed]。ストリーミングモードで LLM を呼び出し、受信したイベントをストリームします。 +1. [`Runner.run()`][agents.run.Runner.run]:非同期で実行され、[`RunResult`][agents.result.RunResult] を返します。 +2. [`Runner.run_sync()`][agents.run.Runner.run_sync]:同期メソッドで、内部的には `.run()` を実行します。 +3. [`Runner.run_streamed()`][agents.run.Runner.run_streamed]:非同期で実行され、[`RunResultStreaming`][agents.result.RunResultStreaming] を返します。LLM をストリーミングモードで呼び出し、受信したイベントをリアルタイムでストリームします。 ```python from agents import Agent, Runner @@ -19,53 +19,53 @@ async def main(): # Infinite loop's dance. ``` -詳細は[結果ガイド](results.md)をご覧ください。 +詳細は [results guide](results.md) をご覧ください。 ## エージェントループ -`Runner` の実行メソッドを使用する際、開始エージェントと入力を渡します。入力は文字列(ユーザーメッセージと見なされる)または OpenAI Responses API のアイテムリストのいずれかです。 +`Runner` の run メソッドを使用する際、開始するエージェントと入力を渡します。入力は文字列(ユーザーメッセージと見なされます)または入力アイテムのリスト(OpenAI Responses API のアイテム)を指定できます。 -ランナーは次のループを実行します: +runner は次のようなループを実行します。 1. 現在のエージェントと入力で LLM を呼び出します。 2. LLM が出力を生成します。 - 1. LLM が `final_output` を返すと、ループは終了し、結果を返します。 - 2. LLM がハンドオフを行う場合、現在のエージェントと入力を更新し、ループを再実行します。 - 3. LLM がツール呼び出しを生成する場合、それらを実行し、結果を追加し、ループを再実行します。 + 1. LLM が `final_output` を返した場合、ループは終了し、結果を返します。 + 2. LLM がハンドオフを行った場合、現在のエージェントと入力を更新し、ループを再実行します。 + 3. LLM がツール呼び出しを生成した場合、それらのツール呼び出しを実行し、結果を追加してループを再実行します。 3. 渡された `max_turns` を超えた場合、[`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] 例外を発生させます。 !!! note - LLM の出力が「最終出力」と見なされるかどうかのルールは、望ましいタイプのテキスト出力を生成し、ツール呼び出しがないことです。 + LLM の出力が「final output」と見なされるルールは、希望する型のテキスト出力が生成され、ツール呼び出しがない場合です。 ## ストリーミング -ストリーミングを使用すると、LLM が実行される際にストリーミングイベントを受け取ることができます。ストリームが完了すると、[`RunResultStreaming`][agents.result.RunResultStreaming] は実行に関する完全な情報を含み、生成されたすべての新しい出力を含みます。ストリーミングイベントには `.stream_events()` を呼び出せます。[ストリーミングガイド](streaming.md)で詳細を確認してください。 +ストリーミングを利用すると、LLM の実行中にストリーミングイベントを受け取ることができます。ストリームが完了すると、[`RunResultStreaming`][agents.result.RunResultStreaming] に実行に関するすべての情報(新たに生成されたすべての出力を含む)が格納されます。ストリーミングイベントは `.stream_events()` で取得できます。詳細は [streaming guide](streaming.md) をご覧ください。 -## 実行設定 +## Run config -`run_config` パラメーターを使用して、エージェント実行のグローバル設定を構成できます: +`run_config` パラメーターでは、エージェント実行のグローバル設定をいくつか構成できます。 -- [`model`][agents.run.RunConfig.model]: 各エージェントの `model` に関係なく、使用するグローバル LLM モデルを設定できます。 -- [`model_provider`][agents.run.RunConfig.model_provider]: モデル名を検索するためのモデルプロバイダーで、デフォルトは OpenAI です。 -- [`model_settings`][agents.run.RunConfig.model_settings]: エージェント固有の設定を上書きします。たとえば、グローバルな `temperature` や `top_p` を設定できます。 -- [`input_guardrails`][agents.run.RunConfig.input_guardrails], [`output_guardrails`][agents.run.RunConfig.output_guardrails]: すべての実行に含める入力または出力ガードレールのリスト。 -- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]: すべてのハンドオフに適用するグローバル入力フィルター。ハンドオフに既にフィルターがない場合に適用されます。入力フィルターを使用して、新しいエージェントに送信される入力を編集できます。詳細は [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] のドキュメントを参照してください。 -- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]: 実行全体の[トレーシング](tracing.md)を無効にできます。 -- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]: トレースに LLM やツール呼び出しの入出力など、潜在的に機密性のあるデータを含めるかどうかを設定します。 -- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]: 実行のトレーシングワークフロー名、トレース ID、トレースグループ ID を設定します。少なくとも `workflow_name` を設定することをお勧めします。グループ ID は、複数の実行にわたってトレースをリンクするためのオプションフィールドです。 -- [`trace_metadata`][agents.run.RunConfig.trace_metadata]: すべてのトレースに含めるメタデータ。 +- [`model`][agents.run.RunConfig.model]:各エージェントの `model` 設定に関わらず、グローバルで使用する LLM モデルを指定できます。 +- [`model_provider`][agents.run.RunConfig.model_provider]:モデル名を検索するためのモデルプロバイダーで、デフォルトは OpenAI です。 +- [`model_settings`][agents.run.RunConfig.model_settings]:エージェント固有の設定を上書きします。たとえば、グローバルな `temperature` や `top_p` を設定できます。 +- [`input_guardrails`][agents.run.RunConfig.input_guardrails], [`output_guardrails`][agents.run.RunConfig.output_guardrails]:すべての実行に含める入力または出力ガードレールのリストです。 +- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]:すべてのハンドオフに適用するグローバルな入力フィルターです(ハンドオフに既にフィルターがない場合)。入力フィルターを使うと、新しいエージェントに送信する入力を編集できます。詳細は [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] のドキュメントをご覧ください。 +- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]:実行全体の [トレーシング](tracing.md) を無効にできます。 +- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]:トレースに LLM やツール呼び出しの入出力など、機密性のあるデータを含めるかどうかを設定します。 +- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]:実行のトレーシングワークフロー名、トレース ID、トレースグループ ID を設定します。少なくとも `workflow_name` の設定を推奨します。グループ ID は複数の実行にまたがるトレースをリンクするためのオプション項目です。 +- [`trace_metadata`][agents.run.RunConfig.trace_metadata]:すべてのトレースに含めるメタデータです。 -## 会話/チャットスレッド +## 会話・チャットスレッド -いずれかの実行メソッドを呼び出すと、1つ以上のエージェントが実行される可能性があります(したがって、1つ以上の LLM 呼び出しが行われます)が、チャット会話の単一の論理ターンを表します。例えば: +いずれかの run メソッドを呼び出すと、1 つまたは複数のエージェント(および 1 つまたは複数の LLM 呼び出し)が実行されますが、チャット会話における 1 回の論理的なターンを表します。例: -1. ユーザーターン:ユーザーがテキストを入力 -2. ランナー実行:最初のエージェントが LLM を呼び出し、ツールを実行し、2番目のエージェントにハンドオフし、2番目のエージェントがさらにツールを実行し、出力を生成します。 +1. ユーザーのターン:ユーザーがテキストを入力 +2. Runner の実行:最初のエージェントが LLM を呼び出し、ツールを実行し、2 番目のエージェントにハンドオフ、2 番目のエージェントがさらにツールを実行し、出力を生成 -エージェント実行の終了時に、ユーザーに何を表示するかを選択できます。例えば、エージェントによって生成されたすべての新しいアイテムをユーザーに表示するか、最終出力のみを表示するかです。いずれにせよ、ユーザーがフォローアップの質問をする可能性があり、その場合は再度実行メソッドを呼び出すことができます。 +エージェントの実行が終わったら、ユーザーに何を表示するか選択できます。たとえば、エージェントが生成したすべての新しいアイテムをユーザーに見せることも、最終出力だけを見せることもできます。いずれの場合も、ユーザーが追加の質問をした場合は、再度 run メソッドを呼び出せます。 -次のターンの入力を取得するには、基本の [`RunResultBase.to_input_list()`][agents.result.RunResultBase.to_input_list] メソッドを使用できます。 +次のターンの入力を取得するには、ベースの [`RunResultBase.to_input_list()`][agents.result.RunResultBase.to_input_list] メソッドを使用できます。 ```python async def main(): @@ -86,10 +86,10 @@ async def main(): ## 例外 -SDK は特定のケースで例外を発生させます。完全なリストは [`agents.exceptions`][] にあります。概要は以下の通りです: +SDK は特定のケースで例外を発生させます。全リストは [`agents.exceptions`][] にあります。概要は以下の通りです。 -- [`AgentsException`][agents.exceptions.AgentsException] は、SDK で発生するすべての例外の基本クラスです。 -- [`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] は、実行が渡された `max_turns` を超えた場合に発生します。 -- [`ModelBehaviorError`][agents.exceptions.ModelBehaviorError] は、モデルが無効な出力を生成した場合に発生します。例:不正な形式の JSON や存在しないツールの使用。 -- [`UserError`][agents.exceptions.UserError] は、SDK を使用する際にエラーを犯した場合に発生します。 -- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered], [`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered] は、[ガードレール](guardrails.md)が作動した場合に発生します。 \ No newline at end of file +- [`AgentsException`][agents.exceptions.AgentsException]:SDK で発生するすべての例外の基底クラスです。 +- [`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded]:run メソッドに渡した `max_turns` を超えた場合に発生します。 +- [`ModelBehaviorError`][agents.exceptions.ModelBehaviorError]:モデルが不正な出力(例:不正な JSON や存在しないツールの使用)を生成した場合に発生します。 +- [`UserError`][agents.exceptions.UserError]:SDK を使用する際に、あなた(SDK を使ってコードを書く人)がエラーを起こした場合に発生します。 +- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered], [`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]:[ガードレール](guardrails.md) が作動した場合に発生します。 \ No newline at end of file diff --git a/docs/ja/streaming.md b/docs/ja/streaming.md index 2f7a8dd2..b820bae5 100644 --- a/docs/ja/streaming.md +++ b/docs/ja/streaming.md @@ -1,14 +1,14 @@ # ストリーミング -ストリーミングを使用すると、エージェントの実行が進行する際の更新を購読できます。これは、エンドユーザーに進捗状況の更新や部分的な応答を表示するのに役立ちます。 +ストリーミングを利用すると、エージェントの実行が進行するにつれて、その更新情報を購読できます。これは、エンドユーザーに進捗状況や部分的な応答を表示するのに役立ちます。 -ストリーミングを行うには、[`Runner.run_streamed()`][agents.run.Runner.run_streamed] を呼び出します。これにより、[`RunResultStreaming`][agents.result.RunResultStreaming] が得られます。`result.stream_events()` を呼び出すと、以下で説明する [`StreamEvent`][agents.stream_events.StreamEvent] オブジェクトの非同期ストリームが得られます。 +ストリーミングを行うには、[`Runner.run_streamed()`][agents.run.Runner.run_streamed] を呼び出します。これにより、[`RunResultStreaming`][agents.result.RunResultStreaming] が返されます。`result.stream_events()` を呼び出すと、下記で説明する [`StreamEvent`][agents.stream_events.StreamEvent] オブジェクトの非同期ストリームが得られます。 -## Raw response events +## raw response events -[`RawResponsesStreamEvent`][agents.stream_events.RawResponsesStreamEvent] は、LLM から直接渡される raw イベントです。これらは OpenAI Responses API フォーマットであり、各イベントにはタイプ(`response.created`、`response.output_text.delta` など)とデータがあります。これらのイベントは、生成され次第、ユーザーに応答メッセージをストリーミングしたい場合に便利です。 +[`RawResponsesStreamEvent`][agents.stream_events.RawResponsesStreamEvent] は、LLM から直接渡される raw イベントです。これらは OpenAI Responses API フォーマットであり、各イベントには type(例:`response.created`、`response.output_text.delta` など)とデータが含まれます。これらのイベントは、応答メッセージが生成され次第、ユーザーにストリーミングしたい場合に便利です。 -例えば、これは LLM によってトークンごとに生成されたテキストを出力します。 +例えば、以下の例では LLM が生成したテキストをトークンごとに出力します。 ```python import asyncio @@ -31,11 +31,11 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## 実行アイテムイベントとエージェントイベント +## Run item イベントとエージェントイベント -[`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent] は、より高レベルのイベントです。アイテムが完全に生成されたときに通知します。これにより、「メッセージが生成された」、「ツールが実行された」などのレベルで進捗状況を更新することができます。同様に、[`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] は、現在のエージェントが変更されたとき(例えば、ハンドオフの結果として)に更新を提供します。 +[`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent] は、より高レベルなイベントです。アイテムが完全に生成されたタイミングを通知します。これにより、「メッセージが生成された」「ツールが実行された」など、各トークン単位ではなく、進捗状況をまとめてユーザーに伝えることができます。同様に、[`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] は、現在のエージェントが変更されたとき(例:ハンドオフの結果として)に更新情報を提供します。 -例えば、これは raw イベントを無視し、ユーザーに更新をストリーミングします。 +例えば、以下の例では raw イベントを無視し、ユーザーに更新情報のみをストリーミングします。 ```python import asyncio diff --git a/docs/ja/tools.md b/docs/ja/tools.md index cd747663..2444891c 100644 --- a/docs/ja/tools.md +++ b/docs/ja/tools.md @@ -1,18 +1,18 @@ # ツール -ツールはエージェントがアクションを実行するためのものです。データの取得、コードの実行、外部 API の呼び出し、さらにはコンピュータの使用などが含まれます。Agent SDK には3つのクラスのツールがあります。 +ツールは エージェント がアクションを実行するためのものです。たとえば、データの取得、コードの実行、外部 API の呼び出し、さらにはコンピュータ操作などが含まれます。Agents SDK には 3 種類のツールクラスがあります。 -- ホストされたツール: これらは LLM サーバー上で AI モデルと一緒に実行されます。OpenAI は、リトリーバル、Web 検索、コンピュータ操作をホストされたツールとして提供しています。 -- 関数呼び出し: 任意の Python 関数をツールとして使用できます。 -- エージェントをツールとして使用: エージェントをツールとして使用し、エージェントが他のエージェントをハンドオフせずに呼び出すことができます。 +- Hosted tools: これらは LLM サーバー上で AI モデルとともに実行されます。OpenAI は retrieval、Web 検索、コンピュータ操作を Hosted tools として提供しています。 +- Function calling: 任意の Python 関数をツールとして利用できます。 +- Agents as tools: エージェントをツールとして利用でき、エージェントが他のエージェントをハンドオフせずに呼び出すことができます。 -## ホストされたツール +## Hosted tools -OpenAI は、[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] を使用する際にいくつかの組み込みツールを提供しています。 +OpenAI は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] を使用する際に、いくつかの組み込みツールを提供しています。 -- [`WebSearchTool`][agents.tool.WebSearchTool] はエージェントが Web 検索を行うことを可能にします。 -- [`FileSearchTool`][agents.tool.FileSearchTool] は OpenAI ベクトルストアから情報を取得することを可能にします。 -- [`ComputerTool`][agents.tool.ComputerTool] はコンピュータ操作タスクを自動化することを可能にします。 +- [`WebSearchTool`][agents.tool.WebSearchTool] は、エージェントが Web 検索を行うことを可能にします。 +- [`FileSearchTool`][agents.tool.FileSearchTool] は、OpenAI ベクトルストアから情報を取得できます。 +- [`ComputerTool`][agents.tool.ComputerTool] は、コンピュータ操作タスクの自動化を可能にします。 ```python from agents import Agent, FileSearchTool, Runner, WebSearchTool @@ -33,16 +33,16 @@ async def main(): print(result.final_output) ``` -## 関数ツール +## Function tools -任意の Python 関数をツールとして使用できます。Agents SDK はツールを自動的にセットアップします。 +任意の Python 関数をツールとして利用できます。Agents SDK が自動的にツールをセットアップします。 -- ツールの名前は Python 関数の名前になります(または名前を指定できます) -- ツールの説明は関数の docstring から取得されます(または説明を指定できます) -- 関数入力のスキーマは関数の引数から自動的に作成されます -- 各入力の説明は、無効にしない限り、関数の docstring から取得されます +- ツール名は Python 関数名になります(または任意の名前を指定できます) +- ツールの説明は関数の docstring から取得されます(または任意の説明を指定できます) +- 関数の引数から自動的に入力スキーマが作成されます +- 各入力の説明は、関数の docstring から取得されます(無効化も可能です) -Python の `inspect` モジュールを使用して関数シグネチャを抽出し、[`griffe`](https://mkdocstrings.github.io/griffe/) を使用して docstring を解析し、`pydantic` を使用してスキーマを作成します。 +Python の `inspect` モジュールを使って関数シグネチャを抽出し、[`griffe`](https://mkdocstrings.github.io/griffe/) で docstring を解析し、`pydantic` でスキーマを作成します。 ```python import json @@ -94,12 +94,12 @@ for tool in agent.tools: ``` -1. 任意の Python 型を関数の引数として使用でき、関数は同期または非同期であることができます。 -2. Docstring が存在する場合、説明や引数の説明を取得するために使用されます。 -3. 関数はオプションで `context` を取ることができ(最初の引数である必要があります)、ツールの名前、説明、使用する docstring スタイルなどのオーバーライドを設定できます。 -4. デコレートされた関数をツールのリストに渡すことができます。 +1. 関数の引数には任意の Python 型を利用でき、同期・非同期どちらの関数も利用可能です。 +2. docstring があれば、説明や引数の説明として利用されます。 +3. 関数はオプションで `context` を最初の引数として受け取れます。また、ツール名や説明、docstring スタイルの指定などのオーバーライドも可能です。 +4. デコレートした関数をツールのリストに渡すことができます。 -??? note "出力を表示するには展開してください" +??? note "出力を展開して表示" ``` fetch_weather @@ -169,14 +169,14 @@ for tool in agent.tools: } ``` -### カスタム関数ツール +### カスタム function tools -時には、Python 関数をツールとして使用したくない場合もあります。その場合、直接 [`FunctionTool`][agents.tool.FunctionTool] を作成できます。以下を提供する必要があります。 +場合によっては、Python 関数をツールとして使いたくないこともあります。その場合は、[`FunctionTool`][agents.tool.FunctionTool] を直接作成できます。必要な情報は以下の通りです。 - `name` - `description` - `params_json_schema`(引数の JSON スキーマ) -- `on_invoke_tool`(コンテキストと引数を JSON 文字列として受け取り、ツールの出力を文字列として返す非同期関数) +- `on_invoke_tool`(context と引数(JSON 文字列)を受け取り、ツールの出力を文字列で返す async 関数) ```python from typing import Any @@ -211,16 +211,16 @@ tool = FunctionTool( ### 引数と docstring の自動解析 -前述のように、ツールのスキーマを抽出するために関数シグネチャを自動的に解析し、ツールおよび個々の引数の説明を抽出するために docstring を解析します。以下の点に注意してください。 +前述の通り、関数シグネチャを自動解析してツールのスキーマを抽出し、docstring からツールや各引数の説明を抽出します。主なポイントは以下の通りです。 -1. シグネチャの解析は `inspect` モジュールを通じて行われます。引数の型を理解するために型注釈を使用し、全体のスキーマを表す Pydantic モデルを動的に構築します。Python の基本コンポーネント、Pydantic モデル、TypedDict など、ほとんどの型をサポートしています。 -2. `griffe` を使用して docstring を解析します。サポートされている docstring フォーマットは `google`、`sphinx`、`numpy` です。docstring フォーマットを自動的に検出しようとしますが、これはベストエフォートであり、`function_tool` を呼び出す際に明示的に設定できます。`use_docstring_info` を `False` に設定することで docstring 解析を無効にすることもできます。 +1. シグネチャの解析は `inspect` モジュールで行います。型アノテーションを利用して引数の型を把握し、Pydantic モデルを動的に構築して全体のスキーマを表現します。Python の基本コンポーネント、Pydantic モデル、TypedDict など、ほとんどの型をサポートしています。 +2. docstring の解析には `griffe` を使用します。サポートされている docstring フォーマットは `google`、`sphinx`、`numpy` です。docstring フォーマットは自動検出を試みますが、`function_tool` 呼び出し時に明示的に指定することもできます。`use_docstring_info` を `False` に設定することで docstring 解析を無効化できます。 スキーマ抽出のコードは [`agents.function_schema`][] にあります。 -## エージェントをツールとして使用 +## Agents as tools -一部のワークフローでは、中央のエージェントが専門エージェントのネットワークをオーケストレーションすることを望むかもしれません。これを行うには、エージェントをツールとしてモデル化します。 +一部のワークフローでは、ハンドオフせずに中央のエージェントが専門エージェントのネットワークをオーケストレーションしたい場合があります。その場合、エージェントをツールとしてモデル化することで実現できます。 ```python from agents import Agent, Runner @@ -259,12 +259,12 @@ async def main(): print(result.final_output) ``` -## 関数ツールでのエラー処理 +## Function tools でのエラー処理 -`@function_tool` を使用して関数ツールを作成する際、`failure_error_function` を渡すことができます。これは、ツール呼び出しがクラッシュした場合に LLM にエラーレスポンスを提供する関数です。 +`@function_tool` で function tool を作成する際、`failure_error_function` を渡すことができます。これは、ツール呼び出しがクラッシュした場合に LLM へエラー応答を提供する関数です。 -- デフォルトでは(何も渡さない場合)、`default_tool_error_function` が実行され、LLM にエラーが発生したことを伝えます。 -- 独自のエラー関数を渡した場合、それが実行され、LLM にレスポンスが送信されます。 -- 明示的に `None` を渡した場合、ツール呼び出しエラーは再度発生し、処理する必要があります。モデルが無効な JSON を生成した場合は `ModelBehaviorError`、コードがクラッシュした場合は `UserError` などが考えられます。 +- デフォルト(何も渡さない場合)では、`default_tool_error_function` が実行され、LLM にエラーが発生したことを伝えます。 +- 独自のエラー関数を渡した場合は、それが実行され、その応答が LLM に送信されます。 +- 明示的に `None` を渡した場合、ツール呼び出し時のエラーは再スローされ、ユーザー側で処理できます。たとえば、モデルが無効な JSON を生成した場合は `ModelBehaviorError`、コードがクラッシュした場合は `UserError` などです。 -`FunctionTool` オブジェクトを手動で作成する場合、`on_invoke_tool` 関数内でエラーを処理する必要があります。 \ No newline at end of file +`FunctionTool` オブジェクトを手動で作成する場合は、`on_invoke_tool` 関数内でエラー処理を行う必要があります。 \ No newline at end of file diff --git a/docs/ja/tracing.md b/docs/ja/tracing.md index 0f573eb8..b7cb18dc 100644 --- a/docs/ja/tracing.md +++ b/docs/ja/tracing.md @@ -1,51 +1,51 @@ # トレーシング -Agents SDK には組み込みのトレーシング機能があり、エージェント実行中のイベント(LLM の生成、ツール呼び出し、ハンドオフ、ガードレール、カスタムイベントなど)の包括的な記録を収集します。[Traces ダッシュボード](https://platform.openai.com/traces)を使用して、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 +Agents SDK にはトレーシング機能が組み込まれており、エージェントの実行中に発生するイベント( LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、カスタムイベントなど)の包括的な記録を収集します。[Traces ダッシュボード](https://platform.openai.com/traces) を使用することで、開発中や本番環境でワークフローのデバッグ、可視化、監視が可能です。 !!!note - トレーシングはデフォルトで有効です。トレーシングを無効にする方法は2つあります: + トレーシングはデフォルトで有効になっています。トレーシングを無効にする方法は 2 つあります: - 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定して、グローバルにトレーシングを無効にすることができます。 - 2. [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定して、単一の実行でトレーシングを無効にすることができます。 + 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定することで、グローバルにトレーシングを無効化できます。 + 2. 単一の実行に対しては [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定することで無効化できます。 -***OpenAI の API を使用している Zero Data Retention (ZDR) ポリシーの下で運営されている組織では、トレーシングは利用できません。*** +***OpenAI の API を使用し、Zero Data Retention (ZDR) ポリシーの下で運用している組織では、トレーシングは利用できません。*** ## トレースとスパン -- **トレース** は「ワークフロー」の単一のエンドツーエンドの操作を表します。スパンで構成されています。トレースには以下のプロパティがあります: - - `workflow_name`: 論理的なワークフローまたはアプリです。例: "Code generation" や "Customer service"。 - - `trace_id`: トレースの一意の ID。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` でなければなりません。 - - `group_id`: 同じ会話からの複数のトレースをリンクするためのオプションのグループ ID。例: チャットスレッド ID。 - - `disabled`: True の場合、トレースは記録されません。 - - `metadata`: トレースのオプションのメタデータ。 -- **スパン** は開始時間と終了時間を持つ操作を表します。スパンには以下があります: - - `started_at` と `ended_at` のタイムスタンプ。 - - 所属するトレースを表す `trace_id` - - 親スパンを指す `parent_id`(ある場合) - - スパンに関する情報を含む `span_data`。例: `AgentSpanData` はエージェントに関する情報を含み、`GenerationSpanData` は LLM の生成に関する情報を含みます。 +- **トレース** は 1 つの「ワークフロー」のエンドツーエンドの操作を表します。トレースはスパンで構成されます。トレースには以下のプロパティがあります: + - `workflow_name`: 論理的なワークフローやアプリ名です。例: "Code generation" や "Customer service" など。 + - `trace_id`: トレースの一意な ID です。指定しない場合は自動生成されます。フォーマットは `trace_<32_alphanumeric>` である必要があります。 + - `group_id`: オプションのグループ ID で、同じ会話からの複数のトレースをリンクするために使用します。例: チャットスレッド ID など。 + - `disabled`: True の場合、このトレースは記録されません。 + - `metadata`: トレースに付加するオプションのメタデータです。 +- **スパン** は開始時刻と終了時刻を持つ操作を表します。スパンには以下があります: + - `started_at` および `ended_at` タイムスタンプ + - 所属するトレースを示す `trace_id` + - このスパンの親スパンを指す `parent_id`(存在する場合) + - スパンに関する情報を含む `span_data`。例: `AgentSpanData` はエージェントに関する情報、`GenerationSpanData` は LLM 生成に関する情報など。 ## デフォルトのトレーシング -デフォルトで、SDK は以下をトレースします: +デフォルトでは、 SDK は以下をトレースします: -- `Runner.{run, run_sync, run_streamed}()` 全体が `trace()` でラップされます。 -- エージェントが実行されるたびに、`agent_span()` でラップされます。 -- LLM の生成は `generation_span()` でラップされます。 -- 関数ツールの呼び出しはそれぞれ `function_span()` でラップされます。 -- ガードレールは `guardrail_span()` でラップされます。 -- ハンドオフは `handoff_span()` でラップされます。 -- 音声入力(音声からテキスト)は `transcription_span()` でラップされます。 -- 音声出力(テキストから音声)は `speech_span()` でラップされます。 -- 関連する音声スパンは `speech_group_span()` の下に配置されることがあります。 +- `Runner.{run, run_sync, run_streamed}()` 全体が `trace()` でラップされます。 +- エージェントが実行されるたびに `agent_span()` でラップされます。 +- LLM 生成は `generation_span()` でラップされます。 +- 関数ツール呼び出しはそれぞれ `function_span()` でラップされます。 +- ガードレールは `guardrail_span()` でラップされます。 +- ハンドオフは `handoff_span()` でラップされます。 +- 音声入力(音声からテキスト)は `transcription_span()` でラップされます。 +- 音声出力(テキストから音声)は `speech_span()` でラップされます。 +- 関連する音声スパンは `speech_group_span()` の下にまとめられる場合があります。 -デフォルトでは、トレースは "Agent trace" と名付けられています。`trace` を使用する場合、この名前を設定することができ、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定できます。 +デフォルトでは、トレース名は "Agent trace" です。`trace` を使用する場合、この名前を設定できます。また、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定することも可能です。 -さらに、[カスタムトレースプロセッサー](#custom-tracing-processors)を設定して、トレースを他の宛先に送信することもできます(置き換えまたは二次的な宛先として)。 +さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを他の宛先に送信することもできます(置き換えや追加の宛先として)。 -## 高レベルのトレース +## より高レベルのトレース -時には、複数の `run()` 呼び出しを単一のトレースの一部にしたいことがあります。これを行うには、コード全体を `trace()` でラップします。 +複数回の `run()` 呼び出しを 1 つのトレースにまとめたい場合があります。その場合、コード全体を `trace()` でラップしてください。 ```python from agents import Agent, Runner, trace @@ -60,57 +60,57 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. `Runner.run` への2つの呼び出しが `with trace()` でラップされているため、個々の実行は2つのトレースを作成するのではなく、全体のトレースの一部になります。 +1. 2 回の `Runner.run` 呼び出しが `with trace()` でラップされているため、個々の実行は 2 つのトレースを作成するのではなく、全体のトレースの一部となります。 ## トレースの作成 -[`trace()`][agents.tracing.trace] 関数を使用してトレースを作成できます。トレースは開始と終了が必要です。以下の2つの方法があります: +[`trace()`][agents.tracing.trace] 関数を使ってトレースを作成できます。トレースは開始と終了が必要です。方法は 2 つあります: -1. **推奨**: トレースをコンテキストマネージャーとして使用します。つまり、`with trace(...) as my_trace` とします。これにより、トレースが自動的に適切なタイミングで開始および終了します。 -2. 手動で [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を呼び出すこともできます。 +1. **推奨**: トレースをコンテキストマネージャーとして使用します。例: `with trace(...) as my_trace`。これにより、トレースの開始と終了が自動的に行われます。 +2. [`trace.start()`][agents.tracing.Trace.start] および [`trace.finish()`][agents.tracing.Trace.finish] を手動で呼び出すこともできます。 -現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡されます。これにより、並行処理でも自動的に機能します。トレースを手動で開始/終了する場合は、`start()`/`finish()` に `mark_as_current` と `reset_current` を渡して現在のトレースを更新する必要があります。 +現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で管理されています。これにより、自動的に並行処理にも対応します。トレースを手動で開始・終了する場合は、`start()`/`finish()` に `mark_as_current` および `reset_current` を渡して現在のトレースを更新する必要があります。 ## スパンの作成 -さまざまな [`*_span()`][agents.tracing.create] メソッドを使用してスパンを作成できます。一般的に、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数が利用可能です。 +さまざまな [`*_span()`][agents.tracing.create] メソッドを使ってスパンを作成できます。通常、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数も用意されています。 -スパンは自動的に現在のトレースの一部となり、Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡される最も近い現在のスパンの下にネストされます。 +スパンは自動的に現在のトレースの一部となり、最も近い現在のスパンの下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で管理されています。 -## センシティブデータ +## 機微なデータ -特定のスパンは潜在的にセンシティブなデータをキャプチャすることがあります。 +一部のスパンは、機微なデータを記録する場合があります。 -`generation_span()` は LLM 生成の入力/出力を保存し、`function_span()` は関数呼び出しの入力/出力を保存します。これらにはセンシティブなデータが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] を使用してそのデータのキャプチャを無効にすることができます。 +`generation_span()` は LLM 生成の入力・出力を保存し、`function_span()` は関数呼び出しの入力・出力を保存します。これらには機微なデータが含まれる場合があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] でそのデータの記録を無効化できます。 -同様に、音声スパンにはデフォルトで入力および出力音声の base64 エンコードされた PCM データが含まれます。この音声データのキャプチャを無効にするには、[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定します。 +同様に、音声スパンはデフォルトで入力・出力音声の base64 エンコード PCM データを含みます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定することで、この音声データの記録を無効化できます。 -## カスタムトレーシングプロセッサー +## カスタムトレースプロセッサー -トレーシングの高レベルアーキテクチャは次のとおりです: +トレーシングの高レベルなアーキテクチャは以下の通りです: -- 初期化時に、トレースを作成する責任を持つグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 -- `TraceProvider` を [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] で構成し、トレース/スパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信し、OpenAI バックエンドにバッチでスパンとトレースをエクスポートします。 +- 初期化時にグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成し、トレースの生成を担当します。 +- `TraceProvider` は [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] で構成されており、トレースやスパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信します。これにより、スパンやトレースが OpenAI バックエンドにバッチでエクスポートされます。 -このデフォルトのセットアップをカスタマイズして、トレースを代替または追加のバックエンドに送信したり、エクスポーターの動作を変更したりするには、次の2つのオプションがあります: +このデフォルト設定をカスタマイズし、トレースを別のバックエンドや追加のバックエンドに送信したり、エクスポーターの動作を変更したりするには、2 つの方法があります: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] を使用して、トレースとスパンが準備できたときに受け取る**追加の**トレースプロセッサーを追加できます。これにより、OpenAI のバックエンドにトレースを送信することに加えて、独自の処理を行うことができます。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] を使用して、デフォルトのプロセッサーを独自のトレースプロセッサーに**置き換える**ことができます。これにより、`TracingProcessor` を含めない限り、トレースは OpenAI バックエンドに送信されません。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] を使うと、**追加の** トレースプロセッサーを追加できます。これにより、トレースやスパンが準備できた時点で独自の処理を行うことができ、OpenAI バックエンドへの送信に加えて独自の処理が可能です。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] を使うと、デフォルトのプロセッサーを独自のトレースプロセッサーに**置き換える**ことができます。この場合、OpenAI バックエンドにトレースが送信されるのは、`TracingProcessor` を含めた場合のみです。 -## 外部トレーシングプロセッサーリスト +## 外部トレースプロセッサー一覧 -- [Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents) -- [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) -- [MLflow (self-hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) -- [MLflow (Databricks hosted](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) -- [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) -- [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents) -- [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk) -- [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration) -- [Keywords AI](https://docs.keywordsai.co/integration/development-frameworks/openai-agent) -- [LangSmith](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_openai_agents_sdk) -- [Maxim AI](https://www.getmaxim.ai/docs/observe/integrations/openai-agents-sdk) -- [Comet Opik](https://www.comet.com/docs/opik/tracing/integrations/openai_agents) -- [Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents) -- [Langtrace](https://docs.langtrace.ai/supported-integrations/llm-frameworks/openai-agents-sdk) -- [Okahu-Monocle](https://github.com/monocle2ai/monocle) \ No newline at end of file +- [Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents) +- [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) +- [MLflow (self-hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) +- [MLflow (Databricks hosted](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) +- [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) +- [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents) +- [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk) +- [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration) +- [Keywords AI](https://docs.keywordsai.co/integration/development-frameworks/openai-agent) +- [LangSmith](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_openai_agents_sdk) +- [Maxim AI](https://www.getmaxim.ai/docs/observe/integrations/openai-agents-sdk) +- [Comet Opik](https://www.comet.com/docs/opik/tracing/integrations/openai_agents) +- [Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents) +- [Langtrace](https://docs.langtrace.ai/supported-integrations/llm-frameworks/openai-agents-sdk) +- [Okahu-Monocle](https://github.com/monocle2ai/monocle) \ No newline at end of file diff --git a/docs/ja/visualization.md b/docs/ja/visualization.md index 1054dd12..c93b3836 100644 --- a/docs/ja/visualization.md +++ b/docs/ja/visualization.md @@ -1,10 +1,10 @@ # エージェントの可視化 -エージェントの可視化では、**Graphviz** を使用してエージェントとその関係の構造化されたグラフィカル表現を生成できます。これは、アプリケーション内でエージェント、ツール、ハンドオフがどのように相互作用するかを理解するのに役立ちます。 +エージェントの可視化では、 **Graphviz** を使用してエージェントとその関係性を構造的にグラフィカルに表現できます。これは、アプリケーション内でエージェント、ツール、ハンドオフがどのように相互作用しているかを理解するのに役立ちます。 ## インストール -オプションの `viz` 依存関係グループをインストールします: +オプションの `viz` 依存グループをインストールします: ```bash pip install "openai-agents[viz]" @@ -12,11 +12,11 @@ pip install "openai-agents[viz]" ## グラフの生成 -`draw_graph` 関数を使用してエージェントの可視化を生成できます。この関数は、以下のような有向グラフを作成します: +`draw_graph` 関数を使ってエージェントの可視化を生成できます。この関数は、以下のような有向グラフを作成します: - **エージェント** は黄色のボックスで表されます。 - **ツール** は緑色の楕円で表されます。 -- **ハンドオフ** はあるエージェントから別のエージェントへの有向エッジです。 +- **ハンドオフ** は、あるエージェントから別のエージェントへの有向エッジで表されます。 ### 使用例 @@ -50,31 +50,32 @@ draw_graph(triage_agent) ![Agent Graph](../assets/images/graph.png) -これにより、**トリアージエージェント** の構造とサブエージェントおよびツールへの接続を視覚的に表現するグラフが生成されます。 +これにより、 **triage agent** の構造とサブエージェントやツールとの接続が視覚的に表現されたグラフが生成されます。 + ## 可視化の理解 生成されたグラフには以下が含まれます: -- エントリーポイントを示す **開始ノード** (`__start__`)。 -- 黄色で塗りつぶされた **長方形** で表されるエージェント。 -- 緑色で塗りつぶされた **楕円** で表されるツール。 +- エントリーポイントを示す **start node** (`__start__`) +- 黄色で塗りつぶされた **長方形** で表されるエージェント +- 緑色で塗りつぶされた **楕円** で表されるツール - 相互作用を示す有向エッジ: - - エージェント間のハンドオフには **実線の矢印**。 - - ツール呼び出しには **点線の矢印**。 -- 実行が終了する場所を示す **終了ノード** (`__end__`)。 + - エージェント間のハンドオフには **実線の矢印** + - ツール呼び出しには **点線の矢印** +- 実行が終了する場所を示す **end node** (`__end__`) ## グラフのカスタマイズ ### グラフの表示 -デフォルトでは、`draw_graph` はグラフをインラインで表示します。別ウィンドウでグラフを表示するには、次のように記述します: +デフォルトでは、`draw_graph` はグラフをインラインで表示します。グラフを別ウィンドウで表示したい場合は、次のように記述します: ```python draw_graph(triage_agent).view() ``` ### グラフの保存 -デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存するには、ファイル名を指定します: +デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存したい場合は、ファイル名を指定します: ```python draw_graph(triage_agent, filename="agent_graph.png") diff --git a/docs/ja/voice/pipeline.md b/docs/ja/voice/pipeline.md index 6df7dd04..50661076 100644 --- a/docs/ja/voice/pipeline.md +++ b/docs/ja/voice/pipeline.md @@ -1,6 +1,6 @@ # パイプラインとワークフロー -[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] は、エージェントのワークフローを音声アプリに変換するのを簡単にするクラスです。実行するワークフローを渡すと、パイプラインが入力音声の文字起こし、音声の終了検出、適切なタイミングでのワークフロー呼び出し、ワークフロー出力の音声化を行います。 +[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] は、エージェントワークフローを音声アプリに簡単に変換できるクラスです。実行したいワークフローを渡すと、パイプラインが入力音声の文字起こし、音声終了の検出、適切なタイミングでのワークフロー呼び出し、ワークフロー出力の音声化までを自動で処理します。 ```mermaid graph LR @@ -30,28 +30,28 @@ graph LR ## パイプラインの設定 -パイプラインを作成する際に、以下のことを設定できます: +パイプラインを作成する際、以下の項目を設定できます。 -1. [`workflow`][agents.voice.workflow.VoiceWorkflowBase]:新しい音声が文字起こしされるたびに実行されるコード。 -2. 使用する [`speech-to-text`][agents.voice.model.STTModel] および [`text-to-speech`][agents.voice.model.TTSModel] モデル。 +1. [`workflow`][agents.voice.workflow.VoiceWorkflowBase]:新しい音声が文字起こしされるたびに実行されるコードです。 +2. 使用する [`speech-to-text`][agents.voice.model.STTModel] および [`text-to-speech`][agents.voice.model.TTSModel] モデル 3. [`config`][agents.voice.pipeline_config.VoicePipelineConfig]:以下のような設定が可能です。 - - モデルプロバイダー:モデル名をモデルにマッピングできます。 - - トレーシング:トレーシングを無効にするかどうか、音声ファイルのアップロード、ワークフロー名、トレース ID など。 - - TTS および STT モデルの設定:プロンプト、言語、使用するデータ型など。 + - モデルプロバイダー:モデル名をモデルにマッピングできます + - トレーシング:トレーシングの有効/無効、音声ファイルのアップロード有無、ワークフロー名、トレース ID など + - TTS および STT モデルの設定:プロンプト、言語、使用するデータ型など ## パイプラインの実行 -パイプラインは [`run()`][agents.voice.pipeline.VoicePipeline.run] メソッドを通じて実行できます。これにより、2 つの形式で音声入力を渡すことができます: +パイプラインは [`run()`][agents.voice.pipeline.VoicePipeline.run] メソッドで実行できます。音声入力は 2 つの形式で渡せます。 -1. [`AudioInput`][agents.voice.input.AudioInput]:完全な音声トランスクリプトがある場合に使用し、その結果を生成するだけです。これは、話者が話し終わったときの検出が不要な場合に便利です。例えば、事前録音された音声や、ユーザーが話し終わったことが明確なプッシュ・トゥ・トークアプリで使用します。 -2. [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput]:ユーザーが話し終わったときの検出が必要な場合に使用します。音声チャンクを検出時にプッシュでき、音声パイプラインが自動的にエージェントワークフローを適切なタイミングで実行します。これは「アクティビティ検出」と呼ばれるプロセスを通じて行われます。 +1. [`AudioInput`][agents.voice.input.AudioInput]:完全な音声トランスクリプトがある場合に使用し、その内容に対する結果のみを生成します。話者が話し終えたタイミングを検出する必要がない場合(例:事前録音音声や push-to-talk アプリなど、ユーザーが話し終えたことが明確な場合)に便利です。 +2. [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput]:ユーザーが話し終えたタイミングを検出する必要がある場合に使用します。音声チャンクを検出ごとにプッシュでき、VoicePipeline が「アクティビティ検出」と呼ばれるプロセスを通じて、適切なタイミングでエージェントワークフローを自動実行します。 ## 結果 -音声パイプライン実行の結果は [`StreamedAudioResult`][agents.voice.result.StreamedAudioResult] です。これは、イベントが発生するたびにストリーミングできるオブジェクトです。いくつかの種類の [`VoiceStreamEvent`][agents.voice.events.VoiceStreamEvent] があります: +VoicePipeline 実行の結果は [`StreamedAudioResult`][agents.voice.result.StreamedAudioResult] です。これは、イベントが発生するたびにストリーミングで受け取れるオブジェクトです。いくつかの種類の [`VoiceStreamEvent`][agents.voice.events.VoiceStreamEvent] があります。 -1. [`VoiceStreamEventAudio`][agents.voice.events.VoiceStreamEventAudio]:音声チャンクを含むイベント。 -2. [`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle]:ターンの開始や終了などのライフサイクルイベントを通知します。 +1. [`VoiceStreamEventAudio`][agents.voice.events.VoiceStreamEventAudio]:音声チャンクを含みます。 +2. [`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle]:ターンの開始や終了など、ライフサイクルイベントを通知します。 3. [`VoiceStreamEventError`][agents.voice.events.VoiceStreamEventError]:エラーイベントです。 ```python @@ -70,6 +70,6 @@ async for event in result.stream(): ## ベストプラクティス -### 中断 +### 割り込み -Agents SDK は現在、[`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] に対する組み込みの中断サポートを提供していません。代わりに、検出されたターンごとにワークフローの別の実行をトリガーします。アプリケーション内で中断を処理したい場合は、[`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] イベントを監視できます。`turn_started` は新しいターンが文字起こしされ、処理が始まったことを示します。`turn_ended` は、関連するすべての音声が送信された後にトリガーされます。これらのイベントを使用して、モデルがターンを開始したときにスピーカーのマイクをミュートし、関連する音声をすべてフラッシュした後にアンミュートすることができます。 \ No newline at end of file +Agents SDK は現在、[`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] に対する組み込みの割り込みサポートを提供していません。そのため、検出された各ターンごとにワークフローの個別実行がトリガーされます。アプリケーション内で割り込みを処理したい場合は、[`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] イベントをリッスンできます。`turn_started` は新しいターンが文字起こしされ、処理が開始されたことを示します。`turn_ended` は該当ターンのすべての音声が送信された後にトリガーされます。これらのイベントを利用して、モデルがターンを開始した際に話者のマイクをミュートし、ターンに関連するすべての音声を送信し終えた後にアンミュートする、といった制御が可能です。 \ No newline at end of file diff --git a/docs/ja/voice/quickstart.md b/docs/ja/voice/quickstart.md index 77e5ef27..9c874020 100644 --- a/docs/ja/voice/quickstart.md +++ b/docs/ja/voice/quickstart.md @@ -2,7 +2,7 @@ ## 前提条件 -Agents SDK の基本的な[クイックスタート手順](../quickstart.md)に従い、仮想環境を設定してください。その後、SDK からオプションの音声依存関係をインストールします。 +Agents SDK の[クイックスタート手順](../quickstart.md)に従い、仮想環境をセットアップしてください。その後、SDK からオプションの音声依存関係をインストールします。 ```bash pip install 'openai-agents[voice]' @@ -10,11 +10,11 @@ pip install 'openai-agents[voice]' ## コンセプト -知っておくべき主なコンセプトは、[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] です。これは3ステップのプロセスです。 +主なコンセプトは [`VoicePipeline`][agents.voice.pipeline.VoicePipeline] です。これは 3 ステップのプロセスです: -1. 音声をテキストに変換する音声認識モデルを実行します。 -2. 通常はエージェントワークフローであるコードを実行して、結果を生成します。 -3. 結果のテキストを音声に戻すテキスト読み上げモデルを実行します。 +1. 音声をテキストに変換する音声認識モデル(speech-to-text)を実行します。 +2. 通常はエージェント的なワークフローであるあなたのコードを実行し、結果を生成します。 +3. 結果のテキストを音声に戻す音声合成モデル(text-to-speech)を実行します。 ```mermaid graph LR @@ -44,7 +44,7 @@ graph LR ## エージェント -まず、いくつかのエージェントを設定しましょう。この SDK でエージェントを構築したことがある場合、これは馴染みがあるはずです。エージェント、ハンドオフ、ツールを用意します。 +まず、いくつかのエージェントをセットアップしましょう。この SDK でエージェントを作成したことがあれば、馴染みがあるはずです。ここでは、複数のエージェント、ハンドオフ、ツールを用意します。 ```python import asyncio @@ -88,14 +88,14 @@ agent = Agent( ## 音声パイプライン -[`SingleAgentVoiceWorkflow`][agents.voice.workflow.SingleAgentVoiceWorkflow] をワークフローとして使用して、シンプルな音声パイプラインを設定します。 +[`SingleAgentVoiceWorkflow`][agents.voice.workflow.SingleAgentVoiceWorkflow] をワークフローとして使い、シンプルな音声パイプラインをセットアップします。 ```python from agents.voice import SingleAgentVoiceWorkflow, VoicePipeline pipeline = VoicePipeline(workflow=SingleAgentVoiceWorkflow(agent)) ``` -## パイプラインを実行する +## パイプラインの実行 ```python import numpy as np @@ -191,4 +191,4 @@ if __name__ == "__main__": asyncio.run(main()) ``` -この例を実行すると、エージェントがあなたに話しかけます! [examples/voice/static](https://github.com/openai/openai-agents-python/tree/main/examples/voice/static) の例をチェックして、自分でエージェントと話すデモを見てください。 \ No newline at end of file +この例を実行すると、エージェントがあなたに話しかけます![examples/voice/static](https://github.com/openai/openai-agents-python/tree/main/examples/voice/static) のコード例をチェックして、実際にエージェントと会話できるデモをご覧ください。 \ No newline at end of file diff --git a/docs/ja/voice/tracing.md b/docs/ja/voice/tracing.md index 1a1df278..639a95f2 100644 --- a/docs/ja/voice/tracing.md +++ b/docs/ja/voice/tracing.md @@ -1,14 +1,14 @@ # トレーシング -[エージェントがトレースされる](../tracing.md)のと同様に、音声パイプラインも自動的にトレースされます。 +[エージェントのトレーシング](../tracing.md)と同様に、音声パイプラインも自動的にトレーシングされます。 -基本的なトレーシング情報については上記のトレーシングドキュメントを参照できますが、パイプラインのトレーシングは [`VoicePipelineConfig`][agents.voice.pipeline_config.VoicePipelineConfig] を通じて追加で設定できます。 +基本的なトレーシング情報については上記のトレーシングドキュメントをご参照いただけますが、さらに [`VoicePipelineConfig`][agents.voice.pipeline_config.VoicePipelineConfig] を使ってパイプラインのトレーシングを設定することも可能です。 -トレーシングに関連する主なフィールドは次のとおりです: +主なトレーシング関連フィールドは以下の通りです: -- [`tracing_disabled`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]: トレーシングを無効にするかどうかを制御します。デフォルトでは、トレーシングは有効です。 -- [`trace_include_sensitive_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_data]: トレースに音声トランスクリプトのような潜在的に機密性のあるデータを含めるかどうかを制御します。これは特に音声パイプライン用であり、ワークフロー内で行われることには関係ありません。 -- [`trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]: トレースに音声データを含めるかどうかを制御します。 -- [`workflow_name`][agents.voice.pipeline_config.VoicePipelineConfig.workflow_name]: トレースワークフローの名前です。 -- [`group_id`][agents.voice.pipeline_config.VoicePipelineConfig.group_id]: 複数のトレースをリンクするための `group_id` です。 -- [`trace_metadata`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]: トレースに含める追加のメタデータです。 \ No newline at end of file +- [`tracing_disabled`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレーシングを無効にするかどうかを制御します。デフォルトではトレーシングは有効です。 +- [`trace_include_sensitive_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_data]:トレースに音声の書き起こしなど、機密性の高いデータを含めるかどうかを制御します。これは特に音声パイプライン用であり、Workflow 内部で発生する内容には適用されません。 +- [`trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]:トレースに音声データを含めるかどうかを制御します。 +- [`workflow_name`][agents.voice.pipeline_config.VoicePipelineConfig.workflow_name]:トレースワークフローの名前です。 +- [`group_id`][agents.voice.pipeline_config.VoicePipelineConfig.group_id]:トレースの `group_id` で、複数のトレースをリンクすることができます。 +- [`trace_metadata`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレースに追加するメタデータです。 \ No newline at end of file diff --git a/docs/scripts/translate_docs.py b/docs/scripts/translate_docs.py index e62a2c09..f261daa0 100644 --- a/docs/scripts/translate_docs.py +++ b/docs/scripts/translate_docs.py @@ -7,7 +7,7 @@ # logging.basicConfig(level=logging.INFO) # logging.getLogger("openai").setLevel(logging.DEBUG) -OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o") +OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4.1") # Define the source and target directories source_dir = "docs" @@ -94,18 +94,17 @@ def built_instructions(target_language: str, lang_code: str) -> str: ) return f"""You are an expert technical translator. -Your task: translate the markdown passed as a user input from English into {target_language}. +Your task: translate the markdown passed as a user input from English into {target_language}. +The inputs are the official OpenAI Agents SDK framework documentation, and your translation outputs'll be used for serving the official {target_language} version of them. Thus, accuracy, clarity, and fidelity to the original are critical. ############################ ## OUTPUT REQUIREMENTS ## ############################ -- Return **only** the translated markdown, with the original markdown structure preserved. -- Do **not** add explanations, comments, or metadata. +You must return **only** the translated markdown. Do not include any commentary, metadata, or explanations. The original markdown structure must be strictly preserved. ######################### ## GENERAL RULES ## ######################### -- The output quality must be great enough to be used for public documentation. - Be professional and polite. - Keep the tone **natural** and concise. - Do not omit any content. If a segment should stay in English, copy it verbatim. @@ -149,9 +148,17 @@ def built_instructions(target_language: str, lang_code: str) -> str: If you are uncertain about a term, leave the original English term in parentheses after your translation. ######################### -## FINAL REMINDER ## +## WORKFLOW ## ######################### -Return **only** the translated markdown text. No extra commentary. + +Follow the following workflow to translate the given markdown text data: + +1. Read the input markdown text given by the user. +2. Translate the markdown file into {target_language}, carefully following the requirements above. +3. Perform a self-review to evaluate the quality of the translation, focusing on naturalness, accuracy, and consistency in detail. +4. If improvements are necessary, refine the content without changing the original meaning. +5. Continue improving the translation until you are fully satisfied with the result. +6. Once the final output is ready, return **only** the translated markdown text. No extra commentary. """ diff --git a/mkdocs.yml b/mkdocs.yml index 3eaf0f4b..51d65e69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -127,7 +127,7 @@ plugins: nav: - はじめに: index.md - クイックスタート: quickstart.md - - サンプル例: examples.md + - コード例: examples.md - ドキュメント: - agents.md - running_agents.md From 96a97af9be9a30c6ca0c2e811425043d53a5ee3f Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 14 Apr 2025 22:45:30 -0400 Subject: [PATCH 39/85] v0.0.10 (#514) --- pyproject.toml | 2 +- uv.lock | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 251a6ab5..c3e46a66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.9" +version = "0.0.10" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index a762c723..1212f790 100644 --- a/uv.lock +++ b/uv.lock @@ -1108,13 +1108,11 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.9" +version = "0.0.10" source = { editable = "." } dependencies = [ - { name = "eval-type-backport" }, { name = "griffe" }, { name = "mcp", marker = "python_full_version >= '3.10'" }, - { name = "mkdocs-static-i18n" }, { name = "openai" }, { name = "pydantic" }, { name = "requests" }, @@ -1134,6 +1132,7 @@ voice = [ [package.dev-dependencies] dev = [ { name = "coverage" }, + { name = "eval-type-backport" }, { name = "graphviz" }, { name = "inline-snapshot" }, { name = "mkdocs" }, @@ -1156,11 +1155,9 @@ dev = [ [package.metadata] requires-dist = [ - { name = "eval-type-backport", specifier = ">=0.2.2" }, { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, - { name = "mkdocs-static-i18n", specifier = ">=1.3.0" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.66.5" }, { name = "pydantic", specifier = ">=2.10,<3" }, @@ -1174,11 +1171,13 @@ provides-extras = ["voice", "viz"] [package.metadata.requires-dev] dev = [ { name = "coverage", specifier = ">=7.6.12" }, + { name = "eval-type-backport", specifier = ">=0.2.2" }, { name = "graphviz" }, { name = "inline-snapshot", specifier = ">=0.20.7" }, { name = "mkdocs", specifier = ">=1.6.0" }, { name = "mkdocs-material", specifier = ">=9.6.0" }, { name = "mkdocs-static-i18n" }, + { name = "mkdocs-static-i18n", specifier = ">=1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.28.0" }, { name = "mypy" }, { name = "playwright", specifier = "==1.50.0" }, From b978b4382ef7e6b7f78982e0d0a9c3c758ec886c Mon Sep 17 00:00:00 2001 From: RonaldChungHueyWu <10584547+ronaldchwu@users.noreply.github.com> Date: Tue, 15 Apr 2025 12:48:01 +1000 Subject: [PATCH 40/85] Support parallel_tool_calls=False for ChatCompletion API (#513) The current ChatCompletion API supports only `parallel_tool_calls=True` or `parallel_tool_calls=NOT_GIVEN` This PR is to support setting `parallel_tool_calls=False`, a common requirement in controlling agent tool use patterns (e.g. ensuring one tool call at the time, to facilitate desired tool calling sequence). I followed the merged [PR#333](https://github.com/openai/openai-agents-python/pull/333) for consistency. --- src/agents/models/openai_chatcompletions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index c12fe68a..712a7998 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -500,7 +500,11 @@ async def _fetch_response( span.span_data.input = converted_messages parallel_tool_calls = ( - True if model_settings.parallel_tool_calls and tools and len(tools) > 0 else NOT_GIVEN + True + if model_settings.parallel_tool_calls and tools and len(tools) > 0 + else False + if model_settings.parallel_tool_calls is False + else NOT_GIVEN ) tool_choice = _Converter.convert_tool_choice(model_settings.tool_choice) response_format = _Converter.convert_response_format(output_schema) From f329eef7e8f1fa24f9378ef3ef838d99244dd7a7 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Tue, 15 Apr 2025 12:46:31 -0400 Subject: [PATCH 41/85] Examples and tests for previous_response_id (#512) Examples + tests --- examples/basic/previous_response_id.py | 66 ++++++++++++++++++++ tests/fake_model.py | 9 +++ tests/test_agent_runner.py | 83 ++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 examples/basic/previous_response_id.py diff --git a/examples/basic/previous_response_id.py b/examples/basic/previous_response_id.py new file mode 100644 index 00000000..b00bf3aa --- /dev/null +++ b/examples/basic/previous_response_id.py @@ -0,0 +1,66 @@ +import asyncio + +from agents import Agent, Runner + +"""This demonstrates usage of the `previous_response_id` parameter to continue a conversation. +The second run passes the previous response ID to the model, which allows it to continue the +conversation without re-sending the previous messages. + +Notes: +1. This only applies to the OpenAI Responses API. Other models will ignore this parameter. +2. Responses are only stored for 30 days as of this writing, so in production you should +store the response ID along with an expiration date; if the response is no longer valid, +you'll need to re-send the previous conversation history. +""" + + +async def main(): + agent = Agent( + name="Assistant", + instructions="You are a helpful assistant. be VERY concise.", + ) + + result = await Runner.run(agent, "What is the largest country in South America?") + print(result.final_output) + # Brazil + + result = await Runner.run( + agent, + "What is the capital of that country?", + previous_response_id=result.last_response_id, + ) + print(result.final_output) + # Brasilia + + +async def main_stream(): + agent = Agent( + name="Assistant", + instructions="You are a helpful assistant. be VERY concise.", + ) + + result = Runner.run_streamed(agent, "What is the largest country in South America?") + + async for event in result.stream_events(): + if event.type == "raw_response_event" and event.data.type == "response.output_text.delta": + print(event.data.delta, end="", flush=True) + + print() + + result = Runner.run_streamed( + agent, + "What is the capital of that country?", + previous_response_id=result.last_response_id, + ) + + async for event in result.stream_events(): + if event.type == "raw_response_event" and event.data.type == "response.output_text.delta": + print(event.data.delta, end="", flush=True) + + +if __name__ == "__main__": + is_stream = input("Run in stream mode? (y/n): ") + if is_stream == "y": + asyncio.run(main_stream()) + else: + asyncio.run(main()) diff --git a/tests/fake_model.py b/tests/fake_model.py index 203479d0..52d3a3b2 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -63,6 +63,7 @@ async def get_response( "model_settings": model_settings, "tools": tools, "output_schema": output_schema, + "previous_response_id": previous_response_id, } with generation_span(disabled=not self.tracing_enabled) as span: @@ -98,6 +99,14 @@ async def stream_response( *, previous_response_id: str | None, ) -> AsyncIterator[TResponseStreamEvent]: + self.last_turn_args = { + "system_instructions": system_instructions, + "input": input, + "model_settings": model_settings, + "tools": tools, + "output_schema": output_schema, + "previous_response_id": previous_response_id, + } with generation_span(disabled=not self.tracing_enabled) as span: output = self.get_next_output() if isinstance(output, Exception): diff --git a/tests/test_agent_runner.py b/tests/test_agent_runner.py index 4f277656..14a278a9 100644 --- a/tests/test_agent_runner.py +++ b/tests/test_agent_runner.py @@ -662,3 +662,86 @@ async def test_model_settings_override(): # temperature is overridden by Runner.run, but max_tokens is not assert model.last_turn_args["model_settings"].temperature == 0.5 assert model.last_turn_args["model_settings"].max_tokens == 1000 + + +@pytest.mark.asyncio +async def test_previous_response_id_passed_between_runs(): + """Test that previous_response_id is passed to the model on subsequent runs.""" + model = FakeModel() + model.set_next_output([get_text_message("done")]) + agent = Agent(name="test", model=model) + + assert model.last_turn_args.get("previous_response_id") is None + await Runner.run(agent, input="test", previous_response_id="resp-non-streamed-test") + assert model.last_turn_args.get("previous_response_id") == "resp-non-streamed-test" + + +@pytest.mark.asyncio +async def test_multi_turn_previous_response_id_passed_between_runs(): + """Test that previous_response_id is passed to the model on subsequent runs.""" + + model = FakeModel() + agent = Agent( + name="test", + model=model, + tools=[get_function_tool("foo", "tool_result")], + ) + + model.add_multiple_turn_outputs( + [ + # First turn: a message and tool call + [get_text_message("a_message"), get_function_tool_call("foo", json.dumps({"a": "b"}))], + # Second turn: text message + [get_text_message("done")], + ] + ) + + assert model.last_turn_args.get("previous_response_id") is None + await Runner.run(agent, input="test", previous_response_id="resp-test-123") + assert model.last_turn_args.get("previous_response_id") == "resp-test-123" + + +@pytest.mark.asyncio +async def test_previous_response_id_passed_between_runs_streamed(): + """Test that previous_response_id is passed to the model on subsequent streamed runs.""" + model = FakeModel() + model.set_next_output([get_text_message("done")]) + agent = Agent( + name="test", + model=model, + ) + + assert model.last_turn_args.get("previous_response_id") is None + result = Runner.run_streamed(agent, input="test", previous_response_id="resp-stream-test") + async for _ in result.stream_events(): + pass + + assert model.last_turn_args.get("previous_response_id") == "resp-stream-test" + + +@pytest.mark.asyncio +async def test_previous_response_id_passed_between_runs_streamed_multi_turn(): + """Test that previous_response_id is passed to the model on subsequent streamed runs.""" + + model = FakeModel() + agent = Agent( + name="test", + model=model, + tools=[get_function_tool("foo", "tool_result")], + ) + + model.add_multiple_turn_outputs( + [ + # First turn: a message and tool call + [get_text_message("a_message"), get_function_tool_call("foo", json.dumps({"a": "b"}))], + # Second turn: text message + [get_text_message("done")], + ] + ) + + assert model.last_turn_args.get("previous_response_id") is None + result = Runner.run_streamed(agent, input="test", previous_response_id="resp-stream-test") + async for _ in result.stream_events(): + pass + + assert model.last_turn_args.get("previous_response_id") == "resp-stream-test" From e625cb495e06df94c76d14282c4c4cc8590fad75 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Tue, 15 Apr 2025 12:48:27 -0400 Subject: [PATCH 42/85] Only include stream_options when streaming (#519) Closes #518 --- src/agents/models/openai_chatcompletions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 712a7998..a4f93cb7 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -528,7 +528,9 @@ async def _fetch_response( reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None store = _Converter.get_store_param(self._get_client(), model_settings) - stream_options = _Converter.get_stream_options_param(self._get_client(), model_settings) + stream_options = _Converter.get_stream_options_param( + self._get_client(), model_settings, stream=stream + ) ret = await self._get_client().chat.completions.create( model=self.model, @@ -591,8 +593,11 @@ def get_store_param(cls, client: AsyncOpenAI, model_settings: ModelSettings) -> @classmethod def get_stream_options_param( - cls, client: AsyncOpenAI, model_settings: ModelSettings + cls, client: AsyncOpenAI, model_settings: ModelSettings, stream: bool ) -> dict[str, bool] | None: + if not stream: + return None + default_include_usage = True if cls.is_openai(client) else None include_usage = ( model_settings.include_usage From ca8e8bed5d0f33e8a0bc3eabd5f1b0a183e73765 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Tue, 15 Apr 2025 14:37:16 -0400 Subject: [PATCH 43/85] v0.0.11 (#520) New release, to incorporate #519 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c3e46a66..1eefadd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.10" +version = "0.0.11" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 1212f790..d4b82546 100644 --- a/uv.lock +++ b/uv.lock @@ -1108,7 +1108,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.10" +version = "0.0.11" source = { editable = "." } dependencies = [ { name = "griffe" }, From ce1abe6006eafbf5d93b1694b8227674b2e756f9 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Tue, 15 Apr 2025 18:26:48 -0400 Subject: [PATCH 44/85] Run CI on all commits, not just ones on main (#521) Was not running on my stacked PRs. --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4f93980a..edd0d898 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,8 +5,7 @@ on: branches: - main pull_request: - branches: - - main + # All PRs, including stacked PRs env: UV_FROZEN: "1" From 80de53e8795bcba4c1a250461477bb5042358371 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Tue, 15 Apr 2025 18:31:17 -0400 Subject: [PATCH 45/85] Extract chat completions conversion code into helper (#522) Small refactor for rest of stack. --- [//]: # (BEGIN SAPLING FOOTER) * #524 * #523 * __->__ #522 --- src/agents/models/chatcmpl_converter.py | 466 +++++++++++++++++ src/agents/models/openai_chatcompletions.py | 477 +----------------- tests/test_openai_chatcompletions.py | 14 +- .../test_openai_chatcompletions_converter.py | 74 +-- tests/test_tool_converter.py | 10 +- 5 files changed, 530 insertions(+), 511 deletions(-) create mode 100644 src/agents/models/chatcmpl_converter.py diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py new file mode 100644 index 00000000..00175a16 --- /dev/null +++ b/src/agents/models/chatcmpl_converter.py @@ -0,0 +1,466 @@ +from __future__ import annotations + +import json +from collections.abc import Iterable +from typing import Any, Literal, cast + +from openai import NOT_GIVEN, NotGiven +from openai.types.chat import ( + ChatCompletionAssistantMessageParam, + ChatCompletionContentPartImageParam, + ChatCompletionContentPartParam, + ChatCompletionContentPartTextParam, + ChatCompletionDeveloperMessageParam, + ChatCompletionMessage, + ChatCompletionMessageParam, + ChatCompletionMessageToolCallParam, + ChatCompletionSystemMessageParam, + ChatCompletionToolChoiceOptionParam, + ChatCompletionToolMessageParam, + ChatCompletionUserMessageParam, +) +from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam +from openai.types.chat.completion_create_params import ResponseFormat +from openai.types.responses import ( + EasyInputMessageParam, + ResponseFileSearchToolCallParam, + ResponseFunctionToolCall, + ResponseFunctionToolCallParam, + ResponseInputContentParam, + ResponseInputImageParam, + ResponseInputTextParam, + ResponseOutputMessage, + ResponseOutputMessageParam, + ResponseOutputRefusal, + ResponseOutputText, +) +from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message + +from ..agent_output import AgentOutputSchema +from ..exceptions import AgentsException, UserError +from ..handoffs import Handoff +from ..items import TResponseInputItem, TResponseOutputItem +from ..tool import FunctionTool, Tool +from .fake_id import FAKE_RESPONSES_ID + + +class Converter: + @classmethod + def convert_tool_choice( + cls, tool_choice: Literal["auto", "required", "none"] | str | None + ) -> ChatCompletionToolChoiceOptionParam | NotGiven: + if tool_choice is None: + return NOT_GIVEN + elif tool_choice == "auto": + return "auto" + elif tool_choice == "required": + return "required" + elif tool_choice == "none": + return "none" + else: + return { + "type": "function", + "function": { + "name": tool_choice, + }, + } + + @classmethod + def convert_response_format( + cls, final_output_schema: AgentOutputSchema | None + ) -> ResponseFormat | NotGiven: + if not final_output_schema or final_output_schema.is_plain_text(): + return NOT_GIVEN + + return { + "type": "json_schema", + "json_schema": { + "name": "final_output", + "strict": final_output_schema.strict_json_schema, + "schema": final_output_schema.json_schema(), + }, + } + + @classmethod + def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TResponseOutputItem]: + items: list[TResponseOutputItem] = [] + + message_item = ResponseOutputMessage( + id=FAKE_RESPONSES_ID, + content=[], + role="assistant", + type="message", + status="completed", + ) + if message.content: + message_item.content.append( + ResponseOutputText(text=message.content, type="output_text", annotations=[]) + ) + if message.refusal: + message_item.content.append( + ResponseOutputRefusal(refusal=message.refusal, type="refusal") + ) + if message.audio: + raise AgentsException("Audio is not currently supported") + + if message_item.content: + items.append(message_item) + + if message.tool_calls: + for tool_call in message.tool_calls: + items.append( + ResponseFunctionToolCall( + id=FAKE_RESPONSES_ID, + call_id=tool_call.id, + arguments=tool_call.function.arguments, + name=tool_call.function.name, + type="function_call", + ) + ) + + return items + + @classmethod + def maybe_easy_input_message(cls, item: Any) -> EasyInputMessageParam | None: + if not isinstance(item, dict): + return None + + keys = item.keys() + # EasyInputMessageParam only has these two keys + if keys != {"content", "role"}: + return None + + role = item.get("role", None) + if role not in ("user", "assistant", "system", "developer"): + return None + + if "content" not in item: + return None + + return cast(EasyInputMessageParam, item) + + @classmethod + def maybe_input_message(cls, item: Any) -> Message | None: + if ( + isinstance(item, dict) + and item.get("type") == "message" + and item.get("role") + in ( + "user", + "system", + "developer", + ) + ): + return cast(Message, item) + + return None + + @classmethod + def maybe_file_search_call(cls, item: Any) -> ResponseFileSearchToolCallParam | None: + if isinstance(item, dict) and item.get("type") == "file_search_call": + return cast(ResponseFileSearchToolCallParam, item) + return None + + @classmethod + def maybe_function_tool_call(cls, item: Any) -> ResponseFunctionToolCallParam | None: + if isinstance(item, dict) and item.get("type") == "function_call": + return cast(ResponseFunctionToolCallParam, item) + return None + + @classmethod + def maybe_function_tool_call_output( + cls, + item: Any, + ) -> FunctionCallOutput | None: + if isinstance(item, dict) and item.get("type") == "function_call_output": + return cast(FunctionCallOutput, item) + return None + + @classmethod + def maybe_item_reference(cls, item: Any) -> ItemReference | None: + if isinstance(item, dict) and item.get("type") == "item_reference": + return cast(ItemReference, item) + return None + + @classmethod + def maybe_response_output_message(cls, item: Any) -> ResponseOutputMessageParam | None: + # ResponseOutputMessage is only used for messages with role assistant + if ( + isinstance(item, dict) + and item.get("type") == "message" + and item.get("role") == "assistant" + ): + return cast(ResponseOutputMessageParam, item) + return None + + @classmethod + def extract_text_content( + cls, content: str | Iterable[ResponseInputContentParam] + ) -> str | list[ChatCompletionContentPartTextParam]: + all_content = cls.extract_all_content(content) + if isinstance(all_content, str): + return all_content + out: list[ChatCompletionContentPartTextParam] = [] + for c in all_content: + if c.get("type") == "text": + out.append(cast(ChatCompletionContentPartTextParam, c)) + return out + + @classmethod + def extract_all_content( + cls, content: str | Iterable[ResponseInputContentParam] + ) -> str | list[ChatCompletionContentPartParam]: + if isinstance(content, str): + return content + out: list[ChatCompletionContentPartParam] = [] + + for c in content: + if isinstance(c, dict) and c.get("type") == "input_text": + casted_text_param = cast(ResponseInputTextParam, c) + out.append( + ChatCompletionContentPartTextParam( + type="text", + text=casted_text_param["text"], + ) + ) + elif isinstance(c, dict) and c.get("type") == "input_image": + casted_image_param = cast(ResponseInputImageParam, c) + if "image_url" not in casted_image_param or not casted_image_param["image_url"]: + raise UserError( + f"Only image URLs are supported for input_image {casted_image_param}" + ) + out.append( + ChatCompletionContentPartImageParam( + type="image_url", + image_url={ + "url": casted_image_param["image_url"], + "detail": casted_image_param["detail"], + }, + ) + ) + elif isinstance(c, dict) and c.get("type") == "input_file": + raise UserError(f"File uploads are not supported for chat completions {c}") + else: + raise UserError(f"Unknown content: {c}") + return out + + @classmethod + def items_to_messages( + cls, + items: str | Iterable[TResponseInputItem], + ) -> list[ChatCompletionMessageParam]: + """ + Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam. + + Rules: + - EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam + - EasyInputMessage or InputMessage (role=system) => ChatCompletionSystemMessageParam + - EasyInputMessage or InputMessage (role=developer) => ChatCompletionDeveloperMessageParam + - InputMessage (role=assistant) => Start or flush a ChatCompletionAssistantMessageParam + - response_output_message => Also produces/flushes a ChatCompletionAssistantMessageParam + - tool calls get attached to the *current* assistant message, or create one if none. + - tool outputs => ChatCompletionToolMessageParam + """ + + if isinstance(items, str): + return [ + ChatCompletionUserMessageParam( + role="user", + content=items, + ) + ] + + result: list[ChatCompletionMessageParam] = [] + current_assistant_msg: ChatCompletionAssistantMessageParam | None = None + + def flush_assistant_message() -> None: + nonlocal current_assistant_msg + if current_assistant_msg is not None: + # The API doesn't support empty arrays for tool_calls + if not current_assistant_msg.get("tool_calls"): + del current_assistant_msg["tool_calls"] + result.append(current_assistant_msg) + current_assistant_msg = None + + def ensure_assistant_message() -> ChatCompletionAssistantMessageParam: + nonlocal current_assistant_msg + if current_assistant_msg is None: + current_assistant_msg = ChatCompletionAssistantMessageParam(role="assistant") + current_assistant_msg["tool_calls"] = [] + return current_assistant_msg + + for item in items: + # 1) Check easy input message + if easy_msg := cls.maybe_easy_input_message(item): + role = easy_msg["role"] + content = easy_msg["content"] + + if role == "user": + flush_assistant_message() + msg_user: ChatCompletionUserMessageParam = { + "role": "user", + "content": cls.extract_all_content(content), + } + result.append(msg_user) + elif role == "system": + flush_assistant_message() + msg_system: ChatCompletionSystemMessageParam = { + "role": "system", + "content": cls.extract_text_content(content), + } + result.append(msg_system) + elif role == "developer": + flush_assistant_message() + msg_developer: ChatCompletionDeveloperMessageParam = { + "role": "developer", + "content": cls.extract_text_content(content), + } + result.append(msg_developer) + elif role == "assistant": + flush_assistant_message() + msg_assistant: ChatCompletionAssistantMessageParam = { + "role": "assistant", + "content": cls.extract_text_content(content), + } + result.append(msg_assistant) + else: + raise UserError(f"Unexpected role in easy_input_message: {role}") + + # 2) Check input message + elif in_msg := cls.maybe_input_message(item): + role = in_msg["role"] + content = in_msg["content"] + flush_assistant_message() + + if role == "user": + msg_user = { + "role": "user", + "content": cls.extract_all_content(content), + } + result.append(msg_user) + elif role == "system": + msg_system = { + "role": "system", + "content": cls.extract_text_content(content), + } + result.append(msg_system) + elif role == "developer": + msg_developer = { + "role": "developer", + "content": cls.extract_text_content(content), + } + result.append(msg_developer) + else: + raise UserError(f"Unexpected role in input_message: {role}") + + # 3) response output message => assistant + elif resp_msg := cls.maybe_response_output_message(item): + flush_assistant_message() + new_asst = ChatCompletionAssistantMessageParam(role="assistant") + contents = resp_msg["content"] + + text_segments = [] + for c in contents: + if c["type"] == "output_text": + text_segments.append(c["text"]) + elif c["type"] == "refusal": + new_asst["refusal"] = c["refusal"] + elif c["type"] == "output_audio": + # Can't handle this, b/c chat completions expects an ID which we dont have + raise UserError( + f"Only audio IDs are supported for chat completions, but got: {c}" + ) + else: + raise UserError(f"Unknown content type in ResponseOutputMessage: {c}") + + if text_segments: + combined = "\n".join(text_segments) + new_asst["content"] = combined + + new_asst["tool_calls"] = [] + current_assistant_msg = new_asst + + # 4) function/file-search calls => attach to assistant + elif file_search := cls.maybe_file_search_call(item): + asst = ensure_assistant_message() + tool_calls = list(asst.get("tool_calls", [])) + new_tool_call = ChatCompletionMessageToolCallParam( + id=file_search["id"], + type="function", + function={ + "name": "file_search_call", + "arguments": json.dumps( + { + "queries": file_search.get("queries", []), + "status": file_search.get("status"), + } + ), + }, + ) + tool_calls.append(new_tool_call) + asst["tool_calls"] = tool_calls + + elif func_call := cls.maybe_function_tool_call(item): + asst = ensure_assistant_message() + tool_calls = list(asst.get("tool_calls", [])) + arguments = func_call["arguments"] if func_call["arguments"] else "{}" + new_tool_call = ChatCompletionMessageToolCallParam( + id=func_call["call_id"], + type="function", + function={ + "name": func_call["name"], + "arguments": arguments, + }, + ) + tool_calls.append(new_tool_call) + asst["tool_calls"] = tool_calls + # 5) function call output => tool message + elif func_output := cls.maybe_function_tool_call_output(item): + flush_assistant_message() + msg: ChatCompletionToolMessageParam = { + "role": "tool", + "tool_call_id": func_output["call_id"], + "content": func_output["output"], + } + result.append(msg) + + # 6) item reference => handle or raise + elif item_ref := cls.maybe_item_reference(item): + raise UserError( + f"Encountered an item_reference, which is not supported: {item_ref}" + ) + + # 7) If we haven't recognized it => fail or ignore + else: + raise UserError(f"Unhandled item type or structure: {item}") + + flush_assistant_message() + return result + + @classmethod + def tool_to_openai(cls, tool: Tool) -> ChatCompletionToolParam: + if isinstance(tool, FunctionTool): + return { + "type": "function", + "function": { + "name": tool.name, + "description": tool.description or "", + "parameters": tool.params_json_schema, + }, + } + + raise UserError( + f"Hosted tools are not supported with the ChatCompletions API. Got tool type: " + f"{type(tool)}, tool: {tool}" + ) + + @classmethod + def convert_handoff_tool(cls, handoff: Handoff[Any]) -> ChatCompletionToolParam: + return { + "type": "function", + "function": { + "name": handoff.tool_name, + "description": handoff.tool_description, + "parameters": handoff.input_json_schema, + }, + } diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index a4f93cb7..e3db9b96 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -3,71 +3,46 @@ import dataclasses import json import time -from collections.abc import AsyncIterator, Iterable +from collections.abc import AsyncIterator from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal, cast, overload -from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream, NotGiven +from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream from openai.types import ChatModel -from openai.types.chat import ( - ChatCompletion, - ChatCompletionAssistantMessageParam, - ChatCompletionChunk, - ChatCompletionContentPartImageParam, - ChatCompletionContentPartParam, - ChatCompletionContentPartTextParam, - ChatCompletionDeveloperMessageParam, - ChatCompletionMessage, - ChatCompletionMessageParam, - ChatCompletionMessageToolCallParam, - ChatCompletionSystemMessageParam, - ChatCompletionToolChoiceOptionParam, - ChatCompletionToolMessageParam, - ChatCompletionUserMessageParam, -) -from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam -from openai.types.chat.completion_create_params import ResponseFormat +from openai.types.chat import ChatCompletion, ChatCompletionChunk from openai.types.completion_usage import CompletionUsage from openai.types.responses import ( - EasyInputMessageParam, Response, ResponseCompletedEvent, ResponseContentPartAddedEvent, ResponseContentPartDoneEvent, ResponseCreatedEvent, - ResponseFileSearchToolCallParam, ResponseFunctionCallArgumentsDeltaEvent, ResponseFunctionToolCall, - ResponseFunctionToolCallParam, - ResponseInputContentParam, - ResponseInputImageParam, - ResponseInputTextParam, ResponseOutputItem, ResponseOutputItemAddedEvent, ResponseOutputItemDoneEvent, ResponseOutputMessage, - ResponseOutputMessageParam, ResponseOutputRefusal, ResponseOutputText, ResponseRefusalDeltaEvent, ResponseTextDeltaEvent, ResponseUsage, ) -from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from .. import _debug from ..agent_output import AgentOutputSchema -from ..exceptions import AgentsException, UserError from ..handoffs import Handoff -from ..items import ModelResponse, TResponseInputItem, TResponseOutputItem, TResponseStreamEvent +from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent from ..logger import logger -from ..tool import FunctionTool, Tool +from ..tool import Tool from ..tracing import generation_span from ..tracing.span_data import GenerationSpanData from ..tracing.spans import Span from ..usage import Usage from ..version import __version__ +from .chatcmpl_converter import Converter from .fake_id import FAKE_RESPONSES_ID from .interface import Model, ModelTracing @@ -152,7 +127,7 @@ async def get_response( "output_tokens": usage.output_tokens, } - items = _Converter.message_to_output_items(response.choices[0].message) + items = Converter.message_to_output_items(response.choices[0].message) return ModelResponse( output=items, @@ -486,7 +461,7 @@ async def _fetch_response( tracing: ModelTracing, stream: bool = False, ) -> ChatCompletion | tuple[Response, AsyncStream[ChatCompletionChunk]]: - converted_messages = _Converter.items_to_messages(input) + converted_messages = Converter.items_to_messages(input) if system_instructions: converted_messages.insert( @@ -506,13 +481,13 @@ async def _fetch_response( if model_settings.parallel_tool_calls is False else NOT_GIVEN ) - tool_choice = _Converter.convert_tool_choice(model_settings.tool_choice) - response_format = _Converter.convert_response_format(output_schema) + tool_choice = Converter.convert_tool_choice(model_settings.tool_choice) + response_format = Converter.convert_response_format(output_schema) - converted_tools = [ToolConverter.to_openai(tool) for tool in tools] if tools else [] + converted_tools = [Converter.tool_to_openai(tool) for tool in tools] if tools else [] for handoff in handoffs: - converted_tools.append(ToolConverter.convert_handoff_tool(handoff)) + converted_tools.append(Converter.convert_handoff_tool(handoff)) if _debug.DONT_LOG_MODEL_DATA: logger.debug("Calling LLM") @@ -526,9 +501,9 @@ async def _fetch_response( ) reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None - store = _Converter.get_store_param(self._get_client(), model_settings) + store = _Helpers.get_store_param(self._get_client(), model_settings) - stream_options = _Converter.get_stream_options_param( + stream_options = _Helpers.get_stream_options_param( self._get_client(), model_settings, stream=stream ) @@ -580,7 +555,7 @@ def _get_client(self) -> AsyncOpenAI: return self._client -class _Converter: +class _Helpers: @classmethod def is_openai(cls, client: AsyncOpenAI): return str(client.base_url).startswith("https://api.openai.com") @@ -606,425 +581,3 @@ def get_stream_options_param( ) stream_options = {"include_usage": include_usage} if include_usage is not None else None return stream_options - - @classmethod - def convert_tool_choice( - cls, tool_choice: Literal["auto", "required", "none"] | str | None - ) -> ChatCompletionToolChoiceOptionParam | NotGiven: - if tool_choice is None: - return NOT_GIVEN - elif tool_choice == "auto": - return "auto" - elif tool_choice == "required": - return "required" - elif tool_choice == "none": - return "none" - else: - return { - "type": "function", - "function": { - "name": tool_choice, - }, - } - - @classmethod - def convert_response_format( - cls, final_output_schema: AgentOutputSchema | None - ) -> ResponseFormat | NotGiven: - if not final_output_schema or final_output_schema.is_plain_text(): - return NOT_GIVEN - - return { - "type": "json_schema", - "json_schema": { - "name": "final_output", - "strict": final_output_schema.strict_json_schema, - "schema": final_output_schema.json_schema(), - }, - } - - @classmethod - def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TResponseOutputItem]: - items: list[TResponseOutputItem] = [] - - message_item = ResponseOutputMessage( - id=FAKE_RESPONSES_ID, - content=[], - role="assistant", - type="message", - status="completed", - ) - if message.content: - message_item.content.append( - ResponseOutputText(text=message.content, type="output_text", annotations=[]) - ) - if message.refusal: - message_item.content.append( - ResponseOutputRefusal(refusal=message.refusal, type="refusal") - ) - if message.audio: - raise AgentsException("Audio is not currently supported") - - if message_item.content: - items.append(message_item) - - if message.tool_calls: - for tool_call in message.tool_calls: - items.append( - ResponseFunctionToolCall( - id=FAKE_RESPONSES_ID, - call_id=tool_call.id, - arguments=tool_call.function.arguments, - name=tool_call.function.name, - type="function_call", - ) - ) - - return items - - @classmethod - def maybe_easy_input_message(cls, item: Any) -> EasyInputMessageParam | None: - if not isinstance(item, dict): - return None - - keys = item.keys() - # EasyInputMessageParam only has these two keys - if keys != {"content", "role"}: - return None - - role = item.get("role", None) - if role not in ("user", "assistant", "system", "developer"): - return None - - if "content" not in item: - return None - - return cast(EasyInputMessageParam, item) - - @classmethod - def maybe_input_message(cls, item: Any) -> Message | None: - if ( - isinstance(item, dict) - and item.get("type") == "message" - and item.get("role") - in ( - "user", - "system", - "developer", - ) - ): - return cast(Message, item) - - return None - - @classmethod - def maybe_file_search_call(cls, item: Any) -> ResponseFileSearchToolCallParam | None: - if isinstance(item, dict) and item.get("type") == "file_search_call": - return cast(ResponseFileSearchToolCallParam, item) - return None - - @classmethod - def maybe_function_tool_call(cls, item: Any) -> ResponseFunctionToolCallParam | None: - if isinstance(item, dict) and item.get("type") == "function_call": - return cast(ResponseFunctionToolCallParam, item) - return None - - @classmethod - def maybe_function_tool_call_output( - cls, - item: Any, - ) -> FunctionCallOutput | None: - if isinstance(item, dict) and item.get("type") == "function_call_output": - return cast(FunctionCallOutput, item) - return None - - @classmethod - def maybe_item_reference(cls, item: Any) -> ItemReference | None: - if isinstance(item, dict) and item.get("type") == "item_reference": - return cast(ItemReference, item) - return None - - @classmethod - def maybe_response_output_message(cls, item: Any) -> ResponseOutputMessageParam | None: - # ResponseOutputMessage is only used for messages with role assistant - if ( - isinstance(item, dict) - and item.get("type") == "message" - and item.get("role") == "assistant" - ): - return cast(ResponseOutputMessageParam, item) - return None - - @classmethod - def extract_text_content( - cls, content: str | Iterable[ResponseInputContentParam] - ) -> str | list[ChatCompletionContentPartTextParam]: - all_content = cls.extract_all_content(content) - if isinstance(all_content, str): - return all_content - out: list[ChatCompletionContentPartTextParam] = [] - for c in all_content: - if c.get("type") == "text": - out.append(cast(ChatCompletionContentPartTextParam, c)) - return out - - @classmethod - def extract_all_content( - cls, content: str | Iterable[ResponseInputContentParam] - ) -> str | list[ChatCompletionContentPartParam]: - if isinstance(content, str): - return content - out: list[ChatCompletionContentPartParam] = [] - - for c in content: - if isinstance(c, dict) and c.get("type") == "input_text": - casted_text_param = cast(ResponseInputTextParam, c) - out.append( - ChatCompletionContentPartTextParam( - type="text", - text=casted_text_param["text"], - ) - ) - elif isinstance(c, dict) and c.get("type") == "input_image": - casted_image_param = cast(ResponseInputImageParam, c) - if "image_url" not in casted_image_param or not casted_image_param["image_url"]: - raise UserError( - f"Only image URLs are supported for input_image {casted_image_param}" - ) - out.append( - ChatCompletionContentPartImageParam( - type="image_url", - image_url={ - "url": casted_image_param["image_url"], - "detail": casted_image_param["detail"], - }, - ) - ) - elif isinstance(c, dict) and c.get("type") == "input_file": - raise UserError(f"File uploads are not supported for chat completions {c}") - else: - raise UserError(f"Unknown content: {c}") - return out - - @classmethod - def items_to_messages( - cls, - items: str | Iterable[TResponseInputItem], - ) -> list[ChatCompletionMessageParam]: - """ - Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam. - - Rules: - - EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam - - EasyInputMessage or InputMessage (role=system) => ChatCompletionSystemMessageParam - - EasyInputMessage or InputMessage (role=developer) => ChatCompletionDeveloperMessageParam - - InputMessage (role=assistant) => Start or flush a ChatCompletionAssistantMessageParam - - response_output_message => Also produces/flushes a ChatCompletionAssistantMessageParam - - tool calls get attached to the *current* assistant message, or create one if none. - - tool outputs => ChatCompletionToolMessageParam - """ - - if isinstance(items, str): - return [ - ChatCompletionUserMessageParam( - role="user", - content=items, - ) - ] - - result: list[ChatCompletionMessageParam] = [] - current_assistant_msg: ChatCompletionAssistantMessageParam | None = None - - def flush_assistant_message() -> None: - nonlocal current_assistant_msg - if current_assistant_msg is not None: - # The API doesn't support empty arrays for tool_calls - if not current_assistant_msg.get("tool_calls"): - del current_assistant_msg["tool_calls"] - result.append(current_assistant_msg) - current_assistant_msg = None - - def ensure_assistant_message() -> ChatCompletionAssistantMessageParam: - nonlocal current_assistant_msg - if current_assistant_msg is None: - current_assistant_msg = ChatCompletionAssistantMessageParam(role="assistant") - current_assistant_msg["tool_calls"] = [] - return current_assistant_msg - - for item in items: - # 1) Check easy input message - if easy_msg := cls.maybe_easy_input_message(item): - role = easy_msg["role"] - content = easy_msg["content"] - - if role == "user": - flush_assistant_message() - msg_user: ChatCompletionUserMessageParam = { - "role": "user", - "content": cls.extract_all_content(content), - } - result.append(msg_user) - elif role == "system": - flush_assistant_message() - msg_system: ChatCompletionSystemMessageParam = { - "role": "system", - "content": cls.extract_text_content(content), - } - result.append(msg_system) - elif role == "developer": - flush_assistant_message() - msg_developer: ChatCompletionDeveloperMessageParam = { - "role": "developer", - "content": cls.extract_text_content(content), - } - result.append(msg_developer) - elif role == "assistant": - flush_assistant_message() - msg_assistant: ChatCompletionAssistantMessageParam = { - "role": "assistant", - "content": cls.extract_text_content(content), - } - result.append(msg_assistant) - else: - raise UserError(f"Unexpected role in easy_input_message: {role}") - - # 2) Check input message - elif in_msg := cls.maybe_input_message(item): - role = in_msg["role"] - content = in_msg["content"] - flush_assistant_message() - - if role == "user": - msg_user = { - "role": "user", - "content": cls.extract_all_content(content), - } - result.append(msg_user) - elif role == "system": - msg_system = { - "role": "system", - "content": cls.extract_text_content(content), - } - result.append(msg_system) - elif role == "developer": - msg_developer = { - "role": "developer", - "content": cls.extract_text_content(content), - } - result.append(msg_developer) - else: - raise UserError(f"Unexpected role in input_message: {role}") - - # 3) response output message => assistant - elif resp_msg := cls.maybe_response_output_message(item): - flush_assistant_message() - new_asst = ChatCompletionAssistantMessageParam(role="assistant") - contents = resp_msg["content"] - - text_segments = [] - for c in contents: - if c["type"] == "output_text": - text_segments.append(c["text"]) - elif c["type"] == "refusal": - new_asst["refusal"] = c["refusal"] - elif c["type"] == "output_audio": - # Can't handle this, b/c chat completions expects an ID which we dont have - raise UserError( - f"Only audio IDs are supported for chat completions, but got: {c}" - ) - else: - raise UserError(f"Unknown content type in ResponseOutputMessage: {c}") - - if text_segments: - combined = "\n".join(text_segments) - new_asst["content"] = combined - - new_asst["tool_calls"] = [] - current_assistant_msg = new_asst - - # 4) function/file-search calls => attach to assistant - elif file_search := cls.maybe_file_search_call(item): - asst = ensure_assistant_message() - tool_calls = list(asst.get("tool_calls", [])) - new_tool_call = ChatCompletionMessageToolCallParam( - id=file_search["id"], - type="function", - function={ - "name": "file_search_call", - "arguments": json.dumps( - { - "queries": file_search.get("queries", []), - "status": file_search.get("status"), - } - ), - }, - ) - tool_calls.append(new_tool_call) - asst["tool_calls"] = tool_calls - - elif func_call := cls.maybe_function_tool_call(item): - asst = ensure_assistant_message() - tool_calls = list(asst.get("tool_calls", [])) - arguments = func_call["arguments"] if func_call["arguments"] else "{}" - new_tool_call = ChatCompletionMessageToolCallParam( - id=func_call["call_id"], - type="function", - function={ - "name": func_call["name"], - "arguments": arguments, - }, - ) - tool_calls.append(new_tool_call) - asst["tool_calls"] = tool_calls - # 5) function call output => tool message - elif func_output := cls.maybe_function_tool_call_output(item): - flush_assistant_message() - msg: ChatCompletionToolMessageParam = { - "role": "tool", - "tool_call_id": func_output["call_id"], - "content": func_output["output"], - } - result.append(msg) - - # 6) item reference => handle or raise - elif item_ref := cls.maybe_item_reference(item): - raise UserError( - f"Encountered an item_reference, which is not supported: {item_ref}" - ) - - # 7) If we haven't recognized it => fail or ignore - else: - raise UserError(f"Unhandled item type or structure: {item}") - - flush_assistant_message() - return result - - -class ToolConverter: - @classmethod - def to_openai(cls, tool: Tool) -> ChatCompletionToolParam: - if isinstance(tool, FunctionTool): - return { - "type": "function", - "function": { - "name": tool.name, - "description": tool.description or "", - "parameters": tool.params_json_schema, - }, - } - - raise UserError( - f"Hosted tools are not supported with the ChatCompletions API. Got tool type: " - f"{type(tool)}, tool: {tool}" - ) - - @classmethod - def convert_handoff_tool(cls, handoff: Handoff[Any]) -> ChatCompletionToolParam: - return { - "type": "function", - "function": { - "name": handoff.tool_name, - "description": handoff.tool_description, - "parameters": handoff.input_json_schema, - }, - } diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index 92d65fda..b0fc1f32 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -31,7 +31,7 @@ generation_span, ) from agents.models.fake_id import FAKE_RESPONSES_ID -from agents.models.openai_chatcompletions import _Converter +from agents.models.openai_chatcompletions import _Helpers @pytest.mark.allow_call_model_methods @@ -301,32 +301,32 @@ def test_store_param(): model_settings = ModelSettings() client = AsyncOpenAI() - assert _Converter.get_store_param(client, model_settings) is True, ( + assert _Helpers.get_store_param(client, model_settings) is True, ( "Should default to True for OpenAI API calls" ) model_settings = ModelSettings(store=False) - assert _Converter.get_store_param(client, model_settings) is False, ( + assert _Helpers.get_store_param(client, model_settings) is False, ( "Should respect explicitly set store=False" ) model_settings = ModelSettings(store=True) - assert _Converter.get_store_param(client, model_settings) is True, ( + assert _Helpers.get_store_param(client, model_settings) is True, ( "Should respect explicitly set store=True" ) client = AsyncOpenAI(base_url="http://www.notopenai.com") model_settings = ModelSettings() - assert _Converter.get_store_param(client, model_settings) is None, ( + assert _Helpers.get_store_param(client, model_settings) is None, ( "Should default to None for non-OpenAI API calls" ) model_settings = ModelSettings(store=False) - assert _Converter.get_store_param(client, model_settings) is False, ( + assert _Helpers.get_store_param(client, model_settings) is False, ( "Should respect explicitly set store=False" ) model_settings = ModelSettings(store=True) - assert _Converter.get_store_param(client, model_settings) is True, ( + assert _Helpers.get_store_param(client, model_settings) is True, ( "Should respect explicitly set store=True" ) diff --git a/tests/test_openai_chatcompletions_converter.py b/tests/test_openai_chatcompletions_converter.py index 73acb8ab..e3a18b25 100644 --- a/tests/test_openai_chatcompletions_converter.py +++ b/tests/test_openai_chatcompletions_converter.py @@ -4,7 +4,7 @@ # See LICENSE file in the project root for full license information. """ -Unit tests for the internal `_Converter` class defined in +Unit tests for the internal `Converter` class defined in `agents.models.openai_chatcompletions`. The converter is responsible for translating between internal "item" structures (e.g., `ResponseOutputMessage` and related types from `openai.types.responses`) and the ChatCompletion message @@ -12,10 +12,10 @@ These tests exercise both conversion directions: -- `_Converter.message_to_output_items` turns a `ChatCompletionMessage` (as +- `Converter.message_to_output_items` turns a `ChatCompletionMessage` (as returned by the OpenAI API) into a list of `ResponseOutputItem` instances. -- `_Converter.items_to_messages` takes in either a simple string prompt, or a +- `Converter.items_to_messages` takes in either a simple string prompt, or a list of input/output items such as `ResponseOutputMessage` and `ResponseFunctionToolCallParam` dicts, and constructs a list of `ChatCompletionMessageParam` dicts suitable for sending back to the API. @@ -41,8 +41,8 @@ from agents.agent_output import AgentOutputSchema from agents.exceptions import UserError from agents.items import TResponseInputItem +from agents.models.chatcmpl_converter import Converter from agents.models.fake_id import FAKE_RESPONSES_ID -from agents.models.openai_chatcompletions import _Converter def test_message_to_output_items_with_text_only(): @@ -51,7 +51,7 @@ def test_message_to_output_items_with_text_only(): into a single ResponseOutputMessage containing one ResponseOutputText. """ msg = ChatCompletionMessage(role="assistant", content="Hello") - items = _Converter.message_to_output_items(msg) + items = Converter.message_to_output_items(msg) # Expect exactly one output item (the message) assert len(items) == 1 message_item = cast(ResponseOutputMessage, items[0]) @@ -72,7 +72,7 @@ def test_message_to_output_items_with_refusal(): with a ResponseOutputRefusal content part. """ msg = ChatCompletionMessage(role="assistant", refusal="I'm sorry") - items = _Converter.message_to_output_items(msg) + items = Converter.message_to_output_items(msg) assert len(items) == 1 message_item = cast(ResponseOutputMessage, items[0]) assert len(message_item.content) == 1 @@ -93,7 +93,7 @@ def test_message_to_output_items_with_tool_call(): function=Function(name="myfn", arguments='{"x":1}'), ) msg = ChatCompletionMessage(role="assistant", content="Hi", tool_calls=[tool_call]) - items = _Converter.message_to_output_items(msg) + items = Converter.message_to_output_items(msg) # Should produce a message item followed by one function tool call item assert len(items) == 2 message_item = cast(ResponseOutputMessage, items[0]) @@ -111,7 +111,7 @@ def test_items_to_messages_with_string_user_content(): A simple string as the items argument should be converted into a user message param dict with the same content. """ - result = _Converter.items_to_messages("Ask me anything") + result = Converter.items_to_messages("Ask me anything") assert isinstance(result, list) assert len(result) == 1 msg = result[0] @@ -130,7 +130,7 @@ def test_items_to_messages_with_easy_input_message(): "content": "How are you?", } ] - messages = _Converter.items_to_messages(items) + messages = Converter.items_to_messages(items) assert len(messages) == 1 out = messages[0] assert out["role"] == "user" @@ -174,7 +174,7 @@ def test_items_to_messages_with_output_message_and_function_call(): resp_msg.model_dump(), # type:ignore func_item, ] - messages = _Converter.items_to_messages(items) + messages = Converter.items_to_messages(items) # Should return a single assistant message assert len(messages) == 1 assistant = messages[0] @@ -197,16 +197,16 @@ def test_items_to_messages_with_output_message_and_function_call(): def test_convert_tool_choice_handles_standard_and_named_options() -> None: """ - The `_Converter.convert_tool_choice` method should return NOT_GIVEN + The `Converter.convert_tool_choice` method should return NOT_GIVEN if no choice is provided, pass through values like "auto", "required", or "none" unchanged, and translate any other string into a function selection dict. """ - assert _Converter.convert_tool_choice(None).__class__.__name__ == "NotGiven" - assert _Converter.convert_tool_choice("auto") == "auto" - assert _Converter.convert_tool_choice("required") == "required" - assert _Converter.convert_tool_choice("none") == "none" - tool_choice_dict = _Converter.convert_tool_choice("mytool") + assert Converter.convert_tool_choice(None).__class__.__name__ == "NotGiven" + assert Converter.convert_tool_choice("auto") == "auto" + assert Converter.convert_tool_choice("required") == "required" + assert Converter.convert_tool_choice("none") == "none" + tool_choice_dict = Converter.convert_tool_choice("mytool") assert isinstance(tool_choice_dict, dict) assert tool_choice_dict["type"] == "function" assert tool_choice_dict["function"]["name"] == "mytool" @@ -214,20 +214,20 @@ def test_convert_tool_choice_handles_standard_and_named_options() -> None: def test_convert_response_format_returns_not_given_for_plain_text_and_dict_for_schemas() -> None: """ - The `_Converter.convert_response_format` method should return NOT_GIVEN + The `Converter.convert_response_format` method should return NOT_GIVEN when no output schema is provided or if the output schema indicates plain text. For structured output schemas, it should return a dict with type `json_schema` and include the generated JSON schema and strict flag from the provided `AgentOutputSchema`. """ # when output is plain text (schema None or output_type str), do not include response_format - assert _Converter.convert_response_format(None).__class__.__name__ == "NotGiven" + assert Converter.convert_response_format(None).__class__.__name__ == "NotGiven" assert ( - _Converter.convert_response_format(AgentOutputSchema(str)).__class__.__name__ == "NotGiven" + Converter.convert_response_format(AgentOutputSchema(str)).__class__.__name__ == "NotGiven" ) # For e.g. integer output, we expect a response_format dict schema = AgentOutputSchema(int) - resp_format = _Converter.convert_response_format(schema) + resp_format = Converter.convert_response_format(schema) assert isinstance(resp_format, dict) assert resp_format["type"] == "json_schema" assert resp_format["json_schema"]["name"] == "final_output" @@ -247,7 +247,7 @@ def test_items_to_messages_with_function_output_item(): "call_id": "somecall", "output": '{"foo": "bar"}', } - messages = _Converter.items_to_messages([func_output_item]) + messages = Converter.items_to_messages([func_output_item]) assert len(messages) == 1 tool_msg = messages[0] assert tool_msg["role"] == "tool" @@ -266,16 +266,16 @@ def test_extract_all_and_text_content_for_strings_and_lists(): should filter to only the textual parts. """ prompt = "just text" - assert _Converter.extract_all_content(prompt) == prompt - assert _Converter.extract_text_content(prompt) == prompt + assert Converter.extract_all_content(prompt) == prompt + assert Converter.extract_text_content(prompt) == prompt text1: ResponseInputTextParam = {"type": "input_text", "text": "one"} text2: ResponseInputTextParam = {"type": "input_text", "text": "two"} - all_parts = _Converter.extract_all_content([text1, text2]) + all_parts = Converter.extract_all_content([text1, text2]) assert isinstance(all_parts, list) assert len(all_parts) == 2 assert all_parts[0]["type"] == "text" and all_parts[0]["text"] == "one" assert all_parts[1]["type"] == "text" and all_parts[1]["text"] == "two" - text_parts = _Converter.extract_text_content([text1, text2]) + text_parts = Converter.extract_text_content([text1, text2]) assert isinstance(text_parts, list) assert all(p["type"] == "text" for p in text_parts) assert [p["text"] for p in text_parts] == ["one", "two"] @@ -288,12 +288,12 @@ def test_items_to_messages_handles_system_and_developer_roles(): `message` typed dicts. """ sys_items: list[TResponseInputItem] = [{"role": "system", "content": "setup"}] - sys_msgs = _Converter.items_to_messages(sys_items) + sys_msgs = Converter.items_to_messages(sys_items) assert len(sys_msgs) == 1 assert sys_msgs[0]["role"] == "system" assert sys_msgs[0]["content"] == "setup" dev_items: list[TResponseInputItem] = [{"role": "developer", "content": "debug"}] - dev_msgs = _Converter.items_to_messages(dev_items) + dev_msgs = Converter.items_to_messages(dev_items) assert len(dev_msgs) == 1 assert dev_msgs[0]["role"] == "developer" assert dev_msgs[0]["content"] == "debug" @@ -301,7 +301,7 @@ def test_items_to_messages_handles_system_and_developer_roles(): def test_maybe_input_message_allows_message_typed_dict(): """ - The `_Converter.maybe_input_message` should recognize a dict with + The `Converter.maybe_input_message` should recognize a dict with "type": "message" and a supported role as an input message. Ensure that such dicts are passed through by `items_to_messages`. """ @@ -311,9 +311,9 @@ def test_maybe_input_message_allows_message_typed_dict(): "role": "user", "content": "hi", } - assert _Converter.maybe_input_message(message_dict) is not None + assert Converter.maybe_input_message(message_dict) is not None # items_to_messages should process this correctly - msgs = _Converter.items_to_messages([message_dict]) + msgs = Converter.items_to_messages([message_dict]) assert len(msgs) == 1 assert msgs[0]["role"] == "user" assert msgs[0]["content"] == "hi" @@ -331,7 +331,7 @@ def test_tool_call_conversion(): type="function_call", ) - messages = _Converter.items_to_messages([function_call]) + messages = Converter.items_to_messages([function_call]) assert len(messages) == 1 tool_msg = messages[0] assert tool_msg["role"] == "assistant" @@ -348,7 +348,7 @@ def test_tool_call_conversion(): @pytest.mark.parametrize("role", ["user", "system", "developer"]) def test_input_message_with_all_roles(role: str): """ - The `_Converter.maybe_input_message` should recognize a dict with + The `Converter.maybe_input_message` should recognize a dict with "type": "message" and a supported role as an input message. Ensure that such dicts are passed through by `items_to_messages`. """ @@ -359,9 +359,9 @@ def test_input_message_with_all_roles(role: str): "role": casted_role, "content": "hi", } - assert _Converter.maybe_input_message(message_dict) is not None + assert Converter.maybe_input_message(message_dict) is not None # items_to_messages should process this correctly - msgs = _Converter.items_to_messages([message_dict]) + msgs = Converter.items_to_messages([message_dict]) assert len(msgs) == 1 assert msgs[0]["role"] == casted_role assert msgs[0]["content"] == "hi" @@ -372,7 +372,7 @@ def test_item_reference_errors(): Test that item references are converted correctly. """ with pytest.raises(UserError): - _Converter.items_to_messages( + Converter.items_to_messages( [ { "type": "item_reference", @@ -392,14 +392,14 @@ def test_unknown_object_errors(): """ with pytest.raises(UserError, match="Unhandled item type or structure"): # Purposely ignore the type error - _Converter.items_to_messages([TestObject()]) # type: ignore + Converter.items_to_messages([TestObject()]) # type: ignore def test_assistant_messages_in_history(): """ Test that assistant messages are added to the history. """ - messages = _Converter.items_to_messages( + messages = Converter.items_to_messages( [ { "role": "user", diff --git a/tests/test_tool_converter.py b/tests/test_tool_converter.py index 1b6ebcf9..918de015 100644 --- a/tests/test_tool_converter.py +++ b/tests/test_tool_converter.py @@ -3,7 +3,7 @@ from agents import Agent, Handoff, function_tool, handoff from agents.exceptions import UserError -from agents.models.openai_chatcompletions import ToolConverter +from agents.models.chatcmpl_converter import Converter from agents.tool import FileSearchTool, WebSearchTool @@ -15,7 +15,7 @@ def test_to_openai_with_function_tool(): some_function(a="foo", b=[1, 2, 3]) tool = function_tool(some_function) - result = ToolConverter.to_openai(tool) + result = Converter.tool_to_openai(tool) assert result["type"] == "function" assert result["function"]["name"] == "some_function" @@ -34,7 +34,7 @@ class Foo(BaseModel): def test_convert_handoff_tool(): agent = Agent(name="test_1", handoff_description="test_2") handoff_obj = handoff(agent=agent) - result = ToolConverter.convert_handoff_tool(handoff_obj) + result = Converter.convert_handoff_tool(handoff_obj) assert result["type"] == "function" assert result["function"]["name"] == Handoff.default_tool_name(agent) @@ -48,7 +48,7 @@ def test_convert_handoff_tool(): def test_tool_converter_hosted_tools_errors(): with pytest.raises(UserError): - ToolConverter.to_openai(WebSearchTool()) + Converter.tool_to_openai(WebSearchTool()) with pytest.raises(UserError): - ToolConverter.to_openai(FileSearchTool(vector_store_ids=["abc"], max_num_results=1)) + Converter.tool_to_openai(FileSearchTool(vector_store_ids=["abc"], max_num_results=1)) From 65cae71b1452dc1942cf667b12756ac18d68456f Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Tue, 15 Apr 2025 18:42:09 -0400 Subject: [PATCH 46/85] Extract chat completions streaming helpers (#523) Small refactor. --- [//]: # (BEGIN SAPLING FOOTER) * #524 * __->__ #523 --- src/agents/models/chatcmpl_stream_handler.py | 290 +++++++++++++++++++ src/agents/models/openai_chatcompletions.py | 286 +----------------- 2 files changed, 301 insertions(+), 275 deletions(-) create mode 100644 src/agents/models/chatcmpl_stream_handler.py diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py new file mode 100644 index 00000000..32f04acb --- /dev/null +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -0,0 +1,290 @@ +from __future__ import annotations + +from collections.abc import AsyncIterator +from dataclasses import dataclass, field + +from openai import AsyncStream +from openai.types.chat import ChatCompletionChunk +from openai.types.completion_usage import CompletionUsage +from openai.types.responses import ( + Response, + ResponseCompletedEvent, + ResponseContentPartAddedEvent, + ResponseContentPartDoneEvent, + ResponseCreatedEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionToolCall, + ResponseOutputItem, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseOutputMessage, + ResponseOutputRefusal, + ResponseOutputText, + ResponseRefusalDeltaEvent, + ResponseTextDeltaEvent, + ResponseUsage, +) +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails + +from ..items import TResponseStreamEvent +from .fake_id import FAKE_RESPONSES_ID + + +@dataclass +class StreamingState: + started: bool = False + text_content_index_and_output: tuple[int, ResponseOutputText] | None = None + refusal_content_index_and_output: tuple[int, ResponseOutputRefusal] | None = None + function_calls: dict[int, ResponseFunctionToolCall] = field(default_factory=dict) + + +class ChatCmplStreamHandler: + @classmethod + async def handle_stream( + cls, + response: Response, + stream: AsyncStream[ChatCompletionChunk], + ) -> AsyncIterator[TResponseStreamEvent]: + usage: CompletionUsage | None = None + state = StreamingState() + + async for chunk in stream: + if not state.started: + state.started = True + yield ResponseCreatedEvent( + response=response, + type="response.created", + ) + + usage = chunk.usage + + if not chunk.choices or not chunk.choices[0].delta: + continue + + delta = chunk.choices[0].delta + + # Handle text + if delta.content: + if not state.text_content_index_and_output: + # Initialize a content tracker for streaming text + state.text_content_index_and_output = ( + 0 if not state.refusal_content_index_and_output else 1, + ResponseOutputText( + text="", + type="output_text", + annotations=[], + ), + ) + # Start a new assistant message stream + assistant_item = ResponseOutputMessage( + id=FAKE_RESPONSES_ID, + content=[], + role="assistant", + type="message", + status="in_progress", + ) + # Notify consumers of the start of a new output message + first content part + yield ResponseOutputItemAddedEvent( + item=assistant_item, + output_index=0, + type="response.output_item.added", + ) + yield ResponseContentPartAddedEvent( + content_index=state.text_content_index_and_output[0], + item_id=FAKE_RESPONSES_ID, + output_index=0, + part=ResponseOutputText( + text="", + type="output_text", + annotations=[], + ), + type="response.content_part.added", + ) + # Emit the delta for this segment of content + yield ResponseTextDeltaEvent( + content_index=state.text_content_index_and_output[0], + delta=delta.content, + item_id=FAKE_RESPONSES_ID, + output_index=0, + type="response.output_text.delta", + ) + # Accumulate the text into the response part + state.text_content_index_and_output[1].text += delta.content + + # Handle refusals (model declines to answer) + if delta.refusal: + if not state.refusal_content_index_and_output: + # Initialize a content tracker for streaming refusal text + state.refusal_content_index_and_output = ( + 0 if not state.text_content_index_and_output else 1, + ResponseOutputRefusal(refusal="", type="refusal"), + ) + # Start a new assistant message if one doesn't exist yet (in-progress) + assistant_item = ResponseOutputMessage( + id=FAKE_RESPONSES_ID, + content=[], + role="assistant", + type="message", + status="in_progress", + ) + # Notify downstream that assistant message + first content part are starting + yield ResponseOutputItemAddedEvent( + item=assistant_item, + output_index=0, + type="response.output_item.added", + ) + yield ResponseContentPartAddedEvent( + content_index=state.refusal_content_index_and_output[0], + item_id=FAKE_RESPONSES_ID, + output_index=0, + part=ResponseOutputText( + text="", + type="output_text", + annotations=[], + ), + type="response.content_part.added", + ) + # Emit the delta for this segment of refusal + yield ResponseRefusalDeltaEvent( + content_index=state.refusal_content_index_and_output[0], + delta=delta.refusal, + item_id=FAKE_RESPONSES_ID, + output_index=0, + type="response.refusal.delta", + ) + # Accumulate the refusal string in the output part + state.refusal_content_index_and_output[1].refusal += delta.refusal + + # Handle tool calls + # Because we don't know the name of the function until the end of the stream, we'll + # save everything and yield events at the end + if delta.tool_calls: + for tc_delta in delta.tool_calls: + if tc_delta.index not in state.function_calls: + state.function_calls[tc_delta.index] = ResponseFunctionToolCall( + id=FAKE_RESPONSES_ID, + arguments="", + name="", + type="function_call", + call_id="", + ) + tc_function = tc_delta.function + + state.function_calls[tc_delta.index].arguments += ( + tc_function.arguments if tc_function else "" + ) or "" + state.function_calls[tc_delta.index].name += ( + tc_function.name if tc_function else "" + ) or "" + state.function_calls[tc_delta.index].call_id += tc_delta.id or "" + + function_call_starting_index = 0 + if state.text_content_index_and_output: + function_call_starting_index += 1 + # Send end event for this content part + yield ResponseContentPartDoneEvent( + content_index=state.text_content_index_and_output[0], + item_id=FAKE_RESPONSES_ID, + output_index=0, + part=state.text_content_index_and_output[1], + type="response.content_part.done", + ) + + if state.refusal_content_index_and_output: + function_call_starting_index += 1 + # Send end event for this content part + yield ResponseContentPartDoneEvent( + content_index=state.refusal_content_index_and_output[0], + item_id=FAKE_RESPONSES_ID, + output_index=0, + part=state.refusal_content_index_and_output[1], + type="response.content_part.done", + ) + + # Actually send events for the function calls + for function_call in state.function_calls.values(): + # First, a ResponseOutputItemAdded for the function call + yield ResponseOutputItemAddedEvent( + item=ResponseFunctionToolCall( + id=FAKE_RESPONSES_ID, + call_id=function_call.call_id, + arguments=function_call.arguments, + name=function_call.name, + type="function_call", + ), + output_index=function_call_starting_index, + type="response.output_item.added", + ) + # Then, yield the args + yield ResponseFunctionCallArgumentsDeltaEvent( + delta=function_call.arguments, + item_id=FAKE_RESPONSES_ID, + output_index=function_call_starting_index, + type="response.function_call_arguments.delta", + ) + # Finally, the ResponseOutputItemDone + yield ResponseOutputItemDoneEvent( + item=ResponseFunctionToolCall( + id=FAKE_RESPONSES_ID, + call_id=function_call.call_id, + arguments=function_call.arguments, + name=function_call.name, + type="function_call", + ), + output_index=function_call_starting_index, + type="response.output_item.done", + ) + + # Finally, send the Response completed event + outputs: list[ResponseOutputItem] = [] + if state.text_content_index_and_output or state.refusal_content_index_and_output: + assistant_msg = ResponseOutputMessage( + id=FAKE_RESPONSES_ID, + content=[], + role="assistant", + type="message", + status="completed", + ) + if state.text_content_index_and_output: + assistant_msg.content.append(state.text_content_index_and_output[1]) + if state.refusal_content_index_and_output: + assistant_msg.content.append(state.refusal_content_index_and_output[1]) + outputs.append(assistant_msg) + + # send a ResponseOutputItemDone for the assistant message + yield ResponseOutputItemDoneEvent( + item=assistant_msg, + output_index=0, + type="response.output_item.done", + ) + + for function_call in state.function_calls.values(): + outputs.append(function_call) + + final_response = response.model_copy() + final_response.output = outputs + final_response.usage = ( + ResponseUsage( + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + output_tokens_details=OutputTokensDetails( + reasoning_tokens=usage.completion_tokens_details.reasoning_tokens + if usage.completion_tokens_details + and usage.completion_tokens_details.reasoning_tokens + else 0 + ), + input_tokens_details=InputTokensDetails( + cached_tokens=usage.prompt_tokens_details.cached_tokens + if usage.prompt_tokens_details and usage.prompt_tokens_details.cached_tokens + else 0 + ), + ) + if usage + else None + ) + + yield ResponseCompletedEvent( + response=final_response, + type="response.completed", + ) diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index e3db9b96..c399168d 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -4,32 +4,12 @@ import json import time from collections.abc import AsyncIterator -from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal, cast, overload from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream from openai.types import ChatModel from openai.types.chat import ChatCompletion, ChatCompletionChunk -from openai.types.completion_usage import CompletionUsage -from openai.types.responses import ( - Response, - ResponseCompletedEvent, - ResponseContentPartAddedEvent, - ResponseContentPartDoneEvent, - ResponseCreatedEvent, - ResponseFunctionCallArgumentsDeltaEvent, - ResponseFunctionToolCall, - ResponseOutputItem, - ResponseOutputItemAddedEvent, - ResponseOutputItemDoneEvent, - ResponseOutputMessage, - ResponseOutputRefusal, - ResponseOutputText, - ResponseRefusalDeltaEvent, - ResponseTextDeltaEvent, - ResponseUsage, -) -from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails +from openai.types.responses import Response from .. import _debug from ..agent_output import AgentOutputSchema @@ -43,6 +23,7 @@ from ..usage import Usage from ..version import __version__ from .chatcmpl_converter import Converter +from .chatcmpl_stream_handler import ChatCmplStreamHandler from .fake_id import FAKE_RESPONSES_ID from .interface import Model, ModelTracing @@ -54,14 +35,6 @@ _HEADERS = {"User-Agent": _USER_AGENT} -@dataclass -class _StreamingState: - started: bool = False - text_content_index_and_output: tuple[int, ResponseOutputText] | None = None - refusal_content_index_and_output: tuple[int, ResponseOutputRefusal] | None = None - function_calls: dict[int, ResponseFunctionToolCall] = field(default_factory=dict) - - class OpenAIChatCompletionsModel(Model): def __init__( self, @@ -168,257 +141,20 @@ async def stream_response( stream=True, ) - usage: CompletionUsage | None = None - state = _StreamingState() - - async for chunk in stream: - if not state.started: - state.started = True - yield ResponseCreatedEvent( - response=response, - type="response.created", - ) - - # The usage is only available in the last chunk - usage = chunk.usage - - if not chunk.choices or not chunk.choices[0].delta: - continue - - delta = chunk.choices[0].delta - - # Handle text - if delta.content: - if not state.text_content_index_and_output: - # Initialize a content tracker for streaming text - state.text_content_index_and_output = ( - 0 if not state.refusal_content_index_and_output else 1, - ResponseOutputText( - text="", - type="output_text", - annotations=[], - ), - ) - # Start a new assistant message stream - assistant_item = ResponseOutputMessage( - id=FAKE_RESPONSES_ID, - content=[], - role="assistant", - type="message", - status="in_progress", - ) - # Notify consumers of the start of a new output message + first content part - yield ResponseOutputItemAddedEvent( - item=assistant_item, - output_index=0, - type="response.output_item.added", - ) - yield ResponseContentPartAddedEvent( - content_index=state.text_content_index_and_output[0], - item_id=FAKE_RESPONSES_ID, - output_index=0, - part=ResponseOutputText( - text="", - type="output_text", - annotations=[], - ), - type="response.content_part.added", - ) - # Emit the delta for this segment of content - yield ResponseTextDeltaEvent( - content_index=state.text_content_index_and_output[0], - delta=delta.content, - item_id=FAKE_RESPONSES_ID, - output_index=0, - type="response.output_text.delta", - ) - # Accumulate the text into the response part - state.text_content_index_and_output[1].text += delta.content - - # Handle refusals (model declines to answer) - if delta.refusal: - if not state.refusal_content_index_and_output: - # Initialize a content tracker for streaming refusal text - state.refusal_content_index_and_output = ( - 0 if not state.text_content_index_and_output else 1, - ResponseOutputRefusal(refusal="", type="refusal"), - ) - # Start a new assistant message if one doesn't exist yet (in-progress) - assistant_item = ResponseOutputMessage( - id=FAKE_RESPONSES_ID, - content=[], - role="assistant", - type="message", - status="in_progress", - ) - # Notify downstream that assistant message + first content part are starting - yield ResponseOutputItemAddedEvent( - item=assistant_item, - output_index=0, - type="response.output_item.added", - ) - yield ResponseContentPartAddedEvent( - content_index=state.refusal_content_index_and_output[0], - item_id=FAKE_RESPONSES_ID, - output_index=0, - part=ResponseOutputText( - text="", - type="output_text", - annotations=[], - ), - type="response.content_part.added", - ) - # Emit the delta for this segment of refusal - yield ResponseRefusalDeltaEvent( - content_index=state.refusal_content_index_and_output[0], - delta=delta.refusal, - item_id=FAKE_RESPONSES_ID, - output_index=0, - type="response.refusal.delta", - ) - # Accumulate the refusal string in the output part - state.refusal_content_index_and_output[1].refusal += delta.refusal - - # Handle tool calls - # Because we don't know the name of the function until the end of the stream, we'll - # save everything and yield events at the end - if delta.tool_calls: - for tc_delta in delta.tool_calls: - if tc_delta.index not in state.function_calls: - state.function_calls[tc_delta.index] = ResponseFunctionToolCall( - id=FAKE_RESPONSES_ID, - arguments="", - name="", - type="function_call", - call_id="", - ) - tc_function = tc_delta.function - - state.function_calls[tc_delta.index].arguments += ( - tc_function.arguments if tc_function else "" - ) or "" - state.function_calls[tc_delta.index].name += ( - tc_function.name if tc_function else "" - ) or "" - state.function_calls[tc_delta.index].call_id += tc_delta.id or "" - - function_call_starting_index = 0 - if state.text_content_index_and_output: - function_call_starting_index += 1 - # Send end event for this content part - yield ResponseContentPartDoneEvent( - content_index=state.text_content_index_and_output[0], - item_id=FAKE_RESPONSES_ID, - output_index=0, - part=state.text_content_index_and_output[1], - type="response.content_part.done", - ) - - if state.refusal_content_index_and_output: - function_call_starting_index += 1 - # Send end event for this content part - yield ResponseContentPartDoneEvent( - content_index=state.refusal_content_index_and_output[0], - item_id=FAKE_RESPONSES_ID, - output_index=0, - part=state.refusal_content_index_and_output[1], - type="response.content_part.done", - ) - - # Actually send events for the function calls - for function_call in state.function_calls.values(): - # First, a ResponseOutputItemAdded for the function call - yield ResponseOutputItemAddedEvent( - item=ResponseFunctionToolCall( - id=FAKE_RESPONSES_ID, - call_id=function_call.call_id, - arguments=function_call.arguments, - name=function_call.name, - type="function_call", - ), - output_index=function_call_starting_index, - type="response.output_item.added", - ) - # Then, yield the args - yield ResponseFunctionCallArgumentsDeltaEvent( - delta=function_call.arguments, - item_id=FAKE_RESPONSES_ID, - output_index=function_call_starting_index, - type="response.function_call_arguments.delta", - ) - # Finally, the ResponseOutputItemDone - yield ResponseOutputItemDoneEvent( - item=ResponseFunctionToolCall( - id=FAKE_RESPONSES_ID, - call_id=function_call.call_id, - arguments=function_call.arguments, - name=function_call.name, - type="function_call", - ), - output_index=function_call_starting_index, - type="response.output_item.done", - ) - - # Finally, send the Response completed event - outputs: list[ResponseOutputItem] = [] - if state.text_content_index_and_output or state.refusal_content_index_and_output: - assistant_msg = ResponseOutputMessage( - id=FAKE_RESPONSES_ID, - content=[], - role="assistant", - type="message", - status="completed", - ) - if state.text_content_index_and_output: - assistant_msg.content.append(state.text_content_index_and_output[1]) - if state.refusal_content_index_and_output: - assistant_msg.content.append(state.refusal_content_index_and_output[1]) - outputs.append(assistant_msg) - - # send a ResponseOutputItemDone for the assistant message - yield ResponseOutputItemDoneEvent( - item=assistant_msg, - output_index=0, - type="response.output_item.done", - ) + final_response: Response | None = None + async for chunk in ChatCmplStreamHandler.handle_stream(response, stream): + yield chunk - for function_call in state.function_calls.values(): - outputs.append(function_call) - - final_response = response.model_copy() - final_response.output = outputs - final_response.usage = ( - ResponseUsage( - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - output_tokens_details=OutputTokensDetails( - reasoning_tokens=usage.completion_tokens_details.reasoning_tokens - if usage.completion_tokens_details - and usage.completion_tokens_details.reasoning_tokens - else 0 - ), - input_tokens_details=InputTokensDetails( - cached_tokens=usage.prompt_tokens_details.cached_tokens - if usage.prompt_tokens_details and usage.prompt_tokens_details.cached_tokens - else 0 - ), - ) - if usage - else None - ) + if chunk.type == "response.completed": + final_response = chunk.response - yield ResponseCompletedEvent( - response=final_response, - type="response.completed", - ) - if tracing.include_data(): + if tracing.include_data() and final_response: span_generation.span_data.output = [final_response.model_dump()] - if usage: + if final_response and final_response.usage: span_generation.span_data.usage = { - "input_tokens": usage.prompt_tokens, - "output_tokens": usage.completion_tokens, + "input_tokens": final_response.usage.input_tokens, + "output_tokens": final_response.usage.output_tokens, } @overload From 0faadf7f7b8daf41dc75a1db54dac4facd236885 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 16 Apr 2025 14:27:23 -0400 Subject: [PATCH 47/85] Show repo name/data in docs (#525) Easy linking back to the repo, plus some social proof (stars/forks etc). --- mkdocs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 51d65e69..e7f43993 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,9 @@ theme: logo: assets/logo.svg favicon: images/favicon-platform.svg +repo_name: openai-agents-python +repo_url: https://github.com/openai/openai-agents-python + plugins: - search - mkdocstrings: From bd404e0f87a14e8b2636c07603fb55d64c5069d8 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 16 Apr 2025 18:48:41 -0400 Subject: [PATCH 48/85] Litellm integration (#524) litellm is a library that abstracts away details/differences for a lot of model providers. Adding an extension, so that any provider can easily be integrated. --- [//]: # (BEGIN SAPLING FOOTER) * #532 * __->__ #524 --- examples/model_providers/litellm_provider.py | 55 + pyproject.toml | 3 +- src/agents/extensions/models/__init__.py | 0 src/agents/extensions/models/litellm_model.py | 380 ++++ src/agents/models/chatcmpl_helpers.py | 37 + src/agents/models/openai_chatcompletions.py | 40 +- tests/test_function_schema.py | 2 +- tests/test_openai_chatcompletions.py | 14 +- tests/voice/conftest.py | 12 +- uv.lock | 1539 +++++++++++++---- 10 files changed, 1701 insertions(+), 381 deletions(-) create mode 100644 examples/model_providers/litellm_provider.py create mode 100644 src/agents/extensions/models/__init__.py create mode 100644 src/agents/extensions/models/litellm_model.py create mode 100644 src/agents/models/chatcmpl_helpers.py diff --git a/examples/model_providers/litellm_provider.py b/examples/model_providers/litellm_provider.py new file mode 100644 index 00000000..4a1a696f --- /dev/null +++ b/examples/model_providers/litellm_provider.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import asyncio + +from agents import Agent, Runner, function_tool, set_tracing_disabled +from agents.extensions.models.litellm_model import LitellmModel + +"""This example uses the LitellmModel directly, to hit any model provider. +You can run it like this: +uv run examples/model_providers/litellm_provider.py --model anthropic/claude-3-5-sonnet-20240620 +or +uv run examples/model_providers/litellm_provider.py --model gemini/gemini-2.0-flash + +Find more providers here: https://docs.litellm.ai/docs/providers +""" + +set_tracing_disabled(disabled=True) + + +@function_tool +def get_weather(city: str): + print(f"[debug] getting weather for {city}") + return f"The weather in {city} is sunny." + + +async def main(model: str, api_key: str): + agent = Agent( + name="Assistant", + instructions="You only respond in haikus.", + model=LitellmModel(model=model, api_key=api_key), + tools=[get_weather], + ) + + result = await Runner.run(agent, "What's the weather in Tokyo?") + print(result.final_output) + + +if __name__ == "__main__": + # First try to get model/api key from args + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--model", type=str, required=False) + parser.add_argument("--api-key", type=str, required=False) + args = parser.parse_args() + + model = args.model + if not model: + model = input("Enter a model name for Litellm: ") + + api_key = args.api_key + if not api_key: + api_key = input("Enter an API key for Litellm: ") + + asyncio.run(main(model, api_key)) diff --git a/pyproject.toml b/pyproject.toml index 1eefadd9..f94eb859 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ Repository = "https://github.com/openai/openai-agents-python" [project.optional-dependencies] voice = ["numpy>=2.2.0, <3; python_version>='3.10'", "websockets>=15.0, <16"] viz = ["graphviz>=0.17"] +litellm = ["litellm>=1.65.0, <2"] [dependency-groups] dev = [ @@ -44,7 +45,7 @@ dev = [ "pytest", "pytest-asyncio", "pytest-mock>=3.14.0", - "rich", + "rich>=13.1.0, <14", "mkdocs>=1.6.0", "mkdocs-material>=9.6.0", "mkdocstrings[python]>=0.28.0", diff --git a/src/agents/extensions/models/__init__.py b/src/agents/extensions/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py new file mode 100644 index 00000000..915a1ccb --- /dev/null +++ b/src/agents/extensions/models/litellm_model.py @@ -0,0 +1,380 @@ +from __future__ import annotations + +import dataclasses +import json +import time +from collections.abc import AsyncIterator +from typing import Any, Literal, cast, overload + +import litellm.types + +from agents.exceptions import ModelBehaviorError + +try: + import litellm +except ImportError as _e: + raise ImportError( + "`litellm` is required to use the LitellmModel. You can install it via the optional " + "dependency group: `pip install 'openai-agents[litellm]'`." + ) from _e + +from openai import NOT_GIVEN, AsyncStream, NotGiven +from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageToolCall +from openai.types.chat.chat_completion_message import ( + Annotation, + AnnotationURLCitation, + ChatCompletionMessage, +) +from openai.types.chat.chat_completion_message_tool_call import Function +from openai.types.responses import Response + +from ... import _debug +from ...agent_output import AgentOutputSchema +from ...handoffs import Handoff +from ...items import ModelResponse, TResponseInputItem, TResponseStreamEvent +from ...logger import logger +from ...model_settings import ModelSettings +from ...models.chatcmpl_converter import Converter +from ...models.chatcmpl_helpers import HEADERS +from ...models.chatcmpl_stream_handler import ChatCmplStreamHandler +from ...models.fake_id import FAKE_RESPONSES_ID +from ...models.interface import Model, ModelTracing +from ...tool import Tool +from ...tracing import generation_span +from ...tracing.span_data import GenerationSpanData +from ...tracing.spans import Span +from ...usage import Usage + + +class LitellmModel(Model): + def __init__( + self, + model: str, + base_url: str | None = None, + api_key: str | None = None, + ): + self.model = model + self.base_url = base_url + self.api_key = api_key + + async def get_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchema | None, + handoffs: list[Handoff], + tracing: ModelTracing, + previous_response_id: str | None, + ) -> ModelResponse: + with generation_span( + model=str(self.model), + model_config=dataclasses.asdict(model_settings) + | {"base_url": str(self.base_url or ""), "model_impl": "litellm"}, + disabled=tracing.is_disabled(), + ) as span_generation: + response = await self._fetch_response( + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + span_generation, + tracing, + stream=False, + ) + + assert isinstance(response.choices[0], litellm.types.utils.Choices) + + if _debug.DONT_LOG_MODEL_DATA: + logger.debug("Received model response") + else: + logger.debug( + f"LLM resp:\n{json.dumps(response.choices[0].message.model_dump(), indent=2)}\n" + ) + + if hasattr(response, "usage"): + response_usage = response.usage + usage = ( + Usage( + requests=1, + input_tokens=response_usage.prompt_tokens, + output_tokens=response_usage.completion_tokens, + total_tokens=response_usage.total_tokens, + ) + if response.usage + else Usage() + ) + else: + usage = Usage() + logger.warning("No usage information returned from Litellm") + + if tracing.include_data(): + span_generation.span_data.output = [response.choices[0].message.model_dump()] + span_generation.span_data.usage = { + "input_tokens": usage.input_tokens, + "output_tokens": usage.output_tokens, + } + + items = Converter.message_to_output_items( + LitellmConverter.convert_message_to_openai(response.choices[0].message) + ) + + return ModelResponse( + output=items, + usage=usage, + response_id=None, + ) + + async def stream_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchema | None, + handoffs: list[Handoff], + tracing: ModelTracing, + *, + previous_response_id: str | None, + ) -> AsyncIterator[TResponseStreamEvent]: + """ + Yields a partial message as it is generated, as well as the usage information. + """ + with generation_span( + model=str(self.model), + model_config=dataclasses.asdict(model_settings) + | {"base_url": str(self.base_url or ""), "model_impl": "litellm"}, + disabled=tracing.is_disabled(), + ) as span_generation: + response, stream = await self._fetch_response( + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + span_generation, + tracing, + stream=True, + ) + + final_response: Response | None = None + async for chunk in ChatCmplStreamHandler.handle_stream(response, stream): + yield chunk + + if chunk.type == "response.completed": + final_response = chunk.response + + if tracing.include_data() and final_response: + span_generation.span_data.output = [final_response.model_dump()] + + if final_response and final_response.usage: + span_generation.span_data.usage = { + "input_tokens": final_response.usage.input_tokens, + "output_tokens": final_response.usage.output_tokens, + } + + @overload + async def _fetch_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchema | None, + handoffs: list[Handoff], + span: Span[GenerationSpanData], + tracing: ModelTracing, + stream: Literal[True], + ) -> tuple[Response, AsyncStream[ChatCompletionChunk]]: ... + + @overload + async def _fetch_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchema | None, + handoffs: list[Handoff], + span: Span[GenerationSpanData], + tracing: ModelTracing, + stream: Literal[False], + ) -> litellm.types.utils.ModelResponse: ... + + async def _fetch_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchema | None, + handoffs: list[Handoff], + span: Span[GenerationSpanData], + tracing: ModelTracing, + stream: bool = False, + ) -> litellm.types.utils.ModelResponse | tuple[Response, AsyncStream[ChatCompletionChunk]]: + converted_messages = Converter.items_to_messages(input) + + if system_instructions: + converted_messages.insert( + 0, + { + "content": system_instructions, + "role": "system", + }, + ) + if tracing.include_data(): + span.span_data.input = converted_messages + + parallel_tool_calls = ( + True + if model_settings.parallel_tool_calls and tools and len(tools) > 0 + else False + if model_settings.parallel_tool_calls is False + else None + ) + tool_choice = Converter.convert_tool_choice(model_settings.tool_choice) + response_format = Converter.convert_response_format(output_schema) + + converted_tools = [Converter.tool_to_openai(tool) for tool in tools] if tools else [] + + for handoff in handoffs: + converted_tools.append(Converter.convert_handoff_tool(handoff)) + + if _debug.DONT_LOG_MODEL_DATA: + logger.debug("Calling LLM") + else: + logger.debug( + f"Calling Litellm model: {self.model}\n" + f"{json.dumps(converted_messages, indent=2)}\n" + f"Tools:\n{json.dumps(converted_tools, indent=2)}\n" + f"Stream: {stream}\n" + f"Tool choice: {tool_choice}\n" + f"Response format: {response_format}\n" + ) + + reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None + + stream_options = None + if stream and model_settings.include_usage is not None: + stream_options = {"include_usage": model_settings.include_usage} + + extra_kwargs = {} + if model_settings.extra_query: + extra_kwargs["extra_query"] = model_settings.extra_query + if model_settings.metadata: + extra_kwargs["metadata"] = model_settings.metadata + + ret = await litellm.acompletion( + model=self.model, + messages=converted_messages, + tools=converted_tools or None, + temperature=model_settings.temperature, + top_p=model_settings.top_p, + frequency_penalty=model_settings.frequency_penalty, + presence_penalty=model_settings.presence_penalty, + max_tokens=model_settings.max_tokens, + tool_choice=self._remove_not_given(tool_choice), + response_format=self._remove_not_given(response_format), + parallel_tool_calls=parallel_tool_calls, + stream=stream, + stream_options=stream_options, + reasoning_effort=reasoning_effort, + extra_headers=HEADERS, + api_key=self.api_key, + base_url=self.base_url, + **extra_kwargs, + ) + + if isinstance(ret, litellm.types.utils.ModelResponse): + return ret + + response = Response( + id=FAKE_RESPONSES_ID, + created_at=time.time(), + model=self.model, + object="response", + output=[], + tool_choice=cast(Literal["auto", "required", "none"], tool_choice) + if tool_choice != NOT_GIVEN + else "auto", + top_p=model_settings.top_p, + temperature=model_settings.temperature, + tools=[], + parallel_tool_calls=parallel_tool_calls or False, + reasoning=model_settings.reasoning, + ) + return response, ret + + def _remove_not_given(self, value: Any) -> Any: + if isinstance(value, NotGiven): + return None + return value + + +class LitellmConverter: + @classmethod + def convert_message_to_openai( + cls, message: litellm.types.utils.Message + ) -> ChatCompletionMessage: + if message.role != "assistant": + raise ModelBehaviorError(f"Unsupported role: {message.role}") + + tool_calls = ( + [LitellmConverter.convert_tool_call_to_openai(tool) for tool in message.tool_calls] + if message.tool_calls + else None + ) + + provider_specific_fields = message.get("provider_specific_fields", None) + refusal = ( + provider_specific_fields.get("refusal", None) if provider_specific_fields else None + ) + + return ChatCompletionMessage( + content=message.content, + refusal=refusal, + role="assistant", + annotations=cls.convert_annotations_to_openai(message), + audio=message.get("audio", None), # litellm deletes audio if not present + tool_calls=tool_calls, + ) + + @classmethod + def convert_annotations_to_openai( + cls, message: litellm.types.utils.Message + ) -> list[Annotation] | None: + annotations: list[litellm.types.llms.openai.ChatCompletionAnnotation] | None = message.get( + "annotations", None + ) + if not annotations: + return None + + return [ + Annotation( + type="url_citation", + url_citation=AnnotationURLCitation( + start_index=annotation["url_citation"]["start_index"], + end_index=annotation["url_citation"]["end_index"], + url=annotation["url_citation"]["url"], + title=annotation["url_citation"]["title"], + ), + ) + for annotation in annotations + ] + + @classmethod + def convert_tool_call_to_openai( + cls, tool_call: litellm.types.utils.ChatCompletionMessageToolCall + ) -> ChatCompletionMessageToolCall: + return ChatCompletionMessageToolCall( + id=tool_call.id, + type="function", + function=Function( + name=tool_call.function.name or "", arguments=tool_call.function.arguments + ), + ) diff --git a/src/agents/models/chatcmpl_helpers.py b/src/agents/models/chatcmpl_helpers.py new file mode 100644 index 00000000..0cee21ec --- /dev/null +++ b/src/agents/models/chatcmpl_helpers.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from openai import AsyncOpenAI + +from ..model_settings import ModelSettings +from ..version import __version__ + +_USER_AGENT = f"Agents/Python {__version__}" +HEADERS = {"User-Agent": _USER_AGENT} + + +class ChatCmplHelpers: + @classmethod + def is_openai(cls, client: AsyncOpenAI): + return str(client.base_url).startswith("https://api.openai.com") + + @classmethod + def get_store_param(cls, client: AsyncOpenAI, model_settings: ModelSettings) -> bool | None: + # Match the behavior of Responses where store is True when not given + default_store = True if cls.is_openai(client) else None + return model_settings.store if model_settings.store is not None else default_store + + @classmethod + def get_stream_options_param( + cls, client: AsyncOpenAI, model_settings: ModelSettings, stream: bool + ) -> dict[str, bool] | None: + if not stream: + return None + + default_include_usage = True if cls.is_openai(client) else None + include_usage = ( + model_settings.include_usage + if model_settings.include_usage is not None + else default_include_usage + ) + stream_options = {"include_usage": include_usage} if include_usage is not None else None + return stream_options diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index c399168d..9989c1ee 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -21,8 +21,8 @@ from ..tracing.span_data import GenerationSpanData from ..tracing.spans import Span from ..usage import Usage -from ..version import __version__ from .chatcmpl_converter import Converter +from .chatcmpl_helpers import HEADERS, ChatCmplHelpers from .chatcmpl_stream_handler import ChatCmplStreamHandler from .fake_id import FAKE_RESPONSES_ID from .interface import Model, ModelTracing @@ -31,10 +31,6 @@ from ..model_settings import ModelSettings -_USER_AGENT = f"Agents/Python {__version__}" -_HEADERS = {"User-Agent": _USER_AGENT} - - class OpenAIChatCompletionsModel(Model): def __init__( self, @@ -237,9 +233,9 @@ async def _fetch_response( ) reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None - store = _Helpers.get_store_param(self._get_client(), model_settings) + store = ChatCmplHelpers.get_store_param(self._get_client(), model_settings) - stream_options = _Helpers.get_stream_options_param( + stream_options = ChatCmplHelpers.get_stream_options_param( self._get_client(), model_settings, stream=stream ) @@ -259,7 +255,7 @@ async def _fetch_response( stream_options=self._non_null_or_not_given(stream_options), store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), - extra_headers=_HEADERS, + extra_headers=HEADERS, extra_query=model_settings.extra_query, extra_body=model_settings.extra_body, metadata=self._non_null_or_not_given(model_settings.metadata), @@ -289,31 +285,3 @@ def _get_client(self) -> AsyncOpenAI: if self._client is None: self._client = AsyncOpenAI() return self._client - - -class _Helpers: - @classmethod - def is_openai(cls, client: AsyncOpenAI): - return str(client.base_url).startswith("https://api.openai.com") - - @classmethod - def get_store_param(cls, client: AsyncOpenAI, model_settings: ModelSettings) -> bool | None: - # Match the behavior of Responses where store is True when not given - default_store = True if cls.is_openai(client) else None - return model_settings.store if model_settings.store is not None else default_store - - @classmethod - def get_stream_options_param( - cls, client: AsyncOpenAI, model_settings: ModelSettings, stream: bool - ) -> dict[str, bool] | None: - if not stream: - return None - - default_include_usage = True if cls.is_openai(client) else None - include_usage = ( - model_settings.include_usage - if model_settings.include_usage is not None - else default_include_usage - ) - stream_options = {"include_usage": include_usage} if include_usage is not None else None - return stream_options diff --git a/tests/test_function_schema.py b/tests/test_function_schema.py index ef1e9c22..5618d8ae 100644 --- a/tests/test_function_schema.py +++ b/tests/test_function_schema.py @@ -99,7 +99,7 @@ def varargs_function(x: int, *numbers: float, flag: bool = False, **kwargs: Any) def test_varargs_function(): """Test a function that uses *args and **kwargs.""" - func_schema = function_schema(varargs_function) + func_schema = function_schema(varargs_function, strict_json_schema=False) # Check JSON schema structure assert isinstance(func_schema.params_json_schema, dict) assert func_schema.params_json_schema.get("title") == "varargs_function_args" diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index b0fc1f32..ba3ec68d 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -30,8 +30,8 @@ OpenAIProvider, generation_span, ) +from agents.models.chatcmpl_helpers import ChatCmplHelpers from agents.models.fake_id import FAKE_RESPONSES_ID -from agents.models.openai_chatcompletions import _Helpers @pytest.mark.allow_call_model_methods @@ -301,32 +301,32 @@ def test_store_param(): model_settings = ModelSettings() client = AsyncOpenAI() - assert _Helpers.get_store_param(client, model_settings) is True, ( + assert ChatCmplHelpers.get_store_param(client, model_settings) is True, ( "Should default to True for OpenAI API calls" ) model_settings = ModelSettings(store=False) - assert _Helpers.get_store_param(client, model_settings) is False, ( + assert ChatCmplHelpers.get_store_param(client, model_settings) is False, ( "Should respect explicitly set store=False" ) model_settings = ModelSettings(store=True) - assert _Helpers.get_store_param(client, model_settings) is True, ( + assert ChatCmplHelpers.get_store_param(client, model_settings) is True, ( "Should respect explicitly set store=True" ) client = AsyncOpenAI(base_url="http://www.notopenai.com") model_settings = ModelSettings() - assert _Helpers.get_store_param(client, model_settings) is None, ( + assert ChatCmplHelpers.get_store_param(client, model_settings) is None, ( "Should default to None for non-OpenAI API calls" ) model_settings = ModelSettings(store=False) - assert _Helpers.get_store_param(client, model_settings) is False, ( + assert ChatCmplHelpers.get_store_param(client, model_settings) is False, ( "Should respect explicitly set store=False" ) model_settings = ModelSettings(store=True) - assert _Helpers.get_store_param(client, model_settings) is True, ( + assert ChatCmplHelpers.get_store_param(client, model_settings) is True, ( "Should respect explicitly set store=True" ) diff --git a/tests/voice/conftest.py b/tests/voice/conftest.py index 7eeb9cec..6ed7422c 100644 --- a/tests/voice/conftest.py +++ b/tests/voice/conftest.py @@ -1,14 +1,12 @@ import os import sys -import pytest - -def pytest_collection_modifyitems(config, items): +# Skip voice tests on Python 3.9 +def pytest_ignore_collect(collection_path, config): if sys.version_info[:2] == (3, 9): this_dir = os.path.dirname(__file__) - skip_marker = pytest.mark.skip(reason="Skipped on Python 3.9") - for item in items: - if item.fspath.dirname.startswith(this_dir): - item.add_marker(skip_marker) + if str(collection_path).startswith(this_dir): + return True + diff --git a/uv.lock b/uv.lock index d4b82546..24d089a5 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,125 @@ resolution-markers = [ "python_full_version < '3.10'", ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, +] + +[[package]] +name = "aiohttp" +version = "3.11.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d9/1c4721d143e14af753f2bf5e3b681883e1f24b592c0482df6fa6e33597fa/aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8", size = 7676826 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/21/6bd4cb580a323b64cda3b11fcb3f68deba77568e97806727a858de57349d/aiohttp-3.11.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb46bb0f24813e6cede6cc07b1961d4b04f331f7112a23b5e21f567da4ee50aa", size = 708259 }, + { url = "https://files.pythonhosted.org/packages/96/8c/7b4b9debe90ffc31931b85ee8612a5c83f34d8fdc6d90ee3eb27b43639e4/aiohttp-3.11.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54eb3aead72a5c19fad07219acd882c1643a1027fbcdefac9b502c267242f955", size = 468886 }, + { url = "https://files.pythonhosted.org/packages/13/da/a7fcd68e62acacf0a1930060afd2c970826f989265893082b6fb9eb25cb5/aiohttp-3.11.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:38bea84ee4fe24ebcc8edeb7b54bf20f06fd53ce4d2cc8b74344c5b9620597fd", size = 455846 }, + { url = "https://files.pythonhosted.org/packages/5d/12/b73d9423253f4c872d276a3771decb0722cb5f962352593bd617445977ba/aiohttp-3.11.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0666afbe984f6933fe72cd1f1c3560d8c55880a0bdd728ad774006eb4241ecd", size = 1587183 }, + { url = "https://files.pythonhosted.org/packages/75/d3/291b57d54719d996e6cb8c1db8b13d01bdb24dca90434815ac7e6a70393f/aiohttp-3.11.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba92a2d9ace559a0a14b03d87f47e021e4fa7681dc6970ebbc7b447c7d4b7cd", size = 1634937 }, + { url = "https://files.pythonhosted.org/packages/be/85/4229eba92b433173065b0b459ab677ca11ead4a179f76ccfe55d8738b188/aiohttp-3.11.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ad1d59fd7114e6a08c4814983bb498f391c699f3c78712770077518cae63ff7", size = 1667980 }, + { url = "https://files.pythonhosted.org/packages/2b/0d/d2423936962e3c711fafd5bb9172a99e6b07dd63e086515aa957d8a991fd/aiohttp-3.11.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b88a2bf26965f2015a771381624dd4b0839034b70d406dc74fd8be4cc053e3", size = 1590365 }, + { url = "https://files.pythonhosted.org/packages/ea/93/04209affc20834982c1ef4214b1afc07743667998a9975d69413e9c1e1c1/aiohttp-3.11.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:576f5ca28d1b3276026f7df3ec841ae460e0fc3aac2a47cbf72eabcfc0f102e1", size = 1547614 }, + { url = "https://files.pythonhosted.org/packages/f6/fb/194ad4e4cae98023ae19556e576347f402ce159e80d74cc0713d460c4a39/aiohttp-3.11.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a2a450bcce4931b295fc0848f384834c3f9b00edfc2150baafb4488c27953de6", size = 1532815 }, + { url = "https://files.pythonhosted.org/packages/33/6d/a4da7adbac90188bf1228c73b6768a607dd279c146721a9ff7dcb75c5ac6/aiohttp-3.11.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:37dcee4906454ae377be5937ab2a66a9a88377b11dd7c072df7a7c142b63c37c", size = 1559005 }, + { url = "https://files.pythonhosted.org/packages/7e/88/2fa9fbfd23fc16cb2cfdd1f290343e085e7e327438041e9c6aa0208a854d/aiohttp-3.11.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4d0c970c0d602b1017e2067ff3b7dac41c98fef4f7472ec2ea26fd8a4e8c2149", size = 1535231 }, + { url = "https://files.pythonhosted.org/packages/f5/8f/9623cd2558e3e182d02dcda8b480643e1c48a0550a86e3050210e98dba27/aiohttp-3.11.16-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:004511d3413737700835e949433536a2fe95a7d0297edd911a1e9705c5b5ea43", size = 1609985 }, + { url = "https://files.pythonhosted.org/packages/f8/a2/53a8d1bfc67130710f1c8091f623cdefe7f85cd5d09e14637ed2ed6e1a6d/aiohttp-3.11.16-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c15b2271c44da77ee9d822552201180779e5e942f3a71fb74e026bf6172ff287", size = 1628842 }, + { url = "https://files.pythonhosted.org/packages/49/3a/35fb43d07489573c6c1f8c6a3e6c657196124a63223705b7feeddaea06f1/aiohttp-3.11.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad9509ffb2396483ceacb1eee9134724443ee45b92141105a4645857244aecc8", size = 1566929 }, + { url = "https://files.pythonhosted.org/packages/d5/82/bb3f4f2cc7677e790ba4c040db7dd8445c234a810ef893a858e217647d38/aiohttp-3.11.16-cp310-cp310-win32.whl", hash = "sha256:634d96869be6c4dc232fc503e03e40c42d32cfaa51712aee181e922e61d74814", size = 416935 }, + { url = "https://files.pythonhosted.org/packages/df/ad/a64db1c18063569d6dff474c46a7d4de7ab85ff55e2a35839b149b1850ea/aiohttp-3.11.16-cp310-cp310-win_amd64.whl", hash = "sha256:938f756c2b9374bbcc262a37eea521d8a0e6458162f2a9c26329cc87fdf06534", size = 442168 }, + { url = "https://files.pythonhosted.org/packages/b1/98/be30539cd84260d9f3ea1936d50445e25aa6029a4cb9707f3b64cfd710f7/aiohttp-3.11.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8cb0688a8d81c63d716e867d59a9ccc389e97ac7037ebef904c2b89334407180", size = 708664 }, + { url = "https://files.pythonhosted.org/packages/e6/27/d51116ce18bdfdea7a2244b55ad38d7b01a4298af55765eed7e8431f013d/aiohttp-3.11.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ad1fb47da60ae1ddfb316f0ff16d1f3b8e844d1a1e154641928ea0583d486ed", size = 468953 }, + { url = "https://files.pythonhosted.org/packages/34/23/eedf80ec42865ea5355b46265a2433134138eff9a4fea17e1348530fa4ae/aiohttp-3.11.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df7db76400bf46ec6a0a73192b14c8295bdb9812053f4fe53f4e789f3ea66bbb", size = 456065 }, + { url = "https://files.pythonhosted.org/packages/36/23/4a5b1ef6cff994936bf96d981dd817b487d9db755457a0d1c2939920d620/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc3a145479a76ad0ed646434d09216d33d08eef0d8c9a11f5ae5cdc37caa3540", size = 1687976 }, + { url = "https://files.pythonhosted.org/packages/d0/5d/c7474b4c3069bb35276d54c82997dff4f7575e4b73f0a7b1b08a39ece1eb/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d007aa39a52d62373bd23428ba4a2546eed0e7643d7bf2e41ddcefd54519842c", size = 1752711 }, + { url = "https://files.pythonhosted.org/packages/64/4c/ee416987b6729558f2eb1b727c60196580aafdb141e83bd78bb031d1c000/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6ddd90d9fb4b501c97a4458f1c1720e42432c26cb76d28177c5b5ad4e332601", size = 1791305 }, + { url = "https://files.pythonhosted.org/packages/58/28/3e1e1884070b95f1f69c473a1995852a6f8516670bb1c29d6cb2dbb73e1c/aiohttp-3.11.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a2f451849e6b39e5c226803dcacfa9c7133e9825dcefd2f4e837a2ec5a3bb98", size = 1674499 }, + { url = "https://files.pythonhosted.org/packages/ad/55/a032b32fa80a662d25d9eb170ed1e2c2be239304ca114ec66c89dc40f37f/aiohttp-3.11.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8df6612df74409080575dca38a5237282865408016e65636a76a2eb9348c2567", size = 1622313 }, + { url = "https://files.pythonhosted.org/packages/b1/df/ca775605f72abbda4e4746e793c408c84373ca2c6ce7a106a09f853f1e89/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78e6e23b954644737e385befa0deb20233e2dfddf95dd11e9db752bdd2a294d3", size = 1658274 }, + { url = "https://files.pythonhosted.org/packages/cc/6c/21c45b66124df5b4b0ab638271ecd8c6402b702977120cb4d5be6408e15d/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:696ef00e8a1f0cec5e30640e64eca75d8e777933d1438f4facc9c0cdf288a810", size = 1666704 }, + { url = "https://files.pythonhosted.org/packages/1d/e2/7d92adc03e3458edd18a21da2575ab84e58f16b1672ae98529e4eeee45ab/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3538bc9fe1b902bef51372462e3d7c96fce2b566642512138a480b7adc9d508", size = 1652815 }, + { url = "https://files.pythonhosted.org/packages/3a/52/7549573cd654ad651e3c5786ec3946d8f0ee379023e22deb503ff856b16c/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ab3367bb7f61ad18793fea2ef71f2d181c528c87948638366bf1de26e239183", size = 1735669 }, + { url = "https://files.pythonhosted.org/packages/d5/54/dcd24a23c7a5a2922123e07a296a5f79ea87ce605f531be068415c326de6/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:56a3443aca82abda0e07be2e1ecb76a050714faf2be84256dae291182ba59049", size = 1760422 }, + { url = "https://files.pythonhosted.org/packages/a7/53/87327fe982fa310944e1450e97bf7b2a28015263771931372a1dfe682c58/aiohttp-3.11.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:61c721764e41af907c9d16b6daa05a458f066015abd35923051be8705108ed17", size = 1694457 }, + { url = "https://files.pythonhosted.org/packages/ce/6d/c5ccf41059267bcf89853d3db9d8d217dacf0a04f4086cb6bf278323011f/aiohttp-3.11.16-cp311-cp311-win32.whl", hash = "sha256:3e061b09f6fa42997cf627307f220315e313ece74907d35776ec4373ed718b86", size = 416817 }, + { url = "https://files.pythonhosted.org/packages/e7/dd/01f6fe028e054ef4f909c9d63e3a2399e77021bb2e1bb51d56ca8b543989/aiohttp-3.11.16-cp311-cp311-win_amd64.whl", hash = "sha256:745f1ed5e2c687baefc3c5e7b4304e91bf3e2f32834d07baaee243e349624b24", size = 442986 }, + { url = "https://files.pythonhosted.org/packages/db/38/100d01cbc60553743baf0fba658cb125f8ad674a8a771f765cdc155a890d/aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27", size = 704881 }, + { url = "https://files.pythonhosted.org/packages/21/ed/b4102bb6245e36591209e29f03fe87e7956e54cb604ee12e20f7eb47f994/aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713", size = 464564 }, + { url = "https://files.pythonhosted.org/packages/3b/e1/a9ab6c47b62ecee080eeb33acd5352b40ecad08fb2d0779bcc6739271745/aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb", size = 456548 }, + { url = "https://files.pythonhosted.org/packages/80/ad/216c6f71bdff2becce6c8776f0aa32cb0fa5d83008d13b49c3208d2e4016/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321", size = 1691749 }, + { url = "https://files.pythonhosted.org/packages/bd/ea/7df7bcd3f4e734301605f686ffc87993f2d51b7acb6bcc9b980af223f297/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e", size = 1736874 }, + { url = "https://files.pythonhosted.org/packages/51/41/c7724b9c87a29b7cfd1202ec6446bae8524a751473d25e2ff438bc9a02bf/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c", size = 1786885 }, + { url = "https://files.pythonhosted.org/packages/86/b3/f61f8492fa6569fa87927ad35a40c159408862f7e8e70deaaead349e2fba/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce", size = 1698059 }, + { url = "https://files.pythonhosted.org/packages/ce/be/7097cf860a9ce8bbb0e8960704e12869e111abcd3fbd245153373079ccec/aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e", size = 1626527 }, + { url = "https://files.pythonhosted.org/packages/1d/1d/aaa841c340e8c143a8d53a1f644c2a2961c58cfa26e7b398d6bf75cf5d23/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b", size = 1644036 }, + { url = "https://files.pythonhosted.org/packages/2c/88/59d870f76e9345e2b149f158074e78db457985c2b4da713038d9da3020a8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540", size = 1685270 }, + { url = "https://files.pythonhosted.org/packages/2b/b1/c6686948d4c79c3745595efc469a9f8a43cab3c7efc0b5991be65d9e8cb8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b", size = 1650852 }, + { url = "https://files.pythonhosted.org/packages/fe/94/3e42a6916fd3441721941e0f1b8438e1ce2a4c49af0e28e0d3c950c9b3c9/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e", size = 1704481 }, + { url = "https://files.pythonhosted.org/packages/b1/6d/6ab5854ff59b27075c7a8c610597d2b6c38945f9a1284ee8758bc3720ff6/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c", size = 1735370 }, + { url = "https://files.pythonhosted.org/packages/73/2a/08a68eec3c99a6659067d271d7553e4d490a0828d588e1daa3970dc2b771/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71", size = 1697619 }, + { url = "https://files.pythonhosted.org/packages/61/d5/fea8dbbfb0cd68fbb56f0ae913270a79422d9a41da442a624febf72d2aaf/aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2", size = 411710 }, + { url = "https://files.pythonhosted.org/packages/33/fb/41cde15fbe51365024550bf77b95a4fc84ef41365705c946da0421f0e1e0/aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682", size = 438012 }, + { url = "https://files.pythonhosted.org/packages/52/52/7c712b2d9fb4d5e5fd6d12f9ab76e52baddfee71e3c8203ca7a7559d7f51/aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489", size = 698005 }, + { url = "https://files.pythonhosted.org/packages/51/3e/61057814f7247666d43ac538abcd6335b022869ade2602dab9bf33f607d2/aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50", size = 461106 }, + { url = "https://files.pythonhosted.org/packages/4f/85/6b79fb0ea6e913d596d5b949edc2402b20803f51b1a59e1bbc5bb7ba7569/aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133", size = 453394 }, + { url = "https://files.pythonhosted.org/packages/4b/04/e1bb3fcfbd2c26753932c759593a32299aff8625eaa0bf8ff7d9c0c34a36/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0", size = 1666643 }, + { url = "https://files.pythonhosted.org/packages/0e/27/97bc0fdd1f439b8f060beb3ba8fb47b908dc170280090801158381ad7942/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca", size = 1721948 }, + { url = "https://files.pythonhosted.org/packages/2c/4f/bc4c5119e75c05ef15c5670ef1563bbe25d4ed4893b76c57b0184d815e8b/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d", size = 1774454 }, + { url = "https://files.pythonhosted.org/packages/73/5b/54b42b2150bb26fdf795464aa55ceb1a49c85f84e98e6896d211eabc6670/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb", size = 1677785 }, + { url = "https://files.pythonhosted.org/packages/10/ee/a0fe68916d3f82eae199b8535624cf07a9c0a0958c7a76e56dd21140487a/aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4", size = 1608456 }, + { url = "https://files.pythonhosted.org/packages/8b/48/83afd779242b7cf7e1ceed2ff624a86d3221e17798061cf9a79e0b246077/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7", size = 1622424 }, + { url = "https://files.pythonhosted.org/packages/6f/27/452f1d5fca1f516f9f731539b7f5faa9e9d3bf8a3a6c3cd7c4b031f20cbd/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd", size = 1660943 }, + { url = "https://files.pythonhosted.org/packages/d6/e1/5c7d63143b8d00c83b958b9e78e7048c4a69903c760c1e329bf02bac57a1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f", size = 1622797 }, + { url = "https://files.pythonhosted.org/packages/46/9e/2ac29cca2746ee8e449e73cd2fcb3d454467393ec03a269d50e49af743f1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd", size = 1687162 }, + { url = "https://files.pythonhosted.org/packages/ad/6b/eaa6768e02edebaf37d77f4ffb74dd55f5cbcbb6a0dbf798ccec7b0ac23b/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34", size = 1718518 }, + { url = "https://files.pythonhosted.org/packages/e5/18/dda87cbad29472a51fa058d6d8257dfce168289adaeb358b86bd93af3b20/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913", size = 1675254 }, + { url = "https://files.pythonhosted.org/packages/32/d9/d2fb08c614df401d92c12fcbc60e6e879608d5e8909ef75c5ad8d4ad8aa7/aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979", size = 410698 }, + { url = "https://files.pythonhosted.org/packages/ce/ed/853e36d5a33c24544cfa46585895547de152dfef0b5c79fa675f6e4b7b87/aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802", size = 436395 }, + { url = "https://files.pythonhosted.org/packages/4b/6e/a423a6fd07e651f6078da862128031cff2f333e995f5efe30bb110c97041/aiohttp-3.11.16-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bbcba75fe879ad6fd2e0d6a8d937f34a571f116a0e4db37df8079e738ea95c71", size = 709172 }, + { url = "https://files.pythonhosted.org/packages/bf/8d/925f3c893523118e5dc729d340df2283d68e7adfa77192908ae63f1ec904/aiohttp-3.11.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:87a6e922b2b2401e0b0cf6b976b97f11ec7f136bfed445e16384fbf6fd5e8602", size = 469390 }, + { url = "https://files.pythonhosted.org/packages/49/57/8a27b793480887bd23288364138c9db2f58cd3cff28945809aa062d019dc/aiohttp-3.11.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccf10f16ab498d20e28bc2b5c1306e9c1512f2840f7b6a67000a517a4b37d5ee", size = 456246 }, + { url = "https://files.pythonhosted.org/packages/e8/e5/e8114c5b1336357089cacf5a4ff298335429f0a0e75dea3ffefd3d4d82e5/aiohttp-3.11.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb3d0cc5cdb926090748ea60172fa8a213cec728bd6c54eae18b96040fcd6227", size = 1590764 }, + { url = "https://files.pythonhosted.org/packages/db/49/ec13c0ad70c4843169111265c47dd568437be354aea4ac732dc6f2e79842/aiohttp-3.11.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d07502cc14ecd64f52b2a74ebbc106893d9a9717120057ea9ea1fd6568a747e7", size = 1638375 }, + { url = "https://files.pythonhosted.org/packages/0f/0d/78a64579b054fa3c0e72083912d4410f5514dc0cd03bef5644d4f1e4e6ed/aiohttp-3.11.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:776c8e959a01e5e8321f1dec77964cb6101020a69d5a94cd3d34db6d555e01f7", size = 1672027 }, + { url = "https://files.pythonhosted.org/packages/54/11/06602ab3446fe96519998b79c762cf0921b620e702bd7659a5e8b998d0e0/aiohttp-3.11.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0902e887b0e1d50424112f200eb9ae3dfed6c0d0a19fc60f633ae5a57c809656", size = 1589609 }, + { url = "https://files.pythonhosted.org/packages/34/1b/6bdebdf702d7f339579e9d3c2e784ca6e5867e247dd7b8690c004431ab57/aiohttp-3.11.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87fd812899aa78252866ae03a048e77bd11b80fb4878ce27c23cade239b42b2", size = 1547540 }, + { url = "https://files.pythonhosted.org/packages/88/dd/5d0c0a936baaabbf7467851c0cc9f1aedab67428479a528ea14ab852c730/aiohttp-3.11.16-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a950c2eb8ff17361abd8c85987fd6076d9f47d040ebffce67dce4993285e973", size = 1534880 }, + { url = "https://files.pythonhosted.org/packages/a8/ff/2245148b047833eb7b37f5754ece17ade561a46c40d6fecc3ed3f5eae1c1/aiohttp-3.11.16-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c10d85e81d0b9ef87970ecbdbfaeec14a361a7fa947118817fcea8e45335fa46", size = 1557692 }, + { url = "https://files.pythonhosted.org/packages/c4/1c/fe0dd097427c295ae49b6c10e37eda546036fd8de75bc43d69df392b9377/aiohttp-3.11.16-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7951decace76a9271a1ef181b04aa77d3cc309a02a51d73826039003210bdc86", size = 1538918 }, + { url = "https://files.pythonhosted.org/packages/94/58/10af247fb0084327579ebaccfd1f9c2f759ec972b204b31598debfa0829a/aiohttp-3.11.16-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14461157d8426bcb40bd94deb0450a6fa16f05129f7da546090cebf8f3123b0f", size = 1609351 }, + { url = "https://files.pythonhosted.org/packages/d3/91/b1f0928b6d2eb0c47ecee7122067a8ad330f812795d8f16343d206394040/aiohttp-3.11.16-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9756d9b9d4547e091f99d554fbba0d2a920aab98caa82a8fb3d3d9bee3c9ae85", size = 1630514 }, + { url = "https://files.pythonhosted.org/packages/88/51/3319add72ea4053bee66825aef3e691ee4b26d0a22b7f817d73b0af02d38/aiohttp-3.11.16-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:87944bd16b7fe6160607f6a17808abd25f17f61ae1e26c47a491b970fb66d8cb", size = 1567084 }, + { url = "https://files.pythonhosted.org/packages/e5/93/e90a84c263f02f01efd6f32042c08d7f7d88338cb18d91c5b1752accffeb/aiohttp-3.11.16-cp39-cp39-win32.whl", hash = "sha256:92b7ee222e2b903e0a4b329a9943d432b3767f2d5029dbe4ca59fb75223bbe2e", size = 417187 }, + { url = "https://files.pythonhosted.org/packages/11/b8/7200f637f223199d8f3e7add720ab19843b9969ffa89b758b5649cab8099/aiohttp-3.11.16-cp39-cp39-win_amd64.whl", hash = "sha256:17ae4664031aadfbcb34fd40ffd90976671fa0c0286e6c4113989f78bebab37a", size = 442378 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -39,6 +158,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + [[package]] name = "babel" version = "2.17.0" @@ -236,72 +373,72 @@ wheels = [ [[package]] name = "coverage" -version = "7.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/36/465f5492443265e1278f9a82ffe6aeed3f1db779da0d6e7d4611a5cfb6af/coverage-7.7.0.tar.gz", hash = "sha256:cd879d4646055a573775a1cec863d00c9ff8c55860f8b17f6d8eee9140c06166", size = 809969 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/f5/2b801fe88f199707cf9ec66dcee036e7073b5a208a4a161b64371b3f1e35/coverage-7.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a538a23119d1e2e2ce077e902d02ea3d8e0641786ef6e0faf11ce82324743944", size = 210608 }, - { url = "https://files.pythonhosted.org/packages/07/44/bcc030cf977d1069a28742c0a67284c6e831ef172f914055b3d45da83f89/coverage-7.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1586ad158523f4133499a4f322b230e2cfef9cc724820dbd58595a5a236186f4", size = 211042 }, - { url = "https://files.pythonhosted.org/packages/2c/3f/b427f17e1bcf3e1f5ac42fc0b6cb623616f2aedcfc7fde17a058afb62568/coverage-7.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b6c96d69928a3a6767fab8dc1ce8a02cf0156836ccb1e820c7f45a423570d98", size = 240168 }, - { url = "https://files.pythonhosted.org/packages/58/92/6e8d71c5e651f152ffc518ec4cd7add87035533e88af29e233635c0f0dfb/coverage-7.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f18d47641282664276977c604b5a261e51fefc2980f5271d547d706b06a837f", size = 238079 }, - { url = "https://files.pythonhosted.org/packages/40/33/1c25ae35c16972dc379c24cd7dde20359d076dee00687825c92a53e43b02/coverage-7.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a1e18a85bd066c7c556d85277a7adf4651f259b2579113844835ba1a74aafd", size = 239216 }, - { url = "https://files.pythonhosted.org/packages/4d/3d/adf40bdd07a49e1880632c1bc6b31f42d32cf0bfe6b4d294a8f706d70078/coverage-7.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:70f0925c4e2bfc965369f417e7cc72538fd1ba91639cf1e4ef4b1a6b50439b3b", size = 239126 }, - { url = "https://files.pythonhosted.org/packages/72/a5/51e39811cd0ec0569a25fe8e6bac0a00efa222a3e49d51d64f5ba0dce24a/coverage-7.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b0fac2088ec4aaeb5468b814bd3ff5e5978364bfbce5e567c44c9e2854469f6c", size = 237842 }, - { url = "https://files.pythonhosted.org/packages/ab/b7/c5796906cd9eed6d258138f1fddc8d6af01b6d07b3c183bac4a9731ac383/coverage-7.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3e212a894d8ae07fde2ca8b43d666a6d49bbbddb10da0f6a74ca7bd31f20054", size = 238136 }, - { url = "https://files.pythonhosted.org/packages/d7/8a/bd34ea3c602b3ef323a001d375f9b1d663e901079bb26b5f9b8f96fae32b/coverage-7.7.0-cp310-cp310-win32.whl", hash = "sha256:f32b165bf6dfea0846a9c9c38b7e1d68f313956d60a15cde5d1709fddcaf3bee", size = 213320 }, - { url = "https://files.pythonhosted.org/packages/94/60/6e7efe849e305a233623a80aaeba7ebb02809fa63ab8a1e49c4323b8083b/coverage-7.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2454b12a3f12cc4698f3508912e6225ec63682e2ca5a96f80a2b93cef9e63f3", size = 214219 }, - { url = "https://files.pythonhosted.org/packages/e8/ec/9e0c9358a3bd56b1ddbf266b889ea9d51ee29e58fb72712d5600663fa806/coverage-7.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0a207c87a9f743c8072d059b4711f8d13c456eb42dac778a7d2e5d4f3c253a7", size = 210722 }, - { url = "https://files.pythonhosted.org/packages/be/bd/7b47a4302423a13960ee30682900d7ca20cee15c978b1d9ea9594d59d352/coverage-7.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d673e3add00048215c2cc507f1228a7523fd8bf34f279ac98334c9b07bd2656", size = 211154 }, - { url = "https://files.pythonhosted.org/packages/c6/7c/ae54d9022440196bf9f3fad535388678a3db186980ff58a4956ddeb849a2/coverage-7.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f81fe93dc1b8e5673f33443c0786c14b77e36f1025973b85e07c70353e46882b", size = 243787 }, - { url = "https://files.pythonhosted.org/packages/2d/21/913a2a2d89a2221f4410fbea4ff84e64ddf4367a4b9eb2c328bd01a1a401/coverage-7.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8c7524779003d59948c51b4fcbf1ca4e27c26a7d75984f63488f3625c328b9b", size = 241473 }, - { url = "https://files.pythonhosted.org/packages/40/f1/5ae36fffd542fb86ab3b2d5e012af0840265f3dd001ad0ffabe9e4dbdcf6/coverage-7.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c124025430249118d018dcedc8b7426f39373527c845093132196f2a483b6dd", size = 243259 }, - { url = "https://files.pythonhosted.org/packages/47/1b/abc87bad7f606a4df321bd8300413fe13700099a163e7d63453c7c70c1b2/coverage-7.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f559c36d5cdc448ee13e7e56ed7b6b5d44a40a511d584d388a0f5d940977ba", size = 242904 }, - { url = "https://files.pythonhosted.org/packages/e0/b3/ff0cf15f5709996727dda2fa00af6f4da92ea3e16168400346f2f742341a/coverage-7.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:37cbc7b0d93dfd133e33c7ec01123fbb90401dce174c3b6661d8d36fb1e30608", size = 241079 }, - { url = "https://files.pythonhosted.org/packages/05/c9/fcad82aad05b1eb8040e6c25ae7a1303716cc05718d4dd326e0fab31aa14/coverage-7.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7d2a65876274acf544703e943c010b60bd79404e3623a1e5d52b64a6e2728de5", size = 241617 }, - { url = "https://files.pythonhosted.org/packages/59/9f/d1efe149afa5c3a459c08bf04f7e6917ef4ee8e3440df5c3e87d6b972870/coverage-7.7.0-cp311-cp311-win32.whl", hash = "sha256:f5a2f71d6a91238e7628f23538c26aa464d390cbdedf12ee2a7a0fb92a24482a", size = 213372 }, - { url = "https://files.pythonhosted.org/packages/88/d2/4b58f03e399185b01fb3168d4b870882de9c7a10e273f99c8f25ec690302/coverage-7.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae8006772c6b0fa53c33747913473e064985dac4d65f77fd2fdc6474e7cd54e4", size = 214285 }, - { url = "https://files.pythonhosted.org/packages/b7/47/f7b870caa26082ff8033be074ac61dc175a6b0c965adf7b910f92a6d7cfe/coverage-7.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:056d3017ed67e7ddf266e6f57378ece543755a4c9231e997789ab3bd11392c94", size = 210907 }, - { url = "https://files.pythonhosted.org/packages/ea/eb/40b39bdc6c1da403257f0fcb2c1b2fd81ff9f66c13abbe3862f42780e1c1/coverage-7.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33c1394d8407e2771547583b66a85d07ed441ff8fae5a4adb4237ad39ece60db", size = 211162 }, - { url = "https://files.pythonhosted.org/packages/53/08/42a2db41b4646d6261122773e222dd7105e2306526f2d7846de6fee808ec/coverage-7.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fbb7a0c3c21908520149d7751cf5b74eb9b38b54d62997b1e9b3ac19a8ee2fe", size = 245223 }, - { url = "https://files.pythonhosted.org/packages/78/2a/0ceb328a7e67e8639d5c7800b8161d4b5f489073ac8d5ac33b11eadee218/coverage-7.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb356e7ae7c2da13f404bf8f75be90f743c6df8d4607022e759f5d7d89fe83f8", size = 242114 }, - { url = "https://files.pythonhosted.org/packages/ba/68/42b13b849d40af1581830ff06c60f4ec84649764f4a58d5c6e20ae11cbd4/coverage-7.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce730d484038e97f27ea2dbe5d392ec5c2261f28c319a3bb266f6b213650135", size = 244371 }, - { url = "https://files.pythonhosted.org/packages/68/66/ab7c3b9fdbeb8bdd322f5b67b1886463834dba2014a534caba60fb0075ea/coverage-7.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa4dff57fc21a575672176d5ab0ef15a927199e775c5e8a3d75162ab2b0c7705", size = 244134 }, - { url = "https://files.pythonhosted.org/packages/01/74/b833d299a479681957d6b238e16a0725586e1d56ec1e43658f3184550bb0/coverage-7.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b667b91f4f714b17af2a18e220015c941d1cf8b07c17f2160033dbe1e64149f0", size = 242353 }, - { url = "https://files.pythonhosted.org/packages/f9/c5/0ed656d65da39bbab8e8fc367dc3d465a7501fea0f2b1caccfb4f6361c9f/coverage-7.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:693d921621a0c8043bfdc61f7d4df5ea6d22165fe8b807cac21eb80dd94e4bbd", size = 243543 }, - { url = "https://files.pythonhosted.org/packages/87/b5/142bcff3828e4cce5d4c9ddc9222de1664464263acca09638e4eb0dbda7c/coverage-7.7.0-cp312-cp312-win32.whl", hash = "sha256:52fc89602cde411a4196c8c6894afb384f2125f34c031774f82a4f2608c59d7d", size = 213543 }, - { url = "https://files.pythonhosted.org/packages/29/74/99d226985def03284bad6a9aff27a1079a8881ec7523b5980b00a5260527/coverage-7.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ce8cf59e09d31a4915ff4c3b94c6514af4c84b22c4cc8ad7c3c546a86150a92", size = 214344 }, - { url = "https://files.pythonhosted.org/packages/45/2f/df6235ec963b9eb6b6b2f3c24f70448f1ffa13b9a481c155a6caff176395/coverage-7.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4545485fef7a8a2d8f30e6f79ce719eb154aab7e44217eb444c1d38239af2072", size = 210934 }, - { url = "https://files.pythonhosted.org/packages/f3/85/ff19510bf642e334845318ddb73a550d2b17082831fa9ae053ce72288be7/coverage-7.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1393e5aa9441dafb0162c36c8506c648b89aea9565b31f6bfa351e66c11bcd82", size = 211212 }, - { url = "https://files.pythonhosted.org/packages/2d/6a/af6582a419550d35eacc3e1bf9f4a936dda0ae559632a0bc4e3aef694ac8/coverage-7.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316f29cc3392fa3912493ee4c83afa4a0e2db04ff69600711f8c03997c39baaa", size = 244727 }, - { url = "https://files.pythonhosted.org/packages/55/62/7c49526111c91f3d7d27e111c22c8d08722f5b661c3f031b625b4d7bc4d9/coverage-7.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ffde1d6bc2a92f9c9207d1ad808550873748ac2d4d923c815b866baa343b3f", size = 241768 }, - { url = "https://files.pythonhosted.org/packages/62/4b/2dc27700782be9795cbbbe98394dd19ef74815d78d5027ed894972cd1b4a/coverage-7.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:416e2a8845eaff288f97eaf76ab40367deafb9073ffc47bf2a583f26b05e5265", size = 243790 }, - { url = "https://files.pythonhosted.org/packages/d3/11/9cc1ae56d3015edca69437f3121c2b44de309f6828980b29e4cc9b13246d/coverage-7.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5efdeff5f353ed3352c04e6b318ab05c6ce9249c25ed3c2090c6e9cadda1e3b2", size = 243861 }, - { url = "https://files.pythonhosted.org/packages/db/e4/2398ed93edcf42ff43002d91c37be11514d825cec382606654fd44f4b8fa/coverage-7.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:57f3bd0d29bf2bd9325c0ff9cc532a175110c4bf8f412c05b2405fd35745266d", size = 241942 }, - { url = "https://files.pythonhosted.org/packages/ec/fe/b6bd35b17a2b8d26bdb21d5ea4351a837ec01edf552655e833629af05b90/coverage-7.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ab7090f04b12dc6469882ce81244572779d3a4b67eea1c96fb9ecc8c607ef39", size = 243228 }, - { url = "https://files.pythonhosted.org/packages/6d/06/d8701bae1e5d865edeb00a6c2a71bd7659ca6af349789271c6fd16a57909/coverage-7.7.0-cp313-cp313-win32.whl", hash = "sha256:180e3fc68ee4dc5af8b33b6ca4e3bb8aa1abe25eedcb958ba5cff7123071af68", size = 213572 }, - { url = "https://files.pythonhosted.org/packages/d7/c1/7e67780bfcaed6bed20100c9e1b2645e3414577b4bdad169578325249045/coverage-7.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:55143aa13c49491f5606f05b49ed88663446dce3a4d3c5d77baa4e36a16d3573", size = 214372 }, - { url = "https://files.pythonhosted.org/packages/ed/25/50b0447442a415ad3da33093c589d9ef945dd6933225f1ce0ac97476397e/coverage-7.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc41374d2f27d81d6558f8a24e5c114580ffefc197fd43eabd7058182f743322", size = 211774 }, - { url = "https://files.pythonhosted.org/packages/13/cc/3daddc707e934d3c0aafaa4a9b217f53fcf4133d4e40cc6ae63aa51243b8/coverage-7.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:89078312f06237417adda7c021c33f80f7a6d2db8572a5f6c330d89b080061ce", size = 211995 }, - { url = "https://files.pythonhosted.org/packages/98/99/c92f43355d3d67f6bf8c946a350f2174e18f9ea7c8a1e36c9eb84ab7d20b/coverage-7.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b2f144444879363ea8834cd7b6869d79ac796cb8f864b0cfdde50296cd95816", size = 256226 }, - { url = "https://files.pythonhosted.org/packages/25/62/65f0f33c08e0a1632f1e487b9c2d252e8bad6a77a942836043972b0ba6d2/coverage-7.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60e6347d1ed882b1159ffea172cb8466ee46c665af4ca397edbf10ff53e9ffaf", size = 251937 }, - { url = "https://files.pythonhosted.org/packages/b2/10/99a9565aaeb159aade178c6509c8324a9c9e825b01f02242a37c2a8869f8/coverage-7.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb203c0afffaf1a8f5b9659a013f8f16a1b2cad3a80a8733ceedc968c0cf4c57", size = 254276 }, - { url = "https://files.pythonhosted.org/packages/a7/12/206196edbf0b82250b11bf5c252fe25ebaa2b7c8d66edb0c194e7b3403fe/coverage-7.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ad0edaa97cb983d9f2ff48cadddc3e1fb09f24aa558abeb4dc9a0dbacd12cbb4", size = 255366 }, - { url = "https://files.pythonhosted.org/packages/a5/82/a2abb8d4cdd99c6a443ab6682c0eee5797490a2113a45ffaa8b6b31c5dcc/coverage-7.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c5f8a5364fc37b2f172c26a038bc7ec4885f429de4a05fc10fdcb53fb5834c5c", size = 253536 }, - { url = "https://files.pythonhosted.org/packages/4d/7d/3747e000e60ad5dd8157bd978f99979967d56cb35c55235980c85305db86/coverage-7.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4e09534037933bf6eb31d804e72c52ec23219b32c1730f9152feabbd7499463", size = 254344 }, - { url = "https://files.pythonhosted.org/packages/45/56/7c33f8a6de1b3b079374d2ae490ccf76fb7c094a23f72d10f071989fc3ef/coverage-7.7.0-cp313-cp313t-win32.whl", hash = "sha256:1b336d06af14f8da5b1f391e8dec03634daf54dfcb4d1c4fb6d04c09d83cef90", size = 214284 }, - { url = "https://files.pythonhosted.org/packages/95/ab/657bfa6171800a67bd1c005402f06d6b78610820ef1364ea4f85b04bbb5b/coverage-7.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b54a1ee4c6f1905a436cbaa04b26626d27925a41cbc3a337e2d3ff7038187f07", size = 215445 }, - { url = "https://files.pythonhosted.org/packages/d1/42/0e77be6f2fafe7f3de88ddf9f8d9a0d8e9a75f9517081d261d31439908c7/coverage-7.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c8fbce80b2b8bf135d105aa8f5b36eae0c57d702a1cc3ebdea2a6f03f6cdde5", size = 210604 }, - { url = "https://files.pythonhosted.org/packages/0e/62/a82adc7818545fca3987367c6b20f239645678438f7da5827a4960bcbe7f/coverage-7.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9710521f07f526de30ccdead67e6b236fe996d214e1a7fba8b36e2ba2cd8261", size = 211031 }, - { url = "https://files.pythonhosted.org/packages/a6/50/a98b418fcaf531b2829b2a06f47f8c5cbc0dcce4a9aa63c5f30bf47d1a92/coverage-7.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7789e700f33f2b133adae582c9f437523cd5db8de845774988a58c360fc88253", size = 239791 }, - { url = "https://files.pythonhosted.org/packages/58/f7/0a8f891fce6f389b1062a520aff130fa6974433efeb549dd19cbdccc76b3/coverage-7.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c36093aca722db73633cf2359026ed7782a239eb1c6db2abcff876012dc4cf", size = 237718 }, - { url = "https://files.pythonhosted.org/packages/a9/8f/362c91661e6c43ff86b65b15bbb60ad1ad4924e9d1e35a0d5f08eb3337c4/coverage-7.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c075d167a6ec99b798c1fdf6e391a1d5a2d054caffe9593ba0f97e3df2c04f0e", size = 238820 }, - { url = "https://files.pythonhosted.org/packages/dd/4b/56520dba6f38ad59e96cdeb8c7eafa47781576d2baabdfa10f8c1813b37b/coverage-7.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d013c07061751ae81861cae6ec3a4fe04e84781b11fd4b6b4201590234b25c7b", size = 238595 }, - { url = "https://files.pythonhosted.org/packages/4d/e6/acfae468bd1f9b691b29d42f93bfd7080c05021103f03580934c066a3844/coverage-7.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:104bf640f408f4e115b85110047c7f27377e1a8b7ba86f7db4fa47aa49dc9a8e", size = 236820 }, - { url = "https://files.pythonhosted.org/packages/22/4f/9b65332326b0c5b7de197a52e766e2bd547beec6948e1d5c4063289e3281/coverage-7.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:39abcacd1ed54e2c33c54bdc488b310e8ef6705833f7148b6eb9a547199d375d", size = 237800 }, - { url = "https://files.pythonhosted.org/packages/bb/99/1c2214678731517d91774b75ed5c0f72feefee3270c232c286b314518d7d/coverage-7.7.0-cp39-cp39-win32.whl", hash = "sha256:8e336b56301774ace6be0017ff85c3566c556d938359b61b840796a0202f805c", size = 213341 }, - { url = "https://files.pythonhosted.org/packages/21/30/4d9ae5544f839da30e42e03850d1dfe4ab184d6307ed971e70178760a68d/coverage-7.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c938c6ae59be67ac19a7204e079efc94b38222cd7d0269f96e45e18cddeaa59", size = 214227 }, - { url = "https://files.pythonhosted.org/packages/cb/69/6a5eac32d2e8721274ef75df1b9fd6a8f7e8231e41ff7bc5501f19835f25/coverage-7.7.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:3b0e6e54591ae0d7427def8a4d40fca99df6b899d10354bab73cd5609807261c", size = 202813 }, - { url = "https://files.pythonhosted.org/packages/2a/ac/60f409a448e5b0e9b8539716f683568aa5848c1be903cdbbc805a552cdf8/coverage-7.7.0-py3-none-any.whl", hash = "sha256:708f0a1105ef2b11c79ed54ed31f17e6325ac936501fc373f24be3e6a578146a", size = 202803 }, +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 }, + { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 }, + { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 }, + { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 }, + { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 }, + { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 }, + { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 }, + { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 }, + { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 }, + { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, + { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, + { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, + { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, + { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, + { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, + { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, + { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, + { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, + { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, + { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, + { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, + { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, + { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, + { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, + { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, + { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, + { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, + { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, + { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, + { url = "https://files.pythonhosted.org/packages/60/0c/5da94be095239814bf2730a28cffbc48d6df4304e044f80d39e1ae581997/coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f", size = 211377 }, + { url = "https://files.pythonhosted.org/packages/d5/cb/b9e93ebf193a0bb89dbcd4f73d7b0e6ecb7c1b6c016671950e25f041835e/coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", size = 211803 }, + { url = "https://files.pythonhosted.org/packages/78/1a/cdbfe9e1bb14d3afcaf6bb6e1b9ba76c72666e329cd06865bbd241efd652/coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", size = 240561 }, + { url = "https://files.pythonhosted.org/packages/59/04/57f1223f26ac018d7ce791bfa65b0c29282de3e041c1cd3ed430cfeac5a5/coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", size = 238488 }, + { url = "https://files.pythonhosted.org/packages/b7/b1/0f25516ae2a35e265868670384feebe64e7857d9cffeeb3887b0197e2ba2/coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", size = 239589 }, + { url = "https://files.pythonhosted.org/packages/e0/a4/99d88baac0d1d5a46ceef2dd687aac08fffa8795e4c3e71b6f6c78e14482/coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", size = 239366 }, + { url = "https://files.pythonhosted.org/packages/ea/9e/1db89e135feb827a868ed15f8fc857160757f9cab140ffee21342c783ceb/coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", size = 237591 }, + { url = "https://files.pythonhosted.org/packages/1b/6d/ac4d6fdfd0e201bc82d1b08adfacb1e34b40d21a22cdd62cfaf3c1828566/coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", size = 238572 }, + { url = "https://files.pythonhosted.org/packages/25/5e/917cbe617c230f7f1745b6a13e780a3a1cd1cf328dbcd0fd8d7ec52858cd/coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", size = 213966 }, + { url = "https://files.pythonhosted.org/packages/bd/93/72b434fe550135869f9ea88dd36068af19afce666db576e059e75177e813/coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", size = 214852 }, + { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, ] [[package]] @@ -346,6 +483,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/79/29d44c4af36b2b240725dce566b20f63f9b36ef267aaaa64ee7466f4f2f8/frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", size = 94451 }, + { url = "https://files.pythonhosted.org/packages/47/47/0c999aeace6ead8a44441b4f4173e2261b18219e4ad1fe9a479871ca02fc/frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", size = 54301 }, + { url = "https://files.pythonhosted.org/packages/8d/60/107a38c1e54176d12e06e9d4b5d755b677d71d1219217cee063911b1384f/frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", size = 52213 }, + { url = "https://files.pythonhosted.org/packages/17/62/594a6829ac5679c25755362a9dc93486a8a45241394564309641425d3ff6/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", size = 240946 }, + { url = "https://files.pythonhosted.org/packages/7e/75/6c8419d8f92c80dd0ee3f63bdde2702ce6398b0ac8410ff459f9b6f2f9cb/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", size = 264608 }, + { url = "https://files.pythonhosted.org/packages/88/3e/82a6f0b84bc6fb7e0be240e52863c6d4ab6098cd62e4f5b972cd31e002e8/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", size = 261361 }, + { url = "https://files.pythonhosted.org/packages/fd/85/14e5f9ccac1b64ff2f10c927b3ffdf88772aea875882406f9ba0cec8ad84/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", size = 231649 }, + { url = "https://files.pythonhosted.org/packages/ee/59/928322800306f6529d1852323014ee9008551e9bb027cc38d276cbc0b0e7/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", size = 241853 }, + { url = "https://files.pythonhosted.org/packages/7d/bd/e01fa4f146a6f6c18c5d34cab8abdc4013774a26c4ff851128cd1bd3008e/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", size = 243652 }, + { url = "https://files.pythonhosted.org/packages/a5/bd/e4771fd18a8ec6757033f0fa903e447aecc3fbba54e3630397b61596acf0/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", size = 241734 }, + { url = "https://files.pythonhosted.org/packages/21/13/c83821fa5544af4f60c5d3a65d054af3213c26b14d3f5f48e43e5fb48556/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", size = 260959 }, + { url = "https://files.pythonhosted.org/packages/71/f3/1f91c9a9bf7ed0e8edcf52698d23f3c211d8d00291a53c9f115ceb977ab1/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", size = 262706 }, + { url = "https://files.pythonhosted.org/packages/4c/22/4a256fdf5d9bcb3ae32622c796ee5ff9451b3a13a68cfe3f68e2c95588ce/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", size = 250401 }, + { url = "https://files.pythonhosted.org/packages/af/89/c48ebe1f7991bd2be6d5f4ed202d94960c01b3017a03d6954dd5fa9ea1e8/frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", size = 45498 }, + { url = "https://files.pythonhosted.org/packages/28/2f/cc27d5f43e023d21fe5c19538e08894db3d7e081cbf582ad5ed366c24446/frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", size = 51622 }, + { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, + { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, + { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, + { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, + { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, + { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, + { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, + { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, + { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, + { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, + { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/da/4d/d94ff0fb0f5313902c132817c62d19cdc5bdcd0c195d392006ef4b779fc6/frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", size = 95319 }, + { url = "https://files.pythonhosted.org/packages/8c/1b/d90e554ca2b483d31cb2296e393f72c25bdc38d64526579e95576bfda587/frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", size = 54749 }, + { url = "https://files.pythonhosted.org/packages/f8/66/7fdecc9ef49f8db2aa4d9da916e4ecf357d867d87aea292efc11e1b2e932/frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", size = 52718 }, + { url = "https://files.pythonhosted.org/packages/08/04/e2fddc92135276e07addbc1cf413acffa0c2d848b3e54cacf684e146df49/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", size = 241756 }, + { url = "https://files.pythonhosted.org/packages/c6/52/be5ff200815d8a341aee5b16b6b707355e0ca3652953852238eb92b120c2/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", size = 267718 }, + { url = "https://files.pythonhosted.org/packages/88/be/4bd93a58be57a3722fc544c36debdf9dcc6758f761092e894d78f18b8f20/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", size = 263494 }, + { url = "https://files.pythonhosted.org/packages/32/ba/58348b90193caa096ce9e9befea6ae67f38dabfd3aacb47e46137a6250a8/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", size = 232838 }, + { url = "https://files.pythonhosted.org/packages/f6/33/9f152105227630246135188901373c4f322cc026565ca6215b063f4c82f4/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", size = 242912 }, + { url = "https://files.pythonhosted.org/packages/a0/10/3db38fb3ccbafadd80a1b0d6800c987b0e3fe3ef2d117c6ced0246eea17a/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", size = 244763 }, + { url = "https://files.pythonhosted.org/packages/e2/cd/1df468fdce2f66a4608dffe44c40cdc35eeaa67ef7fd1d813f99a9a37842/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", size = 242841 }, + { url = "https://files.pythonhosted.org/packages/ee/5f/16097a5ca0bb6b6779c02cc9379c72fe98d56115d4c54d059fb233168fb6/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", size = 263407 }, + { url = "https://files.pythonhosted.org/packages/0f/f7/58cd220ee1c2248ee65a32f5b4b93689e3fe1764d85537eee9fc392543bc/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", size = 265083 }, + { url = "https://files.pythonhosted.org/packages/62/b8/49768980caabf81ac4a2d156008f7cbd0107e6b36d08a313bb31035d9201/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", size = 251564 }, + { url = "https://files.pythonhosted.org/packages/cb/83/619327da3b86ef957ee7a0cbf3c166a09ed1e87a3f7f1ff487d7d0284683/frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", size = 45691 }, + { url = "https://files.pythonhosted.org/packages/8b/28/407bc34a745151ed2322c690b6e7d83d7101472e81ed76e1ebdac0b70a78/frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", size = 51767 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "fsspec" +version = "2025.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/d8/8425e6ba5fcec61a1d16e41b1b71d2bf9344f1fe48012c2b48b9620feae5/fsspec-2025.3.2.tar.gz", hash = "sha256:e52c77ef398680bbd6a98c0e628fbc469491282981209907bbc8aea76a04fdc6", size = 299281 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/4b/e0cfc1a6f17e990f3e64b7d941ddc4acdc7b19d6edd51abf495f32b1a9e4/fsspec-2025.3.2-py3-none-any.whl", hash = "sha256:2daf8dc3d1dfa65b6aa37748d112773a7a08416f6c70d96b264c96476ecaf711", size = 194435 }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -369,75 +608,76 @@ wheels = [ [[package]] name = "greenlet" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, - { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, - { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, - { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, - { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, - { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, - { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, - { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, - { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, - { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, - { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, - { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, - { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, - { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, - { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, - { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, - { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, - { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, - { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, - { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, - { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, - { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, - { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, - { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, - { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, - { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, - { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, - { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, - { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, - { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, - { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, - { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, - { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, - { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, - { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, - { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/bd/1d330ca53f844c463cb63cf4ca1ed1798a50b8fd1e1db576cbb473b8c1b3/greenlet-3.2.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:b7a7b7f2bad3ca72eb2fa14643f1c4ca11d115614047299d89bc24a3b11ddd09", size = 267375 }, + { url = "https://files.pythonhosted.org/packages/a3/a7/7ec4461f7a6a9f8963f2be793a99763e9cd66bc07599011620a75bb3900e/greenlet-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60e77242e38e99ecaede853755bbd8165e0b20a2f1f3abcaa6f0dceb826a7411", size = 625728 }, + { url = "https://files.pythonhosted.org/packages/59/8a/70b63c74b3e27df7827777e206395ee190a0cf8f85cd1b3674b7992651f1/greenlet-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f32d7c70b1c26844fd0e4e56a1da852b493e4e1c30df7b07274a1e5a9b599e", size = 636992 }, + { url = "https://files.pythonhosted.org/packages/5e/d8/dc3e8157b045423f75e2fb327d4c6f20246b5cc12a09f0c7f28860be5dea/greenlet-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97bc1be4bad83b70d8b8627ada6724091af41139616696e59b7088f358583b9", size = 632888 }, + { url = "https://files.pythonhosted.org/packages/2c/fb/6868c1c796ff6f9893d5b312c36c6c9d31c8be98e435210bfe1e5e6f8624/greenlet-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f56a0103deb5570c8d6a0bb4ddf8a7a28931973ad7ed7a883460a67e599b32", size = 631647 }, + { url = "https://files.pythonhosted.org/packages/56/54/a4bdefd2664382c7652fde5d7c2d8851b88161c65fbeeed15b351e5d9fc6/greenlet-3.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2919b126eeb63ca5fa971501cd20cd6cdb5522369a8e39548bbc73a3e10b8b41", size = 580585 }, + { url = "https://files.pythonhosted.org/packages/e9/20/53a45e165c228b4d490a15918377a6ef16cf4ea9ddf5974d4b49e5c81650/greenlet-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:844acfd479ee380f3810415e682c9ee941725fb90b45e139bb7fd6f85c6c9a30", size = 1109798 }, + { url = "https://files.pythonhosted.org/packages/95/c4/f9be6264cc19b8ea2c868e1a0b06546de7da2aa296400845cd4abdbb877b/greenlet-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b986f1a6467710e7ffeeeac1777da0318c95bbfcc467acbd0bd35abc775f558", size = 1133421 }, + { url = "https://files.pythonhosted.org/packages/0a/d6/14648d06627db2db62d633d5d6af96866cea7e38b02b8e4992cd33c58e00/greenlet-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:29449a2b82ed7ce11f8668c31ef20d31e9d88cd8329eb933098fab5a8608a93a", size = 294968 }, + { url = "https://files.pythonhosted.org/packages/2d/d3/0a25528e54eca3c57524d2ef1f63283c8c6db466c785218036ab7fc2d4ff/greenlet-3.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b99de16560097b9984409ded0032f101f9555e1ab029440fc6a8b5e76dbba7ac", size = 268620 }, + { url = "https://files.pythonhosted.org/packages/ff/40/f937eb7c1e641ca12089265c57874fcdd173c6c8aabdec3a494641d81eb9/greenlet-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0bc5776ac2831c022e029839bf1b9d3052332dcf5f431bb88c8503e27398e31", size = 628787 }, + { url = "https://files.pythonhosted.org/packages/12/8d/f248691502cb85ce8b18d442032dbde5d3dd16ff2d15593cbee33c40f29c/greenlet-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dcb1108449b55ff6bc0edac9616468f71db261a4571f27c47ccf3530a7f8b97", size = 640838 }, + { url = "https://files.pythonhosted.org/packages/d5/f1/2a572bf4fc667e8835ed8c4ef8b729eccd0666ed9e6db8c61c5796fd2dc9/greenlet-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a68a25a08f51fc8b66b113d1d9863ee123cdb0e8f1439aed9fc795cd6f85cf", size = 636760 }, + { url = "https://files.pythonhosted.org/packages/12/d6/f9ecc8dcb17516a0f4ab91df28497303e8d2d090d509fe3e1b1a85b23e90/greenlet-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fee6f518868e8206c617f4084a83ad4d7a3750b541bf04e692dfa02e52e805d", size = 636001 }, + { url = "https://files.pythonhosted.org/packages/fc/b2/28ab943ff898d6aad3e0ab88fad722c892a43375fabb9789dcc29075da36/greenlet-3.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6fad8a9ca98b37951a053d7d2d2553569b151cd8c4ede744806b94d50d7f8f73", size = 583936 }, + { url = "https://files.pythonhosted.org/packages/44/a8/dedd1517fae684c3c08ff53ab8b03e328015da4b52d2bd993279ac3a8c3d/greenlet-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e14541f9024a280adb9645143d6a0a51fda6f7c5695fd96cb4d542bb563442f", size = 1112901 }, + { url = "https://files.pythonhosted.org/packages/45/23/15cf5d4bc864c3dc0dcb708bcaa81cd1a3dc2012326d32ad8a46d77a645e/greenlet-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7f163d04f777e7bd229a50b937ecc1ae2a5b25296e6001445e5433e4f51f5191", size = 1138328 }, + { url = "https://files.pythonhosted.org/packages/ba/82/c7cf91e89451a922c049ac1f0123de091260697e26e8b98d299555ad96a5/greenlet-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:39801e633a978c3f829f21022501e7b0c3872683d7495c1850558d1a6fb95ed0", size = 295415 }, + { url = "https://files.pythonhosted.org/packages/0e/8d/3c55e88ab01866fb696f68d6c94587a1b7ec8c8a9c56b1383ad05bc14811/greenlet-3.2.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7d08b88ee8d506ca1f5b2a58744e934d33c6a1686dd83b81e7999dfc704a912f", size = 270391 }, + { url = "https://files.pythonhosted.org/packages/8b/6f/4a15185a386992ba4fbb55f88c1a189b75c7ce6e145b43ae4e50754d1969/greenlet-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58ef3d637c54e2f079064ca936556c4af3989144e4154d80cfd4e2a59fc3769c", size = 637202 }, + { url = "https://files.pythonhosted.org/packages/71/f8/60214debfe3b9670bafac97bfc40e318cbddb4ff4b5cf07df119c4a56dcd/greenlet-3.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33ea7e7269d6f7275ce31f593d6dcfedd97539c01f63fbdc8d84e493e20b1b2c", size = 651391 }, + { url = "https://files.pythonhosted.org/packages/a9/44/fb5e067a728a4df73a30863973912ba6eb01f3d910caaf129ef789ca222d/greenlet-3.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e61d426969b68b2170a9f853cc36d5318030494576e9ec0bfe2dc2e2afa15a68", size = 646118 }, + { url = "https://files.pythonhosted.org/packages/f0/3e/f329b452869d8bc07dbaa112c0175de5e666a7d15eb243781481fb59b863/greenlet-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04e781447a4722e30b4861af728cb878d73a3df79509dc19ea498090cea5d204", size = 648079 }, + { url = "https://files.pythonhosted.org/packages/56/e5/813a2e8e842289579391cbd3ae6e6e6a3d2fcad8bdd89bd549a4035ab057/greenlet-3.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2392cc41eeed4055978c6b52549ccd9effd263bb780ffd639c0e1e7e2055ab0", size = 603825 }, + { url = "https://files.pythonhosted.org/packages/4a/11/0bad66138622d0c1463b0b87935cefd397f9f04fac325a838525a3aa4da7/greenlet-3.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:430cba962c85e339767235a93450a6aaffed6f9c567e73874ea2075f5aae51e1", size = 1119582 }, + { url = "https://files.pythonhosted.org/packages/17/26/0f8a4d222b9014af88bb8b5d921305308dd44de667c01714817dc9fb91fb/greenlet-3.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5e57ff52315bfc0c5493917f328b8ba3ae0c0515d94524453c4d24e7638cbb53", size = 1147452 }, + { url = "https://files.pythonhosted.org/packages/8a/d4/70d262492338c4939f97dca310c45b002a3af84b265720f0e9b135bc85b2/greenlet-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:211a9721f540e454a02e62db7956263e9a28a6cf776d4b9a7213844e36426333", size = 296217 }, + { url = "https://files.pythonhosted.org/packages/c9/43/c0b655d4d7eae19282b028bcec449e5c80626ad0d8d0ca3703f9b1c29258/greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", size = 269131 }, + { url = "https://files.pythonhosted.org/packages/7c/7d/c8f51c373c7f7ac0f73d04a6fd77ab34f6f643cb41a0d186d05ba96708e7/greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", size = 637323 }, + { url = "https://files.pythonhosted.org/packages/89/65/c3ee41b2e56586737d6e124b250583695628ffa6b324855b3a1267a8d1d9/greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", size = 651430 }, + { url = "https://files.pythonhosted.org/packages/f0/07/33bd7a3dcde1db7259371d026ce76be1eb653d2d892334fc79a500b3c5ee/greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", size = 645798 }, + { url = "https://files.pythonhosted.org/packages/35/5b/33c221a6a867030b0b770513a1b78f6c30e04294131dafdc8da78906bbe6/greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", size = 648271 }, + { url = "https://files.pythonhosted.org/packages/4d/dd/d6452248fa6093504e3b7525dc2bdc4e55a4296ec6ee74ba241a51d852e2/greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", size = 606779 }, + { url = "https://files.pythonhosted.org/packages/9d/24/160f04d2589bcb15b8661dcd1763437b22e01643626899a4139bf98f02af/greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", size = 1117968 }, + { url = "https://files.pythonhosted.org/packages/6c/ff/c6e3f3a5168fef5209cfd9498b2b5dd77a0bf29dfc686a03dcc614cf4432/greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", size = 1145510 }, + { url = "https://files.pythonhosted.org/packages/dc/62/5215e374819052e542b5bde06bd7d4a171454b6938c96a2384f21cb94279/greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", size = 296004 }, + { url = "https://files.pythonhosted.org/packages/62/6d/dc9c909cba5cbf4b0833fce69912927a8ca74791c23c47b9fd4f28092108/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b", size = 629900 }, + { url = "https://files.pythonhosted.org/packages/5e/a9/f3f304fbbbd604858ff3df303d7fa1d8f7f9e45a6ef74481aaf03aaac021/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", size = 635270 }, + { url = "https://files.pythonhosted.org/packages/34/92/4b7b4e2e23ecc723cceef9fe3898e78c8e14e106cc7ba2f276a66161da3e/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", size = 632534 }, + { url = "https://files.pythonhosted.org/packages/da/7f/91f0ecbe72c9d789fb7f400b39da9d1e87fcc2cf8746a9636479ba79ab01/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/9f/59/e449a44ce52b13751f55376d85adc155dd311608f6d2aa5b6bd2c8d15486/greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", size = 593697 }, + { url = "https://files.pythonhosted.org/packages/bb/09/cca3392927c5c990b7a8ede64ccd0712808438d6490d63ce6b8704d6df5f/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", size = 1105762 }, + { url = "https://files.pythonhosted.org/packages/4d/b9/3d201f819afc3b7a8cd7ebe645f1a17799603e2d62c968154518f79f4881/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", size = 1125173 }, + { url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908 }, + { url = "https://files.pythonhosted.org/packages/e6/35/06d5fca767ae4660d0f8087bd0552bf7a70e590bad16d0dbd94e1628f4ba/greenlet-3.2.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:4174fa6fa214e8924cedf332b6f2395ba2b9879f250dacd3c361b2fca86f58af", size = 266169 }, + { url = "https://files.pythonhosted.org/packages/00/0a/009c70774c23dd5c353cff5da84320f3c3e92a4e7ee39cf42e0ae2186030/greenlet-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6017a4d430fad5229e397ad464db504ae70cb7b903757c4688cee6c25d6ce8d8", size = 623864 }, + { url = "https://files.pythonhosted.org/packages/04/e2/df53870438ec52e9a1a0fe7da97d25292dd11e1626a13496e27c18eced0d/greenlet-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78b721dfadc60e3639141c0e1f19d23953c5b4b98bfcaf04ce40f79e4f01751c", size = 635665 }, + { url = "https://files.pythonhosted.org/packages/c0/c5/ec035ba7b6c66b475ac12a06d544cae211d65afb6ac3af39215d422bf679/greenlet-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fd2583024ff6cd5d4f842d446d001de4c4fe1264fdb5f28ddea28f6488866df", size = 630948 }, + { url = "https://files.pythonhosted.org/packages/c5/06/3d98e958b27c06b23c531761eef75f2efea7c3a446ab1eb57b70bad8528e/greenlet-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da3bd464c2cc411b723e3d4afc27b13c219ac077ba897bac88443ae45f5ec", size = 630224 }, + { url = "https://files.pythonhosted.org/packages/28/68/bba631f01f3a4df8f45fb4cd3888c54a113829df0612fc380bef20d35664/greenlet-3.2.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2688b3bd3198cc4bad7a79648a95fee088c24a0f6abd05d3639e6c3040ded015", size = 579090 }, + { url = "https://files.pythonhosted.org/packages/4b/8a/bf0a3c944b446716954a9a6f97f51fdd64ed38864d4fba16835e95be0f06/greenlet-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1cf89e2d92bae0d7e2d6093ce0bed26feeaf59a5d588e3984e35fcd46fc41090", size = 1108320 }, + { url = "https://files.pythonhosted.org/packages/1e/fe/4c2daea17f56d41df38af74a7e50fed718a618bfb7e86ac9399560c48d97/greenlet-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b3538711e7c0efd5f7a8fc1096c4db9598d6ed99dc87286b31e4ce9f8a8da67", size = 1132392 }, + { url = "https://files.pythonhosted.org/packages/98/96/c44981a880025a1731ac0c5d83bdb36b1a184c59266c22a4d19041aef19b/greenlet-3.2.0-cp39-cp39-win32.whl", hash = "sha256:ce531d7c424ef327a391de7a9777a6c93a38e1f89e18efa903a1c4ba11f85905", size = 277720 }, + { url = "https://files.pythonhosted.org/packages/7b/9d/7448f8ba7cc29c7113aeb06b70b28be910a3d19a112e5c56885cff7977e5/greenlet-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7b162de2fb61b4c7f4b5d749408bf3280cae65db9b5a6aaf7f922ac829faa67c", size = 294842 }, ] [[package]] name = "griffe" -version = "1.6.2" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/f2/b00eb72b853ecb5bf31dd47857cdf6767e380ca24ec2910d43b3fa7cc500/griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91", size = 392836 } +sdist = { url = "https://files.pythonhosted.org/packages/59/08/7df7e90e34d08ad890bd71d7ba19451052f88dc3d2c483d228d1331a4736/griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c", size = 394919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/bc/bd8b7de5e748e078b6be648e76b47189a9182b1ac1eb7791ff7969f39f27/griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee", size = 128638 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/38b408f41064c9fcdbb0ea27c1bd13a1c8657c4846e04dab9f5ea770602c/griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423", size = 129187 }, ] [[package]] @@ -451,15 +691,15 @@ wheels = [ [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, + { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, ] [[package]] @@ -486,6 +726,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] +[[package]] +name = "huggingface-hub" +version = "0.30.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433 }, +] + [[package]] name = "idna" version = "3.10" @@ -500,7 +758,7 @@ name = "importlib-metadata" version = "8.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } wheels = [ @@ -518,17 +776,18 @@ wheels = [ [[package]] name = "inline-snapshot" -version = "0.20.8" +version = "0.22.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, + { name = "pytest" }, { name = "rich" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/69/79babd0f6ad54c430fba36fb7677774398225287482cf494a15394c75894/inline_snapshot-0.20.8.tar.gz", hash = "sha256:52373c15b63097215d1136f292962553f325a5e966957b489fe4326d6fbc77c0", size = 92748 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/a9/f5c35bdf19f4a93adc281a89cd6cbc19114db649fee5d509257712c6c5b2/inline_snapshot-0.22.3.tar.gz", hash = "sha256:34c02a8567dafc88bb720872edde792ff5e665c8726f6af3bfc5fa85dd0016be", size = 259515 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/74/5222a632fd8d3202ddef383b71c8b6c31a9d77989030efba5be561163d41/inline_snapshot-0.20.8-py3-none-any.whl", hash = "sha256:bded4e142b8817930e4df428b88c462308a8f01ad699852e7574a54bad7ea9f2", size = 48157 }, + { url = "https://files.pythonhosted.org/packages/d8/35/dde6c4fcc46ba87cfe8521ac909174d51d46f1c2490673e0077c3bb0091d/inline_snapshot-0.22.3-py3-none-any.whl", hash = "sha256:2e3f076664a61742a615aa769d30f560acf37c640340a93caf0fe410b4ab8495", size = 50291 }, ] [[package]] @@ -614,6 +873,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/13/c10f17dcddd1b4c1313418e64ace5e77cc4f7313246140fb09044516a62c/jiter-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e8b36d8a16a61993be33e75126ad3d8aa29cf450b09576f3c427d27647fcb4aa", size = 208879 }, ] +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + [[package]] name = "linkify-it-py" version = "2.0.3" @@ -626,16 +912,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820 }, ] +[[package]] +name = "litellm" +version = "1.66.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/21/12562c37310254456afdd277454dac4d14b8b40796216e8a438a9e1c5e86/litellm-1.66.1.tar.gz", hash = "sha256:98f7add913e5eae2131dd412ee27532d9a309defd9dbb64f6c6c42ea8a2af068", size = 7203211 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/33/fdc4615ca621940406e3b0b303e900bc2868cfcd8c62c4a6f5e7d2f6a56c/litellm-1.66.1-py3-none-any.whl", hash = "sha256:1f601fea3f086c1d2d91be60b9db115082a2f3a697e4e0def72f8b9c777c7232", size = 7559553 }, +] + [[package]] name = "markdown" -version = "3.7" +version = "3.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210 }, ] [[package]] @@ -831,7 +1139,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.9" +version = "9.6.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -846,9 +1154,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/cb/6dd3b6a7925429c0229738098ee874dbf7fa02db55558adb2c5bf86077b2/mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c", size = 3948083 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/7e/c65e330e99daa5813e7594e57a09219ad041ed631604a72588ec7c11b34b/mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff", size = 3951595 } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/7c/ea5a671b2ff5d0e3f3108a7f7d75b541d683e4969aaead2a8f3e59e0fc27/mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1", size = 8697935 }, + { url = "https://files.pythonhosted.org/packages/19/91/79a15a772151aca0d505f901f6bbd4b85ee1fe54100256a6702056bab121/mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263", size = 8703720 }, ] [[package]] @@ -874,7 +1182,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.29.0" +version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, @@ -884,11 +1192,10 @@ dependencies = [ { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185 } +sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824 }, + { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, ] [package.optional-dependencies] @@ -898,7 +1205,7 @@ python = [ [[package]] name = "mkdocstrings-python" -version = "1.16.7" +version = "1.16.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, @@ -906,9 +1213,123 @@ dependencies = [ { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/e0/cc35acb47593c138efbfc9dc296ccc26b7ad4452e868fd309f05f6ba0ded/mkdocstrings_python-1.16.7.tar.gz", hash = "sha256:cdfc1a99fe5f6f0d90446a364ef7cac12014a4ef46114b2677a58cec84007117", size = 1475398 } +sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/5e/dc978b9fd6331e2070369579ad8f52145e9ef22a69bfc2811110be95e6d4/mkdocstrings_python-1.16.7-py3-none-any.whl", hash = "sha256:a5589a5be247a28ba651287f83630c69524042f8055d93b5c203d804a3409333", size = 1998312 }, + { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112 }, +] + +[[package]] +name = "multidict" +version = "6.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/44/45e798d4cd1b5dfe41ddf36266c7aca6d954e3c7a8b0d599ad555ce2b4f8/multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5", size = 65822 }, + { url = "https://files.pythonhosted.org/packages/10/fb/9ea024f928503f8c758f8463759d21958bf27b1f7a1103df73e5022e6a7c/multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188", size = 38706 }, + { url = "https://files.pythonhosted.org/packages/6d/eb/7013316febca37414c0e1469fccadcb1a0e4315488f8f57ca5d29b384863/multidict-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7", size = 37979 }, + { url = "https://files.pythonhosted.org/packages/64/28/5a7bf4e7422613ea80f9ebc529d3845b20a422cfa94d4355504ac98047ee/multidict-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291", size = 220233 }, + { url = "https://files.pythonhosted.org/packages/52/05/b4c58850f71befde6a16548968b48331a155a80627750b150bb5962e4dea/multidict-6.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685", size = 217762 }, + { url = "https://files.pythonhosted.org/packages/99/a3/393e23bba1e9a00f95b3957acd8f5e3ee3446e78c550f593be25f9de0483/multidict-6.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf", size = 230699 }, + { url = "https://files.pythonhosted.org/packages/9c/a7/52c63069eb1a079f824257bb8045d93e692fa2eb34d08323d1fdbdfc398a/multidict-6.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1", size = 226801 }, + { url = "https://files.pythonhosted.org/packages/2c/e9/40d2b73e7d6574d91074d83477a990e3701affbe8b596010d4f5e6c7a6fa/multidict-6.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef", size = 219833 }, + { url = "https://files.pythonhosted.org/packages/e4/6a/0572b22fe63c632254f55a1c1cb7d29f644002b1d8731d6103a290edc754/multidict-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9", size = 212920 }, + { url = "https://files.pythonhosted.org/packages/33/fe/c63735db9dece0053868b2d808bcc2592a83ce1830bc98243852a2b34d42/multidict-6.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078", size = 225263 }, + { url = "https://files.pythonhosted.org/packages/47/c2/2db296d64d41525110c27ed38fadd5eb571c6b936233e75a5ea61b14e337/multidict-6.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7", size = 214249 }, + { url = "https://files.pythonhosted.org/packages/7e/74/8bc26e54c79f9a0f111350b1b28a9cacaaee53ecafccd53c90e59754d55a/multidict-6.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451", size = 221650 }, + { url = "https://files.pythonhosted.org/packages/af/d7/2ce87606e3799d9a08a941f4c170930a9895886ea8bd0eca75c44baeebe3/multidict-6.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666", size = 231235 }, + { url = "https://files.pythonhosted.org/packages/07/e1/d191a7ad3b90c613fc4b130d07a41c380e249767586148709b54d006ca17/multidict-6.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c", size = 226056 }, + { url = "https://files.pythonhosted.org/packages/24/05/a57490cf6a8d5854f4af2d17dfc54924f37fbb683986e133b76710a36079/multidict-6.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5", size = 220014 }, + { url = "https://files.pythonhosted.org/packages/5c/b1/be04fa9f08c684e9e27cca85b4ab94c10f017ec07c4c631af9c8c10bb275/multidict-6.4.3-cp310-cp310-win32.whl", hash = "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e", size = 35042 }, + { url = "https://files.pythonhosted.org/packages/d9/ca/8888f99892513001fa900eef11bafbf38ff3485109510487de009da85748/multidict-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887", size = 38506 }, + { url = "https://files.pythonhosted.org/packages/16/e0/53cf7f27eda48fffa53cfd4502329ed29e00efb9e4ce41362cbf8aa54310/multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", size = 65259 }, + { url = "https://files.pythonhosted.org/packages/44/79/1dcd93ce7070cf01c2ee29f781c42b33c64fce20033808f1cc9ec8413d6e/multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", size = 38451 }, + { url = "https://files.pythonhosted.org/packages/f4/35/2292cf29ab5f0d0b3613fad1b75692148959d3834d806be1885ceb49a8ff/multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad", size = 37706 }, + { url = "https://files.pythonhosted.org/packages/f6/d1/6b157110b2b187b5a608b37714acb15ee89ec773e3800315b0107ea648cd/multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", size = 226669 }, + { url = "https://files.pythonhosted.org/packages/40/7f/61a476450651f177c5570e04bd55947f693077ba7804fe9717ee9ae8de04/multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", size = 223182 }, + { url = "https://files.pythonhosted.org/packages/51/7b/eaf7502ac4824cdd8edcf5723e2e99f390c879866aec7b0c420267b53749/multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", size = 235025 }, + { url = "https://files.pythonhosted.org/packages/3b/f6/facdbbd73c96b67a93652774edd5778ab1167854fa08ea35ad004b1b70ad/multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", size = 231481 }, + { url = "https://files.pythonhosted.org/packages/70/57/c008e861b3052405eebf921fd56a748322d8c44dcfcab164fffbccbdcdc4/multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", size = 223492 }, + { url = "https://files.pythonhosted.org/packages/30/4d/7d8440d3a12a6ae5d6b202d6e7f2ac6ab026e04e99aaf1b73f18e6bc34bc/multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", size = 217279 }, + { url = "https://files.pythonhosted.org/packages/7f/e7/bca0df4dd057597b94138d2d8af04eb3c27396a425b1b0a52e082f9be621/multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", size = 228733 }, + { url = "https://files.pythonhosted.org/packages/88/f5/383827c3f1c38d7c92dbad00a8a041760228573b1c542fbf245c37bbca8a/multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", size = 218089 }, + { url = "https://files.pythonhosted.org/packages/36/8a/a5174e8a7d8b94b4c8f9c1e2cf5d07451f41368ffe94d05fc957215b8e72/multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", size = 225257 }, + { url = "https://files.pythonhosted.org/packages/8c/76/1d4b7218f0fd00b8e5c90b88df2e45f8af127f652f4e41add947fa54c1c4/multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", size = 234728 }, + { url = "https://files.pythonhosted.org/packages/64/44/18372a4f6273fc7ca25630d7bf9ae288cde64f29593a078bff450c7170b6/multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", size = 230087 }, + { url = "https://files.pythonhosted.org/packages/0f/ae/28728c314a698d8a6d9491fcacc897077348ec28dd85884d09e64df8a855/multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", size = 223137 }, + { url = "https://files.pythonhosted.org/packages/22/50/785bb2b3fe16051bc91c70a06a919f26312da45c34db97fc87441d61e343/multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", size = 34959 }, + { url = "https://files.pythonhosted.org/packages/2f/63/2a22e099ae2f4d92897618c00c73a09a08a2a9aa14b12736965bf8d59fd3/multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", size = 38541 }, + { url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019 }, + { url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925 }, + { url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008 }, + { url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374 }, + { url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869 }, + { url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949 }, + { url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032 }, + { url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517 }, + { url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291 }, + { url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982 }, + { url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823 }, + { url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714 }, + { url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739 }, + { url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809 }, + { url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934 }, + { url = "https://files.pythonhosted.org/packages/12/c1/259386a9ad6840ff7afc686da96808b503d152ac4feb3a96c651dc4f5abf/multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", size = 35242 }, + { url = "https://files.pythonhosted.org/packages/06/24/c8fdff4f924d37225dc0c56a28b1dca10728fc2233065fafeb27b4b125be/multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", size = 38635 }, + { url = "https://files.pythonhosted.org/packages/6c/4b/86fd786d03915c6f49998cf10cd5fe6b6ac9e9a071cb40885d2e080fb90d/multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", size = 63831 }, + { url = "https://files.pythonhosted.org/packages/45/05/9b51fdf7aef2563340a93be0a663acba2c428c4daeaf3960d92d53a4a930/multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", size = 37888 }, + { url = "https://files.pythonhosted.org/packages/0b/43/53fc25394386c911822419b522181227ca450cf57fea76e6188772a1bd91/multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", size = 36852 }, + { url = "https://files.pythonhosted.org/packages/8a/68/7b99c751e822467c94a235b810a2fd4047d4ecb91caef6b5c60116991c4b/multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", size = 223644 }, + { url = "https://files.pythonhosted.org/packages/80/1b/d458d791e4dd0f7e92596667784fbf99e5c8ba040affe1ca04f06b93ae92/multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", size = 230446 }, + { url = "https://files.pythonhosted.org/packages/e2/46/9793378d988905491a7806d8987862dc5a0bae8a622dd896c4008c7b226b/multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", size = 231070 }, + { url = "https://files.pythonhosted.org/packages/a7/b8/b127d3e1f8dd2a5bf286b47b24567ae6363017292dc6dec44656e6246498/multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", size = 229956 }, + { url = "https://files.pythonhosted.org/packages/0c/93/f70a4c35b103fcfe1443059a2bb7f66e5c35f2aea7804105ff214f566009/multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", size = 222599 }, + { url = "https://files.pythonhosted.org/packages/63/8c/e28e0eb2fe34921d6aa32bfc4ac75b09570b4d6818cc95d25499fe08dc1d/multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", size = 216136 }, + { url = "https://files.pythonhosted.org/packages/72/f5/fbc81f866585b05f89f99d108be5d6ad170e3b6c4d0723d1a2f6ba5fa918/multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", size = 228139 }, + { url = "https://files.pythonhosted.org/packages/bb/ba/7d196bad6b85af2307d81f6979c36ed9665f49626f66d883d6c64d156f78/multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", size = 226251 }, + { url = "https://files.pythonhosted.org/packages/cc/e2/fae46a370dce79d08b672422a33df721ec8b80105e0ea8d87215ff6b090d/multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", size = 221868 }, + { url = "https://files.pythonhosted.org/packages/26/20/bbc9a3dec19d5492f54a167f08546656e7aef75d181d3d82541463450e88/multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", size = 233106 }, + { url = "https://files.pythonhosted.org/packages/ee/8d/f30ae8f5ff7a2461177f4d8eb0d8f69f27fb6cfe276b54ec4fd5a282d918/multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", size = 230163 }, + { url = "https://files.pythonhosted.org/packages/15/e9/2833f3c218d3c2179f3093f766940ded6b81a49d2e2f9c46ab240d23dfec/multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", size = 225906 }, + { url = "https://files.pythonhosted.org/packages/f1/31/6edab296ac369fd286b845fa5dd4c409e63bc4655ed8c9510fcb477e9ae9/multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", size = 35238 }, + { url = "https://files.pythonhosted.org/packages/23/57/2c0167a1bffa30d9a1383c3dab99d8caae985defc8636934b5668830d2ef/multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", size = 38799 }, + { url = "https://files.pythonhosted.org/packages/c9/13/2ead63b9ab0d2b3080819268acb297bd66e238070aa8d42af12b08cbee1c/multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", size = 68642 }, + { url = "https://files.pythonhosted.org/packages/85/45/f1a751e1eede30c23951e2ae274ce8fad738e8a3d5714be73e0a41b27b16/multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", size = 40028 }, + { url = "https://files.pythonhosted.org/packages/a7/29/fcc53e886a2cc5595cc4560df333cb9630257bda65003a7eb4e4e0d8f9c1/multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", size = 39424 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/056c81119d8b88703971f937b371795cab1407cd3c751482de5bfe1a04a9/multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", size = 226178 }, + { url = "https://files.pythonhosted.org/packages/a3/79/3b7e5fea0aa80583d3a69c9d98b7913dfd4fbc341fb10bb2fb48d35a9c21/multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", size = 222617 }, + { url = "https://files.pythonhosted.org/packages/06/db/3ed012b163e376fc461e1d6a67de69b408339bc31dc83d39ae9ec3bf9578/multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", size = 227919 }, + { url = "https://files.pythonhosted.org/packages/b1/db/0433c104bca380989bc04d3b841fc83e95ce0c89f680e9ea4251118b52b6/multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", size = 226097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/910db2618175724dd254b7ae635b6cd8d2947a8b76b0376de7b96d814dab/multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", size = 220706 }, + { url = "https://files.pythonhosted.org/packages/d1/af/aa176c6f5f1d901aac957d5258d5e22897fe13948d1e69063ae3d5d0ca01/multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", size = 211728 }, + { url = "https://files.pythonhosted.org/packages/e7/42/d51cc5fc1527c3717d7f85137d6c79bb7a93cd214c26f1fc57523774dbb5/multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", size = 226276 }, + { url = "https://files.pythonhosted.org/packages/28/6b/d836dea45e0b8432343ba4acf9a8ecaa245da4c0960fb7ab45088a5e568a/multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", size = 212069 }, + { url = "https://files.pythonhosted.org/packages/55/34/0ee1a7adb3560e18ee9289c6e5f7db54edc312b13e5c8263e88ea373d12c/multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", size = 217858 }, + { url = "https://files.pythonhosted.org/packages/04/08/586d652c2f5acefe0cf4e658eedb4d71d4ba6dfd4f189bd81b400fc1bc6b/multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", size = 226988 }, + { url = "https://files.pythonhosted.org/packages/82/e3/cc59c7e2bc49d7f906fb4ffb6d9c3a3cf21b9f2dd9c96d05bef89c2b1fd1/multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", size = 220435 }, + { url = "https://files.pythonhosted.org/packages/e0/32/5c3a556118aca9981d883f38c4b1bfae646f3627157f70f4068e5a648955/multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", size = 221494 }, + { url = "https://files.pythonhosted.org/packages/b9/3b/1599631f59024b75c4d6e3069f4502409970a336647502aaf6b62fb7ac98/multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", size = 41775 }, + { url = "https://files.pythonhosted.org/packages/e8/4e/09301668d675d02ca8e8e1a3e6be046619e30403f5ada2ed5b080ae28d02/multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", size = 45946 }, + { url = "https://files.pythonhosted.org/packages/62/41/609ef2253da5d1686a85456b8315dec648a45a1d547074db225e94b3dd61/multidict-6.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21", size = 65724 }, + { url = "https://files.pythonhosted.org/packages/b5/4e/3a2daf9ccbdb503df7b91cbee240fccc96dd3287397b05ed59673b196cde/multidict-6.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b", size = 38659 }, + { url = "https://files.pythonhosted.org/packages/04/f8/3a7ec724c51ad9c1534ebb0a60020e24c12b1fe4c60a4fdd0c97a3383cf4/multidict-6.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459", size = 37927 }, + { url = "https://files.pythonhosted.org/packages/7f/c5/76c9a8cd657b3a44daf08f14faebb558b00fa22698f58ee7fa3876ade2e4/multidict-6.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840", size = 217990 }, + { url = "https://files.pythonhosted.org/packages/ac/b9/6ccb5bfc3747546e096f34c8b2ee91ccab0a92fefe7a9addc4ef9055ab4d/multidict-6.4.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39", size = 213431 }, + { url = "https://files.pythonhosted.org/packages/0b/e9/95af61c79ffabb4a4331fe0736280ef30b324b67772fd018faf408d73f7d/multidict-6.4.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f", size = 228087 }, + { url = "https://files.pythonhosted.org/packages/04/d2/bd7454b40e4d0f21771b2aa077c0e3f4dfb965f209ffce21112743cdadaa/multidict-6.4.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343", size = 224061 }, + { url = "https://files.pythonhosted.org/packages/7a/f9/b50679179dd909ba28ce49dca551b40a8349aaed64beececd8ab64589b65/multidict-6.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2", size = 216133 }, + { url = "https://files.pythonhosted.org/packages/8f/47/9b77c483a5183ed734d1272cbe685d7313922806d686c63748997374afc1/multidict-6.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6", size = 209868 }, + { url = "https://files.pythonhosted.org/packages/6e/b1/c621ed6098e81404098236a08f7be9274e364cdb0fed12de837030235d19/multidict-6.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e", size = 221723 }, + { url = "https://files.pythonhosted.org/packages/3a/9f/77f41726c1a3e5651e37c67aea5736645484834efd06795b2f8d38318890/multidict-6.4.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1", size = 211008 }, + { url = "https://files.pythonhosted.org/packages/00/66/eec0484c1de91439ce4e054f754f0ecb1c9d1a5fa09a1c12952fb3717ce9/multidict-6.4.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8", size = 216800 }, + { url = "https://files.pythonhosted.org/packages/95/58/a8f07841c6db4bdd8d1ae50cc8910cc63b5078b6dae3b196ec654d888060/multidict-6.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7", size = 227661 }, + { url = "https://files.pythonhosted.org/packages/2a/a5/c50b9430fe79d4b04efda204f22450a23cb4ae895734940541141a858089/multidict-6.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752", size = 221821 }, + { url = "https://files.pythonhosted.org/packages/99/4c/2b69c52c4b1357d197c38a913fcf45b4200af79adfcdf96d88cb02d18f5b/multidict-6.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df", size = 216332 }, + { url = "https://files.pythonhosted.org/packages/1b/39/63d9bd977aed6a053955b30aad38bbfe1f0f8d7462f80760b498387c91ee/multidict-6.4.3-cp39-cp39-win32.whl", hash = "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f", size = 35087 }, + { url = "https://files.pythonhosted.org/packages/8f/d4/c6b8936fa9ff5e77fbba9ba431bc380ad0f8e6442a05c7fb6bfe35fdff60/multidict-6.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897", size = 38680 }, + { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 }, ] [[package]] @@ -964,68 +1385,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] -[[package]] -name = "numpy" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, - { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 }, - { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 }, - { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 }, - { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 }, - { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 }, - { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 }, - { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 }, - { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 }, - { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 }, - { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, - { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, - { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, - { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, - { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, - { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, - { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, - { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, - { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, - { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, - { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, - { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, - { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, - { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, - { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, - { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, - { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, - { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, - { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, - { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 }, - { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 }, - { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 }, - { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 }, - { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 }, - { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 }, - { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 }, - { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 }, - { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 }, - { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 }, - { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 }, - { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 }, - { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 }, - { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, -] - [[package]] name = "numpy" version = "2.2.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 } wheels = [ { url = "https://files.pythonhosted.org/packages/04/89/a79e86e5c1433926ed7d60cb267fb64aa578b6101ab645800fd43b4801de/numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9", size = 21250661 }, @@ -1086,24 +1449,21 @@ wheels = [ [[package]] name = "openai" -version = "1.68.0" +version = "1.74.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "distro" }, { name = "httpx" }, { name = "jiter" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pydantic" }, { name = "sniffio" }, - { name = "sounddevice" }, { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/ea/58102e9bfda09edc963e6e877e39cca12706b46ebf35d5fc9da7b8af10f2/openai-1.68.0.tar.gz", hash = "sha256:c570c06c9ba10f98b891ac30a3dd7b5c89ed48094c711c7a3f35fb5ade6c0757", size = 413039 } +sdist = { url = "https://files.pythonhosted.org/packages/75/86/c605a6e84da0248f2cebfcd864b5a6076ecf78849245af5e11d2a5ec7977/openai-1.74.0.tar.gz", hash = "sha256:592c25b8747a7cad33a841958f5eb859a785caea9ee22b9e4f4a2ec062236526", size = 427571 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/b6/bd67b7031572cba7d8451d82ac4a990b3a96bbd3b037634726b48ac972c8/openai-1.68.0-py3-none-any.whl", hash = "sha256:20e279b0f3a78cb4a95f3eab2a180f3ee30c6a196aeebd6bf642a4f88ab85ee1", size = 605645 }, + { url = "https://files.pythonhosted.org/packages/a9/91/8c150f16a96367e14bd7d20e86e0bbbec3080e3eb593e63f21a7f013f8e4/openai-1.74.0-py3-none-any.whl", hash = "sha256:aff3e0f9fb209836382ec112778667027f4fd6ae38bdb2334bc9e173598b092a", size = 644790 }, ] [[package]] @@ -1121,11 +1481,14 @@ dependencies = [ ] [package.optional-dependencies] +litellm = [ + { name = "litellm" }, +] viz = [ { name = "graphviz" }, ] voice = [ - { name = "numpy", version = "2.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", marker = "python_full_version >= '3.10'" }, { name = "websockets" }, ] @@ -1157,6 +1520,7 @@ dev = [ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, + { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.65.0,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.66.5" }, @@ -1166,7 +1530,7 @@ requires-dist = [ { name = "typing-extensions", specifier = ">=4.12.2,<5" }, { name = "websockets", marker = "extra == 'voice'", specifier = ">=15.0,<16" }, ] -provides-extras = ["voice", "viz"] +provides-extras = ["voice", "viz", "litellm"] [package.metadata.requires-dev] dev = [ @@ -1185,7 +1549,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-mock", specifier = ">=3.14.0" }, - { name = "rich" }, + { name = "rich", specifier = ">=13.1.0,<14" }, { name = "ruff", specifier = "==0.9.2" }, { name = "sounddevice" }, { name = "textual" }, @@ -1256,6 +1620,111 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "propcache" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/56/e27c136101addf877c8291dbda1b3b86ae848f3837ce758510a0d806c92f/propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98", size = 80224 }, + { url = "https://files.pythonhosted.org/packages/63/bd/88e98836544c4f04db97eefd23b037c2002fa173dd2772301c61cd3085f9/propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180", size = 46491 }, + { url = "https://files.pythonhosted.org/packages/15/43/0b8eb2a55753c4a574fc0899885da504b521068d3b08ca56774cad0bea2b/propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71", size = 45927 }, + { url = "https://files.pythonhosted.org/packages/ad/6c/d01f9dfbbdc613305e0a831016844987a1fb4861dd221cd4c69b1216b43f/propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649", size = 206135 }, + { url = "https://files.pythonhosted.org/packages/9a/8a/e6e1c77394088f4cfdace4a91a7328e398ebed745d59c2f6764135c5342d/propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f", size = 220517 }, + { url = "https://files.pythonhosted.org/packages/19/3b/6c44fa59d6418f4239d5db8b1ece757351e85d6f3ca126dfe37d427020c8/propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229", size = 218952 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/4aeb95a1cd085e0558ab0de95abfc5187329616193a1012a6c4c930e9f7a/propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46", size = 206593 }, + { url = "https://files.pythonhosted.org/packages/da/6a/29fa75de1cbbb302f1e1d684009b969976ca603ee162282ae702287b6621/propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7", size = 196745 }, + { url = "https://files.pythonhosted.org/packages/19/7e/2237dad1dbffdd2162de470599fa1a1d55df493b16b71e5d25a0ac1c1543/propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0", size = 203369 }, + { url = "https://files.pythonhosted.org/packages/a4/bc/a82c5878eb3afb5c88da86e2cf06e1fe78b7875b26198dbb70fe50a010dc/propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519", size = 198723 }, + { url = "https://files.pythonhosted.org/packages/17/76/9632254479c55516f51644ddbf747a45f813031af5adcb8db91c0b824375/propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd", size = 200751 }, + { url = "https://files.pythonhosted.org/packages/3e/c3/a90b773cf639bd01d12a9e20c95be0ae978a5a8abe6d2d343900ae76cd71/propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259", size = 210730 }, + { url = "https://files.pythonhosted.org/packages/ed/ec/ad5a952cdb9d65c351f88db7c46957edd3d65ffeee72a2f18bd6341433e0/propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e", size = 213499 }, + { url = "https://files.pythonhosted.org/packages/83/c0/ea5133dda43e298cd2010ec05c2821b391e10980e64ee72c0a76cdbb813a/propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136", size = 207132 }, + { url = "https://files.pythonhosted.org/packages/79/dd/71aae9dec59333064cfdd7eb31a63fa09f64181b979802a67a90b2abfcba/propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42", size = 40952 }, + { url = "https://files.pythonhosted.org/packages/31/0a/49ff7e5056c17dfba62cbdcbb90a29daffd199c52f8e65e5cb09d5f53a57/propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833", size = 45163 }, + { url = "https://files.pythonhosted.org/packages/90/0f/5a5319ee83bd651f75311fcb0c492c21322a7fc8f788e4eef23f44243427/propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", size = 80243 }, + { url = "https://files.pythonhosted.org/packages/ce/84/3db5537e0879942783e2256616ff15d870a11d7ac26541336fe1b673c818/propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", size = 46503 }, + { url = "https://files.pythonhosted.org/packages/e2/c8/b649ed972433c3f0d827d7f0cf9ea47162f4ef8f4fe98c5f3641a0bc63ff/propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", size = 45934 }, + { url = "https://files.pythonhosted.org/packages/59/f9/4c0a5cf6974c2c43b1a6810c40d889769cc8f84cea676cbe1e62766a45f8/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", size = 233633 }, + { url = "https://files.pythonhosted.org/packages/e7/64/66f2f4d1b4f0007c6e9078bd95b609b633d3957fe6dd23eac33ebde4b584/propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", size = 241124 }, + { url = "https://files.pythonhosted.org/packages/aa/bf/7b8c9fd097d511638fa9b6af3d986adbdf567598a567b46338c925144c1b/propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", size = 240283 }, + { url = "https://files.pythonhosted.org/packages/fa/c9/e85aeeeaae83358e2a1ef32d6ff50a483a5d5248bc38510d030a6f4e2816/propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", size = 232498 }, + { url = "https://files.pythonhosted.org/packages/8e/66/acb88e1f30ef5536d785c283af2e62931cb934a56a3ecf39105887aa8905/propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", size = 221486 }, + { url = "https://files.pythonhosted.org/packages/f5/f9/233ddb05ffdcaee4448508ee1d70aa7deff21bb41469ccdfcc339f871427/propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", size = 222675 }, + { url = "https://files.pythonhosted.org/packages/98/b8/eb977e28138f9e22a5a789daf608d36e05ed93093ef12a12441030da800a/propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", size = 215727 }, + { url = "https://files.pythonhosted.org/packages/89/2d/5f52d9c579f67b8ee1edd9ec073c91b23cc5b7ff7951a1e449e04ed8fdf3/propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", size = 217878 }, + { url = "https://files.pythonhosted.org/packages/7a/fd/5283e5ed8a82b00c7a989b99bb6ea173db1ad750bf0bf8dff08d3f4a4e28/propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", size = 230558 }, + { url = "https://files.pythonhosted.org/packages/90/38/ab17d75938ef7ac87332c588857422ae126b1c76253f0f5b1242032923ca/propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", size = 233754 }, + { url = "https://files.pythonhosted.org/packages/06/5d/3b921b9c60659ae464137508d3b4c2b3f52f592ceb1964aa2533b32fcf0b/propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", size = 226088 }, + { url = "https://files.pythonhosted.org/packages/54/6e/30a11f4417d9266b5a464ac5a8c5164ddc9dd153dfa77bf57918165eb4ae/propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", size = 40859 }, + { url = "https://files.pythonhosted.org/packages/1d/3a/8a68dd867da9ca2ee9dfd361093e9cb08cb0f37e5ddb2276f1b5177d7731/propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", size = 45153 }, + { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430 }, + { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637 }, + { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123 }, + { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031 }, + { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100 }, + { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170 }, + { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000 }, + { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262 }, + { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772 }, + { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133 }, + { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741 }, + { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047 }, + { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467 }, + { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022 }, + { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647 }, + { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784 }, + { url = "https://files.pythonhosted.org/packages/58/60/f645cc8b570f99be3cf46714170c2de4b4c9d6b827b912811eff1eb8a412/propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", size = 77865 }, + { url = "https://files.pythonhosted.org/packages/6f/d4/c1adbf3901537582e65cf90fd9c26fde1298fde5a2c593f987112c0d0798/propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", size = 45452 }, + { url = "https://files.pythonhosted.org/packages/d1/b5/fe752b2e63f49f727c6c1c224175d21b7d1727ce1d4873ef1c24c9216830/propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", size = 44800 }, + { url = "https://files.pythonhosted.org/packages/62/37/fc357e345bc1971e21f76597028b059c3d795c5ca7690d7a8d9a03c9708a/propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", size = 225804 }, + { url = "https://files.pythonhosted.org/packages/0d/f1/16e12c33e3dbe7f8b737809bad05719cff1dccb8df4dafbcff5575002c0e/propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", size = 230650 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/018b9f2ed876bf5091e60153f727e8f9073d97573f790ff7cdf6bc1d1fb8/propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", size = 234235 }, + { url = "https://files.pythonhosted.org/packages/45/5f/3faee66fc930dfb5da509e34c6ac7128870631c0e3582987fad161fcb4b1/propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", size = 228249 }, + { url = "https://files.pythonhosted.org/packages/62/1e/a0d5ebda5da7ff34d2f5259a3e171a94be83c41eb1e7cd21a2105a84a02e/propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", size = 214964 }, + { url = "https://files.pythonhosted.org/packages/db/a0/d72da3f61ceab126e9be1f3bc7844b4e98c6e61c985097474668e7e52152/propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", size = 222501 }, + { url = "https://files.pythonhosted.org/packages/18/6d/a008e07ad7b905011253adbbd97e5b5375c33f0b961355ca0a30377504ac/propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", size = 217917 }, + { url = "https://files.pythonhosted.org/packages/98/37/02c9343ffe59e590e0e56dc5c97d0da2b8b19fa747ebacf158310f97a79a/propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", size = 217089 }, + { url = "https://files.pythonhosted.org/packages/53/1b/d3406629a2c8a5666d4674c50f757a77be119b113eedd47b0375afdf1b42/propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", size = 228102 }, + { url = "https://files.pythonhosted.org/packages/cd/a7/3664756cf50ce739e5f3abd48febc0be1a713b1f389a502ca819791a6b69/propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", size = 230122 }, + { url = "https://files.pythonhosted.org/packages/35/36/0bbabaacdcc26dac4f8139625e930f4311864251276033a52fd52ff2a274/propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", size = 226818 }, + { url = "https://files.pythonhosted.org/packages/cc/27/4e0ef21084b53bd35d4dae1634b6d0bad35e9c58ed4f032511acca9d4d26/propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", size = 40112 }, + { url = "https://files.pythonhosted.org/packages/a6/2c/a54614d61895ba6dd7ac8f107e2b2a0347259ab29cbf2ecc7b94fa38c4dc/propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", size = 44034 }, + { url = "https://files.pythonhosted.org/packages/5a/a8/0a4fd2f664fc6acc66438370905124ce62e84e2e860f2557015ee4a61c7e/propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", size = 82613 }, + { url = "https://files.pythonhosted.org/packages/4d/e5/5ef30eb2cd81576256d7b6caaa0ce33cd1d2c2c92c8903cccb1af1a4ff2f/propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", size = 47763 }, + { url = "https://files.pythonhosted.org/packages/87/9a/87091ceb048efeba4d28e903c0b15bcc84b7c0bf27dc0261e62335d9b7b8/propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", size = 47175 }, + { url = "https://files.pythonhosted.org/packages/3e/2f/854e653c96ad1161f96194c6678a41bbb38c7947d17768e8811a77635a08/propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", size = 292265 }, + { url = "https://files.pythonhosted.org/packages/40/8d/090955e13ed06bc3496ba4a9fb26c62e209ac41973cb0d6222de20c6868f/propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", size = 294412 }, + { url = "https://files.pythonhosted.org/packages/39/e6/d51601342e53cc7582449e6a3c14a0479fab2f0750c1f4d22302e34219c6/propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", size = 294290 }, + { url = "https://files.pythonhosted.org/packages/3b/4d/be5f1a90abc1881884aa5878989a1acdafd379a91d9c7e5e12cef37ec0d7/propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", size = 282926 }, + { url = "https://files.pythonhosted.org/packages/57/2b/8f61b998c7ea93a2b7eca79e53f3e903db1787fca9373af9e2cf8dc22f9d/propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", size = 267808 }, + { url = "https://files.pythonhosted.org/packages/11/1c/311326c3dfce59c58a6098388ba984b0e5fb0381ef2279ec458ef99bd547/propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", size = 290916 }, + { url = "https://files.pythonhosted.org/packages/4b/74/91939924b0385e54dc48eb2e4edd1e4903ffd053cf1916ebc5347ac227f7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", size = 262661 }, + { url = "https://files.pythonhosted.org/packages/c2/d7/e6079af45136ad325c5337f5dd9ef97ab5dc349e0ff362fe5c5db95e2454/propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", size = 264384 }, + { url = "https://files.pythonhosted.org/packages/b7/d5/ba91702207ac61ae6f1c2da81c5d0d6bf6ce89e08a2b4d44e411c0bbe867/propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", size = 291420 }, + { url = "https://files.pythonhosted.org/packages/58/70/2117780ed7edcd7ba6b8134cb7802aada90b894a9810ec56b7bb6018bee7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", size = 290880 }, + { url = "https://files.pythonhosted.org/packages/4a/1f/ecd9ce27710021ae623631c0146719280a929d895a095f6d85efb6a0be2e/propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", size = 287407 }, + { url = "https://files.pythonhosted.org/packages/3e/66/2e90547d6b60180fb29e23dc87bd8c116517d4255240ec6d3f7dc23d1926/propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", size = 42573 }, + { url = "https://files.pythonhosted.org/packages/cb/8f/50ad8599399d1861b4d2b6b45271f0ef6af1b09b0a2386a46dbaf19c9535/propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", size = 46757 }, + { url = "https://files.pythonhosted.org/packages/aa/e1/4a782cdc7ebc42dfb44224dabf93b481395a0b6cbc9f0149785edbbab19c/propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6", size = 81368 }, + { url = "https://files.pythonhosted.org/packages/18/c6/9a39b2646a71321815d8d616e890851af9fb327af7d1b9fdce7d2d8377ca/propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf", size = 47037 }, + { url = "https://files.pythonhosted.org/packages/f3/e2/88ad1c4c42861dd09b45924e468c42a1beb2c5267cb960b7a9f6af67dd04/propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c", size = 46462 }, + { url = "https://files.pythonhosted.org/packages/ae/7e/3e3b36854e96be2e881bc6e87293d59c74dd734dd038dd4981474be44e26/propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894", size = 209214 }, + { url = "https://files.pythonhosted.org/packages/11/1a/ac0f757cc0babdc8217056fca85150066cf43bf11db9651e6b7d8e0646d6/propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035", size = 224702 }, + { url = "https://files.pythonhosted.org/packages/92/0a/0cf77d0e984b7058019ffa5385b3efd6962cbd5340a8f278ae103032863a/propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908", size = 223085 }, + { url = "https://files.pythonhosted.org/packages/05/fc/cb52a0caf803caff9b95b0a99e7c9c87f15b7e34ba0feebfd2572b49013d/propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5", size = 209613 }, + { url = "https://files.pythonhosted.org/packages/e5/fc/b1d1fdffbe1e0278ab535f8d21fc6b030889417714a545755bdd5ebe9bb0/propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5", size = 199931 }, + { url = "https://files.pythonhosted.org/packages/23/a9/2a2f8d93d8f526c35dd8dbbc4a1ac22a106712cd821e15e2a6530aea8931/propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7", size = 208937 }, + { url = "https://files.pythonhosted.org/packages/ef/71/5247a264b95e8d4ba86757cf9ad6a523d764bd4579a2d80007a2d4d2b0ad/propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641", size = 202577 }, + { url = "https://files.pythonhosted.org/packages/6f/4e/c8ec771731f1b1e7d07bd8875f1d13c1564b5d60f7483624d021eaef5687/propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294", size = 204669 }, + { url = "https://files.pythonhosted.org/packages/c5/b8/bdfcb1170a7b8504226064d7c0b4deb61acbcc6bb2e754ee25fb36c1b72a/propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf", size = 214334 }, + { url = "https://files.pythonhosted.org/packages/72/c6/fdb9e8ba161a4e12c75a7415cb99314cad195d3b8ae9d770783cec54001e/propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c", size = 218052 }, + { url = "https://files.pythonhosted.org/packages/67/3f/0dd87220f61598b61b590a8b3562142ae475a9c0f694ee32bf97e4e41d44/propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe", size = 210852 }, + { url = "https://files.pythonhosted.org/packages/7b/4e/e332164372af66992c07b470448beb7e36ce7dba6a06c6c2b6131f112e74/propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64", size = 41481 }, + { url = "https://files.pythonhosted.org/packages/61/73/d64abb7bb5d18880ecfac152247c0f1a5807256ea21e4737ce3019afffeb/propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566", size = 45720 }, + { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, +] + [[package]] name = "pycparser" version = "2.22" @@ -1267,113 +1736,126 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, - { url = "https://files.pythonhosted.org/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475 }, - { url = "https://files.pythonhosted.org/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112 }, - { url = "https://files.pythonhosted.org/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780 }, - { url = "https://files.pythonhosted.org/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943 }, - { url = "https://files.pythonhosted.org/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492 }, - { url = "https://files.pythonhosted.org/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714 }, - { url = "https://files.pythonhosted.org/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163 }, - { url = "https://files.pythonhosted.org/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217 }, - { url = "https://files.pythonhosted.org/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899 }, - { url = "https://files.pythonhosted.org/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726 }, - { url = "https://files.pythonhosted.org/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219 }, - { url = "https://files.pythonhosted.org/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, - { url = "https://files.pythonhosted.org/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733 }, - { url = "https://files.pythonhosted.org/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375 }, - { url = "https://files.pythonhosted.org/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307 }, - { url = "https://files.pythonhosted.org/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971 }, - { url = "https://files.pythonhosted.org/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616 }, - { url = "https://files.pythonhosted.org/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943 }, - { url = "https://files.pythonhosted.org/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654 }, - { url = "https://files.pythonhosted.org/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292 }, - { url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961 }, +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, + { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, + { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, + { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, + { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, + { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, + { url = "https://files.pythonhosted.org/packages/49/78/b86bad645cc3e8dfa6858c70ec38939bf350e54004837c48de09474b2b9e/pydantic_core-2.33.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5ab77f45d33d264de66e1884fca158bc920cb5e27fd0764a72f72f5756ae8bdb", size = 2044282 }, + { url = "https://files.pythonhosted.org/packages/3b/00/a02531331773b2bf08743d84c6b776bd6a449d23b3ae6b0e3229d568bac4/pydantic_core-2.33.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7aaba1b4b03aaea7bb59e1b5856d734be011d3e6d98f5bcaa98cb30f375f2ad", size = 1877598 }, + { url = "https://files.pythonhosted.org/packages/a1/fa/32cc152b84a1f420f8a7d80161373e8d87d4ffa077e67d6c8aab3ce1a6ab/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb66263e9ba8fea2aa85e1e5578980d127fb37d7f2e292773e7bc3a38fb0c7b", size = 1911021 }, + { url = "https://files.pythonhosted.org/packages/5e/87/ea553e0d98bce6c4876f8c50f65cb45597eff6e0aaa8b15813e9972bb19d/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f2648b9262607a7fb41d782cc263b48032ff7a03a835581abbf7a3bec62bcf5", size = 1997276 }, + { url = "https://files.pythonhosted.org/packages/f7/9b/60cb9f4b52158b3adac0066492bbadd0b8473f4f8da5bcc73972655b76ef/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723c5630c4259400818b4ad096735a829074601805d07f8cafc366d95786d331", size = 2141348 }, + { url = "https://files.pythonhosted.org/packages/9b/38/374d254e270d4de0add68a8239f4ed0f444fdd7b766ea69244fb9491dccb/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d100e3ae783d2167782391e0c1c7a20a31f55f8015f3293647544df3f9c67824", size = 2753708 }, + { url = "https://files.pythonhosted.org/packages/05/a8/fd79111eb5ab9bc4ef98d8fb0b3a2ffdc80107b2c59859a741ab379c96f8/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177d50460bc976a0369920b6c744d927b0ecb8606fb56858ff542560251b19e5", size = 2008699 }, + { url = "https://files.pythonhosted.org/packages/35/31/2e06619868eb4c18642c5601db420599c1cf9cf50fe868c9ac09cd298e24/pydantic_core-2.33.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3edde68d1a1f9af1273b2fe798997b33f90308fb6d44d8550c89fc6a3647cf6", size = 2123426 }, + { url = "https://files.pythonhosted.org/packages/4a/d0/3531e8783a311802e3db7ee5a1a5ed79e5706e930b1b4e3109ce15eeb681/pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a62c3c3ef6a7e2c45f7853b10b5bc4ddefd6ee3cd31024754a1a5842da7d598d", size = 2087330 }, + { url = "https://files.pythonhosted.org/packages/ac/32/5ff252ed73bacd7677a706ab17723e261a76793f98b305aa20cfc10bbd56/pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c91dbb0ab683fa0cd64a6e81907c8ff41d6497c346890e26b23de7ee55353f96", size = 2258171 }, + { url = "https://files.pythonhosted.org/packages/c9/f9/e96e00f92b8f5b3e2cddc80c5ee6cf038f8a0f238c44b67b01759943a7b4/pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f466e8bf0a62dc43e068c12166281c2eca72121dd2adc1040f3aa1e21ef8599", size = 2258745 }, + { url = "https://files.pythonhosted.org/packages/54/1e/51c86688e809d94797fdf0efc41514f001caec982a05f62d90c180a9639d/pydantic_core-2.33.1-cp39-cp39-win32.whl", hash = "sha256:ab0277cedb698749caada82e5d099dc9fed3f906a30d4c382d1a21725777a1e5", size = 1923626 }, + { url = "https://files.pythonhosted.org/packages/57/18/c2da959fd8d019b70cadafdda2bf845378ada47973e0bad6cc84f56dbe6e/pydantic_core-2.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:5773da0ee2d17136b1f1c6fbde543398d452a6ad2a7b54ea1033e2daa739b8d2", size = 1953703 }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, + { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, + { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, + { url = "https://files.pythonhosted.org/packages/2d/a8/c2c8f29bd18f7ef52de32a6deb9e3ee87ba18b7b2122636aa9f4438cf627/pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7edbc454a29fc6aeae1e1eecba4f07b63b8d76e76a748532233c4c167b4cb9ea", size = 2041791 }, + { url = "https://files.pythonhosted.org/packages/08/ad/328081b1c82543ae49d0650048305058583c51f1a9a56a0d6e87bb3a2443/pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad05b683963f69a1d5d2c2bdab1274a31221ca737dbbceaa32bcb67359453cdd", size = 1873579 }, + { url = "https://files.pythonhosted.org/packages/6e/8a/bc65dbf7e501e88367cdab06a2c1340457c785f0c72288cae737fd80c0fa/pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6a94bf9452c6da9b5d76ed229a5683d0306ccb91cca8e1eea883189780d568", size = 1904189 }, + { url = "https://files.pythonhosted.org/packages/9a/db/30ca6aefda211fb01ef185ca73cb7a0c6e7fe952c524025c8782b5acd771/pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7965c13b3967909a09ecc91f21d09cfc4576bf78140b988904e94f130f188396", size = 2084446 }, + { url = "https://files.pythonhosted.org/packages/f2/89/a12b55286e30c9f476eab7c53c9249ec76faf70430596496ab0309f28629/pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f1fdb790440a34f6ecf7679e1863b825cb5ffde858a9197f851168ed08371e5", size = 2118215 }, + { url = "https://files.pythonhosted.org/packages/8e/55/12721c4a8d7951584ad3d9848b44442559cf1876e0bb424148d1060636b3/pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5277aec8d879f8d05168fdd17ae811dd313b8ff894aeeaf7cd34ad28b4d77e33", size = 2079963 }, + { url = "https://files.pythonhosted.org/packages/bd/0c/3391bd5d6ff62ea998db94732528d9bc32c560b0ed861c39119759461946/pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8ab581d3530611897d863d1a649fb0644b860286b4718db919bfd51ece41f10b", size = 2249388 }, + { url = "https://files.pythonhosted.org/packages/d3/5f/3e4feb042998d7886a9b523b372d83955cbc192a07013dcd24276db078ee/pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0483847fa9ad5e3412265c1bd72aad35235512d9ce9d27d81a56d935ef489672", size = 2255226 }, + { url = "https://files.pythonhosted.org/packages/25/f2/1647933efaaad61846109a27619f3704929e758a09e6431b8f932a053d40/pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3", size = 2081073 }, ] [[package]] @@ -1546,14 +2028,15 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "0.25.3" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, ] [[package]] @@ -1582,11 +2065,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] @@ -1666,6 +2149,105 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, + { url = "https://files.pythonhosted.org/packages/89/23/c4a86df398e57e26f93b13ae63acce58771e04bdde86092502496fa57f9c/regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", size = 482682 }, + { url = "https://files.pythonhosted.org/packages/3c/8b/45c24ab7a51a1658441b961b86209c43e6bb9d39caf1e63f46ce6ea03bc7/regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", size = 287679 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/598de10b17fdafc452d11f7dada11c3be4e379a8671393e4e3da3c4070df/regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", size = 284578 }, + { url = "https://files.pythonhosted.org/packages/49/70/c7eaa219efa67a215846766fde18d92d54cb590b6a04ffe43cef30057622/regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", size = 782012 }, + { url = "https://files.pythonhosted.org/packages/89/e5/ef52c7eb117dd20ff1697968219971d052138965a4d3d9b95e92e549f505/regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", size = 820580 }, + { url = "https://files.pythonhosted.org/packages/5f/3f/9f5da81aff1d4167ac52711acf789df13e789fe6ac9545552e49138e3282/regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", size = 809110 }, + { url = "https://files.pythonhosted.org/packages/86/44/2101cc0890c3621b90365c9ee8d7291a597c0722ad66eccd6ffa7f1bcc09/regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", size = 780919 }, + { url = "https://files.pythonhosted.org/packages/ce/2e/3e0668d8d1c7c3c0d397bf54d92fc182575b3a26939aed5000d3cc78760f/regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", size = 771515 }, + { url = "https://files.pythonhosted.org/packages/a6/49/1bc4584254355e3dba930a3a2fd7ad26ccba3ebbab7d9100db0aff2eedb0/regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", size = 696957 }, + { url = "https://files.pythonhosted.org/packages/c8/dd/42879c1fc8a37a887cd08e358af3d3ba9e23038cd77c7fe044a86d9450ba/regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", size = 768088 }, + { url = "https://files.pythonhosted.org/packages/89/96/c05a0fe173cd2acd29d5e13c1adad8b706bcaa71b169e1ee57dcf2e74584/regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", size = 774752 }, + { url = "https://files.pythonhosted.org/packages/b5/f3/a757748066255f97f14506483436c5f6aded7af9e37bca04ec30c90ca683/regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", size = 838862 }, + { url = "https://files.pythonhosted.org/packages/5c/93/c6d2092fd479dcaeea40fc8fa673822829181ded77d294a7f950f1dda6e2/regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", size = 842622 }, + { url = "https://files.pythonhosted.org/packages/ff/9c/daa99532c72f25051a90ef90e1413a8d54413a9e64614d9095b0c1c154d0/regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", size = 772713 }, + { url = "https://files.pythonhosted.org/packages/13/5d/61a533ccb8c231b474ac8e3a7d70155b00dfc61af6cafdccd1947df6d735/regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", size = 261756 }, + { url = "https://files.pythonhosted.org/packages/dc/7b/e59b7f7c91ae110d154370c24133f947262525b5d6406df65f23422acc17/regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", size = 274110 }, +] + [[package]] name = "requests" version = "2.32.3" @@ -1695,6 +2277,127 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, ] +[[package]] +name = "rpds-py" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/b3/52b213298a0ba7097c7ea96bee95e1947aa84cc816d48cebb539770cdf41/rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e", size = 26863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/cbc43b220c9deb536b07fbd598c97d463bbb7afb788851891252fc920742/rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724", size = 377531 }, + { url = "https://files.pythonhosted.org/packages/42/15/cc4b09ef160483e49c3aab3b56f3d375eadf19c87c48718fb0147e86a446/rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b", size = 362273 }, + { url = "https://files.pythonhosted.org/packages/8c/a2/67718a188a88dbd5138d959bed6efe1cc7413a4caa8283bd46477ed0d1ad/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727", size = 388111 }, + { url = "https://files.pythonhosted.org/packages/e5/e6/cbf1d3163405ad5f4a1a6d23f80245f2204d0c743b18525f34982dec7f4d/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964", size = 394447 }, + { url = "https://files.pythonhosted.org/packages/21/bb/4fe220ccc8a549b38b9e9cec66212dc3385a82a5ee9e37b54411cce4c898/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5", size = 448028 }, + { url = "https://files.pythonhosted.org/packages/a5/41/d2d6e0fd774818c4cadb94185d30cf3768de1c2a9e0143fc8bc6ce59389e/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664", size = 447410 }, + { url = "https://files.pythonhosted.org/packages/a7/a7/6d04d438f53d8bb2356bb000bea9cf5c96a9315e405b577117e344cc7404/rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc", size = 389531 }, + { url = "https://files.pythonhosted.org/packages/23/be/72e6df39bd7ca5a66799762bf54d8e702483fdad246585af96723109d486/rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0", size = 420099 }, + { url = "https://files.pythonhosted.org/packages/8c/c9/ca100cd4688ee0aa266197a5cb9f685231676dd7d573041ca53787b23f4e/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f", size = 564950 }, + { url = "https://files.pythonhosted.org/packages/05/98/908cd95686d33b3ac8ac2e582d7ae38e2c3aa2c0377bf1f5663bafd1ffb2/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f", size = 591778 }, + { url = "https://files.pythonhosted.org/packages/7b/ac/e143726f1dd3215efcb974b50b03bd08a8a1556b404a0a7872af6d197e57/rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875", size = 560421 }, + { url = "https://files.pythonhosted.org/packages/60/28/add1c1d2fcd5aa354f7225d036d4492261759a22d449cff14841ef36a514/rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07", size = 222089 }, + { url = "https://files.pythonhosted.org/packages/b0/ac/81f8066c6de44c507caca488ba336ae30d35d57f61fe10578824d1a70196/rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052", size = 234622 }, + { url = "https://files.pythonhosted.org/packages/80/e6/c1458bbfb257448fdb2528071f1f4e19e26798ed5ef6d47d7aab0cb69661/rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef", size = 377679 }, + { url = "https://files.pythonhosted.org/packages/dd/26/ea4181ef78f58b2c167548c6a833d7dc22408e5b3b181bda9dda440bb92d/rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97", size = 362571 }, + { url = "https://files.pythonhosted.org/packages/56/fa/1ec54dd492c64c280a2249a047fc3369e2789dc474eac20445ebfc72934b/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e", size = 388012 }, + { url = "https://files.pythonhosted.org/packages/3a/be/bad8b0e0f7e58ef4973bb75e91c472a7d51da1977ed43b09989264bf065c/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d", size = 394730 }, + { url = "https://files.pythonhosted.org/packages/35/56/ab417fc90c21826df048fc16e55316ac40876e4b790104ececcbce813d8f/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586", size = 448264 }, + { url = "https://files.pythonhosted.org/packages/b6/75/4c63862d5c05408589196c8440a35a14ea4ae337fa70ded1f03638373f06/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4", size = 446813 }, + { url = "https://files.pythonhosted.org/packages/e7/0c/91cf17dffa9a38835869797a9f041056091ebba6a53963d3641207e3d467/rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae", size = 389438 }, + { url = "https://files.pythonhosted.org/packages/1b/b0/60e6c72727c978276e02851819f3986bc40668f115be72c1bc4d922c950f/rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc", size = 420416 }, + { url = "https://files.pythonhosted.org/packages/a1/d7/f46f85b9f863fb59fd3c534b5c874c48bee86b19e93423b9da8784605415/rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c", size = 565236 }, + { url = "https://files.pythonhosted.org/packages/2a/d1/1467620ded6dd70afc45ec822cdf8dfe7139537780d1f3905de143deb6fd/rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c", size = 592016 }, + { url = "https://files.pythonhosted.org/packages/5d/13/fb1ded2e6adfaa0c0833106c42feb290973f665300f4facd5bf5d7891d9c/rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718", size = 560123 }, + { url = "https://files.pythonhosted.org/packages/1e/df/09fc1857ac7cc2eb16465a7199c314cbce7edde53c8ef21d615410d7335b/rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a", size = 222256 }, + { url = "https://files.pythonhosted.org/packages/ff/25/939b40bc4d54bf910e5ee60fb5af99262c92458f4948239e8c06b0b750e7/rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6", size = 234718 }, + { url = "https://files.pythonhosted.org/packages/1a/e0/1c55f4a3be5f1ca1a4fd1f3ff1504a1478c1ed48d84de24574c4fa87e921/rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205", size = 366945 }, + { url = "https://files.pythonhosted.org/packages/39/1b/a3501574fbf29118164314dbc800d568b8c1c7b3258b505360e8abb3902c/rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7", size = 351935 }, + { url = "https://files.pythonhosted.org/packages/dc/47/77d3d71c55f6a374edde29f1aca0b2e547325ed00a9da820cabbc9497d2b/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9", size = 390817 }, + { url = "https://files.pythonhosted.org/packages/4e/ec/1e336ee27484379e19c7f9cc170f4217c608aee406d3ae3a2e45336bff36/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e", size = 401983 }, + { url = "https://files.pythonhosted.org/packages/07/f8/39b65cbc272c635eaea6d393c2ad1ccc81c39eca2db6723a0ca4b2108fce/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda", size = 451719 }, + { url = "https://files.pythonhosted.org/packages/32/05/05c2b27dd9c30432f31738afed0300659cb9415db0ff7429b05dfb09bbde/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e", size = 442546 }, + { url = "https://files.pythonhosted.org/packages/7d/e0/19383c8b5d509bd741532a47821c3e96acf4543d0832beba41b4434bcc49/rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029", size = 393695 }, + { url = "https://files.pythonhosted.org/packages/9d/15/39f14e96d94981d0275715ae8ea564772237f3fa89bc3c21e24de934f2c7/rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9", size = 427218 }, + { url = "https://files.pythonhosted.org/packages/22/b9/12da7124905a680f690da7a9de6f11de770b5e359f5649972f7181c8bf51/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7", size = 568062 }, + { url = "https://files.pythonhosted.org/packages/88/17/75229017a2143d915f6f803721a6d721eca24f2659c5718a538afa276b4f/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91", size = 596262 }, + { url = "https://files.pythonhosted.org/packages/aa/64/8e8a1d8bd1b6b638d6acb6d41ab2cec7f2067a5b8b4c9175703875159a7c/rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56", size = 564306 }, + { url = "https://files.pythonhosted.org/packages/68/1c/a7eac8d8ed8cb234a9b1064647824c387753343c3fab6ed7c83481ed0be7/rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30", size = 224281 }, + { url = "https://files.pythonhosted.org/packages/bb/46/b8b5424d1d21f2f2f3f2d468660085318d4f74a8df8289e3dd6ad224d488/rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034", size = 239719 }, + { url = "https://files.pythonhosted.org/packages/9d/c3/3607abc770395bc6d5a00cb66385a5479fb8cd7416ddef90393b17ef4340/rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c", size = 367072 }, + { url = "https://files.pythonhosted.org/packages/d8/35/8c7ee0fe465793e3af3298dc5a9f3013bd63e7a69df04ccfded8293a4982/rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c", size = 351919 }, + { url = "https://files.pythonhosted.org/packages/91/d3/7e1b972501eb5466b9aca46a9c31bcbbdc3ea5a076e9ab33f4438c1d069d/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240", size = 390360 }, + { url = "https://files.pythonhosted.org/packages/a2/a8/ccabb50d3c91c26ad01f9b09a6a3b03e4502ce51a33867c38446df9f896b/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8", size = 400704 }, + { url = "https://files.pythonhosted.org/packages/53/ae/5fa5bf0f3bc6ce21b5ea88fc0ecd3a439e7cb09dd5f9ffb3dbe1b6894fc5/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8", size = 450839 }, + { url = "https://files.pythonhosted.org/packages/e3/ac/c4e18b36d9938247e2b54f6a03746f3183ca20e1edd7d3654796867f5100/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b", size = 441494 }, + { url = "https://files.pythonhosted.org/packages/bf/08/b543969c12a8f44db6c0f08ced009abf8f519191ca6985509e7c44102e3c/rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d", size = 393185 }, + { url = "https://files.pythonhosted.org/packages/da/7e/f6eb6a7042ce708f9dfc781832a86063cea8a125bbe451d663697b51944f/rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7", size = 426168 }, + { url = "https://files.pythonhosted.org/packages/38/b0/6cd2bb0509ac0b51af4bb138e145b7c4c902bb4b724d6fd143689d6e0383/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad", size = 567622 }, + { url = "https://files.pythonhosted.org/packages/64/b0/c401f4f077547d98e8b4c2ec6526a80e7cb04f519d416430ec1421ee9e0b/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120", size = 595435 }, + { url = "https://files.pythonhosted.org/packages/9f/ec/7993b6e803294c87b61c85bd63e11142ccfb2373cf88a61ec602abcbf9d6/rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9", size = 563762 }, + { url = "https://files.pythonhosted.org/packages/1f/29/4508003204cb2f461dc2b83dd85f8aa2b915bc98fe6046b9d50d4aa05401/rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143", size = 223510 }, + { url = "https://files.pythonhosted.org/packages/f9/12/09e048d1814195e01f354155fb772fb0854bd3450b5f5a82224b3a319f0e/rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a", size = 239075 }, + { url = "https://files.pythonhosted.org/packages/d2/03/5027cde39bb2408d61e4dd0cf81f815949bb629932a6c8df1701d0257fc4/rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114", size = 362974 }, + { url = "https://files.pythonhosted.org/packages/bf/10/24d374a2131b1ffafb783e436e770e42dfdb74b69a2cd25eba8c8b29d861/rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405", size = 348730 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/1ef88d0516d46cd8df12e5916966dbf716d5ec79b265eda56ba1b173398c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47", size = 387627 }, + { url = "https://files.pythonhosted.org/packages/4e/35/07339051b8b901ecefd449ebf8e5522e92bcb95e1078818cbfd9db8e573c/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272", size = 394094 }, + { url = "https://files.pythonhosted.org/packages/dc/62/ee89ece19e0ba322b08734e95441952062391065c157bbd4f8802316b4f1/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd", size = 449639 }, + { url = "https://files.pythonhosted.org/packages/15/24/b30e9f9e71baa0b9dada3a4ab43d567c6b04a36d1cb531045f7a8a0a7439/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a", size = 438584 }, + { url = "https://files.pythonhosted.org/packages/28/d9/49f7b8f3b4147db13961e19d5e30077cd0854ccc08487026d2cb2142aa4a/rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d", size = 391047 }, + { url = "https://files.pythonhosted.org/packages/49/b0/e66918d0972c33a259ba3cd7b7ff10ed8bd91dbcfcbec6367b21f026db75/rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7", size = 418085 }, + { url = "https://files.pythonhosted.org/packages/e1/6b/99ed7ea0a94c7ae5520a21be77a82306aac9e4e715d4435076ead07d05c6/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d", size = 564498 }, + { url = "https://files.pythonhosted.org/packages/28/26/1cacfee6b800e6fb5f91acecc2e52f17dbf8b0796a7c984b4568b6d70e38/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797", size = 590202 }, + { url = "https://files.pythonhosted.org/packages/a9/9e/57bd2f9fba04a37cef673f9a66b11ca8c43ccdd50d386c455cd4380fe461/rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c", size = 561771 }, + { url = "https://files.pythonhosted.org/packages/9f/cf/b719120f375ab970d1c297dbf8de1e3c9edd26fe92c0ed7178dd94b45992/rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba", size = 221195 }, + { url = "https://files.pythonhosted.org/packages/2d/e5/22865285789f3412ad0c3d7ec4dc0a3e86483b794be8a5d9ed5a19390900/rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350", size = 237354 }, + { url = "https://files.pythonhosted.org/packages/22/ef/a194eaef0d0f2cd3f4c893c5b809a7458aaa7c0a64e60a45a72a04835ed4/rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d", size = 378126 }, + { url = "https://files.pythonhosted.org/packages/c3/8d/9a07f69933204c098760c884f03835ab8fb66e28d2d5f3dd6741720cf29c/rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e", size = 362887 }, + { url = "https://files.pythonhosted.org/packages/29/74/315f42060f2e3cedd77d382a98484a68ef727bd3b5fd7b91825b859a3e85/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65", size = 388661 }, + { url = "https://files.pythonhosted.org/packages/29/22/7ee7bb2b25ecdfcf1265d5a51472814fe60b580f9e1e2746eed9c476310a/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b", size = 394993 }, + { url = "https://files.pythonhosted.org/packages/46/7b/5f40e278d81cd23eea6b88bbac62bacc27ed19412051a1fc4229e8f9367a/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791", size = 448706 }, + { url = "https://files.pythonhosted.org/packages/5a/7a/06aada7ecdb0d02fbc041daee998ae841882fcc8ed3c0f84e72d6832fef1/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9", size = 447369 }, + { url = "https://files.pythonhosted.org/packages/c6/f3/428a9367077268f852db9b3b68b6eda6ee4594ab7dc2d603a2c370619cc0/rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c", size = 390012 }, + { url = "https://files.pythonhosted.org/packages/55/66/24b61f14cd54e525583404afe6e3c221b309d1abd4b0b597a566dd8ee42d/rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58", size = 421576 }, + { url = "https://files.pythonhosted.org/packages/22/56/18b81a4f0550e0d4be700cdcf1415ebf250fd21f9a5a775843dd3588dbf6/rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124", size = 565562 }, + { url = "https://files.pythonhosted.org/packages/42/80/82a935d78f74974f82d38e83fb02430f8e8cc09ad35e06d9a5d2e9b907a7/rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149", size = 592924 }, + { url = "https://files.pythonhosted.org/packages/0d/49/b717e7b93c2ca881d2dac8b23b3a87a4c30f7c762bfd3df0b3953e655f13/rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45", size = 560847 }, + { url = "https://files.pythonhosted.org/packages/1e/26/ba630a291238e7f42d25bc5569d152623f18c21e9183e506585b23325c48/rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103", size = 222570 }, + { url = "https://files.pythonhosted.org/packages/2d/84/01126e25e21f2ed6e63ec4030f78793dfee1a21aff1842136353c9caaed9/rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f", size = 234931 }, + { url = "https://files.pythonhosted.org/packages/99/48/11dae46d0c7f7e156ca0971a83f89c510af0316cd5d42c771b7cef945f0c/rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a", size = 378224 }, + { url = "https://files.pythonhosted.org/packages/33/18/e8398d255369e35d312942f3bb8ecaff013c44968904891be2ab63b3aa94/rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399", size = 363252 }, + { url = "https://files.pythonhosted.org/packages/17/39/dd73ba691f4df3e6834bf982de214086ac3359ab3ac035adfb30041570e3/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098", size = 388871 }, + { url = "https://files.pythonhosted.org/packages/2f/2e/da0530b25cabd0feca2a759b899d2df325069a94281eeea8ac44c6cfeff7/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d", size = 394766 }, + { url = "https://files.pythonhosted.org/packages/4c/ee/dd1c5040a431beb40fad4a5d7868acf343444b0bc43e627c71df2506538b/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e", size = 448712 }, + { url = "https://files.pythonhosted.org/packages/f5/ec/6b93ffbb686be948e4d91ec76f4e6757f8551034b2a8176dd848103a1e34/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1", size = 447150 }, + { url = "https://files.pythonhosted.org/packages/55/d5/a1c23760adad85b432df074ced6f910dd28f222b8c60aeace5aeb9a6654e/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb", size = 390662 }, + { url = "https://files.pythonhosted.org/packages/a5/f3/419cb1f9bfbd3a48c256528c156e00f3349e3edce5ad50cbc141e71f66a5/rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44", size = 421351 }, + { url = "https://files.pythonhosted.org/packages/98/8e/62d1a55078e5ede0b3b09f35e751fa35924a34a0d44d7c760743383cd54a/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33", size = 566074 }, + { url = "https://files.pythonhosted.org/packages/fc/69/b7d1003166d78685da032b3c4ff1599fa536a3cfe6e5ce2da87c9c431906/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164", size = 592398 }, + { url = "https://files.pythonhosted.org/packages/ea/a8/1c98bc99338c37faadd28dd667d336df7409d77b4da999506a0b6b1c0aa2/rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc", size = 561114 }, + { url = "https://files.pythonhosted.org/packages/2b/41/65c91443685a4c7b5f1dd271beadc4a3e063d57c3269221548dd9416e15c/rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5", size = 235548 }, + { url = "https://files.pythonhosted.org/packages/65/53/40bcc246a8354530d51a26d2b5b9afd1deacfb0d79e67295cc74df362f52/rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d", size = 378386 }, + { url = "https://files.pythonhosted.org/packages/80/b0/5ea97dd2f53e3618560aa1f9674e896e63dff95a9b796879a201bc4c1f00/rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a", size = 363440 }, + { url = "https://files.pythonhosted.org/packages/57/9d/259b6eada6f747cdd60c9a5eb3efab15f6704c182547149926c38e5bd0d5/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5", size = 388816 }, + { url = "https://files.pythonhosted.org/packages/94/c1/faafc7183712f89f4b7620c3c15979ada13df137d35ef3011ae83e93b005/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d", size = 395058 }, + { url = "https://files.pythonhosted.org/packages/6c/96/d7fa9d2a7b7604a61da201cc0306a355006254942093779d7121c64700ce/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793", size = 448692 }, + { url = "https://files.pythonhosted.org/packages/96/37/a3146c6eebc65d6d8c96cc5ffdcdb6af2987412c789004213227fbe52467/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba", size = 446462 }, + { url = "https://files.pythonhosted.org/packages/1f/13/6481dfd9ac7de43acdaaa416e3a7da40bc4bb8f5c6ca85e794100aa54596/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea", size = 390460 }, + { url = "https://files.pythonhosted.org/packages/61/e1/37e36bce65e109543cc4ff8d23206908649023549604fa2e7fbeba5342f7/rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032", size = 421609 }, + { url = "https://files.pythonhosted.org/packages/20/dd/1f1a923d6cd798b8582176aca8a0784676f1a0449fb6f07fce6ac1cdbfb6/rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d", size = 565818 }, + { url = "https://files.pythonhosted.org/packages/56/ec/d8da6df6a1eb3a418944a17b1cb38dd430b9e5a2e972eafd2b06f10c7c46/rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25", size = 592627 }, + { url = "https://files.pythonhosted.org/packages/b3/14/c492b9c7d5dd133e13f211ddea6bb9870f99e4f73932f11aa00bc09a9be9/rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba", size = 560885 }, + { url = "https://files.pythonhosted.org/packages/ef/e2/16cbbd7aaa4deaaeef5c90fee8b485c8b3312094cdad31e8006f5a3e5e08/rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6", size = 378245 }, + { url = "https://files.pythonhosted.org/packages/d4/8c/5024dd105bf0a515576b7df8aeeba6556ffdbe2d636dee172c1a30497dd1/rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb", size = 363461 }, + { url = "https://files.pythonhosted.org/packages/a4/6f/3a4efcfa2f4391b69f5d0ed3e6be5d2c5468c24fd2d15b712d2dbefc1749/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1", size = 388839 }, + { url = "https://files.pythonhosted.org/packages/6c/d2/b8e5f0a0e97d295a0ebceb5265ef2e44c3d55e0d0f938d64a5ecfffa715e/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83", size = 394860 }, + { url = "https://files.pythonhosted.org/packages/90/e9/9f1f297bdbc5b871826ad790b6641fc40532d97917916e6bd9f87fdd128d/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046", size = 449314 }, + { url = "https://files.pythonhosted.org/packages/06/ad/62ddbbaead31a1a22f0332958d0ea7c7aeed1b2536c6a51dd66dfae321a2/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391", size = 446376 }, + { url = "https://files.pythonhosted.org/packages/82/a7/05b660d2f3789506e98be69aaf2ccde94e0fc49cd26cd78d7069bc5ba1b8/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3", size = 390560 }, + { url = "https://files.pythonhosted.org/packages/66/1b/79fa0abffb802ff817821a148ce752eaaab87ba3a6a5e6b9f244c00c73d0/rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78", size = 421225 }, + { url = "https://files.pythonhosted.org/packages/6e/9b/368893ad2f7b2ece42cad87c7ec71309b5d93188db28b307eadb48cd28e5/rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3", size = 566071 }, + { url = "https://files.pythonhosted.org/packages/41/75/1cd0a654d300449411e6fd0821f83c1cfc7223da2e8109f586b4d9b89054/rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd", size = 592334 }, + { url = "https://files.pythonhosted.org/packages/31/33/5905e2a2e7612218e25307a9255fc8671b977449d40d62fe317775fe4939/rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796", size = 561111 }, + { url = "https://files.pythonhosted.org/packages/64/bd/f4cc34ac2261a7cb8a48bc90ce1e36dc05f1ec5ac3b4537def20be5df555/rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f", size = 235168 }, +] + [[package]] name = "ruff" version = "0.9.2" @@ -1768,19 +2471,19 @@ wheels = [ [[package]] name = "starlette" -version = "0.46.1" +version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, ] [[package]] name = "textual" -version = "2.1.2" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", extra = ["linkify", "plugins"] }, @@ -1788,9 +2491,76 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/62/4af4689dd971ed4fb3215467624016d53550bff1df9ca02e7625eec07f8b/textual-2.1.2.tar.gz", hash = "sha256:aae3f9fde00c7440be00e3c3ac189e02d014f5298afdc32132f93480f9e09146", size = 1596600 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/1f/df371f1455524a3d0079871e49e3850c82767904e9f4e2bdea6d30a866a7/textual-3.1.0.tar.gz", hash = "sha256:6bcab6581e9753d2a2043caf49f43c5818feb35f8049ed185bd38982bfb310ca", size = 1591879 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6b/d6d37a5fd93c344a27c53cdc4910d8d52cedd3ae63eae3d645fb108bd591/textual-3.1.0-py3-none-any.whl", hash = "sha256:940a765b6fcd562cd88603780343dc98a4e66c1d8d42f09b6a16a474a89aca0c", size = 683799 }, +] + +[[package]] +name = "tiktoken" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/f3/50ec5709fad61641e4411eb1b9ac55b99801d71f1993c29853f256c726c9/tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382", size = 1065770 }, + { url = "https://files.pythonhosted.org/packages/d6/f8/5a9560a422cf1755b6e0a9a436e14090eeb878d8ec0f80e0cd3d45b78bf4/tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108", size = 1009314 }, + { url = "https://files.pythonhosted.org/packages/bc/20/3ed4cfff8f809cb902900ae686069e029db74567ee10d017cb254df1d598/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd", size = 1143140 }, + { url = "https://files.pythonhosted.org/packages/f1/95/cc2c6d79df8f113bdc6c99cdec985a878768120d87d839a34da4bd3ff90a/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de", size = 1197860 }, + { url = "https://files.pythonhosted.org/packages/c7/6c/9c1a4cc51573e8867c9381db1814223c09ebb4716779c7f845d48688b9c8/tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990", size = 1259661 }, + { url = "https://files.pythonhosted.org/packages/cd/4c/22eb8e9856a2b1808d0a002d171e534eac03f96dbe1161978d7389a59498/tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4", size = 894026 }, + { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987 }, + { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155 }, + { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898 }, + { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535 }, + { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548 }, + { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895 }, + { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073 }, + { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075 }, + { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754 }, + { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678 }, + { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283 }, + { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897 }, + { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919 }, + { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877 }, + { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095 }, + { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649 }, + { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465 }, + { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669 }, + { url = "https://files.pythonhosted.org/packages/c4/92/4d681b5c066d417b98f22a0176358d9e606e183c6b61c337d61fb54accb4/tiktoken-0.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c6386ca815e7d96ef5b4ac61e0048cd32ca5a92d5781255e13b31381d28667dc", size = 1066217 }, + { url = "https://files.pythonhosted.org/packages/12/dd/af27bbe186df481666de48cf0f2f4e0643ba9c78b472e7bf70144c663b22/tiktoken-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75f6d5db5bc2c6274b674ceab1615c1778e6416b14705827d19b40e6355f03e0", size = 1009441 }, + { url = "https://files.pythonhosted.org/packages/33/35/2792b7dcb8b150d2767322637513c73a3e80833c19212efea80b31087894/tiktoken-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b16f61e6f4625a57a36496d28dd182a8a60ec20a534c5343ba3cafa156ac7", size = 1144423 }, + { url = "https://files.pythonhosted.org/packages/65/ae/4d1682510172ce3500bbed3b206ebc4efefe280f0bf1179cfb043f88cc16/tiktoken-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebcec91babf21297022882344c3f7d9eed855931466c3311b1ad6b64befb3df", size = 1199002 }, + { url = "https://files.pythonhosted.org/packages/1c/2e/df2dc31dd161190f315829775a9652ea01d60f307af8f98e35bdd14a6a93/tiktoken-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e5fd49e7799579240f03913447c0cdfa1129625ebd5ac440787afc4345990427", size = 1260610 }, + { url = "https://files.pythonhosted.org/packages/70/22/e8fc1bf9cdecc439b7ddc28a45b976a8c699a38874c070749d855696368a/tiktoken-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:26242ca9dc8b58e875ff4ca078b9a94d2f0813e6a535dcd2205df5d49d927cc7", size = 894215 }, +] + +[[package]] +name = "tokenizers" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/81/9df1988c908cbba77f10fecb8587496b3dff2838d4510457877a521d87fd/textual-2.1.2-py3-none-any.whl", hash = "sha256:95f37f49e930838e721bba8612f62114d410a3019665b6142adabc14c2fb9611", size = 680148 }, + { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767 }, + { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555 }, + { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541 }, + { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058 }, + { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278 }, + { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253 }, + { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225 }, + { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874 }, + { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448 }, + { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877 }, + { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645 }, + { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380 }, + { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506 }, + { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481 }, ] [[package]] @@ -1855,23 +2625,35 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.0.20250306" +version = "2.32.0.20250328" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/1a/beaeff79ef9efd186566ba5f0d95b44ae21f6d31e9413bcfbef3489b6ae3/types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1", size = 23012 } +sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/26/645d89f56004aa0ba3b96fec27793e3c7e62b40982ee069e52568922b6db/types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b", size = 20673 }, + { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, ] [[package]] @@ -1885,25 +2667,25 @@ wheels = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", marker = "python_full_version >= '3.10'" }, { name = "h11", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, ] [[package]] @@ -2019,6 +2801,105 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] +[[package]] +name = "yarl" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/4d/8a8f57caccce49573e567744926f88c6ab3ca0b47a257806d1cf88584c5f/yarl-1.19.0.tar.gz", hash = "sha256:01e02bb80ae0dbed44273c304095295106e1d9470460e773268a27d11e594892", size = 184396 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/0f/e5bd0d7d98bb194a30740dea2c4324f85dfc2f8daba9d7bc7e47b45d1034/yarl-1.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0bae32f8ebd35c04d6528cedb4a26b8bf25339d3616b04613b97347f919b76d3", size = 144954 }, + { url = "https://files.pythonhosted.org/packages/07/bf/2acc4b643dbdfc823d0d2058768197198a3d93b41fffb41b83359c520a4d/yarl-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8015a076daf77823e7ebdcba474156587391dab4e70c732822960368c01251e6", size = 96613 }, + { url = "https://files.pythonhosted.org/packages/ca/38/c60ccca9aad0bb939e665b63a4e1550fecc922971f1f246dd7ad709a1a72/yarl-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9973ac95327f5d699eb620286c39365990b240031672b5c436a4cd00539596c5", size = 94408 }, + { url = "https://files.pythonhosted.org/packages/9a/43/2d5b49b4784743d88054e612a97aee2a9d2d463983c6a8e2fa4c872b294a/yarl-1.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd4b5fbd7b9dde785cfeb486b8cca211a0b138d4f3a7da27db89a25b3c482e5c", size = 330774 }, + { url = "https://files.pythonhosted.org/packages/3b/48/7decce219b6eedce321345f61461ee140ee6b3faf4875efe518f0e7b5817/yarl-1.19.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75460740005de5a912b19f657848aef419387426a40f581b1dc9fac0eb9addb5", size = 323399 }, + { url = "https://files.pythonhosted.org/packages/67/2f/d6253528e49ce1c6f5119ec5269314752b06dd670f5a81721648d98b1dc7/yarl-1.19.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57abd66ca913f2cfbb51eb3dbbbac3648f1f6983f614a4446e0802e241441d2a", size = 343329 }, + { url = "https://files.pythonhosted.org/packages/fc/6b/efeb1a088e8addbf5841a84b74dad2a06346b0e4a712eb269a0cd9ada8b7/yarl-1.19.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46ade37911b7c99ce28a959147cb28bffbd14cea9e7dd91021e06a8d2359a5aa", size = 338275 }, + { url = "https://files.pythonhosted.org/packages/a6/b6/31acc2efcaf6999fd256d11f26ccc95ea773bc790ad1973331d7294b25db/yarl-1.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8346ec72ada749a6b5d82bff7be72578eab056ad7ec38c04f668a685abde6af0", size = 334014 }, + { url = "https://files.pythonhosted.org/packages/79/16/1deb54324842479e4d8b34841a383653587dfcc403c132f88b493f0c513e/yarl-1.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e4cb14a6ee5b6649ccf1c6d648b4da9220e8277d4d4380593c03cc08d8fe937", size = 322007 }, + { url = "https://files.pythonhosted.org/packages/80/77/4a073cec4f40ce84897510ee9d347bc10128f715be59b36e5c037463523b/yarl-1.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:66fc1c2926a73a2fb46e4b92e3a6c03904d9bc3a0b65e01cb7d2b84146a8bd3b", size = 336569 }, + { url = "https://files.pythonhosted.org/packages/73/e1/2f0455379bbee5f4ece8bc0968106386ec4e74237e8d68ced00bbff0a1fc/yarl-1.19.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:5a70201dd1e0a4304849b6445a9891d7210604c27e67da59091d5412bc19e51c", size = 336384 }, + { url = "https://files.pythonhosted.org/packages/74/e0/307aa8ae96bc0e72644855c76e8960019fc24c511a5dda73f05214da46f0/yarl-1.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4807aab1bdeab6ae6f296be46337a260ae4b1f3a8c2fcd373e236b4b2b46efd", size = 340454 }, + { url = "https://files.pythonhosted.org/packages/af/19/2dcdb1e5eef26751c9e79369d1f80d6a1162dababb5070f62bc5b1a8f81e/yarl-1.19.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ae584afe81a1de4c1bb06672481050f0d001cad13163e3c019477409f638f9b7", size = 355804 }, + { url = "https://files.pythonhosted.org/packages/c1/af/8c1e102c6d61713ed31022ab8f8866d263b87cb8f466c37f20a99019d169/yarl-1.19.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30eaf4459df6e91f21b2999d1ee18f891bcd51e3cbe1de301b4858c84385895b", size = 359877 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/c3c4bd85ecc7f189e14d21c3bea67ce389511d9178a302d97281868477aa/yarl-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0e617d45d03c8dec0dfce6f51f3e1b8a31aa81aaf4a4d1442fdb232bcf0c6d8c", size = 351282 }, + { url = "https://files.pythonhosted.org/packages/c6/85/0994f1c607b0520ef007717ff74f3317df3f7b7f32756ba2bf26c0c58ddf/yarl-1.19.0-cp310-cp310-win32.whl", hash = "sha256:32ba32d0fa23893fd8ea8d05bdb05de6eb19d7f2106787024fd969f4ba5466cb", size = 86529 }, + { url = "https://files.pythonhosted.org/packages/59/00/39bc8da1f67614633a099a44a5f69d056bb4d65a8e52a4003460e3fa4cc7/yarl-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:545575ecfcd465891b51546c2bcafdde0acd2c62c2097d8d71902050b20e4922", size = 92707 }, + { url = "https://files.pythonhosted.org/packages/9b/df/5fa7cd75e46306e0f9baf38a7c8969ff6730ea503b86232e85cb740304cf/yarl-1.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:163ff326680de5f6d4966954cf9e3fe1bf980f5fee2255e46e89b8cf0f3418b5", size = 145126 }, + { url = "https://files.pythonhosted.org/packages/2a/be/c1b52129cd2166ab7337f08e701a61baa7c260c7b03b534098cc8297aecc/yarl-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a626c4d9cca298d1be8625cff4b17004a9066330ac82d132bbda64a4c17c18d3", size = 96691 }, + { url = "https://files.pythonhosted.org/packages/8d/39/ad62139b45515f9bf129c805aeaaedf86fd93ae57ffe911f4caeabef3e74/yarl-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:961c3e401ea7f13d02b8bb7cb0c709152a632a6e14cdc8119e9c6ee5596cd45d", size = 94505 }, + { url = "https://files.pythonhosted.org/packages/be/be/04e3202cdc9bb5f81761e327af7095cffb0d81e32421a6b87f926052d2ae/yarl-1.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a39d7b807ab58e633ed760f80195cbd145b58ba265436af35f9080f1810dfe64", size = 355485 }, + { url = "https://files.pythonhosted.org/packages/00/7d/1463203663ca1ae62af8fb9ebc9601dd07f04dbced7edb1df3141a2cb2fe/yarl-1.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4228978fb59c6b10f60124ba8e311c26151e176df364e996f3f8ff8b93971b5", size = 344569 }, + { url = "https://files.pythonhosted.org/packages/b0/1b/5263203017348669e637bb73856fb9632110538e92d5e9f8214fcc764da9/yarl-1.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ba536b17ecf3c74a94239ec1137a3ad3caea8c0e4deb8c8d2ffe847d870a8c5", size = 371426 }, + { url = "https://files.pythonhosted.org/packages/78/59/90ca5f16d56b7741e5383951acc2e065fce41920eb5d8fda3065b5e288dc/yarl-1.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a251e00e445d2e9df7b827c9843c0b87f58a3254aaa3f162fb610747491fe00f", size = 368102 }, + { url = "https://files.pythonhosted.org/packages/84/f2/5e33aa0251ffd2c2a9041bf887e163eeefdc1dca238fdabac444d9463c3f/yarl-1.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9b92431d8b4d4ca5ccbfdbac95b05a3a6cd70cd73aa62f32f9627acfde7549c", size = 358740 }, + { url = "https://files.pythonhosted.org/packages/22/9e/ba92d234c81cf94495fc01eaa0b6000175733f76bd63e60ff748bce22c81/yarl-1.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec2f56edaf476f70b5831bbd59700b53d9dd011b1f77cd4846b5ab5c5eafdb3f", size = 346965 }, + { url = "https://files.pythonhosted.org/packages/8d/0b/d4f53136ef12ddad540855a886d7503a6cc17cfabb9a03ce0c179f3b9e51/yarl-1.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acf9b92c4245ac8b59bc7ec66a38d3dcb8d1f97fac934672529562bb824ecadb", size = 368547 }, + { url = "https://files.pythonhosted.org/packages/31/4b/35ec8622908a728f378a8511f0ab2d47878b2c0b8cbe035f2d907914a5fc/yarl-1.19.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:57711f1465c06fee8825b95c0b83e82991e6d9425f9a042c3c19070a70ac92bf", size = 357610 }, + { url = "https://files.pythonhosted.org/packages/c1/71/1f39f7c55b0684834d945a2bcfdfe59e6e02ca2483a3d33c2f77a0c3b177/yarl-1.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:528e86f5b1de0ad8dd758ddef4e0ed24f5d946d4a1cef80ffb2d4fca4e10f122", size = 365331 }, + { url = "https://files.pythonhosted.org/packages/2e/13/57675964de5c8ccf6427df93ac97f9bb7328f3f8f7ebc31a5f5a286ab1c0/yarl-1.19.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3b77173663e075d9e5a57e09d711e9da2f3266be729ecca0b8ae78190990d260", size = 378624 }, + { url = "https://files.pythonhosted.org/packages/d4/c6/5868e40f8da041ed0c3b5fd8c08cece849d9f609e970e6043308767fbb60/yarl-1.19.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d8717924cf0a825b62b1a96fc7d28aab7f55a81bf5338b8ef41d7a76ab9223e9", size = 383981 }, + { url = "https://files.pythonhosted.org/packages/f4/3f/e40124c986d96741d3d341ffac35be42b6df82ef8c18b5984ca2e7d838dd/yarl-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0df9f0221a78d858793f40cbea3915c29f969c11366646a92ca47e080a14f881", size = 378868 }, + { url = "https://files.pythonhosted.org/packages/01/eb/caf2774c770288bd87a818b11f3a56ada6a855f1987d93421aae01a175bf/yarl-1.19.0-cp311-cp311-win32.whl", hash = "sha256:8b3ade62678ee2c7c10dcd6be19045135e9badad53108f7d2ed14896ee396045", size = 86446 }, + { url = "https://files.pythonhosted.org/packages/4a/97/d4fe6168c1bb789507ffeb58c2e8c675a7e71de732dc02e12bda904c1362/yarl-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:0626ee31edb23ac36bdffe607231de2cca055ad3a5e2dc5da587ef8bc6a321bc", size = 93121 }, + { url = "https://files.pythonhosted.org/packages/b8/70/44ef8f69d61cb5123167a4dda87f6c739a833fbdb2ed52960b4e8409d65c/yarl-1.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b687c334da3ff8eab848c9620c47a253d005e78335e9ce0d6868ed7e8fd170b", size = 146855 }, + { url = "https://files.pythonhosted.org/packages/c3/94/38c14d6c8217cc818647689f2dd647b976ced8fea08d0ac84e3c8168252b/yarl-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b0fe766febcf523a2930b819c87bb92407ae1368662c1bc267234e79b20ff894", size = 97523 }, + { url = "https://files.pythonhosted.org/packages/35/a5/43a613586a6255105c4655a911c307ef3420e49e540d6ae2c5829863fb25/yarl-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:742ceffd3c7beeb2b20d47cdb92c513eef83c9ef88c46829f88d5b06be6734ee", size = 95540 }, + { url = "https://files.pythonhosted.org/packages/d4/60/ed26049f4a8b06ebfa6d5f3cb6a51b152fd57081aa818b6497474f65a631/yarl-1.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af682a1e97437382ee0791eacbf540318bd487a942e068e7e0a6c571fadbbd3", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/49/a6/b84899cab411f49af5986cfb44b514040788d81c8084f5811e6a7c0f1ce6/yarl-1.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63702f1a098d0eaaea755e9c9d63172be1acb9e2d4aeb28b187092bcc9ca2d17", size = 338889 }, + { url = "https://files.pythonhosted.org/packages/cc/ce/0704f7166a781b1f81bdd45c4f49eadbae0230ebd35b9ec7cd7769d3a6ff/yarl-1.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3560dcba3c71ae7382975dc1e912ee76e50b4cd7c34b454ed620d55464f11876", size = 353107 }, + { url = "https://files.pythonhosted.org/packages/75/e5/0ecd6f2a9cc4264c16d8dfb0d3d71ba8d03cb58f3bcd42b1df4358331189/yarl-1.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68972df6a0cc47c8abaf77525a76ee5c5f6ea9bbdb79b9565b3234ded3c5e675", size = 353128 }, + { url = "https://files.pythonhosted.org/packages/ad/c7/cd0fd1de581f1c2e8f996e704c9fd979e00106f18eebd91b0173cf1a13c6/yarl-1.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5684e7ff93ea74e47542232bd132f608df4d449f8968fde6b05aaf9e08a140f9", size = 349107 }, + { url = "https://files.pythonhosted.org/packages/e6/34/ba3e5a20bd1d6a09034fc7985aaf1309976f2a7a5aefd093c9e56f6e1e0c/yarl-1.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8182ad422bfacdebd4759ce3adc6055c0c79d4740aea1104e05652a81cd868c6", size = 335144 }, + { url = "https://files.pythonhosted.org/packages/1e/98/d9b7beb932fade015906efe0980aa7d522b8f93cf5ebf1082e74faa314b7/yarl-1.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aee5b90a5a9b71ac57400a7bdd0feaa27c51e8f961decc8d412e720a004a1791", size = 360795 }, + { url = "https://files.pythonhosted.org/packages/9a/11/70b8770039cc54af5948970591517a1e1d093df3f04f328c655c9a0fefb7/yarl-1.19.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8c0b2371858d5a814b08542d5d548adb03ff2d7ab32f23160e54e92250961a72", size = 360140 }, + { url = "https://files.pythonhosted.org/packages/d4/67/708e3e36fafc4d9d96b4eecc6c8b9f37c8ad50df8a16c7a1d5ba9df53050/yarl-1.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd430c2b7df4ae92498da09e9b12cad5bdbb140d22d138f9e507de1aa3edfea3", size = 364431 }, + { url = "https://files.pythonhosted.org/packages/c3/8b/937fbbcc895553a7e16fcd86ae4e0724c6ac9468237ad8e7c29cc3b1c9d9/yarl-1.19.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a93208282c0ccdf73065fd76c6c129bd428dba5ff65d338ae7d2ab27169861a0", size = 373832 }, + { url = "https://files.pythonhosted.org/packages/f8/ca/288ddc2230c9b6647fe907504f1119adb41252ac533eb564d3fc73511215/yarl-1.19.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b8179280cdeb4c36eb18d6534a328f9d40da60d2b96ac4a295c5f93e2799e9d9", size = 378122 }, + { url = "https://files.pythonhosted.org/packages/4f/5a/79e1ef31d14968fbfc0ecec70a6683b574890d9c7550c376dd6d40de7754/yarl-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eda3c2b42dc0c389b7cfda2c4df81c12eeb552019e0de28bde8f913fc3d1fcf3", size = 375178 }, + { url = "https://files.pythonhosted.org/packages/95/38/9b0e56bf14026c3f550ad6425679f6d1a2f4821d70767f39d6f4c56a0820/yarl-1.19.0-cp312-cp312-win32.whl", hash = "sha256:57f3fed859af367b9ca316ecc05ce79ce327d6466342734305aa5cc380e4d8be", size = 86172 }, + { url = "https://files.pythonhosted.org/packages/b3/96/5c2f3987c4bb4e5cdebea3caf99a45946b13a9516f849c02222203d99860/yarl-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:5507c1f7dd3d41251b67eecba331c8b2157cfd324849879bebf74676ce76aff7", size = 92617 }, + { url = "https://files.pythonhosted.org/packages/cd/a7/222144efa2f4a47363a5fee27d8a1d24851283b5a7f628890805fe7f7a66/yarl-1.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:59281b9ed27bc410e0793833bcbe7fc149739d56ffa071d1e0fe70536a4f7b61", size = 144789 }, + { url = "https://files.pythonhosted.org/packages/72/4f/3ee8de3f94baa33c0716260b0048b1fd5306f104b3efc6e1713693e7063e/yarl-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d27a6482ad5e05e8bafd47bf42866f8a1c0c3345abcb48d4511b3c29ecc197dc", size = 96685 }, + { url = "https://files.pythonhosted.org/packages/3e/7c/fbeebf875c1ededd872d6fefabd8a8526ef8aba6e9e8bcdf230d895d487b/yarl-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7a8e19fd5a6fdf19a91f2409665c7a089ffe7b9b5394ab33c0eec04cbecdd01f", size = 94307 }, + { url = "https://files.pythonhosted.org/packages/f3/ff/b7a9c1d7df37e594b43b7a8030e228ccd4ce361eeff24a92b17fe210e57d/yarl-1.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cda34ab19099c3a1685ad48fe45172536610c312b993310b5f1ca3eb83453b36", size = 342811 }, + { url = "https://files.pythonhosted.org/packages/79/e2/9e092876b2156c1d386e4864e85eba541ccabf2b9dcc47da64624bad0cc9/yarl-1.19.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7908a25d33f94852b479910f9cae6cdb9e2a509894e8d5f416c8342c0253c397", size = 336928 }, + { url = "https://files.pythonhosted.org/packages/71/24/648d99c134f2e14fc01ba790ad36ab56815e00069e60a12a4af893448b83/yarl-1.19.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e66c14d162bac94973e767b24de5d7e6c5153f7305a64ff4fcba701210bcd638", size = 351021 }, + { url = "https://files.pythonhosted.org/packages/0c/ee/7278d475784d407d1990a5939722e66a0fef057046fb5f1721f0a6eb156c/yarl-1.19.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c03607bf932aa4cfae371e2dc9ca8b76faf031f106dac6a6ff1458418140c165", size = 354454 }, + { url = "https://files.pythonhosted.org/packages/15/ae/242546114e052a7de21a75bd7d4860266439f90bbc21c5e4dd696866d91d/yarl-1.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9931343d1c1f4e77421687b6b94bbebd8a15a64ab8279adf6fbb047eff47e536", size = 347594 }, + { url = "https://files.pythonhosted.org/packages/46/2c/35f4347f76ea4c986e9c1f774b085f489b3a1bf1503c67a4dfc5d8e68e92/yarl-1.19.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:262087a8a0d73e1d169d45c2baf968126f93c97cf403e1af23a7d5455d52721f", size = 334113 }, + { url = "https://files.pythonhosted.org/packages/20/89/3086bc8ec8d7bd505531c51056452d7ae6af906d29c427374f1170ac1938/yarl-1.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70f384921c24e703d249a6ccdabeb57dd6312b568b504c69e428a8dd3e8e68ca", size = 361037 }, + { url = "https://files.pythonhosted.org/packages/a1/5b/2c9765524a70d1c51922b41c91caa30c8094a416734349166e1a3d8de055/yarl-1.19.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:756b9ea5292a2c180d1fe782a377bc4159b3cfefaca7e41b5b0a00328ef62fa9", size = 361025 }, + { url = "https://files.pythonhosted.org/packages/ca/f8/c4a190bcc3cd98fb428d1dd31519e58004153dc7f2acd1236ecae54e3433/yarl-1.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cbeb9c145d534c240a63b6ecc8a8dd451faeb67b3dc61d729ec197bb93e29497", size = 364397 }, + { url = "https://files.pythonhosted.org/packages/6b/fb/f65b1347be8e12ac4e3e37a9bb880e6b9b604f252aaafd88e4879b1e9348/yarl-1.19.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:087ae8f8319848c18e0d114d0f56131a9c017f29200ab1413b0137ad7c83e2ae", size = 374065 }, + { url = "https://files.pythonhosted.org/packages/1c/c5/102cc3b9baad1a76f9127453ad08e0f5bc9c996c18128b1e28fe03817d6c/yarl-1.19.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362f5480ba527b6c26ff58cff1f229afe8b7fdd54ee5ffac2ab827c1a75fc71c", size = 381341 }, + { url = "https://files.pythonhosted.org/packages/f7/ce/f5dc0439320dfe59fadab8cdd24ac324be19cf6ae4736422c7e2a510ddf3/yarl-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f408d4b4315e814e5c3668094e33d885f13c7809cbe831cbdc5b1bb8c7a448f4", size = 376552 }, + { url = "https://files.pythonhosted.org/packages/a9/4a/4833a134c76af987eff3ce8cb71e42932234120e6be061eb2555061e8844/yarl-1.19.0-cp313-cp313-win32.whl", hash = "sha256:24e4c367ad69988a2283dd45ea88172561ca24b2326b9781e164eb46eea68345", size = 85878 }, + { url = "https://files.pythonhosted.org/packages/32/e9/59327daab3af8f79221638a8f0d11474d20f6a8fbc41e9da80c5ef69e688/yarl-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:0110f91c57ab43d1538dfa92d61c45e33b84df9257bd08fcfcda90cce931cbc9", size = 92448 }, + { url = "https://files.pythonhosted.org/packages/f0/77/38ee2b6ea52fa46efb3a68c17d066760a2e873c99837001922dad3c5d4e5/yarl-1.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85ac908cd5a97bbd3048cca9f1bf37b932ea26c3885099444f34b0bf5d5e9fa6", size = 146440 }, + { url = "https://files.pythonhosted.org/packages/08/14/4c2f8696bf09d851d299e4af62bf005e6087f162cd34b8c88c332d8580ea/yarl-1.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6ba0931b559f1345df48a78521c31cfe356585670e8be22af84a33a39f7b9221", size = 97490 }, + { url = "https://files.pythonhosted.org/packages/8d/b9/a67586d46e9c68ecae6162164539c50fdeab3f4722decda4f6ea9f7bf4fd/yarl-1.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5bc503e1c1fee1b86bcb58db67c032957a52cae39fe8ddd95441f414ffbab83e", size = 95236 }, + { url = "https://files.pythonhosted.org/packages/76/01/2f3c33ef91f9292bb4bb59654fc5f6e0c24780de74cc993f583dec7c6adb/yarl-1.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d995122dcaf180fd4830a9aa425abddab7c0246107c21ecca2fa085611fa7ce9", size = 330624 }, + { url = "https://files.pythonhosted.org/packages/43/fd/64e414ffba8f19e5d151c06e9402a0a0054f0c8f5d5e25519612d5d583ad/yarl-1.19.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:217f69e60a14da4eed454a030ea8283f8fbd01a7d6d81e57efb865856822489b", size = 325798 }, + { url = "https://files.pythonhosted.org/packages/7a/84/813be2b6b8c4c5bdafa5e0c0e5b17213f45fd10efbfaaa1279a917201373/yarl-1.19.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad67c8f13a4b79990082f72ef09c078a77de2b39899aabf3960a48069704973", size = 348176 }, + { url = "https://files.pythonhosted.org/packages/4f/06/81f9a80e243e043f0dc6a043d1a89dc004b06e3f71fb7c83f9013959bb5b/yarl-1.19.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dff065a1a8ed051d7e641369ba1ad030d5a707afac54cf4ede7069b959898835", size = 343497 }, + { url = "https://files.pythonhosted.org/packages/ec/8a/abbed688dd85b5a29e91ed9a7f4cce9efe925083d7567f341ece0b36cc7e/yarl-1.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada882e26b16ee651ab6544ce956f2f4beaed38261238f67c2a96db748e17741", size = 336969 }, + { url = "https://files.pythonhosted.org/packages/33/1a/7a6316473afec0b57e1cbf2ccaa02df9f138c0e447b43e85e8b1a4e7a549/yarl-1.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67a56b1acc7093451ea2de0687aa3bd4e58d6b4ef6cbeeaad137b45203deaade", size = 328910 }, + { url = "https://files.pythonhosted.org/packages/29/07/ba204b362147a04a5e172af726887156ae4e098fab826aa9d7269fbdbf89/yarl-1.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e97d2f0a06b39e231e59ebab0e6eec45c7683b339e8262299ac952707bdf7688", size = 342614 }, + { url = "https://files.pythonhosted.org/packages/e1/43/555be0062c999a610ad2c7b5a78695f25a70890be8c3e9ae555386b20cd3/yarl-1.19.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a5288adb7c59d0f54e4ad58d86fb06d4b26e08a59ed06d00a1aac978c0e32884", size = 340438 }, + { url = "https://files.pythonhosted.org/packages/26/17/703f82dbac560b9a47cee7c83abad923ac98f062eda9430dab098c28a3c9/yarl-1.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1efbf4d03e6eddf5da27752e0b67a8e70599053436e9344d0969532baa99df53", size = 343236 }, + { url = "https://files.pythonhosted.org/packages/e7/2c/a73354c4cc84e39a1eb83c1fabce01a75640a7fcf4183e5d3e99b1e510bd/yarl-1.19.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f228f42f29cc87db67020f7d71624102b2c837686e55317b16e1d3ef2747a993", size = 358432 }, + { url = "https://files.pythonhosted.org/packages/f2/b5/5213af4695344281637d65005b781151008446bbd852a4b6a1b47b6952fa/yarl-1.19.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c515f7dd60ca724e4c62b34aeaa603188964abed2eb66bb8e220f7f104d5a187", size = 359656 }, + { url = "https://files.pythonhosted.org/packages/d0/7d/00c56abbb3bec635dbe1f0ffb11f04eefc9ec2e1af24f10b34ed5d4e154d/yarl-1.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4815ec6d3d68a96557fa71bd36661b45ac773fb50e5cfa31a7e843edb098f060", size = 353732 }, + { url = "https://files.pythonhosted.org/packages/84/4f/37e5c9162af1a494f9854683869c67be271c5e66f75b0c7010c78a025356/yarl-1.19.0-cp39-cp39-win32.whl", hash = "sha256:9fac2dd1c5ecb921359d9546bc23a6dcc18c6acd50c6d96f118188d68010f497", size = 87082 }, + { url = "https://files.pythonhosted.org/packages/55/7f/ef6a2a6d95671430364ec801286ed748cc9808bd747f038639158b5f308d/yarl-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:5864f539ce86b935053bfa18205fa08ce38e9a40ea4d51b19ce923345f0ed5db", size = 93180 }, + { url = "https://files.pythonhosted.org/packages/a4/06/ae25a353e8f032322df6f30d6bb1fc329773ee48e1a80a2196ccb8d1206b/yarl-1.19.0-py3-none-any.whl", hash = "sha256:a727101eb27f66727576630d02985d8a065d09cd0b5fcbe38a5793f71b2a97ef", size = 45990 }, +] + [[package]] name = "zipp" version = "3.21.0" From 472e8c13bdf530c962349ae04407336f17bc2f74 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 16 Apr 2025 18:54:22 -0400 Subject: [PATCH 49/85] Docs for LiteLLM integration (#532) --- docs/ja/quickstart.md | 2 +- docs/{models.md => models/index.md} | 6 +- docs/models/litellm.md | 73 +++++++++++++++++++ docs/quickstart.md | 2 +- docs/ref/extensions/litellm.md | 3 + mkdocs.yml | 5 +- src/agents/extensions/models/litellm_model.py | 8 +- 7 files changed, 90 insertions(+), 9 deletions(-) rename docs/{models.md => models/index.md} (95%) create mode 100644 docs/models/litellm.md create mode 100644 docs/ref/extensions/litellm.md diff --git a/docs/ja/quickstart.md b/docs/ja/quickstart.md index 1a4913c7..d45b7aa4 100644 --- a/docs/ja/quickstart.md +++ b/docs/ja/quickstart.md @@ -186,4 +186,4 @@ if __name__ == "__main__": - [エージェント](agents.md) の設定方法について学ぶ - [エージェントの実行](running_agents.md) について学ぶ -- [tools](tools.md)、[guardrails](guardrails.md)、[models](models.md) について学ぶ \ No newline at end of file +- [tools](tools.md)、[guardrails](guardrails.md)、[models](models/index.md) について学ぶ diff --git a/docs/models.md b/docs/models/index.md similarity index 95% rename from docs/models.md rename to docs/models/index.md index 1ce6e8f2..4cf4f643 100644 --- a/docs/models.md +++ b/docs/models/index.md @@ -70,9 +70,9 @@ You can use other LLM providers in 3 ways (examples [here](https://github.com/op 1. [`set_default_openai_client`][agents.set_default_openai_client] is useful in cases where you want to globally use an instance of `AsyncOpenAI` as the LLM client. This is for cases where the LLM provider has an OpenAI compatible API endpoint, and you can set the `base_url` and `api_key`. See a configurable example in [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py). 2. [`ModelProvider`][agents.models.interface.ModelProvider] is at the `Runner.run` level. This lets you say "use a custom model provider for all agents in this run". See a configurable example in [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py). -3. [`Agent.model`][agents.agent.Agent.model] lets you specify the model on a specific Agent instance. This enables you to mix and match different providers for different agents. See a configurable example in [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py). +3. [`Agent.model`][agents.agent.Agent.model] lets you specify the model on a specific Agent instance. This enables you to mix and match different providers for different agents. See a configurable example in [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py). An easy way to use most available models is via the [LiteLLM integration](./litellm.md). -In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](tracing.md). +In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](../tracing.md). !!! note @@ -86,7 +86,7 @@ If you get errors related to tracing, this is because traces are uploaded to Ope 1. Disable tracing entirely: [`set_tracing_disabled(True)`][agents.set_tracing_disabled]. 2. Set an OpenAI key for tracing: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]. This API key will only be used for uploading traces, and must be from [platform.openai.com](https://platform.openai.com/). -3. Use a non-OpenAI trace processor. See the [tracing docs](tracing.md#custom-tracing-processors). +3. Use a non-OpenAI trace processor. See the [tracing docs](../tracing.md#custom-tracing-processors). ### Responses API support diff --git a/docs/models/litellm.md b/docs/models/litellm.md new file mode 100644 index 00000000..90572a28 --- /dev/null +++ b/docs/models/litellm.md @@ -0,0 +1,73 @@ +# Using any model via LiteLLM + +!!! note + + The LiteLLM integration is in beta. You may run into issues with some model providers, especially smaller ones. Please report any issues via [Github issues](https://github.com/openai/openai-agents-python/issues) and we'll fix quickly. + +[LiteLLM](https://docs.litellm.ai/docs/) is a library that allows you to use 100+ models via a single interface. We've added a LiteLLM integration to allow you to use any AI model in the Agents SDK. + +## Setup + +You'll need to ensure `litellm` is available. You can do this by installing the optional `litellm` dependency group: + +```bash +pip install "openai-agents[litellm]" +``` + +Once done, you can use [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] in any agent. + +## Example + +This is a fully working example. When you run it, you'll be prompted for a model name and API key. For example, you could enter: + +- `openai/gpt-4.1` for the model, and your OpenAI API key +- `anthropic/claude-3-5-sonnet-20240620` for the model, and your Anthropic API key +- etc + +For a full list of models supported in LiteLLM, see the [litellm providers docs](https://docs.litellm.ai/docs/providers). + +```python +from __future__ import annotations + +import asyncio + +from agents import Agent, Runner, function_tool, set_tracing_disabled +from agents.extensions.models.litellm_model import LitellmModel + +@function_tool +def get_weather(city: str): + print(f"[debug] getting weather for {city}") + return f"The weather in {city} is sunny." + + +async def main(model: str, api_key: str): + agent = Agent( + name="Assistant", + instructions="You only respond in haikus.", + model=LitellmModel(model=model, api_key=api_key), + tools=[get_weather], + ) + + result = await Runner.run(agent, "What's the weather in Tokyo?") + print(result.final_output) + + +if __name__ == "__main__": + # First try to get model/api key from args + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--model", type=str, required=False) + parser.add_argument("--api-key", type=str, required=False) + args = parser.parse_args() + + model = args.model + if not model: + model = input("Enter a model name for Litellm: ") + + api_key = args.api_key + if not api_key: + api_key = input("Enter an API key for Litellm: ") + + asyncio.run(main(model, api_key)) +``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 9513fdb8..213d16e5 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -186,4 +186,4 @@ Learn how to build more complex agentic flows: - Learn about how to configure [Agents](agents.md). - Learn about [running agents](running_agents.md). -- Learn about [tools](tools.md), [guardrails](guardrails.md) and [models](models.md). +- Learn about [tools](tools.md), [guardrails](guardrails.md) and [models](models/index.md). diff --git a/docs/ref/extensions/litellm.md b/docs/ref/extensions/litellm.md new file mode 100644 index 00000000..7bd67fde --- /dev/null +++ b/docs/ref/extensions/litellm.md @@ -0,0 +1,3 @@ +# `LiteLLM Models` + +::: agents.extensions.models.litellm_model diff --git a/mkdocs.yml b/mkdocs.yml index e7f43993..286359dc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,7 +66,9 @@ plugins: - context.md - guardrails.md - multi_agent.md - - models.md + - Models: + - models/index.md + - models/litellm.md - config.md - visualization.md - Voice agents: @@ -123,6 +125,7 @@ plugins: - Extensions: - ref/extensions/handoff_filters.md - ref/extensions/handoff_prompt.md + - ref/extensions/litellm.md - locale: ja name: 日本語 diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 915a1ccb..0fc277c3 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -47,6 +47,11 @@ class LitellmModel(Model): + """This class enables using any model via LiteLLM. LiteLLM allows you to acess OpenAPI, + Anthropic, Gemini, Mistral, and many other models. + See supported models here: [litellm models](https://docs.litellm.ai/docs/providers). + """ + def __init__( self, model: str, @@ -140,9 +145,6 @@ async def stream_response( *, previous_response_id: str | None, ) -> AsyncIterator[TResponseStreamEvent]: - """ - Yields a partial message as it is generated, as well as the usage information. - """ with generation_span( model=str(self.model), model_config=dataclasses.asdict(model_settings) From 56396061631693119ef570bed078a84acd2d2811 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 17 Apr 2025 10:29:09 +0900 Subject: [PATCH 50/85] Docs: Switch to o3 model; exclude translated pages from search (#533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request introduces the following changes: 1. **Exclude translated pages from search**: I explored ways to make the search plugin work with the i18n plugin, but it would require extensive custom JavaScript hacks. So for now, I’m holding off on this work. 2. **Switch from GPT-4.1 to o3 for even better translation quality**: While 4.1 performs well, o3 shows even greater quality for this task, and there’s no reason to avoid using it. --- docs/ja/agents.md | 48 +++++++------- docs/ja/config.md | 28 ++++---- docs/ja/context.md | 52 ++++++++------- docs/ja/examples.md | 41 +++++++----- docs/ja/guardrails.md | 58 +++++++++-------- docs/ja/handoffs.md | 40 +++++++----- docs/ja/index.md | 38 ++++++----- docs/ja/mcp.md | 38 ++++++----- docs/ja/models/index.md | 116 +++++++++++++++++++++++++++++++++ docs/ja/models/litellm.md | 77 ++++++++++++++++++++++ docs/ja/multi_agent.md | 50 +++++++------- docs/ja/quickstart.md | 42 ++++++------ docs/ja/results.md | 44 +++++++------ docs/ja/running_agents.md | 82 ++++++++++++----------- docs/ja/streaming.md | 20 +++--- docs/ja/tools.md | 93 ++++++++++++++++---------- docs/ja/tracing.md | 110 ++++++++++++++++--------------- docs/ja/visualization.md | 33 +++++----- docs/ja/voice/pipeline.md | 38 ++++++----- docs/ja/voice/quickstart.md | 22 ++++--- docs/ja/voice/tracing.md | 20 +++--- docs/scripts/translate_docs.py | 27 +++++++- mkdocs.yml | 4 +- 23 files changed, 719 insertions(+), 402 deletions(-) create mode 100644 docs/ja/models/index.md create mode 100644 docs/ja/models/litellm.md diff --git a/docs/ja/agents.md b/docs/ja/agents.md index f4b108e8..828b3635 100644 --- a/docs/ja/agents.md +++ b/docs/ja/agents.md @@ -1,14 +1,18 @@ +--- +search: + exclude: true +--- # エージェント -エージェントは、アプリケーションの中核となる基本コンポーネントです。エージェントとは、instructions とツールで構成された大規模言語モデル(LLM)のことです。 +エージェントはアプリの主要な構成ブロックです。エージェントは、大規模言語モデル ( LLM ) に instructions と tools を設定したものです。 ## 基本設定 -エージェントで最も一般的に設定するプロパティは以下の通りです。 +エージェントで最も一般的に設定するプロパティは次のとおりです。 -- `instructions`:developer message や システムプロンプト(system prompt)とも呼ばれます。 -- `model`:どの LLM を使用するか、また `model_settings` で temperature や top_p などのモデル調整パラメーターを設定できます。 -- `tools`:エージェントがタスクを達成するために使用できるツールです。 +- `instructions`: 開発者メッセージまたは system prompt とも呼ばれます。 +- `model`: 使用する LLM と、temperature や top_p などのモデル調整パラメーターを指定する任意の `model_settings`。 +- `tools`: エージェントがタスクを達成するために利用できるツール。 ```python from agents import Agent, ModelSettings, function_tool @@ -27,7 +31,7 @@ agent = Agent( ## コンテキスト -エージェントは `context` 型に対して汎用的です。コンテキストは依存性注入ツールであり、`Runner.run()` に渡すオブジェクトです。これはすべてのエージェント、ツール、ハンドオフなどに渡され、エージェント実行時の依存関係や状態をまとめて管理します。任意の Python オブジェクトを context として指定できます。 +エージェントはその `context` 型について汎用的です。コンテキストは依存性注入の手段で、`Runner.run()` に渡すオブジェクトです。これはすべてのエージェント、ツール、ハンドオフなどに渡され、エージェント実行時の依存関係や状態をまとめて保持します。任意の Python オブジェクトをコンテキストとして渡せます。 ```python @dataclass @@ -45,7 +49,7 @@ agent = Agent[UserContext]( ## 出力タイプ -デフォルトでは、エージェントはプレーンテキスト(つまり `str`)出力を生成します。特定の型の出力をエージェントに生成させたい場合は、`output_type` パラメーターを使用できます。一般的な選択肢として [Pydantic](https://docs.pydantic.dev/) オブジェクトがありますが、Pydantic の [TypeAdapter](https://docs.pydantic.dev/latest/api/type_adapter/) でラップできる型(dataclasses、リスト、TypedDict など)であればサポートしています。 +デフォルトでは、エージェントはプレーンテキスト ( つまり `str` ) を出力します。特定の型で出力させたい場合は `output_type` パラメーターを使用します。一般的には [Pydantic](https://docs.pydantic.dev/) オブジェクトを利用しますが、Pydantic の [TypeAdapter](https://docs.pydantic.dev/latest/api/type_adapter/) でラップ可能な型であれば何でも対応します。たとえば dataclass、list、TypedDict などです。 ```python from pydantic import BaseModel @@ -66,11 +70,11 @@ agent = Agent( !!! note - `output_type` を指定すると、モデルは通常のプレーンテキスト応答の代わりに [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用するよう指示されます。 + `output_type` を渡すと、モデルは通常のプレーンテキスト応答の代わりに [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使用するよう指示されます。 ## ハンドオフ -ハンドオフは、エージェントが委任できるサブエージェントです。ハンドオフのリストを指定すると、エージェントは必要に応じてそれらに処理を委任できます。これは、単一タスクに特化したモジュール型のエージェントをオーケストレーションする強力なパターンです。詳細は [handoffs](handoffs.md) ドキュメントをご覧ください。 +ハンドオフは、エージェントが委譲できるサブエージェントです。ハンドオフのリストを渡しておくと、エージェントは必要に応じてそれらに処理を委譲できます。これにより、単一のタスクに特化したモジュール式エージェントを編成できる強力なパターンが実現します。詳細は [handoffs](handoffs.md) ドキュメントをご覧ください。 ```python from agents import Agent @@ -91,7 +95,7 @@ triage_agent = Agent( ## 動的 instructions -多くの場合、エージェント作成時に instructions を指定できますが、関数を使って動的に instructions を提供することも可能です。この関数はエージェントと context を受け取り、プロンプトを返す必要があります。通常の関数と `async` 関数の両方が利用可能です。 +通常はエージェント作成時に instructions を指定しますが、関数を介して動的に instructions を提供することもできます。その関数はエージェントとコンテキストを受け取り、プロンプトを返す必要があります。同期関数と `async` 関数の両方に対応しています。 ```python def dynamic_instructions( @@ -106,17 +110,17 @@ agent = Agent[UserContext]( ) ``` -## ライフサイクルイベント(フック) +## ライフサイクルイベント (hooks) -エージェントのライフサイクルを監視したい場合があります。たとえば、イベントを記録したり、特定のイベント発生時にデータを事前取得したりしたい場合です。`hooks` プロパティを使ってエージェントのライフサイクルにフックできます。[`AgentHooks`][agents.lifecycle.AgentHooks] クラスをサブクラス化し、関心のあるメソッドをオーバーライドしてください。 +場合によっては、エージェントのライフサイクルを観察したいことがあります。たとえば、イベントをログに記録したり、特定のイベント発生時にデータを事前取得したりする場合です。`hooks` プロパティを使ってエージェントのライフサイクルにフックできます。[`AgentHooks`][agents.lifecycle.AgentHooks] クラスをサブクラス化し、関心のあるメソッドをオーバーライドしてください。 ## ガードレール -ガードレールを使うと、エージェントの実行と並行して user 入力のチェックやバリデーションを行えます。たとえば、user の入力が関連性のある内容かどうかをスクリーニングできます。詳細は [guardrails](guardrails.md) ドキュメントをご覧ください。 +ガードレールを使うと、エージェントの実行と並行してユーザー入力に対するチェックやバリデーションを実行できます。たとえば、ユーザーの入力内容が関連しているかをスクリーニングできます。詳細は [guardrails](guardrails.md) ドキュメントをご覧ください。 -## エージェントのクローン/コピー +## エージェントの複製 -エージェントの `clone()` メソッドを使うことで、エージェントを複製し、任意のプロパティを変更できます。 +`clone()` メソッドを使用すると、エージェントを複製し、必要に応じて任意のプロパティを変更できます。 ```python pirate_agent = Agent( @@ -133,15 +137,15 @@ robot_agent = pirate_agent.clone( ## ツール使用の強制 -ツールのリストを指定しても、必ずしも LLM がツールを使用するとは限りません。[`ModelSettings.tool_choice`][agents.model_settings.ModelSettings.tool_choice] を設定することでツールの使用を強制できます。有効な値は以下の通りです。 +ツールの一覧を渡しても、LLM が必ずツールを使用するとは限りません。[`ModelSettings.tool_choice`][agents.model_settings.ModelSettings.tool_choice] を設定することでツール使用を強制できます。有効な値は次のとおりです。 -1. `auto`:LLM がツールを使うかどうかを自動で判断します。 -2. `required`:LLM にツールの使用を必須とします(どのツールを使うかは賢く選択されます)。 -3. `none`:LLM にツールを _使わない_ ことを要求します。 -4. 特定の文字列(例:`my_tool`)を指定すると、その特定のツールの使用を必須とします。 +1. `auto` — ツールを使用するかどうかを LLM が判断します。 +2. `required` — LLM にツール使用を必須化します ( ただし使用するツールは自動選択 )。 +3. `none` — LLM にツールを使用しないことを要求します。 +4. 特定の文字列 ( 例: `my_tool` ) — その特定のツールを LLM に使用させます。 !!! note - 無限ループを防ぐため、フレームワークはツール呼び出し後に自動的に `tool_choice` を "auto" にリセットします。この挙動は [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice] で設定可能です。無限ループは、ツールの execution results が LLM に送信され、`tool_choice` のために再度ツール呼び出しが発生し、これが繰り返されることで発生します。 + 無限ループを防ぐため、フレームワークはツール呼び出し後に `tool_choice` を自動的に "auto" にリセットします。この動作は [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice] で設定できます。無限ループが起こる理由は、ツールの結果が LLM に送られ、`tool_choice` により再びツール呼び出しが生成される、という流れが繰り返されるからです。 - ツール呼び出し後にエージェントを完全に停止させたい場合(auto モードで継続させたくない場合)は、[`Agent.tool_use_behavior="stop_on_first_tool"`] を設定できます。これにより、ツールの出力がそのまま最終応答として使用され、以降の LLM 処理は行われません。 \ No newline at end of file + ツール呼び出し後にエージェントを完全に停止させたい場合 ( auto モードで続行させたくない場合 ) は、[`Agent.tool_use_behavior="stop_on_first_tool"`] を設定してください。これにより、ツールの出力を LL M の追加処理なしにそのまま最終応答として返します。 \ No newline at end of file diff --git a/docs/ja/config.md b/docs/ja/config.md index c547a793..bf76b9fb 100644 --- a/docs/ja/config.md +++ b/docs/ja/config.md @@ -1,8 +1,12 @@ +--- +search: + exclude: true +--- # SDK の設定 ## API キーとクライアント -デフォルトでは、SDK はインポート時に LLM リクエストやトレーシングのために `OPENAI_API_KEY` 環境変数を探します。アプリの起動前にこの環境変数を設定できない場合は、[set_default_openai_key()][agents.set_default_openai_key] 関数を使ってキーを設定できます。 +デフォルトでは、 SDK はインポートされた時点で LLM リクエストとトレーシングに使用する `OPENAI_API_KEY` 環境変数を探します。アプリ起動前にこの環境変数を設定できない場合は、 [set_default_openai_key()][agents.set_default_openai_key] 関数を利用してキーを設定できます。 ```python from agents import set_default_openai_key @@ -10,7 +14,7 @@ from agents import set_default_openai_key set_default_openai_key("sk-...") ``` -また、使用する OpenAI クライアントを設定することも可能です。デフォルトでは、SDK は環境変数または上記で設定したデフォルトキーを使って `AsyncOpenAI` インスタンスを作成します。これを変更したい場合は、[set_default_openai_client()][agents.set_default_openai_client] 関数を利用してください。 +また、使用する OpenAI クライアントを構成することも可能です。デフォルトでは、 SDK は環境変数または上記で設定したデフォルトキーを用いて `AsyncOpenAI` インスタンスを作成します。これを変更するには、 [set_default_openai_client()][agents.set_default_openai_client] 関数を使用します。 ```python from openai import AsyncOpenAI @@ -20,7 +24,7 @@ custom_client = AsyncOpenAI(base_url="...", api_key="...") set_default_openai_client(custom_client) ``` -さらに、使用する OpenAI API をカスタマイズすることもできます。デフォルトでは OpenAI Responses API を使用していますが、[set_default_openai_api()][agents.set_default_openai_api] 関数を使って Chat Completions API を利用するように上書きできます。 +さらに、使用する OpenAI API をカスタマイズすることもできます。既定では OpenAI Responses API を利用します。これを Chat Completions API に変更するには、 [set_default_openai_api()][agents.set_default_openai_api] 関数を使用してください。 ```python from agents import set_default_openai_api @@ -30,7 +34,7 @@ set_default_openai_api("chat_completions") ## トレーシング -トレーシングはデフォルトで有効になっています。デフォルトでは、上記のセクションで説明した OpenAI API キー(環境変数または設定したデフォルトキー)を使用します。トレーシング専用の API キーを設定したい場合は、[`set_tracing_export_api_key`][agents.set_tracing_export_api_key] 関数を利用してください。 +トレーシングはデフォルトで有効になっています。前述の OpenAI API キー(環境変数または設定したデフォルトキー)が自動的に使用されます。トレーシングで使用する API キーを個別に設定したい場合は、 [`set_tracing_export_api_key`][agents.set_tracing_export_api_key] 関数を利用してください。 ```python from agents import set_tracing_export_api_key @@ -38,7 +42,7 @@ from agents import set_tracing_export_api_key set_tracing_export_api_key("sk-...") ``` -また、[`set_tracing_disabled()`][agents.set_tracing_disabled] 関数を使ってトレーシングを完全に無効化することもできます。 +トレーシングを完全に無効化するには、 [`set_tracing_disabled()`][agents.set_tracing_disabled] 関数を呼び出します。 ```python from agents import set_tracing_disabled @@ -48,9 +52,9 @@ set_tracing_disabled(True) ## デバッグログ -SDK には、ハンドラーが設定されていない 2 つの Python ロガーがあります。デフォルトでは、警告やエラーは `stdout` に送信されますが、それ以外のログは抑制されます。 + SDK にはハンドラーが設定されていない Python ロガーが 2 つあります。デフォルトでは、警告とエラーは `stdout` に出力されますが、それ以外のログは抑制されます。 -詳細なログ出力を有効にするには、[`enable_verbose_stdout_logging()`][agents.enable_verbose_stdout_logging] 関数を使用してください。 +詳細なログを有効にするには、 [`enable_verbose_stdout_logging()`][agents.enable_verbose_stdout_logging] 関数を使用します。 ```python from agents import enable_verbose_stdout_logging @@ -58,7 +62,7 @@ from agents import enable_verbose_stdout_logging enable_verbose_stdout_logging() ``` -また、ハンドラーやフィルター、フォーマッターなどを追加してログをカスタマイズすることも可能です。詳細は [Python ロギングガイド](https://docs.python.org/3/howto/logging.html) をご覧ください。 +必要に応じて、ハンドラー、フィルター、フォーマッターなどを追加してログをカスタマイズすることも可能です。詳しくは [Python ロギングガイド](https://docs.python.org/3/howto/logging.html) を参照してください。 ```python import logging @@ -77,17 +81,17 @@ logger.setLevel(logging.WARNING) logger.addHandler(logging.StreamHandler()) ``` -### ログ内の機微なデータ +### ログに含まれる機微情報 -一部のログには機微なデータ(たとえば ユーザー データ)が含まれる場合があります。これらのデータのログ出力を無効にしたい場合は、以下の環境変数を設定してください。 +特定のログには機微情報(たとえば ユーザー データ)が含まれる場合があります。この情報が記録されるのを防ぎたい場合は、次の環境変数を設定してください。 -LLM の入力および出力のログ出力を無効にするには: +LLM の入力および出力のログを無効にする: ```bash export OPENAI_AGENTS_DONT_LOG_MODEL_DATA=1 ``` -ツールの入力および出力のログ出力を無効にするには: +ツールの入力および出力のログを無効にする: ```bash export OPENAI_AGENTS_DONT_LOG_TOOL_DATA=1 diff --git a/docs/ja/context.md b/docs/ja/context.md index 4240ceee..72c0938c 100644 --- a/docs/ja/context.md +++ b/docs/ja/context.md @@ -1,29 +1,33 @@ +--- +search: + exclude: true +--- # コンテキスト管理 -コンテキストは多義的な用語です。主に関心を持つべきコンテキストには、次の 2 つの大きなクラスがあります。 +コンテキストという言葉には複数の意味があります。ここでは主に 2 つのコンテキストについて説明します。 -1. コード内でローカルに利用可能なコンテキスト:これは、ツール関数の実行時や `on_handoff` のようなコールバック、ライフサイクルフックなどで必要となるデータや依存関係です。 -2. LLM に利用可能なコンテキスト:これは、LLM がレスポンスを生成する際に参照できるデータです。 +1. コード内でローカルに利用できるコンテキスト: ツール関数の実行時や `on_handoff` などのコールバック、ライフサイクルフックで必要となるデータや依存関係です。 +2. LLM が参照できるコンテキスト: LLM がレスポンスを生成する際に見えるデータです。 ## ローカルコンテキスト -これは [`RunContextWrapper`][agents.run_context.RunContextWrapper] クラスおよびその中の [`context`][agents.run_context.RunContextWrapper.context] プロパティによって表現されます。仕組みは以下の通りです。 +ローカルコンテキストは [`RunContextWrapper`][agents.run_context.RunContextWrapper] クラスと、その中の [`context`][agents.run_context.RunContextWrapper.context] プロパティで表現されます。仕組みは次のとおりです。 -1. 任意の Python オブジェクトを作成します。一般的なパターンとしては、dataclass や Pydantic オブジェクトを使います。 -2. そのオブジェクトを各種 run メソッド(例:`Runner.run(..., **context=whatever**))`)に渡します。 -3. すべてのツール呼び出しやライフサイクルフックなどには、ラッパーオブジェクト `RunContextWrapper[T]` が渡されます。ここで `T` はコンテキストオブジェクトの型を表し、`wrapper.context` からアクセスできます。 +1. 任意の Python オブジェクトを作成します。一般的なパターンとして dataclass や Pydantic オブジェクトを使用します。 +2. そのオブジェクトを各種 run メソッド(例: `Runner.run(..., **context=whatever** )`)に渡します。 +3. すべてのツール呼び出しやライフサイクルフックには、ラッパーオブジェクト `RunContextWrapper[T]` が渡されます。ここで `T` はコンテキストオブジェクトの型で、`wrapper.context` からアクセスできます。 -**最も重要**な注意点:特定のエージェント実行において、すべてのエージェント、ツール関数、ライフサイクルなどは、同じ _型_ のコンテキストを使用する必要があります。 +**最重要ポイント**: あるエージェントの実行において、エージェント・ツール関数・ライフサイクルフックなどはすべて同じ _型_ のコンテキストを使用しなければなりません。 -コンテキストは以下のような用途で利用できます。 +コンテキストでは次のような用途が考えられます。 -- 実行時のコンテキストデータ(例:ユーザー名/uid やユーザーに関するその他の情報など) -- 依存関係(例:ロガーオブジェクト、データフェッチャーなど) +- 実行に関するデータ(例: ユーザー名 / uid やその他のユーザー情報) +- 依存オブジェクト(例: ロガー、データフェッチャーなど) - ヘルパー関数 -!!! danger "注意" +!!! danger "Note" - コンテキストオブジェクトは **LLM には送信されません**。これは純粋にローカルなオブジェクトであり、読み書きやメソッド呼び出しが可能です。 + コンテキストオブジェクトは LLM には送信されません。あくまでローカルのオブジェクトであり、読み書きやメソッド呼び出しが可能です。 ```python import asyncio @@ -61,17 +65,17 @@ if __name__ == "__main__": asyncio.run(main()) ``` -1. これはコンテキストオブジェクトです。ここでは dataclass を使用していますが、任意の型を利用できます。 -2. これはツールです。`RunContextWrapper[UserInfo]` を受け取っていることが分かります。ツールの実装はコンテキストから値を読み取ります。 -3. エージェントにはジェネリック型 `UserInfo` を指定しています。これにより、型チェッカーがエラーを検出できます(例えば、異なるコンテキスト型を受け取るツールを渡そうとした場合など)。 -4. コンテキストは `run` 関数に渡されます。 -5. エージェントは正しくツールを呼び出し、年齢を取得します。 +1. これがコンテキストオブジェクトです。ここでは dataclass を使っていますが、任意の型を使用できます。 +2. これはツールです。`RunContextWrapper[UserInfo]` を受け取り、実装内でコンテキストを参照しています。 +3. エージェントにジェネリック `UserInfo` を付与することで、型チェッカーが誤りを検出できます(たとえば別のコンテキスト型を受け取るツールを渡した場合など)。 +4. `run` 関数にコンテキストを渡します。 +5. エージェントはツールを正しく呼び出し、年齢を取得します。 -## エージェント/LLM コンテキスト +## エージェント / LLM コンテキスト -LLM が呼び出される際、**唯一** 参照できるデータは会話履歴からのものです。つまり、LLM に新しいデータを利用させたい場合は、そのデータを履歴に含める必要があります。これを実現する方法はいくつかあります。 +LLM が呼び出されるとき、LLM が参照できるデータは会話履歴に含まれるものだけです。したがって、新しいデータを LLM に渡したい場合は、そのデータを履歴に含める形で提供する必要があります。方法はいくつかあります。 -1. エージェントの `instructions` に追加する。この方法は「システムプロンプト」や「開発者メッセージ」とも呼ばれます。システムプロンプトは静的な文字列でも、コンテキストを受け取って文字列を出力する動的な関数でも構いません。たとえば、ユーザー名や現在の日付など、常に有用な情報に適しています。 -2. `Runner.run` 関数を呼び出す際に `input` に追加する。この方法は `instructions` と似ていますが、[chain of command](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command) の下位メッセージとして追加できます。 -3. 関数ツールを通じて公開する。この方法は _オンデマンド_ のコンテキストに適しています。LLM が必要なタイミングでツールを呼び出し、データを取得できます。 -4. リトリーバルや Web 検索を利用する。これらはファイルやデータベース(リトリーバル)、または Web(Web 検索)から関連データを取得できる特別なツールです。関連するコンテキストデータに基づいたレスポンスを「グラウンディング」するのに役立ちます。 \ No newline at end of file +1. Agent の `instructions` に追加する。いわゆる「system prompt」や「developer message」と呼ばれるものです。システムプロンプトは静的な文字列でも、コンテキストを受け取って文字列を返す動的な関数でも構いません。ユーザー名や現在の日付など、常に有用な情報を渡す際によく使われます。 +2. `Runner.run` 呼び出し時の `input` に追加する。`instructions` と似ていますが、[chain of command](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command) の下位レイヤーにメッセージを配置できます。 +3. 関数ツール経由で公開する。オンデマンドで取得するコンテキストに適しており、LLM が必要に応じてツールを呼び出してデータを取得します。 +4. retrieval や web search を使う。これらは特別なツールで、ファイルやデータベースから関連データを取得する(retrieval)、もしくは Web から取得する(web search)ことができます。レスポンスを関連コンテキストで「グラウンディング」するのに有効です。 \ No newline at end of file diff --git a/docs/ja/examples.md b/docs/ja/examples.md index c2393c8e..00f634ec 100644 --- a/docs/ja/examples.md +++ b/docs/ja/examples.md @@ -1,40 +1,45 @@ +--- +search: + exclude: true +--- # コード例 -SDK のさまざまなサンプル実装については、[リポジトリ](https://github.com/openai/openai-agents-python/tree/main/examples) のコード例セクションをご覧ください。これらのコード例は、異なるパターンや機能を示すいくつかのカテゴリーに整理されています。 +リポジトリの [examples セクション](https://github.com/openai/openai-agents-python/tree/main/examples) には、 SDK のさまざまなサンプル実装が用意されています。これらの例は、異なるパターンや機能を示す複数のカテゴリーに整理されています。 + ## カテゴリー - **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - このカテゴリーのコード例では、よく使われるエージェント設計パターンを紹介しています。 + このカテゴリーの例では、一般的なエージェント設計パターンを紹介しています。 - - 決定論的なワークフロー - - ツールとしてのエージェント - - エージェントの並列実行 + - 決定論的ワークフロー + - ツールとしてのエージェント + - エージェントの並列実行 - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** - これらのコード例では、SDK の基本的な機能を紹介しています。 + SDK の基礎的な機能を示す例です。 - - 動的なシステムプロンプト - - ストリーミング出力 - - ライフサイクルイベント + - 動的なシステムプロンプト + - ストリーミング出力 + - ライフサイクルイベント - **[tool examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** - OpenAI がホストするツール(Web 検索やファイル検索など)の実装方法や、それらをエージェントに統合する方法を学べます。 + Web 検索やファイル検索など、 OpenAI がホストするツールの実装方法と、それらをエージェントに統合する方法を学べます。 - **[model providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - OpenAI 以外のモデルを SDK で利用する方法を紹介しています。 + OpenAI 以外のモデルを SDK で利用する方法を探ります。 - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - エージェントのハンドオフの実践的なコード例をご覧いただけます。 + エージェントのハンドオフを実践的に示す例です。 - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - Model context protocol (MCP) を使ったエージェントの構築方法を学べます。 + MCP を使ったエージェントの構築方法を学べます。 -- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** および **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 実際のユースケースを示す、より発展的な 2 つのコード例です。 +- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)** と **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** + より実践的なユースケースを示す、拡張された 2 つの例です。 - - **customer_service**: 航空会社向けカスタマーサービスシステムの例。 - - **research_bot**: シンプルなディープリサーチクローン。 + - **customer_service**: 航空会社向けカスタマーサービスシステムの例 + - **research_bot**: シンプルなディープリサーチクローン - **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** - TTS および STT モデルを利用した音声エージェントのコード例をご覧いただけます。 \ No newline at end of file + TTS と STT モデルを用いた音声エージェントの例をご覧ください。 \ No newline at end of file diff --git a/docs/ja/guardrails.md b/docs/ja/guardrails.md index 2525f8c2..e7b02a6e 100644 --- a/docs/ja/guardrails.md +++ b/docs/ja/guardrails.md @@ -1,43 +1,47 @@ +--- +search: + exclude: true +--- # ガードレール -ガードレールは、エージェントと _並行して_ 実行され、ユーザー入力のチェックやバリデーションを行うことができます。例えば、非常に賢い(そのため遅くて高価な)モデルを使ってカスタマーリクエストに対応するエージェントがあるとします。悪意のあるユーザーがモデルに数学の宿題を手伝わせるようなリクエストを送ることは避けたいでしょう。そこで、ガードレールを高速かつ安価なモデルで実行できます。ガードレールが悪意のある利用を検知した場合、即座にエラーを発生させ、高価なモデルの実行を止めて時間やコストを節約できます。 +ガードレールは エージェント と _並列_ に実行され、 ユーザー入力 のチェックとバリデーションを行います。たとえば、顧客からのリクエストを支援するために非常に賢い (そのため遅く / 高価な) モデルを使うエージェントがあるとします。悪意のある ユーザー がモデルに数学の宿題を手伝わせようとするのは避けたいですよね。その場合、 高速 / 低コスト のモデルでガードレールを実行できます。ガードレールが悪意のある利用を検知した場合、即座にエラーを送出して高価なモデルの実行を停止し、時間と費用を節約できます。 -ガードレールには 2 種類あります: +ガードレールには 2 種類あります。 -1. 入力ガードレール:最初のユーザー入力に対して実行されます -2. 出力ガードレール:最終的なエージェント出力に対して実行されます +1. Input ガードレールは最初の ユーザー入力 に対して実行されます +2. Output ガードレールは最終的なエージェント出力に対して実行されます -## 入力ガードレール +## Input ガードレール -入力ガードレールは 3 ステップで実行されます: +Input ガードレールは 3 つのステップで実行されます。 -1. まず、ガードレールはエージェントに渡されたものと同じ入力を受け取ります。 -2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`InputGuardrailResult`][agents.guardrail.InputGuardrailResult] でラップされます。 -3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な対応や例外処理が可能です。 +1. まず、ガードレールはエージェントに渡されたものと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され [`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`InputGuardrailResult`][agents.guardrail.InputGuardrailResult] でラップされます。 +3. 最後に [`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered] 例外が送出されるので、 ユーザー への適切な応答や例外処理を行えます。 !!! Note - 入力ガードレールはユーザー入力に対して実行されることを想定しているため、エージェントのガードレールは *最初* のエージェントでのみ実行されます。「なぜ `guardrails` プロパティがエージェントにあり、`Runner.run` に渡さないのか?」と疑問に思うかもしれません。これは、ガードレールが実際のエージェントに関連することが多いためです。異なるエージェントごとに異なるガードレールを実行するため、コードを同じ場所にまとめておくと可読性が向上します。 + Input ガードレールは ユーザー入力 に対して実行されることを想定しているため、エージェントのガードレールが実行されるのはそのエージェントが *最初* のエージェントである場合だけです。「なぜ `guardrails` プロパティがエージェントにあり、 `Runner.run` に渡さないのか?」と思うかもしれません。ガードレールは実際の エージェント に密接に関連する場合が多く、エージェントごとに異なるガードレールを実行するため、コードを同じ場所に置くことで可読性が向上するからです。 -## 出力ガードレール +## Output ガードレール -出力ガードレールも 3 ステップで実行されます: +Output ガードレールは 3 つのステップで実行されます。 -1. まず、ガードレールはエージェントに渡されたものと同じ入力を受け取ります。 -2. 次に、ガードレール関数が実行され、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`OutputGuardrailResult`][agents.guardrail.OutputGuardrailResult] でラップされます。 -3. 最後に、[`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered] 例外が発生し、ユーザーへの適切な対応や例外処理が可能です。 +1. まず、ガードレールはエージェントに渡されたものと同じ入力を受け取ります。 +2. 次に、ガードレール関数が実行され [`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を生成し、それが [`OutputGuardrailResult`][agents.guardrail.OutputGuardrailResult] でラップされます。 +3. 最後に [`.tripwire_triggered`][agents.guardrail.GuardrailFunctionOutput.tripwire_triggered] が true かどうかを確認します。true の場合、[`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered] 例外が送出されるので、 ユーザー への適切な応答や例外処理を行えます。 !!! Note - 出力ガードレールは最終的なエージェント出力に対して実行されることを想定しているため、エージェントのガードレールは *最後* のエージェントでのみ実行されます。入力ガードレールと同様に、ガードレールが実際のエージェントに関連することが多いため、コードを同じ場所にまとめておくと可読性が向上します。 + Output ガードレールは最終的なエージェント出力に対して実行されることを想定しているため、エージェントのガードレールが実行されるのはそのエージェントが *最後* のエージェントである場合だけです。Input ガードレール同様、ガードレールは実際の エージェント に密接に関連するため、コードを同じ場所に置くことで可読性が向上します。 -## トリップワイヤー +## トリップワイヤ -入力または出力がガードレールに失敗した場合、ガードレールはトリップワイヤーでこれを通知できます。トリップワイヤーが発動したガードレールを検知した時点で、即座に `{Input,Output}GuardrailTripwireTriggered` 例外を発生させ、エージェントの実行を停止します。 +入力または出力がガードレールに失敗した場合、ガードレールはトリップワイヤを用いてそれを通知できます。ガードレールがトリップワイヤを発火したことを検知すると、ただちに `{Input,Output}GuardrailTripwireTriggered` 例外を送出してエージェントの実行を停止します。 ## ガードレールの実装 -入力を受け取り、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を返す関数を用意する必要があります。この例では、内部でエージェントを実行することでこれを実現します。 +入力を受け取り、[`GuardrailFunctionOutput`][agents.guardrail.GuardrailFunctionOutput] を返す関数を用意する必要があります。次の例では、内部で エージェント を実行してこれを行います。 ```python from pydantic import BaseModel @@ -90,12 +94,12 @@ async def main(): print("Math homework guardrail tripped") ``` -1. このエージェントをガードレール関数内で使用します。 -2. これはエージェントの入力やコンテキストを受け取り、結果を返すガードレール関数です。 -3. ガードレールの結果に追加情報を含めることができます。 -4. これはワークフローを定義する実際のエージェントです。 +1. この エージェント をガードレール関数内で使用します。 +2. これはエージェントの入力 / コンテキストを受け取り、結果を返すガードレール関数です。 +3. ガードレール結果に追加情報を含めることができます。 +4. これはワークフローを定義する実際のエージェントです。 -出力ガードレールも同様です。 +Output ガードレールも同様です。 ```python from pydantic import BaseModel @@ -148,7 +152,7 @@ async def main(): print("Math output guardrail tripped") ``` -1. これは実際のエージェントの出力型です。 -2. これはガードレールの出力型です。 -3. これはエージェントの出力を受け取り、結果を返すガードレール関数です。 +1. これは実際のエージェントの出力型です。 +2. これはガードレールの出力型です。 +3. これはエージェントの出力を受け取り、結果を返すガードレール関数です。 4. これはワークフローを定義する実際のエージェントです。 \ No newline at end of file diff --git a/docs/ja/handoffs.md b/docs/ja/handoffs.md index ae3b8b12..c0e99556 100644 --- a/docs/ja/handoffs.md +++ b/docs/ja/handoffs.md @@ -1,18 +1,22 @@ +--- +search: + exclude: true +--- # ハンドオフ -ハンドオフは、エージェントが他のエージェントにタスクを委任できる仕組みです。これは、異なるエージェントがそれぞれ異なる分野に特化しているシナリオで特に有用です。たとえば、カスタマーサポートアプリでは、注文状況、返金、FAQ などのタスクをそれぞれ専門に扱うエージェントが存在する場合があります。 +ハンドオフを使用すると、エージェント がタスクを別の エージェント に委譲できます。これは、複数の エージェント がそれぞれ異なる分野を専門とするシナリオで特に便利です。たとえばカスタマーサポートアプリでは、注文状況、返金、 FAQ などのタスクを個別に担当する エージェント を用意できます。 -ハンドオフは LLM からはツールとして認識されます。たとえば、`Refund Agent` という名前のエージェントへのハンドオフがある場合、そのツール名は `transfer_to_refund_agent` となります。 +ハンドオフは LLM からはツールとして認識されます。そのため、`Refund Agent` という エージェント へのハンドオフであれば、ツール名は `transfer_to_refund_agent` になります。 ## ハンドオフの作成 -すべてのエージェントは [`handoffs`][agents.agent.Agent.handoffs] パラメーターを持っており、これは直接 `Agent` を指定することも、ハンドオフをカスタマイズする `Handoff` オブジェクトを指定することもできます。 +すべての エージェント には [`handoffs`][agents.agent.Agent.handoffs] パラメーターがあり、直接 `Agent` を渡すことも、ハンドオフをカスタマイズする `Handoff` オブジェクトを渡すこともできます。 -Agents SDK で提供されている [`handoff()`][agents.handoffs.handoff] 関数を使ってハンドオフを作成できます。この関数では、ハンドオフ先のエージェントや、オプションのオーバーライドや入力フィルターを指定できます。 +Agents SDK が提供する [`handoff()`][agents.handoffs.handoff] 関数を使ってハンドオフを作成できます。この関数では、引き継ぎ先の エージェント を指定し、オーバーライドや入力フィルターをオプションで設定できます。 ### 基本的な使い方 -シンプルなハンドオフの作成方法は以下の通りです。 +シンプルなハンドオフを作成する例を示します。 ```python from agents import Agent, handoff @@ -24,18 +28,18 @@ refund_agent = Agent(name="Refund agent") triage_agent = Agent(name="Triage agent", handoffs=[billing_agent, handoff(refund_agent)]) ``` -1. エージェントを直接指定する(例:`billing_agent`)ことも、`handoff()` 関数を使うこともできます。 +1. `billing_agent` のように エージェント を直接指定することも、`handoff()` 関数を使用することもできます。 ### `handoff()` 関数によるハンドオフのカスタマイズ -[`handoff()`][agents.handoffs.handoff] 関数では、さまざまなカスタマイズが可能です。 +[`handoff()`][agents.handoffs.handoff] 関数を使うと、ハンドオフを細かくカスタマイズできます。 -- `agent`: ハンドオフ先のエージェントです。 -- `tool_name_override`: デフォルトでは `Handoff.default_tool_name()` 関数が使われ、`transfer_to_` となります。これを上書きできます。 -- `tool_description_override`: デフォルトのツール説明(`Handoff.default_tool_description()`)を上書きできます。 -- `on_handoff`: ハンドオフが呼び出されたときに実行されるコールバック関数です。たとえば、ハンドオフが呼び出されたタイミングでデータ取得を開始するなどに便利です。この関数はエージェントコンテキストを受け取り、オプションで LLM が生成した入力も受け取れます。入力データは `input_type` パラメーターで制御します。 -- `input_type`: ハンドオフで期待される入力の型(オプション)です。 -- `input_filter`: 次のエージェントが受け取る入力をフィルタリングできます。詳細は下記をご覧ください。 +- `agent`: ここで指定した エージェント に処理が引き渡されます。 +- `tool_name_override`: デフォルトでは `Handoff.default_tool_name()` が使用され、`transfer_to_` という名前になります。これを上書きできます。 +- `tool_description_override`: `Handoff.default_tool_description()` が返すデフォルトのツール説明を上書きします。 +- `on_handoff`: ハンドオフ実行時に呼び出されるコールバック関数です。ハンドオフが呼ばれたタイミングでデータ取得を開始するなどに便利です。この関数は エージェント のコンテキストを受け取り、オプションで LLM が生成した入力も受け取れます。渡されるデータは `input_type` パラメーターで制御します。 +- `input_type`: ハンドオフが受け取る入力の型(任意)。 +- `input_filter`: 次の エージェント が受け取る入力をフィルタリングできます。詳細は後述します。 ```python from agents import Agent, handoff, RunContextWrapper @@ -55,7 +59,7 @@ handoff_obj = handoff( ## ハンドオフ入力 -状況によっては、LLM にハンドオフ時に何らかのデータを提供してほしい場合があります。たとえば、「エスカレーションエージェント」へのハンドオフを考えてみましょう。この場合、理由を提供して記録できるようにしたいことがあります。 +場合によっては、 LLM がハンドオフを呼び出す際に追加のデータを渡してほしいことがあります。たとえば「Escalation エージェント」へのハンドオフでは、ログ用に理由を渡してもらいたいかもしれません。 ```python from pydantic import BaseModel @@ -79,9 +83,9 @@ handoff_obj = handoff( ## 入力フィルター -ハンドオフが発生すると、新しいエージェントが会話を引き継ぎ、これまでの会話履歴全体を見ることができます。これを変更したい場合は、[`input_filter`][agents.handoffs.Handoff.input_filter] を設定できます。入力フィルターは、[`HandoffInputData`][agents.handoffs.HandoffInputData] を受け取り、新しい `HandoffInputData` を返す関数です。 +ハンドオフが発生すると、新しい エージェント が会話を引き継ぎ、これまでの会話履歴全体を閲覧できる状態になります。これを変更したい場合は [`input_filter`][agents.handoffs.Handoff.input_filter] を設定してください。入力フィルターは、[`HandoffInputData`][agents.handoffs.HandoffInputData] として渡される既存の入力を受け取り、新しい `HandoffInputData` を返す関数です。 -よくあるパターン(たとえば履歴からすべてのツール呼び出しを削除するなど)は、[`agents.extensions.handoff_filters`][] で実装されています。 +よくあるパターン(たとえば履歴からすべてのツール呼び出しを削除するなど)は [`agents.extensions.handoff_filters`][] に実装済みです。 ```python from agents import Agent, handoff @@ -95,11 +99,11 @@ handoff_obj = handoff( ) ``` -1. これにより、`FAQ agent` が呼び出されたときに履歴からすべてのツールが自動的に削除されます。 +1. これにより `FAQ agent` が呼ばれた際に、履歴からすべてのツール呼び出しが自動で削除されます。 ## 推奨プロンプト -LLM がハンドオフを正しく理解できるようにするため、エージェントにハンドオフに関する情報を含めることを推奨します。[`agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`][] で推奨されるプレフィックスを利用するか、[`agents.extensions.handoff_prompt.prompt_with_handoff_instructions`][] を呼び出して、推奨データをプロンプトに自動追加できます。 +LLM がハンドオフを正しく理解できるよう、エージェント にハンドオフに関する情報を含めることを推奨します。事前に用意したプレフィックス [`agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`][] を利用するか、[`agents.extensions.handoff_prompt.prompt_with_handoff_instructions`][] を呼び出してプロンプトに推奨情報を自動で追加できます。 ```python from agents import Agent diff --git a/docs/ja/index.md b/docs/ja/index.md index 3b306464..39692a16 100644 --- a/docs/ja/index.md +++ b/docs/ja/index.md @@ -1,28 +1,32 @@ +--- +search: + exclude: true +--- # OpenAI Agents SDK -[OpenAI Agents SDK](https://github.com/openai/openai-agents-python) は、非常に少ない抽象化で、軽量かつ使いやすいパッケージで エージェント 型 AI アプリを構築できる SDK です。これは、以前のエージェント向け実験プロジェクト [Swarm](https://github.com/openai/swarm/tree/main) の本番運用向けアップグレード版です。Agents SDK には、非常に少数の基本コンポーネントが含まれています。 +[OpenAI Agents SDK](https://github.com/openai/openai-agents-python) は、抽象化をほとんど排した軽量で使いやすいパッケージにより、エージェントベースの AI アプリを構築できるようにします。これは、以前のエージェント向け実験プロジェクトである [Swarm](https://github.com/openai/swarm/tree/main) をプロダクションレベルへとアップグレードしたものです。Agents SDK にはごく少数の基本コンポーネントがあります。 -- **エージェント**: instructions と tools を備えた LLM -- **ハンドオフ**:特定のタスクを他のエージェントに委任できる仕組み -- **ガードレール**:エージェントへの入力を検証できる仕組み +- **エージェント**: instructions と tools を備えた LLM +- **ハンドオフ**: エージェントが特定タスクを他のエージェントへ委任するしくみ +- **ガードレール**: エージェントへの入力を検証する機能 -Python と組み合わせることで、これらの基本コンポーネントは tools と エージェント 間の複雑な関係を表現でき、急な学習コストなしに実用的なアプリケーションを構築できます。さらに、SDK には組み込みの **トレーシング** 機能があり、エージェント フローの可視化やデバッグ、評価、さらにはアプリケーション向けモデルのファインチューニングも可能です。 +Python と組み合わせることで、これらのコンポーネントはツールとエージェント間の複雑な関係を表現でき、学習コストを抑えつつ実際のアプリケーションを構築できます。さらに SDK には、エージェントフローを可視化・デバッグできる **トレーシング** が標準搭載されており、評価やファインチューニングにも活用可能です。 -## なぜ Agents SDK を使うのか +## Agents SDK を使用する理由 SDK には 2 つの設計原則があります。 -1. 使う価値のある十分な機能を持ちつつ、学習が容易なほど少ない基本コンポーネントで構成されていること。 -2. すぐに使い始められるが、動作を細かくカスタマイズできること。 +1. 使う価値のある十分な機能を備えつつ、学習が早いようコンポーネント数を絞る。 +2. すぐに使い始められる初期設定で動作しつつ、挙動を細かくカスタマイズできる。 -SDK の主な特徴は以下の通りです。 +主な機能は次のとおりです。 -- エージェントループ: tools の呼び出し、LLM への実行結果の送信、LLM が完了するまでのループ処理を自動で行います。 -- Python ファースト:新しい抽象化を学ぶ必要なく、Python の言語機能でエージェントのオーケストレーションや連携が可能です。 -- ハンドオフ:複数のエージェント間での調整や委任を実現する強力な機能です。 -- ガードレール:エージェントと並行して入力検証やチェックを実行し、チェックに失敗した場合は早期に処理を中断します。 -- 関数ツール:任意の Python 関数を自動でスキーマ生成・Pydantic ベースのバリデーション付きツールに変換できます。 -- トレーシング:組み込みのトレーシングでワークフローの可視化・デバッグ・モニタリングができ、OpenAI の評価・ファインチューニング・蒸留ツールも利用可能です。 +- エージェントループ: ツール呼び出し、結果を LLM に送信、LLM が完了するまでのループを自動で処理。 +- Python ファースト: 新しい抽象化を学ばずに、言語標準機能でエージェントをオーケストレーション。 +- ハンドオフ: 複数エージェント間の協調と委譲を実現する強力な機能。 +- ガードレール: エージェントと並列で入力バリデーションを実行し、失敗時に早期終了。 +- 関数ツール: 任意の Python 関数をツール化し、自動スキーマ生成と Pydantic での検証を提供。 +- トレーシング: フローの可視化・デバッグ・モニタリングに加え、OpenAI の評価・ファインチューニング・蒸留ツールを利用可能。 ## インストール @@ -30,7 +34,7 @@ SDK の主な特徴は以下の通りです。 pip install openai-agents ``` -## Hello World サンプル +## Hello World の例 ```python from agents import Agent, Runner @@ -45,7 +49,7 @@ print(result.final_output) # Infinite loop's dance. ``` -(_実行する場合は、`OPENAI_API_KEY` 環境変数を設定してください_) +(_これを実行する場合は、`OPENAI_API_KEY` 環境変数を設定してください_) ```bash export OPENAI_API_KEY=sk-... diff --git a/docs/ja/mcp.md b/docs/ja/mcp.md index d93252fb..7cdaa57e 100644 --- a/docs/ja/mcp.md +++ b/docs/ja/mcp.md @@ -1,21 +1,25 @@ +--- +search: + exclude: true +--- # Model context protocol (MCP) -[Model context protocol](https://modelcontextprotocol.io/introduction)(通称 MCP)は、 LLM にツールやコンテキストを提供するための方法です。MCP ドキュメントからの引用です: +[Model context protocol](https://modelcontextprotocol.io/introduction)(通称 MCP)は、 LLM にツールとコンテキストを提供するための仕組みです。MCP のドキュメントでは次のように説明されています。 -> MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンプロトコルです。MCP を AI アプリケーションのための USB-C ポートのようなものと考えてください。USB-C がさまざまな周辺機器やアクセサリにデバイスを接続する標準的な方法を提供するのと同様に、MCP は AI モデルをさまざまなデータソースやツールに接続する標準的な方法を提供します。 +> MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンプロトコルです。MCP は AI アプリケーションにとっての USB‑C ポートのようなものと考えてください。USB‑C が各種デバイスを周辺機器と接続するための標準化された方法を提供するのと同様に、MCP は AI モデルをさまざまなデータソースやツールと接続するための標準化された方法を提供します。 -Agents SDK は MCP をサポートしています。これにより、幅広い MCP サーバーを利用して、エージェントにツールを提供することができます。 +Agents SDK は MCP をサポートしており、これにより幅広い MCP サーバーをエージェントにツールとして追加できます。 ## MCP サーバー -現在、MCP 仕様では、使用するトランスポートメカニズムに基づいて 2 種類のサーバーが定義されています: +現在、MCP 仕様では使用するトランスポート方式に基づき 2 種類のサーバーが定義されています。 -1. **stdio** サーバーは、アプリケーションのサブプロセスとして実行されます。ローカルで実行されるものと考えることができます。 -2. **HTTP over SSE** サーバーはリモートで実行されます。URL を介して接続します。 +1. **stdio** サーバー: アプリケーションのサブプロセスとして実行されます。ローカルで動かすイメージです。 +2. **HTTP over SSE** サーバー: リモートで動作し、 URL 経由で接続します。 -これらのサーバーに接続するには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] および [`MCPServerSse`][agents.mcp.server.MCPServerSse] クラスを使用できます。 +これらのサーバーへは [`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] クラスを使用して接続できます。 -例えば、[公式 MCP ファイルシステムサーバー](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem) を使用する場合は、次のようになります。 +たとえば、[公式 MCP filesystem サーバー](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem)を利用する場合は次のようになります。 ```python async with MCPServerStdio( @@ -29,7 +33,7 @@ async with MCPServerStdio( ## MCP サーバーの利用 -MCP サーバーはエージェントに追加できます。Agents SDK は、エージェントが実行されるたびに MCP サーバーの `list_tools()` を呼び出します。これにより、LLM は MCP サーバーのツールを認識できるようになります。LLM が MCP サーバーのツールを呼び出すと、SDK はそのサーバーの `call_tool()` を呼び出します。 +MCP サーバーはエージェントに追加できます。Agents SDK はエージェント実行時に毎回 MCP サーバーへ `list_tools()` を呼び出し、 LLM に MCP サーバーのツールを認識させます。LLM が MCP サーバーのツールを呼び出すと、SDK はそのサーバーへ `call_tool()` を実行します。 ```python @@ -42,19 +46,19 @@ agent=Agent( ## キャッシュ -エージェントが実行されるたびに、MCP サーバーの `list_tools()` が呼び出されます。特にサーバーがリモートの場合、これはレイテンシの原因となることがあります。ツールリストを自動的にキャッシュするには、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] および [`MCPServerSse`][agents.mcp.server.MCPServerSse] の両方に `cache_tools_list=True` を渡すことができます。ツールリストが変更されないことが確実な場合のみ、この設定を行ってください。 +エージェントが実行されるたびに、MCP サーバーへ `list_tools()` が呼び出されます。サーバーがリモートの場合は特にレイテンシが発生します。ツール一覧を自動でキャッシュしたい場合は、[`MCPServerStdio`][agents.mcp.server.MCPServerStdio] と [`MCPServerSse`][agents.mcp.server.MCPServerSse] の両方に `cache_tools_list=True` を渡してください。ツール一覧が変更されないと確信できる場合のみ使用してください。 -キャッシュを無効化したい場合は、サーバーで `invalidate_tools_cache()` を呼び出すことができます。 +キャッシュを無効化したい場合は、サーバーで `invalidate_tools_cache()` を呼び出します。 -## エンドツーエンドの code examples +## エンドツーエンドのコード例 -[examples/mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp) で、完全な動作 code examples をご覧いただけます。 +完全な動作例は [examples/mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp) をご覧ください。 ## トレーシング -[トレーシング](../tracing.md) は、以下を含む MCP の操作を自動的に記録します: +[トレーシング](./tracing.md) は MCP の操作を自動的にキャプチャします。具体的には次の内容が含まれます。 -1. MCP サーバーへのツールリスト取得の呼び出し -2. 関数呼び出しに関する MCP 関連情報 +1. ツール一覧取得のための MCP サーバー呼び出し +2. 関数呼び出しに関する MCP 情報 -![MCP トレーシングのスクリーンショット](../assets/images/mcp-tracing.jpg) \ No newline at end of file +![MCP Tracing Screenshot](../assets/images/mcp-tracing.jpg) \ No newline at end of file diff --git a/docs/ja/models/index.md b/docs/ja/models/index.md new file mode 100644 index 00000000..a40ae38f --- /dev/null +++ b/docs/ja/models/index.md @@ -0,0 +1,116 @@ +--- +search: + exclude: true +--- +# モデル + +Agents SDK には、標準で 2 種類の OpenAI モデルサポートが含まれています。 + +- **推奨**: [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] — 新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を利用して OpenAI API を呼び出します。 +- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] — [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を利用して OpenAI API を呼び出します。 + +## モデルの組み合わせ + +1 つのワークフロー内で、エージェントごとに異なるモデルを使用したい場合があります。たとえば、振り分けには小さく高速なモデルを、複雑なタスクには大きく高性能なモデルを使う、といった使い分けです。[`Agent`][agents.Agent] を設定する際は、以下のいずれかで特定のモデルを指定できます。 + +1. OpenAI モデル名を直接渡す +2. 任意のモデル名と、それを `Model` インスタンスへマッピングできる [`ModelProvider`][agents.models.interface.ModelProvider] を渡す +3. [`Model`][agents.models.interface.Model] 実装を直接渡す + +!!!note + SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形に対応していますが、ワークフローごとに 1 つのモデル形を使用することを推奨します。2 つの形ではサポートする機能・ツールが異なるためです。どうしても混在させる場合は、利用するすべての機能が両方で利用可能であることを確認してください。 + +```python +from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel +import asyncio + +spanish_agent = Agent( + name="Spanish agent", + instructions="You only speak Spanish.", + model="o3-mini", # (1)! +) + +english_agent = Agent( + name="English agent", + instructions="You only speak English", + model=OpenAIChatCompletionsModel( # (2)! + model="gpt-4o", + openai_client=AsyncOpenAI() + ), +) + +triage_agent = Agent( + name="Triage agent", + instructions="Handoff to the appropriate agent based on the language of the request.", + handoffs=[spanish_agent, english_agent], + model="gpt-3.5-turbo", +) + +async def main(): + result = await Runner.run(triage_agent, input="Hola, ¿cómo estás?") + print(result.final_output) +``` + +1. OpenAI モデル名を直接指定 +2. [`Model`][agents.models.interface.Model] 実装を提供 + +エージェントで使用するモデルをさらに細かく設定したい場合は、`temperature` などのオプションを指定できる [`ModelSettings`][agents.models.interface.ModelSettings] を渡します。 + +```python +from agents import Agent, ModelSettings + +english_agent = Agent( + name="English agent", + instructions="You only speak English", + model="gpt-4o", + model_settings=ModelSettings(temperature=0.1), +) +``` + +## 他の LLM プロバイダーの利用 + +他の LLM プロバイダーは 3 通りの方法で利用できます(コード例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/))。 + +1. [`set_default_openai_client`][agents.set_default_openai_client] + OpenAI 互換の API エンドポイントを持つ場合に、`AsyncOpenAI` インスタンスをグローバルに LLM クライアントとして設定できます。`base_url` と `api_key` を設定するケースです。設定例は [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)。 + +2. [`ModelProvider`][agents.models.interface.ModelProvider] + `Runner.run` レベルで「この実行中のすべてのエージェントにカスタムモデルプロバイダーを使う」と宣言できます。設定例は [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)。 + +3. [`Agent.model`][agents.agent.Agent.model] + 特定の Agent インスタンスにモデルを指定できます。エージェントごとに異なるプロバイダーを組み合わせられます。設定例は [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)。多くのモデルを簡単に使う方法として [LiteLLM 連携](./litellm.md) があります。 + +`platform.openai.com` の API キーを持たない場合は、`set_tracing_disabled()` でトレーシングを無効化するか、[別のトレーシングプロセッサー](../tracing.md) を設定することを推奨します。 + +!!! note + これらの例では Chat Completions API/モデルを使用しています。多くの LLM プロバイダーがまだ Responses API をサポートしていないためです。もしプロバイダーが Responses API をサポートしている場合は、Responses の使用を推奨します。 + +## 他の LLM プロバイダーでよくある問題 + +### Tracing クライアントの 401 エラー + +トレースは OpenAI サーバーへアップロードされるため、OpenAI API キーがない場合にエラーになります。解決策は次の 3 つです。 + +1. トレーシングを完全に無効化する: [`set_tracing_disabled(True)`][agents.set_tracing_disabled] +2. トレーシング用の OpenAI キーを設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key] + このキーはトレースのアップロードにのみ使用され、[platform.openai.com](https://platform.openai.com/) のものが必要です。 +3. OpenAI 以外のトレースプロセッサーを使う。詳しくは [tracing ドキュメント](../tracing.md#custom-tracing-processors) を参照してください。 + +### Responses API サポート + +SDK は既定で Responses API を使用しますが、多くの LLM プロバイダーはまだ対応していません。そのため 404 などのエラーが発生する場合があります。対処方法は 2 つです。 + +1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出す + 環境変数 `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に機能します。 +2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使用する + コード例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 + +### structured outputs のサポート + +一部のモデルプロバイダーは [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。その場合、次のようなエラーが発生することがあります。 + +``` +BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}} +``` + +これは一部プロバイダーの制限で、JSON 出力はサポートしていても `json_schema` を指定できません。現在修正に取り組んでいますが、JSON スキーマ出力をサポートしているプロバイダーを利用することを推奨します。そうでない場合、不正な JSON によりアプリが頻繁に壊れる可能性があります。 \ No newline at end of file diff --git a/docs/ja/models/litellm.md b/docs/ja/models/litellm.md new file mode 100644 index 00000000..651d7a51 --- /dev/null +++ b/docs/ja/models/litellm.md @@ -0,0 +1,77 @@ +--- +search: + exclude: true +--- +# LiteLLM 経由でのモデル利用 + +!!! note + + LiteLLM との統合は現在ベータ版です。特に小規模なモデルプロバイダーでは問題が発生する可能性があります。問題を見つけた場合は、[GitHub Issues](https://github.com/openai/openai-agents-python/issues) からご報告ください。迅速に対応いたします。 + +[LiteLLM](https://docs.litellm.ai/docs/) は、1 つのインターフェースで 100 以上のモデルを利用できるライブラリです。Agents SDK では LiteLLM との統合により、任意の AI モデルを使用できます。 + +## セットアップ + +`litellm` がインストールされていることを確認してください。オプションの `litellm` 依存関係グループをインストールすることで対応できます。 + +```bash +pip install "openai-agents[litellm]" +``` + +インストール後、任意のエージェントで [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] を利用できます。 + +## 例 + +以下は動作する完全なサンプルです。実行するとモデル名と API キーの入力を求められます。例えば次のように入力できます。 + +- `openai/gpt-4.1` をモデル名に、OpenAI API キーを入力 +- `anthropic/claude-3-5-sonnet-20240620` をモデル名に、Anthropic API キーを入力 +- その他 + +LiteLLM でサポートされているモデルの全リストは、[litellm providers docs](https://docs.litellm.ai/docs/providers) を参照してください。 + +```python +from __future__ import annotations + +import asyncio + +from agents import Agent, Runner, function_tool, set_tracing_disabled +from agents.extensions.models.litellm_model import LitellmModel + +@function_tool +def get_weather(city: str): + print(f"[debug] getting weather for {city}") + return f"The weather in {city} is sunny." + + +async def main(model: str, api_key: str): + agent = Agent( + name="Assistant", + instructions="You only respond in haikus.", + model=LitellmModel(model=model, api_key=api_key), + tools=[get_weather], + ) + + result = await Runner.run(agent, "What's the weather in Tokyo?") + print(result.final_output) + + +if __name__ == "__main__": + # First try to get model/api key from args + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--model", type=str, required=False) + parser.add_argument("--api-key", type=str, required=False) + args = parser.parse_args() + + model = args.model + if not model: + model = input("Enter a model name for Litellm: ") + + api_key = args.api_key + if not api_key: + api_key = input("Enter an API key for Litellm: ") + + asyncio.run(main(model, api_key)) +``` \ No newline at end of file diff --git a/docs/ja/multi_agent.md b/docs/ja/multi_agent.md index e5604f6f..a179fed3 100644 --- a/docs/ja/multi_agent.md +++ b/docs/ja/multi_agent.md @@ -1,37 +1,41 @@ -# 複数のエージェントのオーケストレーション +--- +search: + exclude: true +--- +# 複数エージェントのオーケストレーション -オーケストレーションとは、アプリ内でのエージェントの流れを指します。どのエージェントが、どの順番で実行され、次に何が起こるかをどのように決定するかということです。エージェントをオーケストレーションする主な方法は 2 つあります。 +オーケストレーションとは、アプリ内でエージェントがどのように流れるかを指します。どのエージェントが、どの順序で実行され、その後どう決定するかを制御します。エージェントをオーケストレーションする主な方法は次の 2 つです。 -1. LLM に意思決定を任せる方法:これは LLM の知能を活用し、計画・推論・意思決定を行わせるものです。 -2. コードによるオーケストレーション:コードでエージェントの流れを制御する方法です。 +1. LLM に判断させる: LLM の知能を活用し、計画・推論を行い、その結果に基づいて次のステップを決定します。 +2. コードでオーケストレーションする: コード側でエージェントの流れを定義します。 -これらのパターンは組み合わせて使うこともできます。それぞれにメリット・デメリットがあり、以下で説明します。 +これらのパターンは組み合わせて使用できます。それぞれにトレードオフがあり、以下で説明します。 ## LLM によるオーケストレーション -エージェントとは、instructions、tools、handoffs を備えた LLM です。つまり、オープンエンドなタスクが与えられた場合、LLM は自律的にタスクへの取り組み方を計画し、tools を使ってアクションを実行・データを取得し、handoffs を使ってサブエージェントにタスクを委任できます。たとえば、リサーチエージェントには以下のような tools を持たせることができます。 +エージェントとは、 instructions、ツール、ハンドオフを備えた LLM です。オープンエンドなタスクが与えられた場合、 LLM はタスクをどのように進めるかを自律的に計画し、ツールを使ってアクションやデータ取得を行い、ハンドオフでサブエージェントへタスクを委譲できます。たとえば、リサーチエージェントには次のようなツールを装備できます。 -- Web 検索でオンライン情報を探す -- ファイル検索やリトリーバルで独自データや接続先を検索する -- コンピュータ操作でコンピュータ上のアクションを実行する -- コード実行でデータ分析を行う -- 計画やレポート作成などに特化したエージェントへの handoffs +- Web 検索でオンライン情報を取得する +- ファイル検索で独自データや接続を調べる +- コンピュータ操作でコンピュータ上のアクションを実行する +- コード実行でデータ分析を行う +- 計画立案やレポート作成などに長けた専門エージェントへのハンドオフ -このパターンは、タスクがオープンエンドで LLM の知能に頼りたい場合に最適です。ここで重要なポイントは次のとおりです。 +このパターンはタスクがオープンエンドで、 LLM の知能に頼りたい場合に最適です。重要な戦術は次のとおりです。 -1. 良いプロンプトに投資しましょう。利用可能な tools、使い方、守るべきパラメーターを明確に伝えます。 -2. アプリをモニタリングし、改善を繰り返しましょう。問題が発生した箇所を確認し、プロンプトを改善します。 -3. エージェントに内省と改善を促しましょう。たとえばループで実行し、自己批評させたり、エラーメッセージを与えて改善させたりします。 -4. 何でもできる汎用エージェントではなく、1 つのタスクに特化したエージェントを用意しましょう。 -5. [evals](https://platform.openai.com/docs/guides/evals) に投資しましょう。これによりエージェントを訓練し、タスクの精度を向上させることができます。 +1. 良いプロンプトに投資する。利用可能なツール、使い方、守るべきパラメーターを明確に示します。 +2. アプリを監視し、改善を繰り返す。問題が起きた箇所を特定し、プロンプトを改善します。 +3. エージェントに内省と改善を許可する。たとえばループで実行し自己批評させたり、エラーメッセージを渡して修正させたりします。 +4. 何でもこなす汎用エージェントより、特定タスクに特化したエージェントを用意します。 +5. [evals](https://platform.openai.com/docs/guides/evals) に投資する。これによりエージェントを訓練し、タスク性能を向上できます。 ## コードによるオーケストレーション -LLM によるオーケストレーションは強力ですが、コードによるオーケストレーションは、速度・コスト・パフォーマンスの面でより決定的かつ予測可能になります。よく使われるパターンは次のとおりです。 +LLM によるオーケストレーションは強力ですが、コードでオーケストレーションすると速度・コスト・性能の面でより決定的かつ予測可能になります。よく使われるパターンは次のとおりです。 -- [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使い、コードで検査できる適切な形式のデータを生成する。たとえば、エージェントにタスクをいくつかのカテゴリーに分類させ、そのカテゴリーに応じて次のエージェントを選択することができます。 -- 複数のエージェントを連鎖させ、一方の出力を次の入力に変換する。たとえば、ブログ記事の作成タスクを「リサーチ→アウトライン作成→記事執筆→批評→改善」といった一連のステップに分解できます。 -- タスクを実行するエージェントと、評価・フィードバックを行うエージェントを `while` ループで回し、評価者が基準を満たしたと判断するまで繰り返します。 -- 複数のエージェントを並列で実行する(例:Python の基本コンポーネントである `asyncio.gather` を利用)。これは、互いに依存しない複数のタスクを高速に処理したい場合に有効です。 +- [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) を使って、コード側で検査できる 適切な形式のデータ を生成する。たとえばエージェントにタスクをいくつかのカテゴリーに分類させ、そのカテゴリーに応じて次のエージェントを選択します。 +- あるエージェントの出力を次のエージェントの入力に変換して複数エージェントをチェーンする。ブログ記事執筆を「リサーチ → アウトライン作成 → 記事執筆 → 批評 → 改善」という一連のステップに分解できます。 +- タスクを実行するエージェントを `while` ループで回し、評価とフィードバックを行うエージェントと組み合わせ、評価者が基準を満たしたと判断するまで繰り返します。 +- `asyncio.gather` など Python の基本コンポーネントを用いて複数エージェントを並列実行する。互いに依存しない複数タスクがある場合に高速化できます。 -[`examples/agent_patterns`](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) には、さまざまな code examples をご用意しています。 \ No newline at end of file +[`examples/agent_patterns`](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) には多数のコード例があります。 \ No newline at end of file diff --git a/docs/ja/quickstart.md b/docs/ja/quickstart.md index d45b7aa4..30c3219d 100644 --- a/docs/ja/quickstart.md +++ b/docs/ja/quickstart.md @@ -1,8 +1,12 @@ +--- +search: + exclude: true +--- # クイックスタート ## プロジェクトと仮想環境の作成 -この作業は一度だけ行えば十分です。 +これは一度だけ行えば十分です。 ```bash mkdir my_project @@ -12,7 +16,7 @@ python -m venv .venv ### 仮想環境の有効化 -新しいターミナルセッションを開始するたびに、これを実行してください。 +新しいターミナルセッションを開始するたびに実行してください。 ```bash source .venv/bin/activate @@ -32,9 +36,9 @@ pip install openai-agents # or `uv add openai-agents`, etc export OPENAI_API_KEY=sk-... ``` -## 最初のエージェントを作成する +## 最初のエージェントの作成 -エージェントは instructions、名前、およびオプションの設定(例: `model_config` )で定義します。 +エージェントは instructions 、名前、`model_config` などのオプション設定で定義します。 ```python from agents import Agent @@ -45,9 +49,9 @@ agent = Agent( ) ``` -## さらにエージェントを追加する +## さらにエージェントを追加 -追加のエージェントも同様の方法で定義できます。 `handoff_descriptions` は、ハンドオフのルーティングを判断するための追加コンテキストを提供します。 +追加のエージェントも同様の方法で定義できます。`handoff_descriptions` はハンドオフのルーティングを判断するための追加コンテキストを提供します。 ```python from agents import Agent @@ -65,9 +69,9 @@ math_tutor_agent = Agent( ) ``` -## ハンドオフを定義する +## ハンドオフの定義 -各エージェントで、タスクを進めるために選択できる送信先ハンドオフオプションのインベントリを定義できます。 +各エージェントに対して、タスクを進める際に選択できるハンドオフ先の一覧を定義できます。 ```python triage_agent = Agent( @@ -77,9 +81,9 @@ triage_agent = Agent( ) ``` -## エージェントのオーケストレーションを実行する +## エージェントオーケストレーションの実行 -ワークフローが正しく動作し、トリアージエージェントが 2 つの専門エージェント間で正しくルーティングするか確認しましょう。 +ワークフローが実行され、トリアージエージェントが 2 つの専門エージェント間で正しくルーティングすることを確認しましょう。 ```python from agents import Runner @@ -89,9 +93,9 @@ async def main(): print(result.final_output) ``` -## ガードレールを追加する +## ガードレールの追加 -入力または出力に対して実行するカスタムガードレールを定義できます。 +入力または出力に対して実行されるカスタムガードレールを定義できます。 ```python from agents import GuardrailFunctionOutput, Agent, Runner @@ -118,7 +122,7 @@ async def homework_guardrail(ctx, agent, input_data): ## すべてをまとめる -すべてを組み合わせて、ハンドオフと入力ガードレールを使ったワークフロー全体を実行してみましょう。 +ハンドオフと入力ガードレールを組み合わせて、ワークフロー全体を実行してみましょう。 ```python from agents import Agent, InputGuardrail, GuardrailFunctionOutput, Runner @@ -176,14 +180,14 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## トレースを確認する +## トレースの表示 -エージェントの実行中に何が起こったかを確認するには、[OpenAI ダッシュボードの Trace viewer](https://platform.openai.com/traces) にアクセスして、エージェント実行のトレースを表示してください。 +エージェントの実行内容を確認するには、[OpenAI ダッシュボードの Trace viewer](https://platform.openai.com/traces) に移動してトレースを閲覧してください。 ## 次のステップ -より複雑なエージェントフローの構築方法を学びましょう: +より複雑なエージェントフローの構築方法を学びましょう。 -- [エージェント](agents.md) の設定方法について学ぶ -- [エージェントの実行](running_agents.md) について学ぶ -- [tools](tools.md)、[guardrails](guardrails.md)、[models](models/index.md) について学ぶ +- [エージェント](agents.md) の設定方法を学ぶ。 +- [エージェントの実行](running_agents.md) について学ぶ。 +- [ツール](tools.md)、[ガードレール](guardrails.md)、[モデル](models/index.md) について学ぶ。 \ No newline at end of file diff --git a/docs/ja/results.md b/docs/ja/results.md index 6b2c0ccc..cedd9240 100644 --- a/docs/ja/results.md +++ b/docs/ja/results.md @@ -1,52 +1,56 @@ +--- +search: + exclude: true +--- # 結果 -`Runner.run` メソッドを呼び出すと、以下のいずれかが返されます: +`Runner.run` メソッドを呼び出すと、以下のいずれかが返されます。 -- `run` または `run_sync` を呼び出した場合は、[`RunResult`][agents.result.RunResult] -- `run_streamed` を呼び出した場合は、[`RunResultStreaming`][agents.result.RunResultStreaming] +- `run` または `run_sync` を呼び出した場合は [`RunResult`][agents.result.RunResult] +- `run_streamed` を呼び出した場合は [`RunResultStreaming`][agents.result.RunResultStreaming] -これらはどちらも [`RunResultBase`][agents.result.RunResultBase] を継承しており、ほとんどの有用な情報はここに含まれています。 +これらはどちらも [`RunResultBase`][agents.result.RunResultBase] を継承しており、ほとんどの有用な情報はここに格納されています。 ## 最終出力 -[`final_output`][agents.result.RunResultBase.final_output] プロパティには、最後に実行されたエージェントの最終出力が格納されます。これは以下のいずれかです: +[`final_output`][agents.result.RunResultBase.final_output] プロパティには、最後に実行されたエージェントの最終出力が格納されます。内容は以下のいずれかです。 -- 最後のエージェントに `output_type` が定義されていない場合は `str` -- エージェントに `output_type` が定義されている場合は、`last_agent.output_type` 型のオブジェクト +- `output_type` が定義されていない場合は `str` +- `output_type` が定義されている場合は `last_agent.output_type` 型のオブジェクト !!! note - `final_output` の型は `Any` です。ハンドオフのため、静的に型を決定することはできません。ハンドオフが発生した場合、どのエージェントが最後になるか分からないため、出力型の集合を静的に知ることができません。 + `final_output` の型は `Any` です。ハンドオフが発生する可能性があるため、静的に型付けできません。ハンドオフが発生すると、どのエージェントでも最後になり得るため、可能性のある出力型を静的に特定できないのです。 ## 次のターンへの入力 -[`result.to_input_list()`][agents.result.RunResultBase.to_input_list] を使うことで、実行結果を入力リストに変換できます。これは、あなたが提供した元の入力と、エージェント実行中に生成されたアイテムを連結したものです。これにより、あるエージェント実行の出力を別の実行に渡したり、ループで実行して毎回新しいユーザー入力を追加したりするのが簡単になります。 +[`result.to_input_list()`][agents.result.RunResultBase.to_input_list] を使用すると、エージェント実行中に生成されたアイテムを元の入力に連結した入力リストへ変換できます。これにより、あるエージェント実行の出力を別の実行へ渡したり、ループで実行して毎回新しいユーザー入力を追加したりすることが容易になります。 ## 最後のエージェント -[`last_agent`][agents.result.RunResultBase.last_agent] プロパティには、最後に実行されたエージェントが格納されます。アプリケーションによっては、次回ユーザーが何か入力した際にこれが役立つことがよくあります。たとえば、フロントラインのトリアージエージェントが言語特化エージェントにハンドオフする場合、最後のエージェントを保存しておき、次回ユーザーがエージェントにメッセージを送る際に再利用できます。 +[`last_agent`][agents.result.RunResultBase.last_agent] プロパティには、最後に実行されたエージェントが格納されています。アプリケーションによっては、次回ユーザーが入力する際にこれが役立つことがよくあります。例えば、フロントラインのトリアージ エージェントが言語専用のエージェントにハンドオフする場合、最後のエージェントを保存しておき、ユーザーが次にメッセージを送ったときに再利用できます。 ## 新しいアイテム -[`new_items`][agents.result.RunResultBase.new_items] プロパティには、実行中に生成された新しいアイテムが格納されます。これらのアイテムは [`RunItem`][agents.items.RunItem] です。RunItem は LLM によって生成された raw アイテムをラップします。 +[`new_items`][agents.result.RunResultBase.new_items] プロパティには、実行中に生成された新しいアイテムが含まれます。これらのアイテムは [`RunItem`][agents.items.RunItem] です。RunItem は、 LLM が生成した raw アイテムをラップします。 -- [`MessageOutputItem`][agents.items.MessageOutputItem] は LLM からのメッセージを示します。raw アイテムは生成されたメッセージです。 -- [`HandoffCallItem`][agents.items.HandoffCallItem] は LLM がハンドオフツールを呼び出したことを示します。raw アイテムは LLM からのツールコールアイテムです。 -- [`HandoffOutputItem`][agents.items.HandoffOutputItem] はハンドオフが発生したことを示します。raw アイテムはハンドオフツールコールへのツールレスポンスです。また、アイテムからソース/ターゲットエージェントにもアクセスできます。 -- [`ToolCallItem`][agents.items.ToolCallItem] は LLM がツールを呼び出したことを示します。 -- [`ToolCallOutputItem`][agents.items.ToolCallOutputItem] はツールが呼び出されたことを示します。raw アイテムはツールレスポンスです。また、アイテムからツール出力にもアクセスできます。 -- [`ReasoningItem`][agents.items.ReasoningItem] は LLM からの推論アイテムを示します。raw アイテムは生成された推論です。 +- [`MessageOutputItem`][agents.items.MessageOutputItem] — LLM からのメッセージを示します。 raw アイテムは生成されたメッセージです。 +- [`HandoffCallItem`][agents.items.HandoffCallItem] — LLM がハンドオフ ツールを呼び出したことを示します。 raw アイテムは LLM からのツール呼び出しアイテムです。 +- [`HandoffOutputItem`][agents.items.HandoffOutputItem] — ハンドオフが発生したことを示します。 raw アイテムはハンドオフ ツール呼び出しに対するツール応答です。また、アイテムから送信元 / 送信先エージェントにもアクセスできます。 +- [`ToolCallItem`][agents.items.ToolCallItem] — LLM がツールを呼び出したことを示します。 +- [`ToolCallOutputItem`][agents.items.ToolCallOutputItem] — ツールが呼び出されたことを示します。 raw アイテムはツール応答です。また、アイテムからツール出力にもアクセスできます。 +- [`ReasoningItem`][agents.items.ReasoningItem] — LLM からの推論アイテムを示します。 raw アイテムは生成された推論内容です。 ## その他の情報 ### ガードレール結果 -[`input_guardrail_results`][agents.result.RunResultBase.input_guardrail_results] および [`output_guardrail_results`][agents.result.RunResultBase.output_guardrail_results] プロパティには、ガードレールの結果(存在する場合)が格納されます。ガードレール結果には、ログや保存に役立つ有用な情報が含まれることがあるため、これらを利用できるようにしています。 +[`input_guardrail_results`][agents.result.RunResultBase.input_guardrail_results] と [`output_guardrail_results`][agents.result.RunResultBase.output_guardrail_results] プロパティには、ガードレールの結果が存在する場合に格納されます。ガードレール結果には、ログや保存を行いたい有用な情報が含まれることがあるため、これらを参照できるようにしています。 ### raw レスポンス -[`raw_responses`][agents.result.RunResultBase.raw_responses] プロパティには、LLM によって生成された [`ModelResponse`][agents.items.ModelResponse] が格納されます。 +[`raw_responses`][agents.result.RunResultBase.raw_responses] プロパティには、 LLM が生成した [`ModelResponse`][agents.items.ModelResponse] が格納されます。 ### 元の入力 -[`input`][agents.result.RunResultBase.input] プロパティには、`run` メソッドに提供した元の入力が格納されます。ほとんどの場合これは必要ありませんが、必要な場合のために利用可能です。 \ No newline at end of file +[`input`][agents.result.RunResultBase.input] プロパティには、`run` メソッドに渡した元の入力が格納されます。ほとんどの場合は必要ありませんが、必要に応じて参照できるように用意されています。 \ No newline at end of file diff --git a/docs/ja/running_agents.md b/docs/ja/running_agents.md index 1a56fcc7..83d5bec6 100644 --- a/docs/ja/running_agents.md +++ b/docs/ja/running_agents.md @@ -1,10 +1,15 @@ +--- +search: + exclude: true +--- # エージェントの実行 -エージェントは [`Runner`][agents.run.Runner] クラスを使って実行できます。3 つのオプションがあります。 +`Runner` クラス [`Runner`][agents.run.Runner] を使用して エージェント を実行できます。方法は 3 つあります。 -1. [`Runner.run()`][agents.run.Runner.run]:非同期で実行され、[`RunResult`][agents.result.RunResult] を返します。 -2. [`Runner.run_sync()`][agents.run.Runner.run_sync]:同期メソッドで、内部的には `.run()` を実行します。 -3. [`Runner.run_streamed()`][agents.run.Runner.run_streamed]:非同期で実行され、[`RunResultStreaming`][agents.result.RunResultStreaming] を返します。LLM をストリーミングモードで呼び出し、受信したイベントをリアルタイムでストリームします。 +1. 非同期で実行し、[`RunResult`][agents.result.RunResult] を返す [`Runner.run()`][agents.run.Runner.run] +2. 同期メソッドで、内部的には `.run()` を呼び出す [`Runner.run_sync()`][agents.run.Runner.run_sync] +3. 非同期で実行し、[`RunResultStreaming`][agents.result.RunResultStreaming] を返す [`Runner.run_streamed()`][agents.run.Runner.run_streamed] + LLM をストリーミングモードで呼び出し、受信したイベントを逐次 ストリーミング します。 ```python from agents import Agent, Runner @@ -19,53 +24,52 @@ async def main(): # Infinite loop's dance. ``` -詳細は [results guide](results.md) をご覧ください。 +詳細は [結果ガイド](results.md) を参照してください。 ## エージェントループ -`Runner` の run メソッドを使用する際、開始するエージェントと入力を渡します。入力は文字列(ユーザーメッセージと見なされます)または入力アイテムのリスト(OpenAI Responses API のアイテム)を指定できます。 +`Runner` の run メソッドを使用する際は、開始 エージェント と入力を渡します。入力は文字列(ユーザー メッセージと見なされます)または入力項目のリスト(OpenAI Responses API の項目)です。 -runner は次のようなループを実行します。 +Runner は以下のループを実行します。 -1. 現在のエージェントと入力で LLM を呼び出します。 -2. LLM が出力を生成します。 - 1. LLM が `final_output` を返した場合、ループは終了し、結果を返します。 - 2. LLM がハンドオフを行った場合、現在のエージェントと入力を更新し、ループを再実行します。 - 3. LLM がツール呼び出しを生成した場合、それらのツール呼び出しを実行し、結果を追加してループを再実行します。 -3. 渡された `max_turns` を超えた場合、[`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] 例外を発生させます。 +1. 現在の エージェント と現在の入力で LLM を呼び出します。 +2. LLM が出力を生成します。 + 1. `final_output` が返された場合、ループを終了して結果を返します。 + 2. ハンドオフ が発生した場合、現在の エージェント と入力を更新し、ループを再実行します。 + 3. ツール呼び出し がある場合、それらを実行し、結果を追加してループを再実行します。 +3. 指定した `max_turns` を超えた場合、[`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded] 例外を送出します。 !!! note - - LLM の出力が「final output」と見なされるルールは、希望する型のテキスト出力が生成され、ツール呼び出しがない場合です。 + LLM の出力が「最終出力」と見なされる条件は、望ましい型のテキスト出力であり、ツール呼び出しがないことです。 ## ストリーミング -ストリーミングを利用すると、LLM の実行中にストリーミングイベントを受け取ることができます。ストリームが完了すると、[`RunResultStreaming`][agents.result.RunResultStreaming] に実行に関するすべての情報(新たに生成されたすべての出力を含む)が格納されます。ストリーミングイベントは `.stream_events()` で取得できます。詳細は [streaming guide](streaming.md) をご覧ください。 +ストリーミング を使用すると、LLM の実行中に ストリーミング イベントを受け取れます。ストリーム完了後、[`RunResultStreaming`][agents.result.RunResultStreaming] には実行に関する完全な情報(新しく生成されたすべての出力を含む)が格納されます。`.stream_events()` を呼び出して ストリーミング イベントを取得できます。詳しくは [ストリーミングガイド](streaming.md) をご覧ください。 -## Run config +## Run 設定 -`run_config` パラメーターでは、エージェント実行のグローバル設定をいくつか構成できます。 +`run_config` パラメーターにより、エージェント実行のグローバル設定を行えます。 -- [`model`][agents.run.RunConfig.model]:各エージェントの `model` 設定に関わらず、グローバルで使用する LLM モデルを指定できます。 -- [`model_provider`][agents.run.RunConfig.model_provider]:モデル名を検索するためのモデルプロバイダーで、デフォルトは OpenAI です。 -- [`model_settings`][agents.run.RunConfig.model_settings]:エージェント固有の設定を上書きします。たとえば、グローバルな `temperature` や `top_p` を設定できます。 -- [`input_guardrails`][agents.run.RunConfig.input_guardrails], [`output_guardrails`][agents.run.RunConfig.output_guardrails]:すべての実行に含める入力または出力ガードレールのリストです。 -- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]:すべてのハンドオフに適用するグローバルな入力フィルターです(ハンドオフに既にフィルターがない場合)。入力フィルターを使うと、新しいエージェントに送信する入力を編集できます。詳細は [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] のドキュメントをご覧ください。 -- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]:実行全体の [トレーシング](tracing.md) を無効にできます。 -- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]:トレースに LLM やツール呼び出しの入出力など、機密性のあるデータを含めるかどうかを設定します。 -- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]:実行のトレーシングワークフロー名、トレース ID、トレースグループ ID を設定します。少なくとも `workflow_name` の設定を推奨します。グループ ID は複数の実行にまたがるトレースをリンクするためのオプション項目です。 -- [`trace_metadata`][agents.run.RunConfig.trace_metadata]:すべてのトレースに含めるメタデータです。 +- [`model`][agents.run.RunConfig.model]: 各 Agent の `model` 設定に関わらず使用するグローバル LLM モデルを指定します。 +- [`model_provider`][agents.run.RunConfig.model_provider]: モデル名を解決する モデルプロバイダー。デフォルトは OpenAI です。 +- [`model_settings`][agents.run.RunConfig.model_settings]: エージェント固有設定を上書きします。例としてグローバル `temperature` や `top_p` の設定など。 +- [`input_guardrails`][agents.run.RunConfig.input_guardrails], [`output_guardrails`][agents.run.RunConfig.output_guardrails]: すべての実行に適用する入力 / 出力 ガードレール のリスト。 +- [`handoff_input_filter`][agents.run.RunConfig.handoff_input_filter]: ハンドオフ に入力フィルターが設定されていない場合に適用されるグローバル入力フィルター。新しい エージェント へ送信される入力を編集できます。詳細は [`Handoff.input_filter`][agents.handoffs.Handoff.input_filter] を参照してください。 +- [`tracing_disabled`][agents.run.RunConfig.tracing_disabled]: 実行全体の [トレーシング](tracing.md) を無効化します。 +- [`trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]: トレースに LLM やツール呼び出しの入出力など、機微なデータを含めるかどうかを設定します。 +- [`workflow_name`][agents.run.RunConfig.workflow_name], [`trace_id`][agents.run.RunConfig.trace_id], [`group_id`][agents.run.RunConfig.group_id]: 実行のトレーシング ワークフロー名、トレース ID、トレース グループ ID を設定します。少なくとも `workflow_name` の設定を推奨します。`group_id` を設定すると、複数の実行にまたがるトレースをリンクできます。 +- [`trace_metadata`][agents.run.RunConfig.trace_metadata]: すべてのトレースに付与するメタデータ。 -## 会話・チャットスレッド +## 会話 / チャットスレッド -いずれかの run メソッドを呼び出すと、1 つまたは複数のエージェント(および 1 つまたは複数の LLM 呼び出し)が実行されますが、チャット会話における 1 回の論理的なターンを表します。例: +いずれかの run メソッドを呼び出すと、1 つ以上の エージェント が実行され(つまり 1 つ以上の LLM 呼び出しが行われ)、チャット会話の 1 つの論理ターンを表します。例: -1. ユーザーのターン:ユーザーがテキストを入力 -2. Runner の実行:最初のエージェントが LLM を呼び出し、ツールを実行し、2 番目のエージェントにハンドオフ、2 番目のエージェントがさらにツールを実行し、出力を生成 +1. ユーザーターン: ユーザー がテキストを入力 +2. Runner 実行: 最初の エージェント が LLM を呼び出し、ツールを実行し、2 番目の エージェント へハンドオフ。2 番目の エージェント がさらにツールを実行し、最終出力を生成。 -エージェントの実行が終わったら、ユーザーに何を表示するか選択できます。たとえば、エージェントが生成したすべての新しいアイテムをユーザーに見せることも、最終出力だけを見せることもできます。いずれの場合も、ユーザーが追加の質問をした場合は、再度 run メソッドを呼び出せます。 +エージェント実行の終了時に、ユーザー に何を表示するかは自由です。たとえば、エージェント が生成したすべての新しい項目を表示する、または最終出力のみを表示する等です。いずれの場合でも、ユーザー がフォローアップ質問をしたら、再度 run メソッドを呼び出せます。 -次のターンの入力を取得するには、ベースの [`RunResultBase.to_input_list()`][agents.result.RunResultBase.to_input_list] メソッドを使用できます。 +次ターンの入力は、基底クラス [`RunResultBase.to_input_list()`][agents.result.RunResultBase.to_input_list] を使用して取得できます。 ```python async def main(): @@ -86,10 +90,10 @@ async def main(): ## 例外 -SDK は特定のケースで例外を発生させます。全リストは [`agents.exceptions`][] にあります。概要は以下の通りです。 +特定の状況で SDK は例外を送出します。完全な一覧は [`agents.exceptions`][] にあります。概要は以下のとおりです。 -- [`AgentsException`][agents.exceptions.AgentsException]:SDK で発生するすべての例外の基底クラスです。 -- [`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded]:run メソッドに渡した `max_turns` を超えた場合に発生します。 -- [`ModelBehaviorError`][agents.exceptions.ModelBehaviorError]:モデルが不正な出力(例:不正な JSON や存在しないツールの使用)を生成した場合に発生します。 -- [`UserError`][agents.exceptions.UserError]:SDK を使用する際に、あなた(SDK を使ってコードを書く人)がエラーを起こした場合に発生します。 -- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered], [`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]:[ガードレール](guardrails.md) が作動した場合に発生します。 \ No newline at end of file +- [`AgentsException`][agents.exceptions.AgentsException]: SDK が送出するすべての例外の基底クラス +- [`MaxTurnsExceeded`][agents.exceptions.MaxTurnsExceeded]: 実行が `max_turns` を超えた場合に送出 +- [`ModelBehaviorError`][agents.exceptions.ModelBehaviorError]: モデルが不正な出力(例: JSON 形式違反、存在しないツールの呼び出しなど)を生成した場合に送出 +- [`UserError`][agents.exceptions.UserError]: SDK の使用方法に誤りがある場合に送出 +- [`InputGuardrailTripwireTriggered`][agents.exceptions.InputGuardrailTripwireTriggered], [`OutputGuardrailTripwireTriggered`][agents.exceptions.OutputGuardrailTripwireTriggered]: [ガードレール](guardrails.md) が発火した場合に送出 \ No newline at end of file diff --git a/docs/ja/streaming.md b/docs/ja/streaming.md index b820bae5..a8a46cca 100644 --- a/docs/ja/streaming.md +++ b/docs/ja/streaming.md @@ -1,14 +1,18 @@ +--- +search: + exclude: true +--- # ストリーミング -ストリーミングを利用すると、エージェントの実行が進行するにつれて、その更新情報を購読できます。これは、エンドユーザーに進捗状況や部分的な応答を表示するのに役立ちます。 +ストリーミングを使用すると、 エージェント の実行が進行するにつれて発生する更新を購読できます。これにより、エンド ユーザーに進捗状況や部分的な応答を表示するのに役立ちます。 -ストリーミングを行うには、[`Runner.run_streamed()`][agents.run.Runner.run_streamed] を呼び出します。これにより、[`RunResultStreaming`][agents.result.RunResultStreaming] が返されます。`result.stream_events()` を呼び出すと、下記で説明する [`StreamEvent`][agents.stream_events.StreamEvent] オブジェクトの非同期ストリームが得られます。 +ストリーミングを行うには、 [`Runner.run_streamed()`][agents.run.Runner.run_streamed] を呼び出します。これにより [`RunResultStreaming`][agents.result.RunResultStreaming] が返されます。続いて `result.stream_events()` を呼び出すと、後述する [`StreamEvent`][agents.stream_events.StreamEvent] オブジェクトの非同期ストリームを取得できます。 -## raw response events +## raw response イベント -[`RawResponsesStreamEvent`][agents.stream_events.RawResponsesStreamEvent] は、LLM から直接渡される raw イベントです。これらは OpenAI Responses API フォーマットであり、各イベントには type(例:`response.created`、`response.output_text.delta` など)とデータが含まれます。これらのイベントは、応答メッセージが生成され次第、ユーザーにストリーミングしたい場合に便利です。 +[`RawResponsesStreamEvent`][agents.stream_events.RawResponsesStreamEvent] は、 LLM から直接渡される raw なイベントです。これらは OpenAI Responses API 形式であり、各イベントには `response.created` や `response.output_text.delta` などの type とデータが含まれます。生成されたメッセージを即座にユーザーへストリーミングしたい場合に便利です。 -例えば、以下の例では LLM が生成したテキストをトークンごとに出力します。 +たとえば、以下のコードは LLM が生成したテキストをトークンごとに出力します。 ```python import asyncio @@ -31,11 +35,11 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## Run item イベントとエージェントイベント +## Run item イベントと エージェント イベント -[`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent] は、より高レベルなイベントです。アイテムが完全に生成されたタイミングを通知します。これにより、「メッセージが生成された」「ツールが実行された」など、各トークン単位ではなく、進捗状況をまとめてユーザーに伝えることができます。同様に、[`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] は、現在のエージェントが変更されたとき(例:ハンドオフの結果として)に更新情報を提供します。 +[`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent] は、より高レベルなイベントです。アイテムが完全に生成されたタイミングを通知するため、トークン単位ではなく「メッセージが生成された」「ツールが実行された」といったレベルで進捗をプッシュできます。同様に、 [`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] はハンドオフなどで現在の エージェント が変わった際に更新を提供します。 -例えば、以下の例では raw イベントを無視し、ユーザーに更新情報のみをストリーミングします。 +たとえば、以下のコードは raw イベントを無視し、ユーザーへ更新のみをストリーミングします。 ```python import asyncio diff --git a/docs/ja/tools.md b/docs/ja/tools.md index 2444891c..7ab15e47 100644 --- a/docs/ja/tools.md +++ b/docs/ja/tools.md @@ -1,18 +1,22 @@ +--- +search: + exclude: true +--- # ツール -ツールは エージェント がアクションを実行するためのものです。たとえば、データの取得、コードの実行、外部 API の呼び出し、さらにはコンピュータ操作などが含まれます。Agents SDK には 3 種類のツールクラスがあります。 +ツールはエージェントがアクションを実行できるようにします。たとえばデータの取得、コードの実行、外部 API の呼び出し、さらにはコンピュータ操作などです。Agents SDK には次の 3 種類のツールがあります。 -- Hosted tools: これらは LLM サーバー上で AI モデルとともに実行されます。OpenAI は retrieval、Web 検索、コンピュータ操作を Hosted tools として提供しています。 -- Function calling: 任意の Python 関数をツールとして利用できます。 -- Agents as tools: エージェントをツールとして利用でき、エージェントが他のエージェントをハンドオフせずに呼び出すことができます。 +- ホストツール: これらは LLM サーバー上で AI モデルと一緒に実行されます。OpenAI は retrieval、Web 検索、コンピュータ操作をホストツールとして提供しています。 +- 関数呼び出し: 任意の Python 関数をツールとして利用できます。 +- ツールとしてのエージェント: ハンドオフせずに、エージェントから他のエージェントを呼び出すことができます。 -## Hosted tools +## ホストツール OpenAI は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] を使用する際に、いくつかの組み込みツールを提供しています。 -- [`WebSearchTool`][agents.tool.WebSearchTool] は、エージェントが Web 検索を行うことを可能にします。 -- [`FileSearchTool`][agents.tool.FileSearchTool] は、OpenAI ベクトルストアから情報を取得できます。 -- [`ComputerTool`][agents.tool.ComputerTool] は、コンピュータ操作タスクの自動化を可能にします。 +- [`WebSearchTool`][agents.tool.WebSearchTool] はエージェントに Web 検索を行わせます。 +- [`FileSearchTool`][agents.tool.FileSearchTool] は OpenAI ベクトルストアから情報を取得します。 +- [`ComputerTool`][agents.tool.ComputerTool] はコンピュータ操作タスクを自動化します。 ```python from agents import Agent, FileSearchTool, Runner, WebSearchTool @@ -33,16 +37,16 @@ async def main(): print(result.final_output) ``` -## Function tools +## 関数ツール -任意の Python 関数をツールとして利用できます。Agents SDK が自動的にツールをセットアップします。 +任意の Python 関数をツールとして使用できます。Agents SDK が自動的に設定を行います。 -- ツール名は Python 関数名になります(または任意の名前を指定できます) -- ツールの説明は関数の docstring から取得されます(または任意の説明を指定できます) -- 関数の引数から自動的に入力スキーマが作成されます -- 各入力の説明は、関数の docstring から取得されます(無効化も可能です) +- ツールの名前は Python 関数の名前になります(任意で名前を指定することも可能です) +- ツールの説明は関数の docstring から取得されます(任意で説明を指定することも可能です) +- 関数の引数から自動的に入力スキーマを生成します +- 各入力の説明は、無効化しない限り docstring から取得されます -Python の `inspect` モジュールを使って関数シグネチャを抽出し、[`griffe`](https://mkdocstrings.github.io/griffe/) で docstring を解析し、`pydantic` でスキーマを作成します。 +Python の `inspect` モジュールを使用して関数シグネチャを抽出し、[`griffe`](https://mkdocstrings.github.io/griffe/) で docstring を解析し、`pydantic` でスキーマを作成します。 ```python import json @@ -94,12 +98,12 @@ for tool in agent.tools: ``` -1. 関数の引数には任意の Python 型を利用でき、同期・非同期どちらの関数も利用可能です。 -2. docstring があれば、説明や引数の説明として利用されます。 -3. 関数はオプションで `context` を最初の引数として受け取れます。また、ツール名や説明、docstring スタイルの指定などのオーバーライドも可能です。 -4. デコレートした関数をツールのリストに渡すことができます。 +1. 関数の引数には任意の Python 型を使用でき、同期・非同期どちらの関数も利用できます。 +2. docstring が存在する場合、ツールと引数の説明を取得します。 +3. 関数はオプションで `context` を受け取れます(最初の引数である必要があります)。ツール名、説明、docstring のスタイルなどを上書き設定することも可能です。 +4. デコレートされた関数をツールのリストに渡してください。 -??? note "出力を展開して表示" +??? note "展開して出力を確認" ``` fetch_weather @@ -169,14 +173,14 @@ for tool in agent.tools: } ``` -### カスタム function tools +### カスタム関数ツール -場合によっては、Python 関数をツールとして使いたくないこともあります。その場合は、[`FunctionTool`][agents.tool.FunctionTool] を直接作成できます。必要な情報は以下の通りです。 +Python 関数をそのままツールにしたくない場合は、[`FunctionTool`][agents.tool.FunctionTool] を直接作成できます。次を指定する必要があります。 - `name` - `description` - `params_json_schema`(引数の JSON スキーマ) -- `on_invoke_tool`(context と引数(JSON 文字列)を受け取り、ツールの出力を文字列で返す async 関数) +- `on_invoke_tool`(context と引数の JSON 文字列を受け取り、ツールの出力を文字列で返す async 関数) ```python from typing import Any @@ -211,16 +215,16 @@ tool = FunctionTool( ### 引数と docstring の自動解析 -前述の通り、関数シグネチャを自動解析してツールのスキーマを抽出し、docstring からツールや各引数の説明を抽出します。主なポイントは以下の通りです。 +前述のとおり、関数シグネチャを自動解析してツールのスキーマを生成し、docstring を解析してツールおよび個別引数の説明を抽出します。主な注意点は次のとおりです。 -1. シグネチャの解析は `inspect` モジュールで行います。型アノテーションを利用して引数の型を把握し、Pydantic モデルを動的に構築して全体のスキーマを表現します。Python の基本コンポーネント、Pydantic モデル、TypedDict など、ほとんどの型をサポートしています。 -2. docstring の解析には `griffe` を使用します。サポートされている docstring フォーマットは `google`、`sphinx`、`numpy` です。docstring フォーマットは自動検出を試みますが、`function_tool` 呼び出し時に明示的に指定することもできます。`use_docstring_info` を `False` に設定することで docstring 解析を無効化できます。 +1. シグネチャ解析は `inspect` モジュールで行います。型アノテーションを用いて引数の型を認識し、Pydantic モデルを動的に構築して全体のスキーマを表現します。Python の基本型、Pydantic モデル、TypedDict などほとんどの型をサポートします。 +2. `griffe` を使用して docstring を解析します。対応する docstring 形式は `google`、`sphinx`、`numpy` です。形式は自動検出を試みますが、`function_tool` 呼び出し時に明示的に指定することもできます。`use_docstring_info` を `False` に設定すると docstring 解析を無効化できます。 スキーマ抽出のコードは [`agents.function_schema`][] にあります。 -## Agents as tools +## ツールとしてのエージェント -一部のワークフローでは、ハンドオフせずに中央のエージェントが専門エージェントのネットワークをオーケストレーションしたい場合があります。その場合、エージェントをツールとしてモデル化することで実現できます。 +一部のワークフローでは、ハンドオフせずに中央のエージェントが複数の専門エージェントをオーケストレーションしたい場合があります。そのような場合、エージェントをツールとしてモデル化できます。 ```python from agents import Agent, Runner @@ -259,12 +263,33 @@ async def main(): print(result.final_output) ``` -## Function tools でのエラー処理 +### ツールエージェントのカスタマイズ -`@function_tool` で function tool を作成する際、`failure_error_function` を渡すことができます。これは、ツール呼び出しがクラッシュした場合に LLM へエラー応答を提供する関数です。 +`agent.as_tool` 関数はエージェントを簡単にツール化するためのヘルパーです。ただし、すべての設定に対応しているわけではありません(例: `max_turns` は設定不可)。高度なユースケースでは、ツール実装内で `Runner.run` を直接使用してください。 -- デフォルト(何も渡さない場合)では、`default_tool_error_function` が実行され、LLM にエラーが発生したことを伝えます。 -- 独自のエラー関数を渡した場合は、それが実行され、その応答が LLM に送信されます。 -- 明示的に `None` を渡した場合、ツール呼び出し時のエラーは再スローされ、ユーザー側で処理できます。たとえば、モデルが無効な JSON を生成した場合は `ModelBehaviorError`、コードがクラッシュした場合は `UserError` などです。 +```python +@function_tool +async def run_my_agent() -> str: + """A tool that runs the agent with custom configs". + + agent = Agent(name="My agent", instructions="...") + + result = await Runner.run( + agent, + input="...", + max_turns=5, + run_config=... + ) + + return str(result.final_output) +``` + +## 関数ツールでのエラー処理 + +`@function_tool` で関数ツールを作成する際、`failure_error_function` を渡せます。これはツール呼び出しが失敗した場合に LLM へ返すエラーレスポンスを生成する関数です。 + +- 何も指定しない場合、`default_tool_error_function` が実行され、LLM にエラー発生を伝えます。 +- 独自のエラー関数を渡した場合はそちらが実行され、そのレスポンスが LLM へ送信されます。 +- 明示的に `None` を渡すと、ツール呼び出し時のエラーは再送出されます。モデルが無効な JSON を生成した場合は `ModelBehaviorError`、コードがクラッシュした場合は `UserError` などになります。 -`FunctionTool` オブジェクトを手動で作成する場合は、`on_invoke_tool` 関数内でエラー処理を行う必要があります。 \ No newline at end of file +`FunctionTool` オブジェクトを手動で作成する場合は、`on_invoke_tool` 関数内でエラーを処理する必要があります。 \ No newline at end of file diff --git a/docs/ja/tracing.md b/docs/ja/tracing.md index b7cb18dc..0e0d0e77 100644 --- a/docs/ja/tracing.md +++ b/docs/ja/tracing.md @@ -1,51 +1,55 @@ +--- +search: + exclude: true +--- # トレーシング -Agents SDK にはトレーシング機能が組み込まれており、エージェントの実行中に発生するイベント( LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、カスタムイベントなど)の包括的な記録を収集します。[Traces ダッシュボード](https://platform.openai.com/traces) を使用することで、開発中や本番環境でワークフローのデバッグ、可視化、監視が可能です。 +Agents SDK にはビルトインのトレーシング機能があり、エージェントの実行中に発生するイベント―― LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、さらにカスタムイベントまで――を網羅的に記録します。開発時と本番環境の両方で [Traces dashboard](https://platform.openai.com/traces) を使用すると、ワークフローをデバッグ・可視化・モニタリングできます。 !!!note - トレーシングはデフォルトで有効になっています。トレーシングを無効にする方法は 2 つあります: + トレーシングはデフォルトで有効です。無効化する方法は次の 2 つです: - 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定することで、グローバルにトレーシングを無効化できます。 - 2. 単一の実行に対しては [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定することで無効化できます。 + 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定してグローバルに無効化する + 2. 単一の実行に対しては [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定する -***OpenAI の API を使用し、Zero Data Retention (ZDR) ポリシーの下で運用している組織では、トレーシングは利用できません。*** +***OpenAI の API を Zero Data Retention (ZDR) ポリシーで利用している組織では、トレーシングを利用できません。*** ## トレースとスパン -- **トレース** は 1 つの「ワークフロー」のエンドツーエンドの操作を表します。トレースはスパンで構成されます。トレースには以下のプロパティがあります: - - `workflow_name`: 論理的なワークフローやアプリ名です。例: "Code generation" や "Customer service" など。 - - `trace_id`: トレースの一意な ID です。指定しない場合は自動生成されます。フォーマットは `trace_<32_alphanumeric>` である必要があります。 - - `group_id`: オプションのグループ ID で、同じ会話からの複数のトレースをリンクするために使用します。例: チャットスレッド ID など。 - - `disabled`: True の場合、このトレースは記録されません。 - - `metadata`: トレースに付加するオプションのメタデータです。 -- **スパン** は開始時刻と終了時刻を持つ操作を表します。スパンには以下があります: - - `started_at` および `ended_at` タイムスタンプ - - 所属するトレースを示す `trace_id` - - このスパンの親スパンを指す `parent_id`(存在する場合) - - スパンに関する情報を含む `span_data`。例: `AgentSpanData` はエージェントに関する情報、`GenerationSpanData` は LLM 生成に関する情報など。 +- **トレース** は 1 度のワークフロー全体を表します。複数のスパンで構成され、次のプロパティを持ちます: + - `workflow_name`: 論理的なワークフローまたはアプリ名。例: 「Code generation」や「Customer service」 + - `trace_id`: トレースを一意に識別する ID。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 + - `group_id`: オプションのグループ ID。会話内の複数トレースを関連付けます。たとえばチャットスレッド ID など。 + - `disabled`: `True` の場合、このトレースは記録されません。 + - `metadata`: トレースに付随する任意のメタデータ。 +- **スパン** は開始時刻と終了時刻を持つ個々の処理を表します。スパンは以下を保持します: + - `started_at` と `ended_at` タイムスタンプ + - 所属トレースを示す `trace_id` + - 親スパンを指す `parent_id` (存在する場合) + - スパンに関する情報を格納する `span_data`。たとえば `AgentSpanData` にはエージェント情報が、`GenerationSpanData` には LLM 生成情報が含まれます。 ## デフォルトのトレーシング -デフォルトでは、 SDK は以下をトレースします: +デフォルトで SDK は以下をトレースします: -- `Runner.{run, run_sync, run_streamed}()` 全体が `trace()` でラップされます。 -- エージェントが実行されるたびに `agent_span()` でラップされます。 -- LLM 生成は `generation_span()` でラップされます。 -- 関数ツール呼び出しはそれぞれ `function_span()` でラップされます。 -- ガードレールは `guardrail_span()` でラップされます。 -- ハンドオフは `handoff_span()` でラップされます。 -- 音声入力(音声からテキスト)は `transcription_span()` でラップされます。 -- 音声出力(テキストから音声)は `speech_span()` でラップされます。 -- 関連する音声スパンは `speech_group_span()` の下にまとめられる場合があります。 +- `Runner.{run, run_sync, run_streamed}()` 全体を `trace()` でラップ +- エージェントが実行されるたびに `agent_span()` でラップ +- LLM 生成を `generation_span()` でラップ +- 関数ツール呼び出しを `function_span()` でラップ +- ガードレールを `guardrail_span()` でラップ +- ハンドオフを `handoff_span()` でラップ +- 音声入力 (speech‑to‑text) を `transcription_span()` でラップ +- 音声出力 (text‑to‑speech) を `speech_span()` でラップ +- 関連する音声スパンは `speech_group_span()` の下にネストされる場合があります -デフォルトでは、トレース名は "Agent trace" です。`trace` を使用する場合、この名前を設定できます。また、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定することも可能です。 +トレース名はデフォルトで「Agent trace」です。`trace` を使用して指定したり、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定できます。 -さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを他の宛先に送信することもできます(置き換えや追加の宛先として)。 +さらに [カスタムトレーシングプロセッサー](#custom-tracing-processors) を設定して、トレースを別の送信先に出力(置き換えまたは追加)することも可能です。 -## より高レベルのトレース +## 上位レベルのトレース -複数回の `run()` 呼び出しを 1 つのトレースにまとめたい場合があります。その場合、コード全体を `trace()` でラップしてください。 +複数回の `run()` 呼び出しを 1 つのトレースにまとめたい場合があります。その場合、コード全体を `trace()` でラップします。 ```python from agents import Agent, Runner, trace @@ -60,48 +64,50 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. 2 回の `Runner.run` 呼び出しが `with trace()` でラップされているため、個々の実行は 2 つのトレースを作成するのではなく、全体のトレースの一部となります。 +1. `with trace()` で 2 つの `Runner.run` 呼び出しをラップしているため、それぞれが個別のトレースを作成せず、全体で 1 つのトレースになります。 ## トレースの作成 -[`trace()`][agents.tracing.trace] 関数を使ってトレースを作成できます。トレースは開始と終了が必要です。方法は 2 つあります: +[`trace()`][agents.tracing.trace] 関数を使ってトレースを作成できます。開始と終了が必要で、方法は 2 つあります。 -1. **推奨**: トレースをコンテキストマネージャーとして使用します。例: `with trace(...) as my_trace`。これにより、トレースの開始と終了が自動的に行われます。 -2. [`trace.start()`][agents.tracing.Trace.start] および [`trace.finish()`][agents.tracing.Trace.finish] を手動で呼び出すこともできます。 +1. **推奨**: `with trace(...) as my_trace` のようにコンテキストマネージャーとして使用する。開始と終了が自動で行われます。 +2. [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を手動で呼び出す。 -現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で管理されています。これにより、自動的に並行処理にも対応します。トレースを手動で開始・終了する場合は、`start()`/`finish()` に `mark_as_current` および `reset_current` を渡して現在のトレースを更新する必要があります。 +現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で管理されているため、並行処理でも自動で機能します。手動で開始/終了する場合は `start()`/`finish()` に `mark_as_current` と `reset_current` を渡して現在のトレースを更新してください。 ## スパンの作成 -さまざまな [`*_span()`][agents.tracing.create] メソッドを使ってスパンを作成できます。通常、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数も用意されています。 +各種 [`*_span()`][agents.tracing.create] メソッドでスパンを作成できます。一般的には手動で作成する必要はありません。カスタム情報を追跡するための [`custom_span()`][agents.tracing.custom_span] も利用できます。 -スパンは自動的に現在のトレースの一部となり、最も近い現在のスパンの下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で管理されています。 +スパンは自動的に現在のトレースの一部となり、最も近い現在のスパンの下にネストされます。これも Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で管理されています。 -## 機微なデータ +## 機密データ -一部のスパンは、機微なデータを記録する場合があります。 +一部のスパンでは機密データが収集される可能性があります。 -`generation_span()` は LLM 生成の入力・出力を保存し、`function_span()` は関数呼び出しの入力・出力を保存します。これらには機微なデータが含まれる場合があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] でそのデータの記録を無効化できます。 +`generation_span()` には LLM の入力と出力、`function_span()` には関数呼び出しの入力と出力が保存されます。これらに機密データが含まれる場合、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] を使用して記録を無効化できます。 -同様に、音声スパンはデフォルトで入力・出力音声の base64 エンコード PCM データを含みます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定することで、この音声データの記録を無効化できます。 +同様に、音声スパンにはデフォルトで base64 エンコードされた PCM 音声データが含まれます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定して音声データの記録を無効化できます。 -## カスタムトレースプロセッサー +## カスタムトレーシングプロセッサー -トレーシングの高レベルなアーキテクチャは以下の通りです: +トレーシングの高レベル構成は次のとおりです。 -- 初期化時にグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成し、トレースの生成を担当します。 -- `TraceProvider` は [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] で構成されており、トレースやスパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信します。これにより、スパンやトレースが OpenAI バックエンドにバッチでエクスポートされます。 +- 初期化時にグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成し、トレースを生成。 +- `TraceProvider` は [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] を用いてスパン/トレースをバッチ送信し、[`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] が OpenAI バックエンドへバッチでエクスポートします。 -このデフォルト設定をカスタマイズし、トレースを別のバックエンドや追加のバックエンドに送信したり、エクスポーターの動作を変更したりするには、2 つの方法があります: +デフォルト設定を変更して別のバックエンドへ送信したり、エクスポーターの挙動を修正するには次の 2 通りがあります。 -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] を使うと、**追加の** トレースプロセッサーを追加できます。これにより、トレースやスパンが準備できた時点で独自の処理を行うことができ、OpenAI バックエンドへの送信に加えて独自の処理が可能です。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] を使うと、デフォルトのプロセッサーを独自のトレースプロセッサーに**置き換える**ことができます。この場合、OpenAI バックエンドにトレースが送信されるのは、`TracingProcessor` を含めた場合のみです。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] + 既定の送信に加え、**追加** のトレースプロセッサーを登録できます。これにより OpenAI バックエンドへの送信に加えて独自処理が可能です。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] + 既定のプロセッサーを置き換え、**独自** のトレースプロセッサーだけを使用します。OpenAI バックエンドへ送信する場合は、その機能を持つ `TracingProcessor` を含める必要があります。 -## 外部トレースプロセッサー一覧 +## 外部トレーシングプロセッサー一覧 - [Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents) -- [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) -- [MLflow (self-hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) +- [Arize‑Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) +- [MLflow (self‑hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) - [MLflow (Databricks hosted](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) - [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) - [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents) @@ -113,4 +119,4 @@ async def main(): - [Comet Opik](https://www.comet.com/docs/opik/tracing/integrations/openai_agents) - [Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents) - [Langtrace](https://docs.langtrace.ai/supported-integrations/llm-frameworks/openai-agents-sdk) -- [Okahu-Monocle](https://github.com/monocle2ai/monocle) \ No newline at end of file +- [Okahu‑Monocle](https://github.com/monocle2ai/monocle) \ No newline at end of file diff --git a/docs/ja/visualization.md b/docs/ja/visualization.md index c93b3836..3c5c64ec 100644 --- a/docs/ja/visualization.md +++ b/docs/ja/visualization.md @@ -1,10 +1,14 @@ +--- +search: + exclude: true +--- # エージェントの可視化 -エージェントの可視化では、 **Graphviz** を使用してエージェントとその関係性を構造的にグラフィカルに表現できます。これは、アプリケーション内でエージェント、ツール、ハンドオフがどのように相互作用しているかを理解するのに役立ちます。 +エージェントの可視化を使用すると、 ** Graphviz ** を用いてエージェントとその関係を構造化されたグラフィカル表現として生成できます。これは、アプリケーション内でエージェント、ツール、handoffs がどのように相互作用するかを理解するのに役立ちます。 ## インストール -オプションの `viz` 依存グループをインストールします: +オプションの `viz` 依存関係グループをインストールします: ```bash pip install "openai-agents[viz]" @@ -12,11 +16,11 @@ pip install "openai-agents[viz]" ## グラフの生成 -`draw_graph` 関数を使ってエージェントの可視化を生成できます。この関数は、以下のような有向グラフを作成します: +`draw_graph` 関数を使用してエージェントの可視化を生成できます。この関数は有向グラフを作成し、以下のように表現します。 - **エージェント** は黄色のボックスで表されます。 - **ツール** は緑色の楕円で表されます。 -- **ハンドオフ** は、あるエージェントから別のエージェントへの有向エッジで表されます。 +- **handoffs** はエージェント間の有向エッジで示されます。 ### 使用例 @@ -50,32 +54,31 @@ draw_graph(triage_agent) ![Agent Graph](../assets/images/graph.png) -これにより、 **triage agent** の構造とサブエージェントやツールとの接続が視覚的に表現されたグラフが生成されます。 - +これにより、 **triage agent** の構造と、それがサブエージェントやツールとどのようにつながっているかを視覚的に表すグラフが生成されます。 ## 可視化の理解 -生成されたグラフには以下が含まれます: +生成されたグラフには次の要素が含まれます。 - エントリーポイントを示す **start node** (`__start__`) -- 黄色で塗りつぶされた **長方形** で表されるエージェント -- 緑色で塗りつぶされた **楕円** で表されるツール -- 相互作用を示す有向エッジ: - - エージェント間のハンドオフには **実線の矢印** - - ツール呼び出しには **点線の矢印** -- 実行が終了する場所を示す **end node** (`__end__`) +- 黄色の塗りつぶしを持つ **矩形** のエージェント +- 緑色の塗りつぶしを持つ **楕円** のツール +- 相互作用を示す有向エッジ + - エージェント間の handoffs には **実線の矢印** + - ツール呼び出しには **破線の矢印** +- 実行が終了する位置を示す **end node** (`__end__`) ## グラフのカスタマイズ ### グラフの表示 -デフォルトでは、`draw_graph` はグラフをインラインで表示します。グラフを別ウィンドウで表示したい場合は、次のように記述します: +デフォルトでは、`draw_graph` はグラフをインラインで表示します。別ウィンドウでグラフを表示するには、次のように記述します。 ```python draw_graph(triage_agent).view() ``` ### グラフの保存 -デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存したい場合は、ファイル名を指定します: +デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存するには、ファイル名を指定します: ```python draw_graph(triage_agent, filename="agent_graph.png") diff --git a/docs/ja/voice/pipeline.md b/docs/ja/voice/pipeline.md index 50661076..d52fb77f 100644 --- a/docs/ja/voice/pipeline.md +++ b/docs/ja/voice/pipeline.md @@ -1,6 +1,10 @@ -# パイプラインとワークフロー +--- +search: + exclude: true +--- +# パイプラインと ワークフロー -[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] は、エージェントワークフローを音声アプリに簡単に変換できるクラスです。実行したいワークフローを渡すと、パイプラインが入力音声の文字起こし、音声終了の検出、適切なタイミングでのワークフロー呼び出し、ワークフロー出力の音声化までを自動で処理します。 +[`VoicePipeline`][agents.voice.pipeline.VoicePipeline] は、エージェント的なワークフローを音声アプリに簡単に変換できるクラスです。ワークフローを渡すと、パイプラインが入力音声の文字起こし、音声終了の検知、適切なタイミングでのワークフロー呼び出し、そしてワークフロー出力を音声へ変換する処理を担当します。 ```mermaid graph LR @@ -30,29 +34,31 @@ graph LR ## パイプラインの設定 -パイプラインを作成する際、以下の項目を設定できます。 +パイプラインを作成する際に、以下を設定できます。 -1. [`workflow`][agents.voice.workflow.VoiceWorkflowBase]:新しい音声が文字起こしされるたびに実行されるコードです。 +1. [`workflow`][agents.voice.workflow.VoiceWorkflowBase] ‐ 新しい音声が文字起こしされるたびに実行されるコード 2. 使用する [`speech-to-text`][agents.voice.model.STTModel] および [`text-to-speech`][agents.voice.model.TTSModel] モデル -3. [`config`][agents.voice.pipeline_config.VoicePipelineConfig]:以下のような設定が可能です。 - - モデルプロバイダー:モデル名をモデルにマッピングできます - - トレーシング:トレーシングの有効/無効、音声ファイルのアップロード有無、ワークフロー名、トレース ID など - - TTS および STT モデルの設定:プロンプト、言語、使用するデータ型など +3. [`config`][agents.voice.pipeline_config.VoicePipelineConfig] ‐ 以下のような内容を設定可能 + - モデルプロバイダー。モデル名をモデルにマッピングします + - トレーシング。トレーシングの無効化、音声ファイルのアップロード可否、ワークフロー名、トレース ID など + - TTS と STT モデルの設定。プロンプト、言語、使用するデータ型など ## パイプラインの実行 -パイプラインは [`run()`][agents.voice.pipeline.VoicePipeline.run] メソッドで実行できます。音声入力は 2 つの形式で渡せます。 +パイプラインは [`run()`][agents.voice.pipeline.VoicePipeline.run] メソッドで実行できます。音声入力は次の 2 形式で渡せます。 -1. [`AudioInput`][agents.voice.input.AudioInput]:完全な音声トランスクリプトがある場合に使用し、その内容に対する結果のみを生成します。話者が話し終えたタイミングを検出する必要がない場合(例:事前録音音声や push-to-talk アプリなど、ユーザーが話し終えたことが明確な場合)に便利です。 -2. [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput]:ユーザーが話し終えたタイミングを検出する必要がある場合に使用します。音声チャンクを検出ごとにプッシュでき、VoicePipeline が「アクティビティ検出」と呼ばれるプロセスを通じて、適切なタイミングでエージェントワークフローを自動実行します。 +1. [`AudioInput`][agents.voice.input.AudioInput] + 完全な音声トランスクリプトがある場合に使用し、その結果だけを生成したいときに便利です。話者の発話終了を検知する必要がないケース、たとえば録音済み音声やプッシュトゥートーク型アプリのようにユーザーが話し終えたタイミングが明確な場合に向いています。 +2. [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] + ユーザーの発話終了検知が必要な場合に使用します。検出された音声チャンクを順次プッシュでき、音声パイプラインが「アクティビティ検知」と呼ばれるプロセスを通じて適切なタイミングでエージェント ワークフローを自動的に実行します。 ## 結果 -VoicePipeline 実行の結果は [`StreamedAudioResult`][agents.voice.result.StreamedAudioResult] です。これは、イベントが発生するたびにストリーミングで受け取れるオブジェクトです。いくつかの種類の [`VoiceStreamEvent`][agents.voice.events.VoiceStreamEvent] があります。 +音声パイプライン実行の結果は [`StreamedAudioResult`][agents.voice.result.StreamedAudioResult] です。これは発生したイベントをストリーミングで受け取れるオブジェクトです。いくつかの [`VoiceStreamEvent`][agents.voice.events.VoiceStreamEvent] があり、主なものは次のとおりです。 -1. [`VoiceStreamEventAudio`][agents.voice.events.VoiceStreamEventAudio]:音声チャンクを含みます。 -2. [`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle]:ターンの開始や終了など、ライフサイクルイベントを通知します。 -3. [`VoiceStreamEventError`][agents.voice.events.VoiceStreamEventError]:エラーイベントです。 +1. [`VoiceStreamEventAudio`][agents.voice.events.VoiceStreamEventAudio] ‐ 音声チャンクを含みます +2. [`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] ‐ ターンの開始や終了などのライフサイクルイベントを通知します +3. [`VoiceStreamEventError`][agents.voice.events.VoiceStreamEventError] ‐ エラーイベントです ```python @@ -72,4 +78,4 @@ async for event in result.stream(): ### 割り込み -Agents SDK は現在、[`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] に対する組み込みの割り込みサポートを提供していません。そのため、検出された各ターンごとにワークフローの個別実行がトリガーされます。アプリケーション内で割り込みを処理したい場合は、[`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] イベントをリッスンできます。`turn_started` は新しいターンが文字起こしされ、処理が開始されたことを示します。`turn_ended` は該当ターンのすべての音声が送信された後にトリガーされます。これらのイベントを利用して、モデルがターンを開始した際に話者のマイクをミュートし、ターンに関連するすべての音声を送信し終えた後にアンミュートする、といった制御が可能です。 \ No newline at end of file +Agents SDK は現在 [`StreamedAudioInput`][agents.voice.input.StreamedAudioInput] に対して、組み込みの割り込み処理をサポートしていません。そのため、検出された各ターンごとにワークフローが個別に実行されます。アプリケーション内で割り込みを処理したい場合は、[`VoiceStreamEventLifecycle`][agents.voice.events.VoiceStreamEventLifecycle] イベントを監視できます。`turn_started` は新しいターンが文字起こしされ、処理が開始されたことを示します。`turn_ended` は該当ターンのすべての音声が送信された後にトリガーされます。たとえば、モデルがターンを開始した際にスピーカーのマイクをミュートし、そのターンに関連する音声をすべて送信し終えた後にアンミュートするといった制御に、これらのイベントを利用できます。 \ No newline at end of file diff --git a/docs/ja/voice/quickstart.md b/docs/ja/voice/quickstart.md index 9c874020..291b9882 100644 --- a/docs/ja/voice/quickstart.md +++ b/docs/ja/voice/quickstart.md @@ -1,8 +1,12 @@ +--- +search: + exclude: true +--- # クイックスタート ## 前提条件 -Agents SDK の[クイックスタート手順](../quickstart.md)に従い、仮想環境をセットアップしてください。その後、SDK からオプションの音声依存関係をインストールします。 +まずは [クイックスタート手順](../quickstart.md) に従って Agents SDK をセットアップし、仮想環境を作成してください。その後、SDK の音声関連のオプション依存関係をインストールします: ```bash pip install 'openai-agents[voice]' @@ -10,11 +14,11 @@ pip install 'openai-agents[voice]' ## コンセプト -主なコンセプトは [`VoicePipeline`][agents.voice.pipeline.VoicePipeline] です。これは 3 ステップのプロセスです: +押さえておくべき主な概念は [`VoicePipeline`][agents.voice.pipeline.VoicePipeline] です。これは次の 3 ステップから成るプロセスです。 -1. 音声をテキストに変換する音声認識モデル(speech-to-text)を実行します。 -2. 通常はエージェント的なワークフローであるあなたのコードを実行し、結果を生成します。 -3. 結果のテキストを音声に戻す音声合成モデル(text-to-speech)を実行します。 +1. speech-to-text モデルを実行して音声をテキストに変換します。 +2. 通常はエージェント的ワークフローであるあなたのコードを実行し、結果を生成します。 +3. text-to-speech モデルを実行して結果のテキストを再び音声に変換します。 ```mermaid graph LR @@ -44,7 +48,7 @@ graph LR ## エージェント -まず、いくつかのエージェントをセットアップしましょう。この SDK でエージェントを作成したことがあれば、馴染みがあるはずです。ここでは、複数のエージェント、ハンドオフ、ツールを用意します。 +まず、いくつかの エージェント をセットアップしましょう。この SDK でエージェントを構築したことがあれば、見覚えがあるはずです。ここでは複数の エージェント、ハンドオフ、そしてツールを用意します。 ```python import asyncio @@ -88,7 +92,7 @@ agent = Agent( ## 音声パイプライン -[`SingleAgentVoiceWorkflow`][agents.voice.workflow.SingleAgentVoiceWorkflow] をワークフローとして使い、シンプルな音声パイプラインをセットアップします。 +[`SingleAgentVoiceWorkflow`][agents.voice.workflow.SingleAgentVoiceWorkflow] をワークフローとして、シンプルな音声パイプラインを構築します。 ```python from agents.voice import SingleAgentVoiceWorkflow, VoicePipeline @@ -120,7 +124,7 @@ async for event in result.stream(): ``` -## すべてをまとめる +## まとめて実行 ```python import asyncio @@ -191,4 +195,4 @@ if __name__ == "__main__": asyncio.run(main()) ``` -この例を実行すると、エージェントがあなたに話しかけます![examples/voice/static](https://github.com/openai/openai-agents-python/tree/main/examples/voice/static) のコード例をチェックして、実際にエージェントと会話できるデモをご覧ください。 \ No newline at end of file +この例を実行すると、エージェントがあなたに話しかけます。実際にエージェントと会話できるデモは、[examples/voice/static](https://github.com/openai/openai-agents-python/tree/main/examples/voice/static) をご覧ください。 \ No newline at end of file diff --git a/docs/ja/voice/tracing.md b/docs/ja/voice/tracing.md index 639a95f2..21f4788f 100644 --- a/docs/ja/voice/tracing.md +++ b/docs/ja/voice/tracing.md @@ -1,14 +1,18 @@ +--- +search: + exclude: true +--- # トレーシング -[エージェントのトレーシング](../tracing.md)と同様に、音声パイプラインも自動的にトレーシングされます。 +[エージェントのトレーシング](../tracing.md) と同様に、音声パイプラインも自動的にトレーシングされます。 -基本的なトレーシング情報については上記のトレーシングドキュメントをご参照いただけますが、さらに [`VoicePipelineConfig`][agents.voice.pipeline_config.VoicePipelineConfig] を使ってパイプラインのトレーシングを設定することも可能です。 +基本的なトレーシング情報については上記のドキュメントを参照してください。さらに、[`VoicePipelineConfig`][agents.voice.pipeline_config.VoicePipelineConfig] でパイプラインのトレーシング設定を行えます。 -主なトレーシング関連フィールドは以下の通りです: +主なトレーシング関連フィールドは次のとおりです。 -- [`tracing_disabled`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレーシングを無効にするかどうかを制御します。デフォルトではトレーシングは有効です。 -- [`trace_include_sensitive_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_data]:トレースに音声の書き起こしなど、機密性の高いデータを含めるかどうかを制御します。これは特に音声パイプライン用であり、Workflow 内部で発生する内容には適用されません。 -- [`trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]:トレースに音声データを含めるかどうかを制御します。 -- [`workflow_name`][agents.voice.pipeline_config.VoicePipelineConfig.workflow_name]:トレースワークフローの名前です。 -- [`group_id`][agents.voice.pipeline_config.VoicePipelineConfig.group_id]:トレースの `group_id` で、複数のトレースをリンクすることができます。 +- [`tracing_disabled`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレーシングを無効にするかどうかを制御します。デフォルトではトレーシングは有効です。 +- [`trace_include_sensitive_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_data]:トレースに音声テキストなどの機微なデータを含めるかどうかを制御します。これは音声パイプライン専用であり、Workflow 内部で発生する処理には影響しません。 +- [`trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]:トレースに音声データを含めるかどうかを制御します。 +- [`workflow_name`][agents.voice.pipeline_config.VoicePipelineConfig.workflow_name]:トレース Workflow の名前です。 +- [`group_id`][agents.voice.pipeline_config.VoicePipelineConfig.group_id]:複数のトレースを関連付けるための `group_id` です。 - [`trace_metadata`][agents.voice.pipeline_config.VoicePipelineConfig.tracing_disabled]:トレースに追加するメタデータです。 \ No newline at end of file diff --git a/docs/scripts/translate_docs.py b/docs/scripts/translate_docs.py index f261daa0..b2e8b44f 100644 --- a/docs/scripts/translate_docs.py +++ b/docs/scripts/translate_docs.py @@ -7,7 +7,18 @@ # logging.basicConfig(level=logging.INFO) # logging.getLogger("openai").setLevel(logging.DEBUG) -OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4.1") +OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "o3") + +ENABLE_CODE_SNIPPET_EXCLUSION = True +# gpt-4.5 needed this for better quality +ENABLE_SMALL_CHUNK_TRANSLATION = False + +SEARCH_EXCLUSION = """--- +search: + exclude: true +--- +""" + # Define the source and target directories source_dir = "docs" @@ -25,6 +36,7 @@ "Agents SDK", "Hello World", "Model context protocol", + "MCP", "structured outputs", "Chain-of-Thought", "Chat Completions", @@ -109,6 +121,8 @@ def built_instructions(target_language: str, lang_code: str) -> str: - Keep the tone **natural** and concise. - Do not omit any content. If a segment should stay in English, copy it verbatim. - Do not change the markdown data structure, including the indentations. +- Section titles starting with # or ## must be a noun form rather than a sentence. +- Section titles must be translated except for the Do-Not-Translate list. - Keep all placeholders such as `CODE_BLOCK_*` and `CODE_LINE_PREFIX` unchanged. - Convert asset paths: `./assets/…` → `../assets/…`. *Example:* `![img](./assets/pic.png)` → `![img](../assets/pic.png)` @@ -178,10 +192,15 @@ def translate_file(file_path: str, target_path: str, lang_code: str) -> None: code_blocks: list[str] = [] code_block_chunks: list[str] = [] for line in lines: - if len(current_chunk) >= 120 and not in_code_block and line.startswith("#"): + if ( + ENABLE_SMALL_CHUNK_TRANSLATION is True + and len(current_chunk) >= 120 # required for gpt-4.5 + and not in_code_block + and line.startswith("#") + ): chunks.append("\n".join(current_chunk)) current_chunk = [] - if line.strip().startswith("```"): + if ENABLE_CODE_SNIPPET_EXCLUSION is True and line.strip().startswith("```"): code_block_chunks.append(line) if in_code_block is True: code_blocks.append("\n".join(code_block_chunks)) @@ -220,6 +239,8 @@ def translate_file(file_path: str, target_path: str, lang_code: str) -> None: for idx, code_block in enumerate(code_blocks): translated_text = translated_text.replace(f"CODE_BLOCK_{idx:02}", code_block) + # FIXME: enable mkdocs search plugin to seamlessly work with i18n plugin + translated_text = SEARCH_EXCLUSION + translated_text # Save the combined translated content with open(target_path, "w", encoding="utf-8") as f: f.write(translated_text) diff --git a/mkdocs.yml b/mkdocs.yml index 286359dc..ad719670 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -146,7 +146,9 @@ plugins: - context.md - guardrails.md - multi_agent.md - - models.md + - モデル: + - models/index.md + - models/litellm.md - config.md - visualization.md - 音声エージェント: From 4b8472da7ecb36b25b29d3ca96de5f7e54fcd449 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 11:14:25 -0400 Subject: [PATCH 51/85] Examples for image inputs (#553) --- examples/basic/local_image.py | 48 +++++++++++++++++++++++++++ examples/basic/media/image_bison.jpg | Bin 0 -> 235620 bytes examples/basic/remote_image.py | 31 +++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 examples/basic/local_image.py create mode 100644 examples/basic/media/image_bison.jpg create mode 100644 examples/basic/remote_image.py diff --git a/examples/basic/local_image.py b/examples/basic/local_image.py new file mode 100644 index 00000000..d4a784ba --- /dev/null +++ b/examples/basic/local_image.py @@ -0,0 +1,48 @@ +import asyncio +import base64 +import os + +from agents import Agent, Runner + +FILEPATH = os.path.join(os.path.dirname(__file__), "media/image_bison.jpg") + + +def image_to_base64(image_path): + with open(image_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + return encoded_string + + +async def main(): + # Print base64-encoded image + b64_image = image_to_base64(FILEPATH) + + agent = Agent( + name="Assistant", + instructions="You are a helpful assistant.", + ) + + result = await Runner.run( + agent, + [ + { + "role": "user", + "content": [ + { + "type": "input_image", + "detail": "auto", + "image_url": f"data:image/jpeg;base64,{b64_image}", + } + ], + }, + { + "role": "user", + "content": "What do you see in this image?", + }, + ], + ) + print(result.final_output) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/basic/media/image_bison.jpg b/examples/basic/media/image_bison.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b113c91f601eac2369c4b859b18f86a24a228d89 GIT binary patch literal 235620 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 z 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 Date: Mon, 21 Apr 2025 11:58:36 -0400 Subject: [PATCH 52/85] Enable non-strict output types (#539) See #528, some folks are having issues because their output types are not strict-compatible. My approach was: 1. Create `AgentOutputSchemaBase`, which represents the base methods for an output type - the json schema + validation 2. Make the existing `AgentOutputSchema` subclass `AgentOutputSchemaBase` 3. Allow users to pass a `AgentOutputSchemaBase` to `Agent(output_type=...)` --- examples/basic/non_strict_output_type.py | 81 +++++++++++++++++++ src/agents/__init__.py | 3 +- src/agents/_run_impl.py | 6 +- src/agents/agent.py | 11 ++- src/agents/agent_output.py | 66 +++++++++++++-- src/agents/extensions/models/litellm_model.py | 12 +-- src/agents/models/chatcmpl_converter.py | 6 +- src/agents/models/interface.py | 6 +- src/agents/models/openai_chatcompletions.py | 12 +-- src/agents/models/openai_responses.py | 16 ++-- src/agents/result.py | 4 +- src/agents/run.py | 14 ++-- tests/fake_model.py | 6 +- tests/test_agent_config.py | 5 +- .../test_openai_chatcompletions_converter.py | 2 +- tests/test_openai_responses_converter.py | 2 +- tests/test_output_tool.py | 59 +++++++++++++- tests/voice/test_workflow.py | 6 +- 18 files changed, 256 insertions(+), 61 deletions(-) create mode 100644 examples/basic/non_strict_output_type.py diff --git a/examples/basic/non_strict_output_type.py b/examples/basic/non_strict_output_type.py new file mode 100644 index 00000000..49fcc4e2 --- /dev/null +++ b/examples/basic/non_strict_output_type.py @@ -0,0 +1,81 @@ +import asyncio +import json +from dataclasses import dataclass +from typing import Any + +from agents import Agent, AgentOutputSchema, AgentOutputSchemaBase, Runner + +"""This example demonstrates how to use an output type that is not in strict mode. Strict mode +allows us to guarantee valid JSON output, but some schemas are not strict-compatible. + +In this example, we define an output type that is not strict-compatible, and then we run the +agent with strict_json_schema=False. + +We also demonstrate a custom output type. + +To understand which schemas are strict-compatible, see: +https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas +""" + + +@dataclass +class OutputType: + jokes: dict[int, str] + """A list of jokes, indexed by joke number.""" + + +class CustomOutputSchema(AgentOutputSchemaBase): + """A demonstration of a custom output schema.""" + + def is_plain_text(self) -> bool: + return False + + def name(self) -> str: + return "CustomOutputSchema" + + def json_schema(self) -> dict[str, Any]: + return { + "type": "object", + "properties": {"jokes": {"type": "object", "properties": {"joke": {"type": "string"}}}}, + } + + def is_strict_json_schema(self) -> bool: + return False + + def validate_json(self, json_str: str) -> Any: + json_obj = json.loads(json_str) + # Just for demonstration, we'll return a list. + return list(json_obj["jokes"].values()) + + +async def main(): + agent = Agent( + name="Assistant", + instructions="You are a helpful assistant.", + output_type=OutputType, + ) + + input = "Tell me 3 short jokes." + + # First, let's try with a strict output type. This should raise an exception. + try: + result = await Runner.run(agent, input) + raise AssertionError("Should have raised an exception") + except Exception as e: + print(f"Error (expected): {e}") + + # Now let's try again with a non-strict output type. This should work. + # In some cases, it will raise an error - the schema isn't strict, so the model may + # produce an invalid JSON object. + agent.output_type = AgentOutputSchema(OutputType, strict_json_schema=False) + result = await Runner.run(agent, input) + print(result.final_output) + + # Finally, let's try a custom output type. + agent.output_type = CustomOutputSchema() + result = await Runner.run(agent, input) + print(result.final_output) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index db7d312e..6d7c90b4 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -6,7 +6,7 @@ from . import _config from .agent import Agent, ToolsToFinalOutputFunction, ToolsToFinalOutputResult -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchema, AgentOutputSchemaBase from .computer import AsyncComputer, Button, Computer, Environment from .exceptions import ( AgentsException, @@ -158,6 +158,7 @@ def enable_verbose_stdout_logging(): "OpenAIProvider", "OpenAIResponsesModel", "AgentOutputSchema", + "AgentOutputSchemaBase", "Computer", "AsyncComputer", "Environment", diff --git a/src/agents/_run_impl.py b/src/agents/_run_impl.py index 94c181b7..b5a83685 100644 --- a/src/agents/_run_impl.py +++ b/src/agents/_run_impl.py @@ -29,7 +29,7 @@ from openai.types.responses.response_reasoning_item import ResponseReasoningItem from .agent import Agent, ToolsToFinalOutputResult -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchemaBase from .computer import AsyncComputer, Computer from .exceptions import AgentsException, ModelBehaviorError, UserError from .guardrail import InputGuardrail, InputGuardrailResult, OutputGuardrail, OutputGuardrailResult @@ -195,7 +195,7 @@ async def execute_tools_and_side_effects( pre_step_items: list[RunItem], new_response: ModelResponse, processed_response: ProcessedResponse, - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, hooks: RunHooks[TContext], context_wrapper: RunContextWrapper[TContext], run_config: RunConfig, @@ -335,7 +335,7 @@ def process_model_response( agent: Agent[Any], all_tools: list[Tool], response: ModelResponse, - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], ) -> ProcessedResponse: items: list[RunItem] = [] diff --git a/src/agents/agent.py b/src/agents/agent.py index a24456b0..e22f579f 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -8,6 +8,7 @@ from typing_extensions import NotRequired, TypeAlias, TypedDict +from .agent_output import AgentOutputSchemaBase from .guardrail import InputGuardrail, OutputGuardrail from .handoffs import Handoff from .items import ItemHelpers @@ -141,8 +142,14 @@ class Agent(Generic[TContext]): Runs only if the agent produces a final output. """ - output_type: type[Any] | None = None - """The type of the output object. If not provided, the output will be `str`.""" + output_type: type[Any] | AgentOutputSchemaBase | None = None + """The type of the output object. If not provided, the output will be `str`. In most cases, + you should pass a regular Python type (e.g. a dataclass, Pydantic model, TypedDict, etc). + You can customize this in two ways: + 1. If you want non-strict schemas, pass `AgentOutputSchema(MyClass, strict_json_schema=False)`. + 2. If you want to use a custom JSON schema (i.e. without using the SDK's automatic schema) + creation, subclass and pass an `AgentOutputSchemaBase` subclass. + """ hooks: AgentHooks[TContext] | None = None """A class that receives callbacks on various lifecycle events for this agent. diff --git a/src/agents/agent_output.py b/src/agents/agent_output.py index 3262c57d..066bbd83 100644 --- a/src/agents/agent_output.py +++ b/src/agents/agent_output.py @@ -1,3 +1,4 @@ +import abc from dataclasses import dataclass from typing import Any @@ -12,8 +13,46 @@ _WRAPPER_DICT_KEY = "response" +class AgentOutputSchemaBase(abc.ABC): + """An object that captures the JSON schema of the output, as well as validating/parsing JSON + produced by the LLM into the output type. + """ + + @abc.abstractmethod + def is_plain_text(self) -> bool: + """Whether the output type is plain text (versus a JSON object).""" + pass + + @abc.abstractmethod + def name(self) -> str: + """The name of the output type.""" + pass + + @abc.abstractmethod + def json_schema(self) -> dict[str, Any]: + """Returns the JSON schema of the output. Will only be called if the output type is not + plain text. + """ + pass + + @abc.abstractmethod + def is_strict_json_schema(self) -> bool: + """Whether the JSON schema is in strict mode. Strict mode constrains the JSON schema + features, but guarantees valis JSON. See here for details: + https://platform.openai.com/docs/guides/structured-outputs#supported-schemas + """ + pass + + @abc.abstractmethod + def validate_json(self, json_str: str) -> Any: + """Validate a JSON string against the output type. You must return the validated object, + or raise a `ModelBehaviorError` if the JSON is invalid. + """ + pass + + @dataclass(init=False) -class AgentOutputSchema: +class AgentOutputSchema(AgentOutputSchemaBase): """An object that captures the JSON schema of the output, as well as validating/parsing JSON produced by the LLM into the output type. """ @@ -32,7 +71,7 @@ class AgentOutputSchema: _output_schema: dict[str, Any] """The JSON schema of the output.""" - strict_json_schema: bool + _strict_json_schema: bool """Whether the JSON schema is in strict mode. We **strongly** recommend setting this to True, as it increases the likelihood of correct JSON input. """ @@ -45,7 +84,7 @@ def __init__(self, output_type: type[Any], strict_json_schema: bool = True): setting this to True, as it increases the likelihood of correct JSON input. """ self.output_type = output_type - self.strict_json_schema = strict_json_schema + self._strict_json_schema = strict_json_schema if output_type is None or output_type is str: self._is_wrapped = False @@ -70,24 +109,35 @@ def __init__(self, output_type: type[Any], strict_json_schema: bool = True): self._type_adapter = TypeAdapter(output_type) self._output_schema = self._type_adapter.json_schema() - if self.strict_json_schema: - self._output_schema = ensure_strict_json_schema(self._output_schema) + if self._strict_json_schema: + try: + self._output_schema = ensure_strict_json_schema(self._output_schema) + except UserError as e: + raise UserError( + "Strict JSON schema is enabled, but the output type is not valid. " + "Either make the output type strict, or pass output_schema_strict=False to " + "your Agent()" + ) from e def is_plain_text(self) -> bool: """Whether the output type is plain text (versus a JSON object).""" return self.output_type is None or self.output_type is str + def is_strict_json_schema(self) -> bool: + """Whether the JSON schema is in strict mode.""" + return self._strict_json_schema + def json_schema(self) -> dict[str, Any]: """The JSON schema of the output type.""" if self.is_plain_text(): raise UserError("Output type is plain text, so no JSON schema is available") return self._output_schema - def validate_json(self, json_str: str, partial: bool = False) -> Any: + def validate_json(self, json_str: str) -> Any: """Validate a JSON string against the output type. Returns the validated object, or raises a `ModelBehaviorError` if the JSON is invalid. """ - validated = _json.validate_json(json_str, self._type_adapter, partial) + validated = _json.validate_json(json_str, self._type_adapter, partial=False) if self._is_wrapped: if not isinstance(validated, dict): _error_tracing.attach_error_to_current_span( @@ -113,7 +163,7 @@ def validate_json(self, json_str: str, partial: bool = False) -> Any: return validated[_WRAPPER_DICT_KEY] return validated - def output_type_name(self) -> str: + def name(self) -> str: """The name of the output type.""" return _type_to_str(self.output_type) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 0fc277c3..e939ee8d 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -29,7 +29,7 @@ from openai.types.responses import Response from ... import _debug -from ...agent_output import AgentOutputSchema +from ...agent_output import AgentOutputSchemaBase from ...handoffs import Handoff from ...items import ModelResponse, TResponseInputItem, TResponseStreamEvent from ...logger import logger @@ -68,7 +68,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -139,7 +139,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -186,7 +186,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -200,7 +200,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -213,7 +213,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py index 00175a16..613a3745 100644 --- a/src/agents/models/chatcmpl_converter.py +++ b/src/agents/models/chatcmpl_converter.py @@ -36,7 +36,7 @@ ) from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..exceptions import AgentsException, UserError from ..handoffs import Handoff from ..items import TResponseInputItem, TResponseOutputItem @@ -67,7 +67,7 @@ def convert_tool_choice( @classmethod def convert_response_format( - cls, final_output_schema: AgentOutputSchema | None + cls, final_output_schema: AgentOutputSchemaBase | None ) -> ResponseFormat | NotGiven: if not final_output_schema or final_output_schema.is_plain_text(): return NOT_GIVEN @@ -76,7 +76,7 @@ def convert_response_format( "type": "json_schema", "json_schema": { "name": "final_output", - "strict": final_output_schema.strict_json_schema, + "strict": final_output_schema.is_strict_json_schema(), "schema": final_output_schema.json_schema(), }, } diff --git a/src/agents/models/interface.py b/src/agents/models/interface.py index bcf2c1a6..3a79e564 100644 --- a/src/agents/models/interface.py +++ b/src/agents/models/interface.py @@ -5,7 +5,7 @@ from collections.abc import AsyncIterator from typing import TYPE_CHECKING -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..handoffs import Handoff from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent from ..tool import Tool @@ -41,7 +41,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -72,7 +72,7 @@ def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 9989c1ee..9fd10269 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -12,7 +12,7 @@ from openai.types.responses import Response from .. import _debug -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..handoffs import Handoff from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent from ..logger import logger @@ -49,7 +49,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -110,7 +110,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -160,7 +160,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -174,7 +174,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, @@ -187,7 +187,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], span: Span[GenerationSpanData], tracing: ModelTracing, diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index ab4617d4..b751663d 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -18,7 +18,7 @@ ) from .. import _debug -from ..agent_output import AgentOutputSchema +from ..agent_output import AgentOutputSchemaBase from ..exceptions import UserError from ..handoffs import Handoff from ..items import ItemHelpers, ModelResponse, TResponseInputItem @@ -66,7 +66,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -131,7 +131,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, previous_response_id: str | None, @@ -182,7 +182,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], previous_response_id: str | None, stream: Literal[True], @@ -195,7 +195,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], previous_response_id: str | None, stream: Literal[False], @@ -207,7 +207,7 @@ async def _fetch_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], previous_response_id: str | None, stream: Literal[True] | Literal[False] = False, @@ -307,7 +307,7 @@ def convert_tool_choice( @classmethod def get_response_format( - cls, output_schema: AgentOutputSchema | None + cls, output_schema: AgentOutputSchemaBase | None ) -> ResponseTextConfigParam | NotGiven: if output_schema is None or output_schema.is_plain_text(): return NOT_GIVEN @@ -317,7 +317,7 @@ def get_response_format( "type": "json_schema", "name": "final_output", "schema": output_schema.json_schema(), - "strict": output_schema.strict_json_schema, + "strict": output_schema.is_strict_json_schema(), } } diff --git a/src/agents/result.py b/src/agents/result.py index a2a6cc4a..2996eaf9 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -10,7 +10,7 @@ from ._run_impl import QueueCompleteSentinel from .agent import Agent -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchemaBase from .exceptions import InputGuardrailTripwireTriggered, MaxTurnsExceeded from .guardrail import InputGuardrailResult, OutputGuardrailResult from .items import ItemHelpers, ModelResponse, RunItem, TResponseInputItem @@ -124,7 +124,7 @@ class RunResultStreaming(RunResultBase): final_output: Any """The final output of the agent. This is None until the agent has finished running.""" - _current_agent_output_schema: AgentOutputSchema | None = field(repr=False) + _current_agent_output_schema: AgentOutputSchemaBase | None = field(repr=False) _trace: Trace | None = field(repr=False) diff --git a/src/agents/run.py b/src/agents/run.py index e2b0dbce..1fc7b52f 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -19,7 +19,7 @@ get_model_tracing_impl, ) from .agent import Agent -from .agent_output import AgentOutputSchema +from .agent_output import AgentOutputSchema, AgentOutputSchemaBase from .exceptions import ( AgentsException, InputGuardrailTripwireTriggered, @@ -185,7 +185,7 @@ async def run( if current_span is None: handoff_names = [h.agent_name for h in cls._get_handoffs(current_agent)] if output_schema := cls._get_output_schema(current_agent): - output_type_name = output_schema.output_type_name() + output_type_name = output_schema.name() else: output_type_name = "str" @@ -517,7 +517,7 @@ async def _run_streamed_impl( if current_span is None: handoff_names = [h.agent_name for h in cls._get_handoffs(current_agent)] if output_schema := cls._get_output_schema(current_agent): - output_type_name = output_schema.output_type_name() + output_type_name = output_schema.name() else: output_type_name = "str" @@ -789,7 +789,7 @@ async def _get_single_step_result_from_response( original_input: str | list[TResponseInputItem], pre_step_items: list[RunItem], new_response: ModelResponse, - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], hooks: RunHooks[TContext], context_wrapper: RunContextWrapper[TContext], @@ -900,7 +900,7 @@ async def _get_new_response( agent: Agent[TContext], system_prompt: str | None, input: list[TResponseInputItem], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, all_tools: list[Tool], handoffs: list[Handoff], context_wrapper: RunContextWrapper[TContext], @@ -930,9 +930,11 @@ async def _get_new_response( return new_response @classmethod - def _get_output_schema(cls, agent: Agent[Any]) -> AgentOutputSchema | None: + def _get_output_schema(cls, agent: Agent[Any]) -> AgentOutputSchemaBase | None: if agent.output_type is None or agent.output_type is str: return None + elif isinstance(agent.output_type, AgentOutputSchemaBase): + return agent.output_type return AgentOutputSchema(agent.output_type) diff --git a/tests/fake_model.py b/tests/fake_model.py index 52d3a3b2..c6b3ba92 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -5,7 +5,7 @@ from openai.types.responses import Response, ResponseCompletedEvent -from agents.agent_output import AgentOutputSchema +from agents.agent_output import AgentOutputSchemaBase from agents.handoffs import Handoff from agents.items import ( ModelResponse, @@ -51,7 +51,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -93,7 +93,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, diff --git a/tests/test_agent_config.py b/tests/test_agent_config.py index 44339dad..f79c0cf8 100644 --- a/tests/test_agent_config.py +++ b/tests/test_agent_config.py @@ -1,7 +1,7 @@ import pytest from pydantic import BaseModel -from agents import Agent, Handoff, RunContextWrapper, Runner, handoff +from agents import Agent, AgentOutputSchema, Handoff, RunContextWrapper, Runner, handoff @pytest.mark.asyncio @@ -160,8 +160,9 @@ async def test_agent_final_output(): ) schema = Runner._get_output_schema(agent) + assert isinstance(schema, AgentOutputSchema) assert schema is not None assert schema.output_type == Foo - assert schema.strict_json_schema is True + assert schema.is_strict_json_schema() is True assert schema.json_schema() is not None assert not schema.is_plain_text() diff --git a/tests/test_openai_chatcompletions_converter.py b/tests/test_openai_chatcompletions_converter.py index e3a18b25..bcfca549 100644 --- a/tests/test_openai_chatcompletions_converter.py +++ b/tests/test_openai_chatcompletions_converter.py @@ -232,7 +232,7 @@ def test_convert_response_format_returns_not_given_for_plain_text_and_dict_for_s assert resp_format["type"] == "json_schema" assert resp_format["json_schema"]["name"] == "final_output" assert "strict" in resp_format["json_schema"] - assert resp_format["json_schema"]["strict"] == schema.strict_json_schema + assert resp_format["json_schema"]["strict"] == schema.is_strict_json_schema() assert "schema" in resp_format["json_schema"] assert resp_format["json_schema"]["schema"] == schema.json_schema() diff --git a/tests/test_openai_responses_converter.py b/tests/test_openai_responses_converter.py index 34cbac5c..8e486665 100644 --- a/tests/test_openai_responses_converter.py +++ b/tests/test_openai_responses_converter.py @@ -92,7 +92,7 @@ class OutModel(BaseModel): assert inner.get("name") == "final_output" assert isinstance(inner.get("schema"), dict) # Should include a strict flag matching the schema's strictness setting. - assert inner.get("strict") == out_schema.strict_json_schema + assert inner.get("strict") == out_schema.is_strict_json_schema() def test_convert_tools_basic_types_and_includes(): diff --git a/tests/test_output_tool.py b/tests/test_output_tool.py index 86c4b3b5..37c1b1b6 100644 --- a/tests/test_output_tool.py +++ b/tests/test_output_tool.py @@ -1,10 +1,18 @@ import json +from typing import Any import pytest from pydantic import BaseModel from typing_extensions import TypedDict -from agents import Agent, AgentOutputSchema, ModelBehaviorError, Runner, UserError +from agents import ( + Agent, + AgentOutputSchema, + AgentOutputSchemaBase, + ModelBehaviorError, + Runner, + UserError, +) from agents.agent_output import _WRAPPER_DICT_KEY from agents.util import _json @@ -27,6 +35,7 @@ def test_structured_output_pydantic(): output_schema = Runner._get_output_schema(agent) assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, AgentOutputSchema) assert output_schema.output_type == Foo, "Should have the correct output type" assert not output_schema._is_wrapped, "Pydantic objects should not be wrapped" for key, value in Foo.model_json_schema().items(): @@ -45,6 +54,7 @@ def test_structured_output_typed_dict(): agent = Agent(name="test", output_type=Bar) output_schema = Runner._get_output_schema(agent) assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, AgentOutputSchema) assert output_schema.output_type == Bar, "Should have the correct output type" assert not output_schema._is_wrapped, "TypedDicts should not be wrapped" @@ -57,6 +67,7 @@ def test_structured_output_list(): agent = Agent(name="test", output_type=list[str]) output_schema = Runner._get_output_schema(agent) assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, AgentOutputSchema) assert output_schema.output_type == list[str], "Should have the correct output type" assert output_schema._is_wrapped, "Lists should be wrapped" @@ -98,7 +109,7 @@ def test_plain_text_obj_doesnt_produce_schema(): def test_structured_output_is_strict(): output_wrapper = AgentOutputSchema(output_type=Foo) - assert output_wrapper.strict_json_schema + assert output_wrapper.is_strict_json_schema() for key, value in Foo.model_json_schema().items(): assert output_wrapper.json_schema()[key] == value @@ -110,6 +121,48 @@ def test_structured_output_is_strict(): def test_setting_strict_false_works(): output_wrapper = AgentOutputSchema(output_type=Foo, strict_json_schema=False) - assert not output_wrapper.strict_json_schema + assert not output_wrapper.is_strict_json_schema() assert output_wrapper.json_schema() == Foo.model_json_schema() assert output_wrapper.json_schema() == Foo.model_json_schema() + + +_CUSTOM_OUTPUT_SCHEMA_JSON_SCHEMA = { + "type": "object", + "properties": { + "foo": {"type": "string"}, + }, + "required": ["foo"], +} + + +class CustomOutputSchema(AgentOutputSchemaBase): + def is_plain_text(self) -> bool: + return False + + def name(self) -> str: + return "FooBarBaz" + + def json_schema(self) -> dict[str, Any]: + return _CUSTOM_OUTPUT_SCHEMA_JSON_SCHEMA + + def is_strict_json_schema(self) -> bool: + return False + + def validate_json(self, json_str: str) -> Any: + return ["some", "output"] + + +def test_custom_output_schema(): + custom_output_schema = CustomOutputSchema() + agent = Agent(name="test", output_type=custom_output_schema) + output_schema = Runner._get_output_schema(agent) + + assert output_schema, "Should have an output tool config with a structured output type" + assert isinstance(output_schema, CustomOutputSchema) + assert output_schema.json_schema() == _CUSTOM_OUTPUT_SCHEMA_JSON_SCHEMA + assert not output_schema.is_strict_json_schema() + assert not output_schema.is_plain_text() + + json_str = json.dumps({"foo": "bar"}) + validated = output_schema.validate_json(json_str) + assert validated == ["some", "output"] diff --git a/tests/voice/test_workflow.py b/tests/voice/test_workflow.py index 72a3370d..2bdf2a65 100644 --- a/tests/voice/test_workflow.py +++ b/tests/voice/test_workflow.py @@ -9,7 +9,7 @@ from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent from agents import Agent, Model, ModelSettings, ModelTracing, Tool -from agents.agent_output import AgentOutputSchema +from agents.agent_output import AgentOutputSchemaBase from agents.handoffs import Handoff from agents.items import ( ModelResponse, @@ -48,7 +48,7 @@ async def get_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, @@ -62,7 +62,7 @@ async def stream_response( input: str | list[TResponseInputItem], model_settings: ModelSettings, tools: list[Tool], - output_schema: AgentOutputSchema | None, + output_schema: AgentOutputSchemaBase | None, handoffs: list[Handoff], tracing: ModelTracing, *, From 616d8e7f4b1d1b806e2ef903209d968d00cc425b Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 13:08:38 -0400 Subject: [PATCH 53/85] Start and finish streaming trace in impl metod (#540) Closes #435 and closes #538. Unit tests. --- .gitignore | 3 ++- pyproject.toml | 1 + src/agents/result.py | 5 +---- src/agents/run.py | 11 ++++----- tests/fastapi/__init__.py | 0 tests/fastapi/streaming_app.py | 30 +++++++++++++++++++++++++ tests/fastapi/test_streaming_context.py | 29 ++++++++++++++++++++++++ uv.lock | 19 +++++++++++++++- 8 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 tests/fastapi/__init__.py create mode 100644 tests/fastapi/streaming_app.py create mode 100644 tests/fastapi/test_streaming_context.py diff --git a/.gitignore b/.gitignore index 7dd22b88..2e9b9237 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,5 @@ cython_debug/ .ruff_cache/ # PyPI configuration file -.pypirc \ No newline at end of file +.pypirc +.aider* diff --git a/pyproject.toml b/pyproject.toml index f94eb859..c06a8416 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dev = [ "graphviz", "mkdocs-static-i18n>=1.3.0", "eval-type-backport>=0.2.2", + "fastapi >= 0.110.0, <1", ] [tool.uv.workspace] diff --git a/src/agents/result.py b/src/agents/result.py index 2996eaf9..0d8372c8 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -126,7 +126,7 @@ class RunResultStreaming(RunResultBase): _current_agent_output_schema: AgentOutputSchemaBase | None = field(repr=False) - _trace: Trace | None = field(repr=False) + trace: Trace | None = field(repr=False) is_complete: bool = False """Whether the agent has finished running.""" @@ -185,9 +185,6 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: yield item self._event_queue.task_done() - if self._trace: - self._trace.finish(reset_current=True) - self._cleanup_tasks() if self._stored_exception: diff --git a/src/agents/run.py b/src/agents/run.py index 1fc7b52f..38e11152 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -404,10 +404,6 @@ def run_streamed( disabled=run_config.tracing_disabled, ) ) - # Need to start the trace here, because the current trace contextvar is captured at - # asyncio.create_task time - if new_trace: - new_trace.start(mark_as_current=True) output_schema = cls._get_output_schema(starting_agent) context_wrapper: RunContextWrapper[TContext] = RunContextWrapper( @@ -426,7 +422,7 @@ def run_streamed( input_guardrail_results=[], output_guardrail_results=[], _current_agent_output_schema=output_schema, - _trace=new_trace, + trace=new_trace, ) # Kick off the actual agent loop in the background and return the streamed result object. @@ -499,6 +495,9 @@ async def _run_streamed_impl( run_config: RunConfig, previous_response_id: str | None, ): + if streamed_result.trace: + streamed_result.trace.start(mark_as_current=True) + current_span: Span[AgentSpanData] | None = None current_agent = starting_agent current_turn = 0 @@ -625,6 +624,8 @@ async def _run_streamed_impl( finally: if current_span: current_span.finish(reset_current=True) + if streamed_result.trace: + streamed_result.trace.finish(reset_current=True) @classmethod async def _run_single_turn_streamed( diff --git a/tests/fastapi/__init__.py b/tests/fastapi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/fastapi/streaming_app.py b/tests/fastapi/streaming_app.py new file mode 100644 index 00000000..b93ccf3f --- /dev/null +++ b/tests/fastapi/streaming_app.py @@ -0,0 +1,30 @@ +from collections.abc import AsyncIterator + +from fastapi import FastAPI +from starlette.responses import StreamingResponse + +from agents import Agent, Runner, RunResultStreaming + +agent = Agent( + name="Assistant", + instructions="You are a helpful assistant.", +) + + +app = FastAPI() + + +@app.post("/stream") +async def stream(): + result = Runner.run_streamed(agent, input="Tell me a joke") + stream_handler = StreamHandler(result) + return StreamingResponse(stream_handler.stream_events(), media_type="application/x-ndjson") + + +class StreamHandler: + def __init__(self, result: RunResultStreaming): + self.result = result + + async def stream_events(self) -> AsyncIterator[str]: + async for event in self.result.stream_events(): + yield f"{event.type}\n\n" diff --git a/tests/fastapi/test_streaming_context.py b/tests/fastapi/test_streaming_context.py new file mode 100644 index 00000000..ee13045e --- /dev/null +++ b/tests/fastapi/test_streaming_context.py @@ -0,0 +1,29 @@ +import pytest +from httpx import ASGITransport, AsyncClient +from inline_snapshot import snapshot + +from ..fake_model import FakeModel +from ..test_responses import get_text_message +from .streaming_app import agent, app + + +@pytest.mark.asyncio +async def test_streaming_context(): + """This ensures that FastAPI streaming works. The context for this test is that the Runner + method was called in one async context, and the streaming was ended in another context, + leading to a tracing error because the context was closed in the wrong context. This test + ensures that this actually works. + """ + model = FakeModel() + agent.model = model + model.set_next_output([get_text_message("done")]) + + transport = ASGITransport(app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + async with ac.stream("POST", "/stream") as r: + assert r.status_code == 200 + body = (await r.aread()).decode("utf-8") + lines = [line for line in body.splitlines() if line] + assert lines == snapshot( + ["agent_updated_stream_event", "raw_response_event", "run_item_stream_event"] + ) diff --git a/uv.lock b/uv.lock index 24d089a5..3ee65d21 100644 --- a/uv.lock +++ b/uv.lock @@ -483,6 +483,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, +] + [[package]] name = "filelock" version = "3.18.0" @@ -1496,6 +1510,7 @@ voice = [ dev = [ { name = "coverage" }, { name = "eval-type-backport" }, + { name = "fastapi" }, { name = "graphviz" }, { name = "inline-snapshot" }, { name = "mkdocs" }, @@ -1536,6 +1551,7 @@ provides-extras = ["voice", "viz", "litellm"] dev = [ { name = "coverage", specifier = ">=7.6.12" }, { name = "eval-type-backport", specifier = ">=0.2.2" }, + { name = "fastapi", specifier = ">=0.110.0,<1" }, { name = "graphviz" }, { name = "inline-snapshot", specifier = ">=0.20.7" }, { name = "mkdocs", specifier = ">=1.6.0" }, @@ -2474,7 +2490,8 @@ name = "starlette" version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ From 0a3dfa071a0853a115bac067cb18bd6a803d087e Mon Sep 17 00:00:00 2001 From: Yuya Haruna <121998910+yuya-haruna@users.noreply.github.com> Date: Tue, 22 Apr 2025 02:47:51 +0900 Subject: [PATCH 54/85] Fix visualize graph filename to without extension. (#554) Only the file name is needed since graphviz's `render()` automatically adds the file extension. Also, unnecessary .gv (.dot) files are output, so the `cleanup=True` option has been modified to prevent them from being saved. Here is a similar modification, but in a different content. - https://github.com/openai/openai-agents-python/pull/451 --- docs/ja/visualization.md | 4 ++-- docs/visualization.md | 4 +--- src/agents/extensions/visualization.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/ja/visualization.md b/docs/ja/visualization.md index 3c5c64ec..9093bb65 100644 --- a/docs/ja/visualization.md +++ b/docs/ja/visualization.md @@ -81,7 +81,7 @@ draw_graph(triage_agent).view() デフォルトでは、`draw_graph` はグラフをインラインで表示します。ファイルとして保存するには、ファイル名を指定します: ```python -draw_graph(triage_agent, filename="agent_graph.png") +draw_graph(triage_agent, filename="agent_graph") ``` -これにより、作業ディレクトリに `agent_graph.png` が生成されます。 \ No newline at end of file +これにより、作業ディレクトリに `agent_graph.png` が生成されます。 diff --git a/docs/visualization.md b/docs/visualization.md index 00f3126d..409803f7 100644 --- a/docs/visualization.md +++ b/docs/visualization.md @@ -78,9 +78,7 @@ draw_graph(triage_agent).view() By default, `draw_graph` displays the graph inline. To save it as a file, specify a filename: ```python -draw_graph(triage_agent, filename="agent_graph.png") +draw_graph(triage_agent, filename="agent_graph") ``` This will generate `agent_graph.png` in the working directory. - - diff --git a/src/agents/extensions/visualization.py b/src/agents/extensions/visualization.py index 5fb35062..888e262c 100644 --- a/src/agents/extensions/visualization.py +++ b/src/agents/extensions/visualization.py @@ -132,6 +132,6 @@ def draw_graph(agent: Agent, filename: Optional[str] = None) -> graphviz.Source: graph = graphviz.Source(dot_code) if filename: - graph.render(filename, format="png") + graph.render(filename, format="png", cleanup=True) return graph From a0254b0b74180f931c25c617a2548dac0f8aebab Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 15:03:06 -0400 Subject: [PATCH 55/85] RFC: automatically use litellm if possible (#534) ## Summary This replaces the default model provider with a `MultiProvider`, which has the logic: - if the model name starts with `openai/` or doesn't contain "/", use OpenAI - if the model name starts with `litellm/`, use LiteLLM to use the appropriate model provider. It's also extensible, so users can create their own mappings. I also imagine that if we natively supported Anthropic/Gemini etc, we can add it to MultiProvider to make it work. The goal is that it should be really easy to use any model provider. Today if you pass `model="gpt-4.1"`, it works great. But `model="claude-sonnet-3.7"` doesn't. If we can make it that easy, it's a win for devx. I'm not entirely sure if this is a good idea - is it too magical? Is the API too reliant on litellm? Comments welcome. ## Test plan For now, the example. Will add unit tests if we agree its worth mergin. --------- Co-authored-by: Steven Heidel --- examples/model_providers/litellm_auto.py | 41 +++++ .../extensions/models/litellm_provider.py | 21 +++ src/agents/models/multi_provider.py | 144 ++++++++++++++++++ src/agents/run.py | 4 +- 4 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 examples/model_providers/litellm_auto.py create mode 100644 src/agents/extensions/models/litellm_provider.py create mode 100644 src/agents/models/multi_provider.py diff --git a/examples/model_providers/litellm_auto.py b/examples/model_providers/litellm_auto.py new file mode 100644 index 00000000..12b1e891 --- /dev/null +++ b/examples/model_providers/litellm_auto.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import asyncio + +from agents import Agent, Runner, function_tool, set_tracing_disabled + +"""This example uses the built-in support for LiteLLM. To use this, ensure you have the +ANTHROPIC_API_KEY environment variable set. +""" + +set_tracing_disabled(disabled=True) + + +@function_tool +def get_weather(city: str): + print(f"[debug] getting weather for {city}") + return f"The weather in {city} is sunny." + + +async def main(): + agent = Agent( + name="Assistant", + instructions="You only respond in haikus.", + # We prefix with litellm/ to tell the Runner to use the LitellmModel + model="litellm/anthropic/claude-3-5-sonnet-20240620", + tools=[get_weather], + ) + + result = await Runner.run(agent, "What's the weather in Tokyo?") + print(result.final_output) + + +if __name__ == "__main__": + import os + + if os.getenv("ANTHROPIC_API_KEY") is None: + raise ValueError( + "ANTHROPIC_API_KEY is not set. Please set it the environment variable and try again." + ) + + asyncio.run(main()) diff --git a/src/agents/extensions/models/litellm_provider.py b/src/agents/extensions/models/litellm_provider.py new file mode 100644 index 00000000..5a2dc166 --- /dev/null +++ b/src/agents/extensions/models/litellm_provider.py @@ -0,0 +1,21 @@ +from ...models.interface import Model, ModelProvider +from .litellm_model import LitellmModel + +DEFAULT_MODEL: str = "gpt-4.1" + + +class LitellmProvider(ModelProvider): + """A ModelProvider that uses LiteLLM to route to any model provider. You can use it via: + ```python + Runner.run(agent, input, run_config=RunConfig(model_provider=LitellmProvider())) + ``` + See supported models here: [litellm models](https://docs.litellm.ai/docs/providers). + + NOTE: API keys must be set via environment variables. If you're using models that require + additional configuration (e.g. Azure API base or version), those must also be set via the + environment variables that LiteLLM expects. If you have more advanced needs, we recommend + copy-pasting this class and making any modifications you need. + """ + + def get_model(self, model_name: str | None) -> Model: + return LitellmModel(model_name or DEFAULT_MODEL) diff --git a/src/agents/models/multi_provider.py b/src/agents/models/multi_provider.py new file mode 100644 index 00000000..d075ac9b --- /dev/null +++ b/src/agents/models/multi_provider.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from openai import AsyncOpenAI + +from ..exceptions import UserError +from .interface import Model, ModelProvider +from .openai_provider import OpenAIProvider + + +class MultiProviderMap: + """A map of model name prefixes to ModelProviders.""" + + def __init__(self): + self._mapping: dict[str, ModelProvider] = {} + + def has_prefix(self, prefix: str) -> bool: + """Returns True if the given prefix is in the mapping.""" + return prefix in self._mapping + + def get_mapping(self) -> dict[str, ModelProvider]: + """Returns a copy of the current prefix -> ModelProvider mapping.""" + return self._mapping.copy() + + def set_mapping(self, mapping: dict[str, ModelProvider]): + """Overwrites the current mapping with a new one.""" + self._mapping = mapping + + def get_provider(self, prefix: str) -> ModelProvider | None: + """Returns the ModelProvider for the given prefix. + + Args: + prefix: The prefix of the model name e.g. "openai" or "my_prefix". + """ + return self._mapping.get(prefix) + + def add_provider(self, prefix: str, provider: ModelProvider): + """Adds a new prefix -> ModelProvider mapping. + + Args: + prefix: The prefix of the model name e.g. "openai" or "my_prefix". + provider: The ModelProvider to use for the given prefix. + """ + self._mapping[prefix] = provider + + def remove_provider(self, prefix: str): + """Removes the mapping for the given prefix. + + Args: + prefix: The prefix of the model name e.g. "openai" or "my_prefix". + """ + del self._mapping[prefix] + + +class MultiProvider(ModelProvider): + """This ModelProvider maps to a Model based on the prefix of the model name. By default, the + mapping is: + - "openai/" prefix or no prefix -> OpenAIProvider. e.g. "openai/gpt-4.1", "gpt-4.1" + - "litellm/" prefix -> LitellmProvider. e.g. "litellm/openai/gpt-4.1" + + You can override or customize this mapping. + """ + + def __init__( + self, + *, + provider_map: MultiProviderMap | None = None, + openai_api_key: str | None = None, + openai_base_url: str | None = None, + openai_client: AsyncOpenAI | None = None, + openai_organization: str | None = None, + openai_project: str | None = None, + openai_use_responses: bool | None = None, + ) -> None: + """Create a new OpenAI provider. + + Args: + provider_map: A MultiProviderMap that maps prefixes to ModelProviders. If not provided, + we will use a default mapping. See the documentation for this class to see the + default mapping. + openai_api_key: The API key to use for the OpenAI provider. If not provided, we will use + the default API key. + openai_base_url: The base URL to use for the OpenAI provider. If not provided, we will + use the default base URL. + openai_client: An optional OpenAI client to use. If not provided, we will create a new + OpenAI client using the api_key and base_url. + openai_organization: The organization to use for the OpenAI provider. + openai_project: The project to use for the OpenAI provider. + openai_use_responses: Whether to use the OpenAI responses API. + """ + self.provider_map = provider_map + self.openai_provider = OpenAIProvider( + api_key=openai_api_key, + base_url=openai_base_url, + openai_client=openai_client, + organization=openai_organization, + project=openai_project, + use_responses=openai_use_responses, + ) + + self._fallback_providers: dict[str, ModelProvider] = {} + + def _get_prefix_and_model_name(self, model_name: str | None) -> tuple[str | None, str | None]: + if model_name is None: + return None, None + elif "/" in model_name: + prefix, model_name = model_name.split("/", 1) + return prefix, model_name + else: + return None, model_name + + def _create_fallback_provider(self, prefix: str) -> ModelProvider: + if prefix == "litellm": + from ..extensions.models.litellm_provider import LitellmProvider + + return LitellmProvider() + else: + raise UserError(f"Unknown prefix: {prefix}") + + def _get_fallback_provider(self, prefix: str | None) -> ModelProvider: + if prefix is None or prefix == "openai": + return self.openai_provider + elif prefix in self._fallback_providers: + return self._fallback_providers[prefix] + else: + self._fallback_providers[prefix] = self._create_fallback_provider(prefix) + return self._fallback_providers[prefix] + + def get_model(self, model_name: str | None) -> Model: + """Returns a Model based on the model name. The model name can have a prefix, ending with + a "/", which will be used to look up the ModelProvider. If there is no prefix, we will use + the OpenAI provider. + + Args: + model_name: The name of the model to get. + + Returns: + A Model. + """ + prefix, model_name = self._get_prefix_and_model_name(model_name) + + if prefix and self.provider_map and (provider := self.provider_map.get_provider(prefix)): + return provider.get_model(model_name) + else: + return self._get_fallback_provider(prefix).get_model(model_name) diff --git a/src/agents/run.py b/src/agents/run.py index 38e11152..2af558d5 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -34,7 +34,7 @@ from .logger import logger from .model_settings import ModelSettings from .models.interface import Model, ModelProvider -from .models.openai_provider import OpenAIProvider +from .models.multi_provider import MultiProvider from .result import RunResult, RunResultStreaming from .run_context import RunContextWrapper, TContext from .stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent @@ -56,7 +56,7 @@ class RunConfig: agent. The model_provider passed in below must be able to resolve this model name. """ - model_provider: ModelProvider = field(default_factory=OpenAIProvider) + model_provider: ModelProvider = field(default_factory=MultiProvider) """The model provider to use when looking up string model names. Defaults to OpenAI.""" model_settings: ModelSettings | None = None From 942ba9840ba2bedf3726570f33049946ec4849a1 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 16:59:01 -0400 Subject: [PATCH 56/85] Docs and tests for litellm (#561) --- README.md | 4 +-- docs/models/index.md | 55 +++++++++++++++++++++++++++++----------- tests/models/__init__.py | 0 tests/models/conftest.py | 11 ++++++++ tests/models/test_map.py | 20 +++++++++++++++ 5 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 tests/models/__init__.py create mode 100644 tests/models/conftest.py create mode 100644 tests/models/test_map.py diff --git a/README.md b/README.md index bbd4a5a0..7dcd97b3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenAI Agents SDK -The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows. +The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows. It is provider-agnostic, supporting the OpenAI Responses and Chat Completions APIs, as well as 100+ other LLMs. Image of the Agents Tracing UI @@ -13,8 +13,6 @@ The OpenAI Agents SDK is a lightweight yet powerful framework for building multi Explore the [examples](examples) directory to see the SDK in action, and read our [documentation](https://openai.github.io/openai-agents-python/) for more details. -Notably, our SDK [is compatible](https://openai.github.io/openai-agents-python/models/) with any model providers that support the OpenAI Chat Completions API format. - ## Get started 1. Set up your Python environment diff --git a/docs/models/index.md b/docs/models/index.md index 4cf4f643..1c89d778 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -5,11 +5,40 @@ The Agents SDK comes with out-of-the-box support for OpenAI models in two flavor - **Recommended**: the [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel], which calls OpenAI APIs using the new [Responses API](https://platform.openai.com/docs/api-reference/responses). - The [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel], which calls OpenAI APIs using the [Chat Completions API](https://platform.openai.com/docs/api-reference/chat). +## Non-OpenAI models + +You can use most other non-OpenAI models via the [LiteLLM integration](./litellm.md). First, install the litellm dependency group: + +```bash +pip install "openai-agents[litellm]" +``` + +Then, use any of the [supported models](https://docs.litellm.ai/docs/providers) with the `litellm/` prefix: + +```python +claude_agent = Agent(model="litellm/anthropic/claude-3-5-sonnet-20240620", ...) +gemini_agent = Agent(model="litellm/gemini/gemini-2.5-flash-preview-04-17", ...) +``` + +### Other ways to use non-OpenAI models + +You can integrate other LLM providers in 3 more ways (examples [here](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)): + +1. [`set_default_openai_client`][agents.set_default_openai_client] is useful in cases where you want to globally use an instance of `AsyncOpenAI` as the LLM client. This is for cases where the LLM provider has an OpenAI compatible API endpoint, and you can set the `base_url` and `api_key`. See a configurable example in [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py). +2. [`ModelProvider`][agents.models.interface.ModelProvider] is at the `Runner.run` level. This lets you say "use a custom model provider for all agents in this run". See a configurable example in [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py). +3. [`Agent.model`][agents.agent.Agent.model] lets you specify the model on a specific Agent instance. This enables you to mix and match different providers for different agents. See a configurable example in [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py). An easy way to use most available models is via the [LiteLLM integration](./litellm.md). + +In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](../tracing.md). + +!!! note + + In these examples, we use the Chat Completions API/model, because most LLM providers don't yet support the Responses API. If your LLM provider does support it, we recommend using Responses. + ## Mixing and matching models Within a single workflow, you may want to use different models for each agent. For example, you could use a smaller, faster model for triage, while using a larger, more capable model for complex tasks. When configuring an [`Agent`][agents.Agent], you can select a specific model by either: -1. Passing the name of an OpenAI model. +1. Passing the name of a model. 2. Passing any model name + a [`ModelProvider`][agents.models.interface.ModelProvider] that can map that name to a Model instance. 3. Directly providing a [`Model`][agents.models.interface.Model] implementation. @@ -64,20 +93,6 @@ english_agent = Agent( ) ``` -## Using other LLM providers - -You can use other LLM providers in 3 ways (examples [here](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)): - -1. [`set_default_openai_client`][agents.set_default_openai_client] is useful in cases where you want to globally use an instance of `AsyncOpenAI` as the LLM client. This is for cases where the LLM provider has an OpenAI compatible API endpoint, and you can set the `base_url` and `api_key`. See a configurable example in [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py). -2. [`ModelProvider`][agents.models.interface.ModelProvider] is at the `Runner.run` level. This lets you say "use a custom model provider for all agents in this run". See a configurable example in [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py). -3. [`Agent.model`][agents.agent.Agent.model] lets you specify the model on a specific Agent instance. This enables you to mix and match different providers for different agents. See a configurable example in [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py). An easy way to use most available models is via the [LiteLLM integration](./litellm.md). - -In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](../tracing.md). - -!!! note - - In these examples, we use the Chat Completions API/model, because most LLM providers don't yet support the Responses API. If your LLM provider does support it, we recommend using Responses. - ## Common issues with using other LLM providers ### Tracing client error 401 @@ -100,7 +115,17 @@ The SDK uses the Responses API by default, but most other LLM providers don't ye Some model providers don't have support for [structured outputs](https://platform.openai.com/docs/guides/structured-outputs). This sometimes results in an error that looks something like this: ``` + BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}} + ``` This is a shortcoming of some model providers - they support JSON outputs, but don't allow you to specify the `json_schema` to use for the output. We are working on a fix for this, but we suggest relying on providers that do have support for JSON schema output, because otherwise your app will often break because of malformed JSON. + +## Mixing models across providers + +You need to be aware of feature differences between model providers, or you may run into errors. For example, OpenAI supports structured outputs, multimodal input, and hosted file search and web search, but many other providers don't support these features. Be aware of these limitations: + +- Don't send unsupported `tools` to providers that don't understand them +- Filter out multimodal inputs before calling models that are text-only +- Be aware that providers that don't support structured JSON outputs will occasionally produce invalid JSON. diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/models/conftest.py b/tests/models/conftest.py new file mode 100644 index 00000000..79d85d8b --- /dev/null +++ b/tests/models/conftest.py @@ -0,0 +1,11 @@ +import os +import sys + + +# Skip voice tests on Python 3.9 +def pytest_ignore_collect(collection_path, config): + if sys.version_info[:2] == (3, 9): + this_dir = os.path.dirname(__file__) + + if str(collection_path).startswith(this_dir): + return True diff --git a/tests/models/test_map.py b/tests/models/test_map.py new file mode 100644 index 00000000..6b65fc09 --- /dev/null +++ b/tests/models/test_map.py @@ -0,0 +1,20 @@ +from agents import Agent, OpenAIResponsesModel, RunConfig, Runner +from agents.extensions.models.litellm_model import LitellmModel + + +def test_no_prefix_is_openai(): + agent = Agent(model="gpt-4o", instructions="", name="test") + model = Runner._get_model(agent, RunConfig()) + assert isinstance(model, OpenAIResponsesModel) + + +def openai_prefix_is_openai(): + agent = Agent(model="openai/gpt-4o", instructions="", name="test") + model = Runner._get_model(agent, RunConfig()) + assert isinstance(model, OpenAIResponsesModel) + + +def test_litellm_prefix_is_litellm(): + agent = Agent(model="litellm/foo/bar", instructions="", name="test") + model = Runner._get_model(agent, RunConfig()) + assert isinstance(model, LitellmModel) From 2bdf9b7b1121d5a31f500f5e38ac87e195d1de32 Mon Sep 17 00:00:00 2001 From: Steven Heidel Date: Mon, 21 Apr 2025 13:59:46 -0700 Subject: [PATCH 57/85] Pass through organization/project headers to tracing backend, fix speech_group enum (#562) --- src/agents/mcp/server.py | 3 ++- src/agents/tracing/processors.py | 6 ++++++ src/agents/tracing/span_data.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index e70d7ce6..9a137bbd 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -137,9 +137,10 @@ async def cleanup(self): async with self._cleanup_lock: try: await self.exit_stack.aclose() - self.session = None except Exception as e: logger.error(f"Error cleaning up server: {e}") + finally: + self.session = None class MCPServerStdioParams(TypedDict): diff --git a/src/agents/tracing/processors.py b/src/agents/tracing/processors.py index f929d05d..2913b11a 100644 --- a/src/agents/tracing/processors.py +++ b/src/agents/tracing/processors.py @@ -102,6 +102,12 @@ def export(self, items: list[Trace | Span[Any]]) -> None: "OpenAI-Beta": "traces=v1", } + if self.organization: + headers["OpenAI-Organization"] = self.organization + + if self.project: + headers["OpenAI-Project"] = self.project + # Exponential backoff loop attempt = 0 delay = self.base_delay diff --git a/src/agents/tracing/span_data.py b/src/agents/tracing/span_data.py index 1a450280..cb3e8491 100644 --- a/src/agents/tracing/span_data.py +++ b/src/agents/tracing/span_data.py @@ -338,7 +338,7 @@ def __init__( @property def type(self) -> str: - return "speech-group" + return "speech_group" def export(self) -> dict[str, Any]: return { From 83ce49ec7e065a9688783341507b21af6a919de9 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Mon, 21 Apr 2025 23:06:09 -0400 Subject: [PATCH 58/85] v0.0.12 (#564) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c06a8416..eeeb6d3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.11" +version = "0.0.12" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 3ee65d21..3a737cf3 100644 --- a/uv.lock +++ b/uv.lock @@ -1482,7 +1482,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.11" +version = "0.0.12" source = { editable = "." } dependencies = [ { name = "griffe" }, From 111fc9ee66c5f6f4af3ba6faaedba4789da3d03a Mon Sep 17 00:00:00 2001 From: Jonny Kalambay Date: Tue, 22 Apr 2025 19:26:47 -0700 Subject: [PATCH 59/85] Adding extra_headers parameters to ModelSettings (#550) --- src/agents/extensions/models/litellm_model.py | 2 +- src/agents/model_settings.py | 6 +- src/agents/models/openai_chatcompletions.py | 2 +- src/agents/models/openai_responses.py | 2 +- tests/test_extra_headers.py | 92 +++++++++++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/test_extra_headers.py diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index e939ee8d..f5e7752f 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -286,7 +286,7 @@ async def _fetch_response( stream=stream, stream_options=stream_options, reasoning_effort=reasoning_effort, - extra_headers=HEADERS, + extra_headers={**HEADERS, **(model_settings.extra_headers or {})}, api_key=self.api_key, base_url=self.base_url, **extra_kwargs, diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index ed9a0131..fee92b4e 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, fields, replace from typing import Literal -from openai._types import Body, Query +from openai._types import Body, Headers, Query from openai.types.shared import Reasoning @@ -67,6 +67,10 @@ class ModelSettings: """Additional body fields to provide with the request. Defaults to None if not provided.""" + extra_headers: Headers | None = None + """Additional headers to provide with the request. + Defaults to None if not provided.""" + def resolve(self, override: ModelSettings | None) -> ModelSettings: """Produce a new ModelSettings by overlaying any non-None values from the override on top of this instance.""" diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 9fd10269..15bf19cb 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -255,7 +255,7 @@ async def _fetch_response( stream_options=self._non_null_or_not_given(stream_options), store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), - extra_headers=HEADERS, + extra_headers={ **HEADERS, **(model_settings.extra_headers or {}) }, extra_query=model_settings.extra_query, extra_body=model_settings.extra_body, metadata=self._non_null_or_not_given(model_settings.metadata), diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index b751663d..c1ff85b9 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -253,7 +253,7 @@ async def _fetch_response( tool_choice=tool_choice, parallel_tool_calls=parallel_tool_calls, stream=stream, - extra_headers=_HEADERS, + extra_headers={**_HEADERS, **(model_settings.extra_headers or {})}, extra_query=model_settings.extra_query, extra_body=model_settings.extra_body, text=response_format, diff --git a/tests/test_extra_headers.py b/tests/test_extra_headers.py new file mode 100644 index 00000000..f29c2540 --- /dev/null +++ b/tests/test_extra_headers.py @@ -0,0 +1,92 @@ +import pytest +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage + +from agents import ModelSettings, ModelTracing, OpenAIChatCompletionsModel, OpenAIResponsesModel + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_headers_passed_to_openai_responses_model(): + """ + Ensure extra_headers in ModelSettings is passed to the OpenAIResponsesModel client. + """ + called_kwargs = {} + + class DummyResponses: + async def create(self, **kwargs): + nonlocal called_kwargs + called_kwargs = kwargs + class DummyResponse: + id = "dummy" + output = [] + usage = type( + "Usage", (), {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} + )() + return DummyResponse() + + class DummyClient: + def __init__(self): + self.responses = DummyResponses() + + + + model = OpenAIResponsesModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + extra_headers = {"X-Test-Header": "test-value"} + await model.get_response( + system_instructions=None, + input="hi", + model_settings=ModelSettings(extra_headers=extra_headers), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + assert "extra_headers" in called_kwargs + assert called_kwargs["extra_headers"]["X-Test-Header"] == "test-value" + + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_headers_passed_to_openai_client(): + """ + Ensure extra_headers in ModelSettings is passed to the OpenAI client. + """ + called_kwargs = {} + + class DummyCompletions: + async def create(self, **kwargs): + nonlocal called_kwargs + called_kwargs = kwargs + msg = ChatCompletionMessage(role="assistant", content="Hello") + choice = Choice(index=0, finish_reason="stop", message=msg) + return ChatCompletion( + id="resp-id", + created=0, + model="fake", + object="chat.completion", + choices=[choice], + usage=None, + ) + + class DummyClient: + def __init__(self): + self.chat = type("_Chat", (), {"completions": DummyCompletions()})() + self.base_url = "https://api.openai.com" + + model = OpenAIChatCompletionsModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + extra_headers = {"X-Test-Header": "test-value"} + await model.get_response( + system_instructions=None, + input="hi", + model_settings=ModelSettings(extra_headers=extra_headers), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + assert "extra_headers" in called_kwargs + assert called_kwargs["extra_headers"]["X-Test-Header"] == "test-value" From 178020ea33980e5873a82dc715e79f0c6a285623 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 23 Apr 2025 11:29:12 +0900 Subject: [PATCH 60/85] Examples: Fix financial_research_agent instructions (#573) --- examples/financial_research_agent/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/financial_research_agent/main.py b/examples/financial_research_agent/main.py index 3fa8a7e0..b5b6cfdf 100644 --- a/examples/financial_research_agent/main.py +++ b/examples/financial_research_agent/main.py @@ -4,7 +4,7 @@ # Entrypoint for the financial bot example. -# Run this as `python -m examples.financial_bot.main` and enter a +# Run this as `python -m examples.financial_research_agent.main` and enter a # financial research query, for example: # "Write up an analysis of Apple Inc.'s most recent quarter." async def main() -> None: From a113fea0eef82bb37a0a803eaae42c4761d0ebdf Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Wed, 23 Apr 2025 16:51:10 -0700 Subject: [PATCH 61/85] Allow cancel out of the streaming result (#579) Fix for #574 @rm-openai I'm not sure how to add a test within the repo but I have pasted a test script below that seems to work ```python import asyncio from openai.types.responses import ResponseTextDeltaEvent from agents import Agent, Runner async def main(): agent = Agent( name="Joker", instructions="You are a helpful assistant.", ) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") num_visible_event = 0 async for event in result.stream_events(): if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): print(event.data.delta, end="", flush=True) num_visible_event += 1 print(num_visible_event) if num_visible_event == 3: result.cancel() if __name__ == "__main__": asyncio.run(main()) ```` --- src/agents/result.py | 24 +++++++++++++++++++++--- tests/test_cancel_streaming.py | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/test_cancel_streaming.py diff --git a/src/agents/result.py b/src/agents/result.py index 0d8372c8..1f1c7832 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -75,7 +75,9 @@ def final_output_as(self, cls: type[T], raise_if_incorrect_type: bool = False) - def to_input_list(self) -> list[TResponseInputItem]: """Creates a new input list, merging the original input with all the new items generated.""" - original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list(self.input) + original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list( + self.input + ) new_items = [item.to_input_item() for item in self.new_items] return original_items + new_items @@ -152,6 +154,18 @@ def last_agent(self) -> Agent[Any]: """ return self.current_agent + def cancel(self) -> None: + """Cancels the streaming run, stopping all background tasks and marking the run as + complete.""" + self._cleanup_tasks() # Cancel all running tasks + self.is_complete = True # Mark the run as complete to stop event streaming + + # Optionally, clear the event queue to prevent processing stale events + while not self._event_queue.empty(): + self._event_queue.get_nowait() + while not self._input_guardrail_queue.empty(): + self._input_guardrail_queue.get_nowait() + async def stream_events(self) -> AsyncIterator[StreamEvent]: """Stream deltas for new items as they are generated. We're using the types from the OpenAI Responses API, so these are semantic events: each event has a `type` field that @@ -192,13 +206,17 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: def _check_errors(self): if self.current_turn > self.max_turns: - self._stored_exception = MaxTurnsExceeded(f"Max turns ({self.max_turns}) exceeded") + self._stored_exception = MaxTurnsExceeded( + f"Max turns ({self.max_turns}) exceeded" + ) # Fetch all the completed guardrail results from the queue and raise if needed while not self._input_guardrail_queue.empty(): guardrail_result = self._input_guardrail_queue.get_nowait() if guardrail_result.output.tripwire_triggered: - self._stored_exception = InputGuardrailTripwireTriggered(guardrail_result) + self._stored_exception = InputGuardrailTripwireTriggered( + guardrail_result + ) # Check the tasks for any exceptions if self._run_impl_task and self._run_impl_task.done(): diff --git a/tests/test_cancel_streaming.py b/tests/test_cancel_streaming.py new file mode 100644 index 00000000..6d1807d7 --- /dev/null +++ b/tests/test_cancel_streaming.py @@ -0,0 +1,22 @@ +import pytest + +from agents import Agent, Runner + +from .fake_model import FakeModel + + +@pytest.mark.asyncio +async def test_joker_streamed_jokes_with_cancel(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + num_events = 0 + stop_after = 1 # There are two that the model gives back. + + async for _event in result.stream_events(): + num_events += 1 + if num_events == 1: + result.cancel() + + assert num_events == 1, f"Expected {stop_after} visible events, but got {num_events}" From 3755ea86589b8e929c5b2bdd51df9f62c1cad8bf Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 23 Apr 2025 20:39:07 -0400 Subject: [PATCH 62/85] Create to_json_dict for ModelSettings (#582) Now that `ModelSettings` has `Reasoning`, a non-primitive object, `dataclasses.as_dict()` wont work. It will raise an error when you try to serialize (e.g. for tracing). This ensures the object is actually serializable. --- pyproject.toml | 2 +- src/agents/extensions/models/litellm_model.py | 5 +- src/agents/model_settings.py | 17 +++++- src/agents/models/openai_chatcompletions.py | 7 +-- tests/model_settings/test_serialization.py | 59 +++++++++++++++++++ tests/voice/conftest.py | 1 - uv.lock | 8 +-- 7 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 tests/model_settings/test_serialization.py diff --git a/pyproject.toml b/pyproject.toml index eeeb6d3d..12ffff1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.9" license = "MIT" authors = [{ name = "OpenAI", email = "support@openai.com" }] dependencies = [ - "openai>=1.66.5", + "openai>=1.76.0", "pydantic>=2.10, <3", "griffe>=1.5.6, <2", "typing-extensions>=4.12.2, <5", diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index f5e7752f..dc672acd 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import json import time from collections.abc import AsyncIterator @@ -75,7 +74,7 @@ async def get_response( ) -> ModelResponse: with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) + model_config=model_settings.to_json_dict() | {"base_url": str(self.base_url or ""), "model_impl": "litellm"}, disabled=tracing.is_disabled(), ) as span_generation: @@ -147,7 +146,7 @@ async def stream_response( ) -> AsyncIterator[TResponseStreamEvent]: with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) + model_config=model_settings.to_json_dict() | {"base_url": str(self.base_url or ""), "model_impl": "litellm"}, disabled=tracing.is_disabled(), ) as span_generation: diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index fee92b4e..7b016c98 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -1,10 +1,12 @@ from __future__ import annotations +import dataclasses from dataclasses import dataclass, fields, replace -from typing import Literal +from typing import Any, Literal from openai._types import Body, Headers, Query from openai.types.shared import Reasoning +from pydantic import BaseModel @dataclass @@ -83,3 +85,16 @@ def resolve(self, override: ModelSettings | None) -> ModelSettings: if getattr(override, field.name) is not None } return replace(self, **changes) + + def to_json_dict(self) -> dict[str, Any]: + dataclass_dict = dataclasses.asdict(self) + + json_dict: dict[str, Any] = {} + + for field_name, value in dataclass_dict.items(): + if isinstance(value, BaseModel): + json_dict[field_name] = value.model_dump(mode="json") + else: + json_dict[field_name] = value + + return json_dict diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 15bf19cb..89619f83 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import json import time from collections.abc import AsyncIterator @@ -56,8 +55,7 @@ async def get_response( ) -> ModelResponse: with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) - | {"base_url": str(self._client.base_url)}, + model_config=model_settings.to_json_dict() | {"base_url": str(self._client.base_url)}, disabled=tracing.is_disabled(), ) as span_generation: response = await self._fetch_response( @@ -121,8 +119,7 @@ async def stream_response( """ with generation_span( model=str(self.model), - model_config=dataclasses.asdict(model_settings) - | {"base_url": str(self._client.base_url)}, + model_config=model_settings.to_json_dict() | {"base_url": str(self._client.base_url)}, disabled=tracing.is_disabled(), ) as span_generation: response, stream = await self._fetch_response( diff --git a/tests/model_settings/test_serialization.py b/tests/model_settings/test_serialization.py new file mode 100644 index 00000000..d76a58d1 --- /dev/null +++ b/tests/model_settings/test_serialization.py @@ -0,0 +1,59 @@ +import json +from dataclasses import fields + +from openai.types.shared import Reasoning + +from agents.model_settings import ModelSettings + + +def verify_serialization(model_settings: ModelSettings) -> None: + """Verify that ModelSettings can be serialized to a JSON string.""" + json_dict = model_settings.to_json_dict() + json_string = json.dumps(json_dict) + assert json_string is not None + + +def test_basic_serialization() -> None: + """Tests whether ModelSettings can be serialized to a JSON string.""" + + # First, lets create a ModelSettings instance + model_settings = ModelSettings( + temperature=0.5, + top_p=0.9, + max_tokens=100, + ) + + # Now, lets serialize the ModelSettings instance to a JSON string + verify_serialization(model_settings) + + +def test_all_fields_serialization() -> None: + """Tests whether ModelSettings can be serialized to a JSON string.""" + + # First, lets create a ModelSettings instance + model_settings = ModelSettings( + temperature=0.5, + top_p=0.9, + frequency_penalty=0.0, + presence_penalty=0.0, + tool_choice="auto", + parallel_tool_calls=True, + truncation="auto", + max_tokens=100, + reasoning=Reasoning(), + metadata={"foo": "bar"}, + store=False, + include_usage=False, + extra_query={"foo": "bar"}, + extra_body={"foo": "bar"}, + extra_headers={"foo": "bar"}, + ) + + # Verify that every single field is set to a non-None value + for field in fields(model_settings): + assert getattr(model_settings, field.name) is not None, ( + f"You must set the {field.name} field" + ) + + # Now, lets serialize the ModelSettings instance to a JSON string + verify_serialization(model_settings) diff --git a/tests/voice/conftest.py b/tests/voice/conftest.py index 6ed7422c..79d85d8b 100644 --- a/tests/voice/conftest.py +++ b/tests/voice/conftest.py @@ -9,4 +9,3 @@ def pytest_ignore_collect(collection_path, config): if str(collection_path).startswith(this_dir): return True - diff --git a/uv.lock b/uv.lock index 3a737cf3..4c6c370a 100644 --- a/uv.lock +++ b/uv.lock @@ -1463,7 +1463,7 @@ wheels = [ [[package]] name = "openai" -version = "1.74.0" +version = "1.76.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1475,9 +1475,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/86/c605a6e84da0248f2cebfcd864b5a6076ecf78849245af5e11d2a5ec7977/openai-1.74.0.tar.gz", hash = "sha256:592c25b8747a7cad33a841958f5eb859a785caea9ee22b9e4f4a2ec062236526", size = 427571 } +sdist = { url = "https://files.pythonhosted.org/packages/84/51/817969ec969b73d8ddad085670ecd8a45ef1af1811d8c3b8a177ca4d1309/openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2", size = 434660 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/91/8c150f16a96367e14bd7d20e86e0bbbec3080e3eb593e63f21a7f013f8e4/openai-1.74.0-py3-none-any.whl", hash = "sha256:aff3e0f9fb209836382ec112778667027f4fd6ae38bdb2334bc9e173598b092a", size = 644790 }, + { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201 }, ] [[package]] @@ -1538,7 +1538,7 @@ requires-dist = [ { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.65.0,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, - { name = "openai", specifier = ">=1.66.5" }, + { name = "openai", specifier = ">=1.76.0" }, { name = "pydantic", specifier = ">=2.10,<3" }, { name = "requests", specifier = ">=2.0,<3" }, { name = "types-requests", specifier = ">=2.0,<3" }, From af80e3a97123a5a0ad0fba695fbc257163c23224 Mon Sep 17 00:00:00 2001 From: Nathan Brake <33383515+njbrake@users.noreply.github.com> Date: Thu, 24 Apr 2025 12:12:46 -0400 Subject: [PATCH 63/85] Prevent MCP ClientSession hang (#580) Per https://modelcontextprotocol.io/specification/draft/basic/lifecycle#timeouts "Implementations SHOULD establish timeouts for all sent requests, to prevent hung connections and resource exhaustion. When the request has not received a success or error response within the timeout period, the sender SHOULD issue a cancellation notification for that request and stop waiting for a response. SDKs and other middleware SHOULD allow these timeouts to be configured on a per-request basis." I picked 5 seconds since that's the default for SSE --- src/agents/mcp/server.py | 26 ++++++++++++++++++++++---- tests/mcp/test_server_errors.py | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 9a137bbd..9916c92b 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -3,6 +3,7 @@ import abc import asyncio from contextlib import AbstractAsyncContextManager, AsyncExitStack +from datetime import timedelta from pathlib import Path from typing import Any, Literal @@ -54,7 +55,7 @@ async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> C class _MCPServerWithClientSession(MCPServer, abc.ABC): """Base class for MCP servers that use a `ClientSession` to communicate with the server.""" - def __init__(self, cache_tools_list: bool): + def __init__(self, cache_tools_list: bool, client_session_timeout_seconds: float | None): """ Args: cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be @@ -63,12 +64,16 @@ def __init__(self, cache_tools_list: bool): by calling `invalidate_tools_cache()`. You should set this to `True` if you know the server will not change its tools list, because it can drastically improve latency (by avoiding a round-trip to the server every time). + + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. """ self.session: ClientSession | None = None self.exit_stack: AsyncExitStack = AsyncExitStack() self._cleanup_lock: asyncio.Lock = asyncio.Lock() self.cache_tools_list = cache_tools_list + self.client_session_timeout_seconds = client_session_timeout_seconds + # The cache is always dirty at startup, so that we fetch tools at least once self._cache_dirty = True self._tools_list: list[MCPTool] | None = None @@ -101,7 +106,15 @@ async def connect(self): try: transport = await self.exit_stack.enter_async_context(self.create_streams()) read, write = transport - session = await self.exit_stack.enter_async_context(ClientSession(read, write)) + session = await self.exit_stack.enter_async_context( + ClientSession( + read, + write, + timedelta(seconds=self.client_session_timeout_seconds) + if self.client_session_timeout_seconds + else None, + ) + ) await session.initialize() self.session = session except Exception as e: @@ -183,6 +196,7 @@ def __init__( params: MCPServerStdioParams, cache_tools_list: bool = False, name: str | None = None, + client_session_timeout_seconds: float | None = 5, ): """Create a new MCP server based on the stdio transport. @@ -199,8 +213,9 @@ def __init__( improve latency (by avoiding a round-trip to the server every time). name: A readable name for the server. If not provided, we'll create one from the command. + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. """ - super().__init__(cache_tools_list) + super().__init__(cache_tools_list, client_session_timeout_seconds) self.params = StdioServerParameters( command=params["command"], @@ -257,6 +272,7 @@ def __init__( params: MCPServerSseParams, cache_tools_list: bool = False, name: str | None = None, + client_session_timeout_seconds: float | None = 5, ): """Create a new MCP server based on the HTTP with SSE transport. @@ -274,8 +290,10 @@ def __init__( name: A readable name for the server. If not provided, we'll create one from the URL. + + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. """ - super().__init__(cache_tools_list) + super().__init__(cache_tools_list, client_session_timeout_seconds) self.params = params self._name = name or f"sse: {self.params['url']}" diff --git a/tests/mcp/test_server_errors.py b/tests/mcp/test_server_errors.py index bdca7ce6..fbd8db17 100644 --- a/tests/mcp/test_server_errors.py +++ b/tests/mcp/test_server_errors.py @@ -6,7 +6,7 @@ class CrashingClientSessionServer(_MCPServerWithClientSession): def __init__(self): - super().__init__(cache_tools_list=False) + super().__init__(cache_tools_list=False, client_session_timeout_seconds=5) self.cleanup_called = False def create_streams(self): From e11b822d5f075fc32683c8df71ac9388a7df79e5 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:53:39 +0200 Subject: [PATCH 64/85] Fix stream error using LiteLLM (#589) In response to issue #587 , I implemented a solution to first check if `refusal` and `usage` attributes exist in the `delta` object. I added a unit test similar to `test_openai_chatcompletions_stream.py`. Let me know if I should change something. --------- Co-authored-by: Rohan Mehta --- src/agents/models/chatcmpl_stream_handler.py | 6 +- .../test_litellm_chatcompletions_stream.py | 286 ++++++++++++++++++ 2 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 tests/models/test_litellm_chatcompletions_stream.py diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py index 32f04acb..c71adeb5 100644 --- a/src/agents/models/chatcmpl_stream_handler.py +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -56,7 +56,8 @@ async def handle_stream( type="response.created", ) - usage = chunk.usage + # This is always set by the OpenAI API, but not by others e.g. LiteLLM + usage = chunk.usage if hasattr(chunk, "usage") else None if not chunk.choices or not chunk.choices[0].delta: continue @@ -112,7 +113,8 @@ async def handle_stream( state.text_content_index_and_output[1].text += delta.content # Handle refusals (model declines to answer) - if delta.refusal: + # This is always set by the OpenAI API, but not by others e.g. LiteLLM + if hasattr(delta, "refusal") and delta.refusal: if not state.refusal_content_index_and_output: # Initialize a content tracker for streaming refusal text state.refusal_content_index_and_output = ( diff --git a/tests/models/test_litellm_chatcompletions_stream.py b/tests/models/test_litellm_chatcompletions_stream.py new file mode 100644 index 00000000..80bd8ea2 --- /dev/null +++ b/tests/models/test_litellm_chatcompletions_stream.py @@ -0,0 +1,286 @@ +from collections.abc import AsyncIterator + +import pytest +from openai.types.chat.chat_completion_chunk import ( + ChatCompletionChunk, + Choice, + ChoiceDelta, + ChoiceDeltaToolCall, + ChoiceDeltaToolCallFunction, +) +from openai.types.completion_usage import CompletionUsage +from openai.types.responses import ( + Response, + ResponseFunctionToolCall, + ResponseOutputMessage, + ResponseOutputRefusal, + ResponseOutputText, +) + +from agents.extensions.models.litellm_model import LitellmModel +from agents.extensions.models.litellm_provider import LitellmProvider +from agents.model_settings import ModelSettings +from agents.models.interface import ModelTracing + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_stream_response_yields_events_for_text_content(monkeypatch) -> None: + """ + Validate that `stream_response` emits the correct sequence of events when + streaming a simple assistant message consisting of plain text content. + We simulate two chunks of text returned from the chat completion stream. + """ + # Create two chunks that will be emitted by the fake stream. + chunk1 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(content="He"))], + ) + # Mark last chunk with usage so stream_response knows this is final. + chunk2 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(content="llo"))], + usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + ) + + async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: + for c in (chunk1, chunk2): + yield c + + # Patch _fetch_response to inject our fake stream + async def patched_fetch_response(self, *args, **kwargs): + # `_fetch_response` is expected to return a Response skeleton and the async stream + resp = Response( + id="resp-id", + created_at=0, + model="fake-model", + object="response", + output=[], + tool_choice="none", + tools=[], + parallel_tool_calls=False, + ) + return resp, fake_stream() + + monkeypatch.setattr(LitellmModel, "_fetch_response", patched_fetch_response) + model = LitellmProvider().get_model("gpt-4") + output_events = [] + async for event in model.stream_response( + system_instructions=None, + input="", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ): + output_events.append(event) + # We expect a response.created, then a response.output_item.added, content part added, + # two content delta events (for "He" and "llo"), a content part done, the assistant message + # output_item.done, and finally response.completed. + # There should be 8 events in total. + assert len(output_events) == 8 + # First event indicates creation. + assert output_events[0].type == "response.created" + # The output item added and content part added events should mark the assistant message. + assert output_events[1].type == "response.output_item.added" + assert output_events[2].type == "response.content_part.added" + # Two text delta events. + assert output_events[3].type == "response.output_text.delta" + assert output_events[3].delta == "He" + assert output_events[4].type == "response.output_text.delta" + assert output_events[4].delta == "llo" + # After streaming, the content part and item should be marked done. + assert output_events[5].type == "response.content_part.done" + assert output_events[6].type == "response.output_item.done" + # Last event indicates completion of the stream. + assert output_events[7].type == "response.completed" + # The completed response should have one output message with full text. + completed_resp = output_events[7].response + assert isinstance(completed_resp.output[0], ResponseOutputMessage) + assert isinstance(completed_resp.output[0].content[0], ResponseOutputText) + assert completed_resp.output[0].content[0].text == "Hello" + + assert completed_resp.usage, "usage should not be None" + assert completed_resp.usage.input_tokens == 7 + assert completed_resp.usage.output_tokens == 5 + assert completed_resp.usage.total_tokens == 12 + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_stream_response_yields_events_for_refusal_content(monkeypatch) -> None: + """ + Validate that when the model streams a refusal string instead of normal content, + `stream_response` emits the appropriate sequence of events including + `response.refusal.delta` events for each chunk of the refusal message and + constructs a completed assistant message with a `ResponseOutputRefusal` part. + """ + # Simulate refusal text coming in two pieces, like content but using the `refusal` + # field on the delta rather than `content`. + chunk1 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(refusal="No"))], + ) + chunk2 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(refusal="Thanks"))], + usage=CompletionUsage(completion_tokens=2, prompt_tokens=2, total_tokens=4), + ) + + async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: + for c in (chunk1, chunk2): + yield c + + async def patched_fetch_response(self, *args, **kwargs): + resp = Response( + id="resp-id", + created_at=0, + model="fake-model", + object="response", + output=[], + tool_choice="none", + tools=[], + parallel_tool_calls=False, + ) + return resp, fake_stream() + + monkeypatch.setattr(LitellmModel, "_fetch_response", patched_fetch_response) + model = LitellmProvider().get_model("gpt-4") + output_events = [] + async for event in model.stream_response( + system_instructions=None, + input="", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ): + output_events.append(event) + # Expect sequence similar to text: created, output_item.added, content part added, + # two refusal delta events, content part done, output_item.done, completed. + assert len(output_events) == 8 + assert output_events[0].type == "response.created" + assert output_events[1].type == "response.output_item.added" + assert output_events[2].type == "response.content_part.added" + assert output_events[3].type == "response.refusal.delta" + assert output_events[3].delta == "No" + assert output_events[4].type == "response.refusal.delta" + assert output_events[4].delta == "Thanks" + assert output_events[5].type == "response.content_part.done" + assert output_events[6].type == "response.output_item.done" + assert output_events[7].type == "response.completed" + completed_resp = output_events[7].response + assert isinstance(completed_resp.output[0], ResponseOutputMessage) + refusal_part = completed_resp.output[0].content[0] + assert isinstance(refusal_part, ResponseOutputRefusal) + assert refusal_part.refusal == "NoThanks" + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_stream_response_yields_events_for_tool_call(monkeypatch) -> None: + """ + Validate that `stream_response` emits the correct sequence of events when + the model is streaming a function/tool call instead of plain text. + The function call will be split across two chunks. + """ + # Simulate a single tool call whose ID stays constant and function name/args built over chunks. + tool_call_delta1 = ChoiceDeltaToolCall( + index=0, + id="tool-id", + function=ChoiceDeltaToolCallFunction(name="my_", arguments="arg1"), + type="function", + ) + tool_call_delta2 = ChoiceDeltaToolCall( + index=0, + id="tool-id", + function=ChoiceDeltaToolCallFunction(name="func", arguments="arg2"), + type="function", + ) + chunk1 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(tool_calls=[tool_call_delta1]))], + ) + chunk2 = ChatCompletionChunk( + id="chunk-id", + created=1, + model="fake", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(tool_calls=[tool_call_delta2]))], + usage=CompletionUsage(completion_tokens=1, prompt_tokens=1, total_tokens=2), + ) + + async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: + for c in (chunk1, chunk2): + yield c + + async def patched_fetch_response(self, *args, **kwargs): + resp = Response( + id="resp-id", + created_at=0, + model="fake-model", + object="response", + output=[], + tool_choice="none", + tools=[], + parallel_tool_calls=False, + ) + return resp, fake_stream() + + monkeypatch.setattr(LitellmModel, "_fetch_response", patched_fetch_response) + model = LitellmProvider().get_model("gpt-4") + output_events = [] + async for event in model.stream_response( + system_instructions=None, + input="", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ): + output_events.append(event) + # Sequence should be: response.created, then after loop we expect function call-related events: + # one response.output_item.added for function call, a response.function_call_arguments.delta, + # a response.output_item.done, and finally response.completed. + assert output_events[0].type == "response.created" + # The next three events are about the tool call. + assert output_events[1].type == "response.output_item.added" + # The added item should be a ResponseFunctionToolCall. + added_fn = output_events[1].item + assert isinstance(added_fn, ResponseFunctionToolCall) + assert added_fn.name == "my_func" # Name should be concatenation of both chunks. + assert added_fn.arguments == "arg1arg2" + assert output_events[2].type == "response.function_call_arguments.delta" + assert output_events[2].delta == "arg1arg2" + assert output_events[3].type == "response.output_item.done" + assert output_events[4].type == "response.completed" + assert output_events[2].delta == "arg1arg2" + assert output_events[3].type == "response.output_item.done" + assert output_events[4].type == "response.completed" + assert added_fn.name == "my_func" # Name should be concatenation of both chunks. + assert added_fn.arguments == "arg1arg2" + assert output_events[2].type == "response.function_call_arguments.delta" + assert output_events[2].delta == "arg1arg2" + assert output_events[3].type == "response.output_item.done" + assert output_events[4].type == "response.completed" From 45eb41f1e668a45c6b53b64e06fa7db9eab4db46 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 24 Apr 2025 14:45:03 -0400 Subject: [PATCH 65/85] More tests for cancelling streamed run (#590) --- tests/fake_model.py | 5 +- tests/test_cancel_streaming.py | 98 +++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/tests/fake_model.py b/tests/fake_model.py index c6b3ba92..da3019a0 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -127,7 +127,10 @@ async def stream_response( ) -def get_response_obj(output: list[TResponseOutputItem], response_id: str | None = None) -> Response: +def get_response_obj( + output: list[TResponseOutputItem], + response_id: str | None = None, +) -> Response: return Response( id=response_id or "123", created_at=123, diff --git a/tests/test_cancel_streaming.py b/tests/test_cancel_streaming.py index 6d1807d7..3417a3c5 100644 --- a/tests/test_cancel_streaming.py +++ b/tests/test_cancel_streaming.py @@ -1,12 +1,15 @@ +import json + import pytest from agents import Agent, Runner from .fake_model import FakeModel +from .test_responses import get_function_tool, get_function_tool_call, get_text_message @pytest.mark.asyncio -async def test_joker_streamed_jokes_with_cancel(): +async def test_simple_streaming_with_cancel(): model = FakeModel() agent = Agent(name="Joker", model=model) @@ -16,7 +19,98 @@ async def test_joker_streamed_jokes_with_cancel(): async for _event in result.stream_events(): num_events += 1 - if num_events == 1: + if num_events == stop_after: result.cancel() assert num_events == 1, f"Expected {stop_after} visible events, but got {num_events}" + + +@pytest.mark.asyncio +async def test_multiple_events_streaming_with_cancel(): + model = FakeModel() + agent = Agent( + name="Joker", + model=model, + tools=[get_function_tool("foo", "tool_result")], + ) + + model.add_multiple_turn_outputs( + [ + # First turn: a message and tool call + [ + get_text_message("a_message"), + get_function_tool_call("foo", json.dumps({"a": "b"})), + ], + # Second turn: text message + [get_text_message("done")], + ] + ) + + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + num_events = 0 + stop_after = 2 + + async for _ in result.stream_events(): + num_events += 1 + if num_events == stop_after: + result.cancel() + + assert num_events == stop_after, f"Expected {stop_after} visible events, but got {num_events}" + + +@pytest.mark.asyncio +async def test_cancel_prevents_further_events(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + events = [] + async for event in result.stream_events(): + events.append(event) + result.cancel() + break # Cancel after first event + # Try to get more events after cancel + more_events = [e async for e in result.stream_events()] + assert len(events) == 1 + assert more_events == [], "No events should be yielded after cancel()" + + +@pytest.mark.asyncio +async def test_cancel_is_idempotent(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + events = [] + async for event in result.stream_events(): + events.append(event) + result.cancel() + result.cancel() # Call cancel again + break + # Should not raise or misbehave + assert len(events) == 1 + + +@pytest.mark.asyncio +async def test_cancel_before_streaming(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + result.cancel() # Cancel before streaming + events = [e async for e in result.stream_events()] + assert events == [], "No events should be yielded if cancel() is called before streaming." + + +@pytest.mark.asyncio +async def test_cancel_cleans_up_resources(): + model = FakeModel() + agent = Agent(name="Joker", model=model) + result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") + # Start streaming, then cancel + async for _ in result.stream_events(): + result.cancel() + break + # After cancel, queues should be empty and is_complete True + assert result.is_complete, "Result should be marked complete after cancel." + assert result._event_queue.empty(), "Event queue should be empty after cancel." + assert result._input_guardrail_queue.empty(), ( + "Input guardrail queue should be empty after cancel." + ) From 3bbc7c48cb9ee80ed4b3dfbbd55efddf7f77d6a3 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 24 Apr 2025 14:58:38 -0400 Subject: [PATCH 66/85] v0.0.13 (#593) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12ffff1f..c1ae467a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.12" +version = "0.0.13" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 4c6c370a..c6824a08 100644 --- a/uv.lock +++ b/uv.lock @@ -1482,7 +1482,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.12" +version = "0.0.13" source = { editable = "." } dependencies = [ { name = "griffe" }, From 8fd7773a5ef5121d9349edbabebc9522a2f3c4f0 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 24 Apr 2025 18:20:35 -0400 Subject: [PATCH 67/85] Add usage to context in streaming (#595) --- src/agents/result.py | 16 +++++++--------- src/agents/run.py | 3 +++ tests/fake_model.py | 19 ++++++++++++++++--- tests/test_result_cast.py | 3 ++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/agents/result.py b/src/agents/result.py index 1f1c7832..243db155 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -15,6 +15,7 @@ from .guardrail import InputGuardrailResult, OutputGuardrailResult from .items import ItemHelpers, ModelResponse, RunItem, TResponseInputItem from .logger import logger +from .run_context import RunContextWrapper from .stream_events import StreamEvent from .tracing import Trace from .util._pretty_print import pretty_print_result, pretty_print_run_result_streaming @@ -50,6 +51,9 @@ class RunResultBase(abc.ABC): output_guardrail_results: list[OutputGuardrailResult] """Guardrail results for the final output of the agent.""" + context_wrapper: RunContextWrapper[Any] + """The context wrapper for the agent run.""" + @property @abc.abstractmethod def last_agent(self) -> Agent[Any]: @@ -75,9 +79,7 @@ def final_output_as(self, cls: type[T], raise_if_incorrect_type: bool = False) - def to_input_list(self) -> list[TResponseInputItem]: """Creates a new input list, merging the original input with all the new items generated.""" - original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list( - self.input - ) + original_items: list[TResponseInputItem] = ItemHelpers.input_to_new_input_list(self.input) new_items = [item.to_input_item() for item in self.new_items] return original_items + new_items @@ -206,17 +208,13 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: def _check_errors(self): if self.current_turn > self.max_turns: - self._stored_exception = MaxTurnsExceeded( - f"Max turns ({self.max_turns}) exceeded" - ) + self._stored_exception = MaxTurnsExceeded(f"Max turns ({self.max_turns}) exceeded") # Fetch all the completed guardrail results from the queue and raise if needed while not self._input_guardrail_queue.empty(): guardrail_result = self._input_guardrail_queue.get_nowait() if guardrail_result.output.tripwire_triggered: - self._stored_exception = InputGuardrailTripwireTriggered( - guardrail_result - ) + self._stored_exception = InputGuardrailTripwireTriggered(guardrail_result) # Check the tasks for any exceptions if self._run_impl_task and self._run_impl_task.done(): diff --git a/src/agents/run.py b/src/agents/run.py index 2af558d5..849da7bf 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -270,6 +270,7 @@ async def run( _last_agent=current_agent, input_guardrail_results=input_guardrail_results, output_guardrail_results=output_guardrail_results, + context_wrapper=context_wrapper, ) elif isinstance(turn_result.next_step, NextStepHandoff): current_agent = cast(Agent[TContext], turn_result.next_step.new_agent) @@ -423,6 +424,7 @@ def run_streamed( output_guardrail_results=[], _current_agent_output_schema=output_schema, trace=new_trace, + context_wrapper=context_wrapper, ) # Kick off the actual agent loop in the background and return the streamed result object. @@ -696,6 +698,7 @@ async def _run_single_turn_streamed( usage=usage, response_id=event.response.id, ) + context_wrapper.usage.add(usage) streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event)) diff --git a/tests/fake_model.py b/tests/fake_model.py index da3019a0..32f919ef 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -3,7 +3,8 @@ from collections.abc import AsyncIterator from typing import Any -from openai.types.responses import Response, ResponseCompletedEvent +from openai.types.responses import Response, ResponseCompletedEvent, ResponseUsage +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents.agent_output import AgentOutputSchemaBase from agents.handoffs import Handoff @@ -33,6 +34,10 @@ def __init__( ) self.tracing_enabled = tracing_enabled self.last_turn_args: dict[str, Any] = {} + self.hardcoded_usage: Usage | None = None + + def set_hardcoded_usage(self, usage: Usage): + self.hardcoded_usage = usage def set_next_output(self, output: list[TResponseOutputItem] | Exception): self.turn_outputs.append(output) @@ -83,7 +88,7 @@ async def get_response( return ModelResponse( output=output, - usage=Usage(), + usage=self.hardcoded_usage or Usage(), response_id=None, ) @@ -123,13 +128,14 @@ async def stream_response( yield ResponseCompletedEvent( type="response.completed", - response=get_response_obj(output), + response=get_response_obj(output, usage=self.hardcoded_usage), ) def get_response_obj( output: list[TResponseOutputItem], response_id: str | None = None, + usage: Usage | None = None, ) -> Response: return Response( id=response_id or "123", @@ -141,4 +147,11 @@ def get_response_obj( tools=[], top_p=None, parallel_tool_calls=False, + usage=ResponseUsage( + input_tokens=usage.input_tokens if usage else 0, + output_tokens=usage.output_tokens if usage else 0, + total_tokens=usage.total_tokens if usage else 0, + input_tokens_details=InputTokensDetails(cached_tokens=0), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ), ) diff --git a/tests/test_result_cast.py b/tests/test_result_cast.py index ec17e327..c621e735 100644 --- a/tests/test_result_cast.py +++ b/tests/test_result_cast.py @@ -3,7 +3,7 @@ import pytest from pydantic import BaseModel -from agents import Agent, RunResult +from agents import Agent, RunContextWrapper, RunResult def create_run_result(final_output: Any) -> RunResult: @@ -15,6 +15,7 @@ def create_run_result(final_output: Any) -> RunResult: input_guardrail_results=[], output_guardrail_results=[], _last_agent=Agent(name="test"), + context_wrapper=RunContextWrapper(context=None), ) From aa197e1e1431ec8ad381575d0ea5a9667a9c2fcb Mon Sep 17 00:00:00 2001 From: Stefano Baccianella <4247706+mangiucugna@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:11:25 +0200 Subject: [PATCH 68/85] Make the TTS voices type exportable (#577) When using the voice agent in typed code, it is suboptimal and error prone to type the TTS voice variables in your code independently. With this commit we are making the type exportable so that developers can just use that and be future-proof. Example of usage in code: ``` DEFAULT_TTS_VOICE: TTSModelSettings.TTSVoice = "alloy" ... tts_voice: TTSModelSettings.TTSVoice = DEFAULT_TTS_VOICE ... output = await VoicePipeline( workflow=workflow, config=VoicePipelineConfig( tts_settings=TTSModelSettings( buffer_size=512, transform_data=transform_data, voice=tts_voice, instructions=tts_instructions, )) ).run(audio_input) ``` --------- Co-authored-by: Rohan Mehta --- src/agents/voice/__init__.py | 2 ++ src/agents/voice/model.py | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/agents/voice/__init__.py b/src/agents/voice/__init__.py index 499c064c..e11ee446 100644 --- a/src/agents/voice/__init__.py +++ b/src/agents/voice/__init__.py @@ -7,6 +7,7 @@ STTModelSettings, TTSModel, TTSModelSettings, + TTSVoice, VoiceModelProvider, ) from .models.openai_model_provider import OpenAIVoiceModelProvider @@ -30,6 +31,7 @@ "STTModelSettings", "TTSModel", "TTSModelSettings", + "TTSVoice", "VoiceModelProvider", "StreamedAudioResult", "SingleAgentVoiceWorkflow", diff --git a/src/agents/voice/model.py b/src/agents/voice/model.py index 220d4b48..c36a4de7 100644 --- a/src/agents/voice/model.py +++ b/src/agents/voice/model.py @@ -14,14 +14,13 @@ ) DEFAULT_TTS_BUFFER_SIZE = 120 +TTSVoice = Literal["alloy", "ash", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer"] +"""Exportable type for the TTSModelSettings voice enum""" @dataclass class TTSModelSettings: """Settings for a TTS model.""" - - voice: ( - Literal["alloy", "ash", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer"] | None - ) = None + voice: TTSVoice | None = None """ The voice to use for the TTS model. If not provided, the default voice for the respective model will be used. From 4187fba9556683c5ace4706ebd5dbe90cd14e315 Mon Sep 17 00:00:00 2001 From: N V J K Kartik <114015823+NVJKKartik@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:40:59 +0530 Subject: [PATCH 69/85] docs: add FutureAGI to tracing documentation (#592) Hi Team! This PR adds FutureAGI to the tracing documentation as one of the automatic tracing processors for OpenAI agents SDK. ![image](https://github.com/user-attachments/assets/4de3aadc-5efa-4712-8b02-decdedf8f8ef) --- docs/tracing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tracing.md b/docs/tracing.md index ea48a2e2..dd883c5a 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -101,6 +101,7 @@ To customize this default setup, to send traces to alternative or additional bac - [Weights & Biases](https://weave-docs.wandb.ai/guides/integrations/openai_agents) - [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) +- [Future AGI](https://docs.futureagi.com/future-agi/products/observability/auto-instrumentation/openai_agents) - [MLflow (self-hosted/OSS](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) - [MLflow (Databricks hosted](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) - [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) From db0ee9d5a5e7c24adfcc0a98a94540341e43c6e5 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 29 Apr 2025 16:12:24 -0700 Subject: [PATCH 70/85] Update litellm version (#626) Addresses https://github.com/openai/openai-agents-python/issues/614 --- pyproject.toml | 2 +- uv.lock | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c1ae467a..d5600976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ Repository = "https://github.com/openai/openai-agents-python" [project.optional-dependencies] voice = ["numpy>=2.2.0, <3; python_version>='3.10'", "websockets>=15.0, <16"] viz = ["graphviz>=0.17"] -litellm = ["litellm>=1.65.0, <2"] +litellm = ["litellm>=1.67.4.post1, <2"] [dependency-groups] dev = [ diff --git a/uv.lock b/uv.lock index c6824a08..636dbdd1 100644 --- a/uv.lock +++ b/uv.lock @@ -928,7 +928,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.66.1" +version = "1.67.4.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -943,10 +943,7 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/21/12562c37310254456afdd277454dac4d14b8b40796216e8a438a9e1c5e86/litellm-1.66.1.tar.gz", hash = "sha256:98f7add913e5eae2131dd412ee27532d9a309defd9dbb64f6c6c42ea8a2af068", size = 7203211 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/33/fdc4615ca621940406e3b0b303e900bc2868cfcd8c62c4a6f5e7d2f6a56c/litellm-1.66.1-py3-none-any.whl", hash = "sha256:1f601fea3f086c1d2d91be60b9db115082a2f3a697e4e0def72f8b9c777c7232", size = 7559553 }, -] +sdist = { url = "https://files.pythonhosted.org/packages/4d/89/bacf75633dd43d6c5536380fb652c4af25046c29f5c6e5fdb4e8fe5af505/litellm-1.67.4.post1.tar.gz", hash = "sha256:057f2505f82d8c3f83d705c375b0d1931de998b13e239a6b06e16ee351fda648", size = 7243930 } [[package]] name = "markdown" @@ -1535,7 +1532,7 @@ dev = [ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, - { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.65.0,<2" }, + { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.67.4.post1,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.76.0" }, From f9763495b86afcf0c421451a92200e1141fa8dcb Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 30 Apr 2025 08:15:35 -0700 Subject: [PATCH 71/85] 0.0.14 release (#635) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5600976..22b028ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.13" +version = "0.0.14" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 636dbdd1..87dd3cd2 100644 --- a/uv.lock +++ b/uv.lock @@ -1479,7 +1479,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.13" +version = "0.0.14" source = { editable = "." } dependencies = [ { name = "griffe" }, From 2c46dae37787e36f68db954f37addf2ddaa69458 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Wed, 14 May 2025 17:31:42 +0200 Subject: [PATCH 72/85] Fixed a bug for "detail" attribute in input image (#685) When an input image is given as input, the code tries to access the 'detail' key, that may not be present as noted in #159. With this pull request, now it tries to access the key, otherwise set the value to `None`. @pakrym-oai or @rm-openai let me know if you want any changes. --- src/agents/models/chatcmpl_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py index 613a3745..1d599e8c 100644 --- a/src/agents/models/chatcmpl_converter.py +++ b/src/agents/models/chatcmpl_converter.py @@ -234,7 +234,7 @@ def extract_all_content( type="image_url", image_url={ "url": casted_image_param["image_url"], - "detail": casted_image_param["detail"], + "detail": casted_image_param.get("detail", "auto"), }, ) ) From 1994f9d4c4bb807fd5460919b4352b715ef1324a Mon Sep 17 00:00:00 2001 From: Ashok Saravanan <90977640+AshokSaravanan222@users.noreply.github.com> Date: Wed, 14 May 2025 11:34:27 -0500 Subject: [PATCH 73/85] feat: pass extra_body through to LiteLLM acompletion (#638) **Purpose** Allow arbitrary `extra_body` parameters (e.g. `cached_content`) to be forwarded into the LiteLLM call. Useful for context caching in Gemini models ([docs](https://ai.google.dev/gemini-api/docs/caching?lang=python)). **Example usage** ```python import os from agents import Agent, ModelSettings from agents.extensions.models.litellm_model import LitellmModel cache_name = "cachedContents/34jopukfx5di" # previously stored context gemini_model = LitellmModel( model="gemini/gemini-1.5-flash-002", api_key=os.getenv("GOOGLE_API_KEY") ) agent = Agent( name="Cached Gemini Agent", model=gemini_model, model_settings=ModelSettings( extra_body={"cached_content": cache_name} ) ) --- src/agents/extensions/models/litellm_model.py | 2 + tests/models/test_litellm_extra_body.py | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/models/test_litellm_extra_body.py diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index dc672acd..d3b25a19 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -269,6 +269,8 @@ async def _fetch_response( extra_kwargs["extra_query"] = model_settings.extra_query if model_settings.metadata: extra_kwargs["metadata"] = model_settings.metadata + if model_settings.extra_body and isinstance(model_settings.extra_body, dict): + extra_kwargs.update(model_settings.extra_body) ret = await litellm.acompletion( model=self.model, diff --git a/tests/models/test_litellm_extra_body.py b/tests/models/test_litellm_extra_body.py new file mode 100644 index 00000000..ac56c25c --- /dev/null +++ b/tests/models/test_litellm_extra_body.py @@ -0,0 +1,45 @@ +import litellm +import pytest +from litellm.types.utils import Choices, Message, ModelResponse, Usage + +from agents.extensions.models.litellm_model import LitellmModel +from agents.model_settings import ModelSettings +from agents.models.interface import ModelTracing + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_body_is_forwarded(monkeypatch): + """ + Forward `extra_body` entries into litellm.acompletion kwargs. + + This ensures that user-provided parameters (e.g. cached_content) + arrive alongside default arguments. + """ + captured: dict[str, object] = {} + + async def fake_acompletion(model, messages=None, **kwargs): + captured.update(kwargs) + msg = Message(role="assistant", content="ok") + choice = Choices(index=0, message=msg) + return ModelResponse(choices=[choice], usage=Usage(0, 0, 0)) + + monkeypatch.setattr(litellm, "acompletion", fake_acompletion) + settings = ModelSettings( + temperature=0.1, + extra_body={"cached_content": "some_cache", "foo": 123} + ) + model = LitellmModel(model="test-model") + + await model.get_response( + system_instructions=None, + input=[], + model_settings=settings, + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + + assert {"cached_content": "some_cache", "foo": 123}.items() <= captured.items() From 02b6e7013cf00de92b453610f459be40f344e6a8 Mon Sep 17 00:00:00 2001 From: leohpark <135409779+leohpark@users.noreply.github.com> Date: Wed, 14 May 2025 09:37:06 -0700 Subject: [PATCH 74/85] Update search_agent.py (#677) Added missing word "be" in prompt instructions. This is unlikely to change the agent functionality in most cases, but optimal clarity in prompt language is a best practice. --- examples/research_bot/agents/search_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/research_bot/agents/search_agent.py b/examples/research_bot/agents/search_agent.py index 0212ce5b..61f91701 100644 --- a/examples/research_bot/agents/search_agent.py +++ b/examples/research_bot/agents/search_agent.py @@ -3,7 +3,7 @@ INSTRUCTIONS = ( "You are a research assistant. Given a search term, you search the web for that term and " - "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 " + "produce a concise summary of the results. The summary must be 2-3 paragraphs and less than 300 " "words. Capture the main points. Write succinctly, no need to have complete sentences or good " "grammar. This will be consumed by someone synthesizing a report, so its vital you capture the " "essence and ignore any fluff. Do not include any additional commentary other than the summary " From 1847008e0f247e5468d691e4e9d780f984db9a18 Mon Sep 17 00:00:00 2001 From: Akshit97 Date: Thu, 15 May 2025 00:15:14 +0530 Subject: [PATCH 75/85] feat: Streamable HTTP support (#643) Co-authored-by: aagarwal25 --- examples/mcp/streamablehttp_example/README.md | 13 +++ examples/mcp/streamablehttp_example/main.py | 83 ++++++++++++++ examples/mcp/streamablehttp_example/server.py | 33 ++++++ pyproject.toml | 2 +- src/agents/mcp/__init__.py | 4 + src/agents/mcp/server.py | 106 ++++++++++++++++-- uv.lock | 20 +++- 7 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 examples/mcp/streamablehttp_example/README.md create mode 100644 examples/mcp/streamablehttp_example/main.py create mode 100644 examples/mcp/streamablehttp_example/server.py diff --git a/examples/mcp/streamablehttp_example/README.md b/examples/mcp/streamablehttp_example/README.md new file mode 100644 index 00000000..a07fe19b --- /dev/null +++ b/examples/mcp/streamablehttp_example/README.md @@ -0,0 +1,13 @@ +# MCP Streamable HTTP Example + +This example uses a local Streamable HTTP server in [server.py](server.py). + +Run the example via: + +``` +uv run python examples/mcp/streamablehttp_example/main.py +``` + +## Details + +The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The server runs in a sub-process at `https://localhost:8000/mcp`. diff --git a/examples/mcp/streamablehttp_example/main.py b/examples/mcp/streamablehttp_example/main.py new file mode 100644 index 00000000..cc95e798 --- /dev/null +++ b/examples/mcp/streamablehttp_example/main.py @@ -0,0 +1,83 @@ +import asyncio +import os +import shutil +import subprocess +import time +from typing import Any + +from agents import Agent, Runner, gen_trace_id, trace +from agents.mcp import MCPServer, MCPServerStreamableHttp +from agents.model_settings import ModelSettings + + +async def run(mcp_server: MCPServer): + agent = Agent( + name="Assistant", + instructions="Use the tools to answer the questions.", + mcp_servers=[mcp_server], + model_settings=ModelSettings(tool_choice="required"), + ) + + # Use the `add` tool to add two numbers + message = "Add these numbers: 7 and 22." + print(f"Running: {message}") + result = await Runner.run(starting_agent=agent, input=message) + print(result.final_output) + + # Run the `get_weather` tool + message = "What's the weather in Tokyo?" + print(f"\n\nRunning: {message}") + result = await Runner.run(starting_agent=agent, input=message) + print(result.final_output) + + # Run the `get_secret_word` tool + message = "What's the secret word?" + print(f"\n\nRunning: {message}") + result = await Runner.run(starting_agent=agent, input=message) + print(result.final_output) + + +async def main(): + async with MCPServerStreamableHttp( + name="Streamable HTTP Python Server", + params={ + "url": "http://localhost:8000/mcp", + }, + ) as server: + trace_id = gen_trace_id() + with trace(workflow_name="Streamable HTTP Example", trace_id=trace_id): + print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n") + await run(server) + + +if __name__ == "__main__": + # Let's make sure the user has uv installed + if not shutil.which("uv"): + raise RuntimeError( + "uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/" + ) + + # We'll run the Streamable HTTP server in a subprocess. Usually this would be a remote server, but for this + # demo, we'll run it locally at http://localhost:8000/mcp + process: subprocess.Popen[Any] | None = None + try: + this_dir = os.path.dirname(os.path.abspath(__file__)) + server_file = os.path.join(this_dir, "server.py") + + print("Starting Streamable HTTP server at http://localhost:8000/mcp ...") + + # Run `uv run server.py` to start the Streamable HTTP server + process = subprocess.Popen(["uv", "run", server_file]) + # Give it 3 seconds to start + time.sleep(3) + + print("Streamable HTTP server started. Running example...\n\n") + except Exception as e: + print(f"Error starting Streamable HTTP server: {e}") + exit(1) + + try: + asyncio.run(main()) + finally: + if process: + process.terminate() diff --git a/examples/mcp/streamablehttp_example/server.py b/examples/mcp/streamablehttp_example/server.py new file mode 100644 index 00000000..d8f83965 --- /dev/null +++ b/examples/mcp/streamablehttp_example/server.py @@ -0,0 +1,33 @@ +import random + +import requests +from mcp.server.fastmcp import FastMCP + +# Create server +mcp = FastMCP("Echo Server") + + +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers""" + print(f"[debug-server] add({a}, {b})") + return a + b + + +@mcp.tool() +def get_secret_word() -> str: + print("[debug-server] get_secret_word()") + return random.choice(["apple", "banana", "cherry"]) + + +@mcp.tool() +def get_current_weather(city: str) -> str: + print(f"[debug-server] get_current_weather({city})") + + endpoint = "https://wttr.in" + response = requests.get(f"{endpoint}/{city}") + return response.text + + +if __name__ == "__main__": + mcp.run(transport="streamable-http") diff --git a/pyproject.toml b/pyproject.toml index 22b028ae..87a707d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "typing-extensions>=4.12.2, <5", "requests>=2.0, <3", "types-requests>=2.0, <3", - "mcp>=1.6.0, <2; python_version >= '3.10'", + "mcp>=1.8.0, <2; python_version >= '3.10'", ] classifiers = [ "Typing :: Typed", diff --git a/src/agents/mcp/__init__.py b/src/agents/mcp/__init__.py index 1a72a89f..d4eb8fa6 100644 --- a/src/agents/mcp/__init__.py +++ b/src/agents/mcp/__init__.py @@ -5,6 +5,8 @@ MCPServerSseParams, MCPServerStdio, MCPServerStdioParams, + MCPServerStreamableHttp, + MCPServerStreamableHttpParams, ) except ImportError: pass @@ -17,5 +19,7 @@ "MCPServerSseParams", "MCPServerStdio", "MCPServerStdioParams", + "MCPServerStreamableHttp", + "MCPServerStreamableHttpParams", "MCPUtil", ] diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 9916c92b..c5255ead 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -10,7 +10,9 @@ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from mcp import ClientSession, StdioServerParameters, Tool as MCPTool, stdio_client from mcp.client.sse import sse_client -from mcp.types import CallToolResult, JSONRPCMessage +from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client +from mcp.shared.message import SessionMessage +from mcp.types import CallToolResult from typing_extensions import NotRequired, TypedDict from ..exceptions import UserError @@ -83,8 +85,9 @@ def create_streams( self, ) -> AbstractAsyncContextManager[ tuple[ - MemoryObjectReceiveStream[JSONRPCMessage | Exception], - MemoryObjectSendStream[JSONRPCMessage], + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None ] ]: """Create the streams for the server.""" @@ -105,7 +108,11 @@ async def connect(self): """Connect to the server.""" try: transport = await self.exit_stack.enter_async_context(self.create_streams()) - read, write = transport + # streamablehttp_client returns (read, write, get_session_id) + # sse_client returns (read, write) + + read, write, *_ = transport + session = await self.exit_stack.enter_async_context( ClientSession( read, @@ -232,8 +239,9 @@ def create_streams( self, ) -> AbstractAsyncContextManager[ tuple[ - MemoryObjectReceiveStream[JSONRPCMessage | Exception], - MemoryObjectSendStream[JSONRPCMessage], + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None ] ]: """Create the streams for the server.""" @@ -302,8 +310,9 @@ def create_streams( self, ) -> AbstractAsyncContextManager[ tuple[ - MemoryObjectReceiveStream[JSONRPCMessage | Exception], - MemoryObjectSendStream[JSONRPCMessage], + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None ] ]: """Create the streams for the server.""" @@ -318,3 +327,84 @@ def create_streams( def name(self) -> str: """A readable name for the server.""" return self._name + + +class MCPServerStreamableHttpParams(TypedDict): + """Mirrors the params in`mcp.client.streamable_http.streamablehttp_client`.""" + + url: str + """The URL of the server.""" + + headers: NotRequired[dict[str, str]] + """The headers to send to the server.""" + + timeout: NotRequired[timedelta] + """The timeout for the HTTP request. Defaults to 5 seconds.""" + + sse_read_timeout: NotRequired[timedelta] + """The timeout for the SSE connection, in seconds. Defaults to 5 minutes.""" + + terminate_on_close: NotRequired[bool] + """Terminate on close""" + + +class MCPServerStreamableHttp(_MCPServerWithClientSession): + """MCP server implementation that uses the Streamable HTTP transport. See the [spec] + (https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) + for details. + """ + + def __init__( + self, + params: MCPServerStreamableHttpParams, + cache_tools_list: bool = False, + name: str | None = None, + client_session_timeout_seconds: float | None = 5, + ): + """Create a new MCP server based on the Streamable HTTP transport. + + Args: + params: The params that configure the server. This includes the URL of the server, + the headers to send to the server, the timeout for the HTTP request, and the + timeout for the Streamable HTTP connection and whether we need to + terminate on close. + + cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be + cached and only fetched from the server once. If `False`, the tools list will be + fetched from the server on each call to `list_tools()`. The cache can be + invalidated by calling `invalidate_tools_cache()`. You should set this to `True` + if you know the server will not change its tools list, because it can drastically + improve latency (by avoiding a round-trip to the server every time). + + name: A readable name for the server. If not provided, we'll create one from the + URL. + + client_session_timeout_seconds: the read timeout passed to the MCP ClientSession. + """ + super().__init__(cache_tools_list, client_session_timeout_seconds) + + self.params = params + self._name = name or f"streamable_http: {self.params['url']}" + + def create_streams( + self, + ) -> AbstractAsyncContextManager[ + tuple[ + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback | None + ] + ]: + """Create the streams for the server.""" + return streamablehttp_client( + url=self.params["url"], + headers=self.params.get("headers", None), + timeout=self.params.get("timeout", timedelta(seconds=30)), + sse_read_timeout=self.params.get("sse_read_timeout", timedelta(seconds=60 * 5)), + terminate_on_close=self.params.get("terminate_on_close", True) + ) + + @property + def name(self) -> str: + """A readable name for the server.""" + return self._name diff --git a/uv.lock b/uv.lock index 87dd3cd2..6ccc1996 100644 --- a/uv.lock +++ b/uv.lock @@ -1047,7 +1047,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.6.0" +version = "1.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, @@ -1055,13 +1055,14 @@ dependencies = [ { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, { name = "pydantic", marker = "python_full_version >= '3.10'" }, { name = "pydantic-settings", marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", marker = "python_full_version >= '3.10'" }, { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, { name = "starlette", marker = "python_full_version >= '3.10'" }, - { name = "uvicorn", marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/13/16b712e8a3be6a736b411df2fc6b4e75eb1d3e99b1cd57a3a1decf17f612/mcp-1.8.1.tar.gz", hash = "sha256:ec0646271d93749f784d2316fb5fe6102fb0d1be788ec70a9e2517e8f2722c0e", size = 265605 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, + { url = "https://files.pythonhosted.org/packages/1c/5d/91cf0d40e40ae9ecf8d4004e0f9611eea86085aa0b5505493e0ff53972da/mcp-1.8.1-py3-none-any.whl", hash = "sha256:948e03783859fa35abe05b9b6c0a1d5519be452fc079dc8d7f682549591c1770", size = 119761 }, ] [[package]] @@ -1533,7 +1534,7 @@ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.67.4.post1,<2" }, - { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.6.0,<2" }, + { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.8.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=1.76.0" }, { name = "pydantic", specifier = ">=2.10,<3" }, @@ -2085,6 +2086,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + [[package]] name = "python-xlib" version = "0.33" From 5fe096df67cf7ada53f0bd38954b7f12743b790b Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Thu, 15 May 2025 18:41:00 -0400 Subject: [PATCH 76/85] v0.0.15 (#701) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87a707d3..672258c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.14" +version = "0.0.15" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" From c282324d9519b037ced8abe3678c86f7dd344a97 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Sun, 18 May 2025 10:24:24 -0700 Subject: [PATCH 77/85] Create AGENTS.md (#707) Adding an AGENTS.md file for Codex use --- AGENTS.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ff37db32 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,69 @@ +Welcome to the OpenAI Agents SDK repository. This file contains the main points for new contributors. + +## Repository overview + +- **Source code**: `src/agents/` contains the implementation. +- **Tests**: `tests/` with a short guide in `tests/README.md`. +- **Examples**: under `examples/`. +- **Documentation**: markdown pages live in `docs/` with `mkdocs.yml` controlling the site. +- **Utilities**: developer commands are defined in the `Makefile`. +- **PR template**: `.github/PULL_REQUEST_TEMPLATE/pull_request_template.md` describes the information every PR must include. + +## Local workflow + +1. Format, lint and type‑check your changes: + + ```bash + make format + make lint + make mypy + ``` + +2. Run the tests: + + ```bash + make tests + ``` + + To run a single test, use `uv run pytest -s -k `. + +3. Build the documentation (optional but recommended for docs changes): + + ```bash + make build-docs + ``` + + Coverage can be generated with `make coverage`. + +## Snapshot tests + +Some tests rely on inline snapshots. See `tests/README.md` for details on updating them: + +```bash +make snapshots-fix # update existing snapshots +make snapshots-create # create new snapshots +``` + +Run `make tests` again after updating snapshots to ensure they pass. + +## Style notes + +- Write comments as full sentences and end them with a period. + +## Pull request expectations + +PRs should use the template located at `.github/PULL_REQUEST_TEMPLATE/pull_request_template.md`. Provide a summary, test plan and issue number if applicable, then check that: + +- New tests are added when needed. +- Documentation is updated. +- `make lint` and `make format` have been run. +- The full test suite passes. + +Commit messages should be concise and written in the imperative mood. Small, focused commits are preferred. + +## What reviewers look for + +- Tests covering new behaviour. +- Consistent style: code formatted with `ruff format`, imports sorted, and type hints passing `mypy`. +- Clear documentation for any public API changes. +- Clean history and a helpful PR description. From 003cbfe5f5820cd73ad3adfbae56c054e3cb73ca Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Sun, 18 May 2025 19:25:08 +0200 Subject: [PATCH 78/85] Added mcp 'instructions' attribute to the server (#706) Added the `instructions` attribute to the MCP servers to solve #704 . Let me know if you want to add an example to the documentation. --- src/agents/mcp/server.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index c5255ead..414b517a 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -12,7 +12,7 @@ from mcp.client.sse import sse_client from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client from mcp.shared.message import SessionMessage -from mcp.types import CallToolResult +from mcp.types import CallToolResult, InitializeResult from typing_extensions import NotRequired, TypedDict from ..exceptions import UserError @@ -73,6 +73,7 @@ def __init__(self, cache_tools_list: bool, client_session_timeout_seconds: float self.exit_stack: AsyncExitStack = AsyncExitStack() self._cleanup_lock: asyncio.Lock = asyncio.Lock() self.cache_tools_list = cache_tools_list + self.server_initialize_result: InitializeResult | None = None self.client_session_timeout_seconds = client_session_timeout_seconds @@ -122,7 +123,8 @@ async def connect(self): else None, ) ) - await session.initialize() + server_result = await session.initialize() + self.server_initialize_result = server_result self.session = session except Exception as e: logger.error(f"Error initializing MCP server: {e}") From 428c9a65bf0c17198d9a2a616159b9eb8badb2b6 Mon Sep 17 00:00:00 2001 From: franz101 Date: Mon, 19 May 2025 21:41:11 +0200 Subject: [PATCH 79/85] Add Galileo to external tracing processors list (#662) --- docs/tracing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tracing.md b/docs/tracing.md index dd883c5a..4a9c1bd9 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -115,3 +115,4 @@ To customize this default setup, to send traces to alternative or additional bac - [Langfuse](https://langfuse.com/docs/integrations/openaiagentssdk/openai-agents) - [Langtrace](https://docs.langtrace.ai/supported-integrations/llm-frameworks/openai-agents-sdk) - [Okahu-Monocle](https://github.com/monocle2ai/monocle) +- [Galileo](https://v2docs.galileo.ai/integrations/openai-agent-integration#openai-agent-integration) From 466b44df180718a5d53c45293db2f57b6e719f95 Mon Sep 17 00:00:00 2001 From: WJPBProjects <76624567+WJPBProjects@users.noreply.github.com> Date: Tue, 20 May 2025 18:23:56 +0100 Subject: [PATCH 80/85] Dev/add usage details to Usage class (#726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR to enhance the `Usage` object and related logic, to support more granular token accounting, matching the details available in the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) . Specifically, it: - Adds `input_tokens_details` and `output_tokens_details` fields to the `Usage` dataclass, storing detailed token breakdowns (e.g., `cached_tokens`, `reasoning_tokens`). - Flows this change through - Updates and extends tests to match - Adds a test for the Usage.add method ### Motivation - Aligns the SDK’s usage with the latest OpenAI responses API Usage object - Supports downstream use cases that require fine-grained token usage data (e.g., billing, analytics, optimization) requested by startups --------- Co-authored-by: Wulfie Bain --- src/agents/extensions/models/litellm_model.py | 11 ++++ src/agents/models/openai_chatcompletions.py | 15 +++++- src/agents/models/openai_responses.py | 2 + src/agents/run.py | 2 + src/agents/usage.py | 22 +++++++- .../test_litellm_chatcompletions_stream.py | 16 +++++- tests/test_extra_headers.py | 20 ++++--- tests/test_openai_chatcompletions.py | 17 +++++- tests/test_openai_chatcompletions_stream.py | 16 +++++- tests/test_responses_tracing.py | 20 ++++++- tests/test_usage.py | 52 +++++++++++++++++++ 11 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 tests/test_usage.py diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index d3b25a19..ffb2c3c1 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -6,6 +6,7 @@ from typing import Any, Literal, cast, overload import litellm.types +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents.exceptions import ModelBehaviorError @@ -107,6 +108,16 @@ async def get_response( input_tokens=response_usage.prompt_tokens, output_tokens=response_usage.completion_tokens, total_tokens=response_usage.total_tokens, + input_tokens_details=InputTokensDetails( + cached_tokens=getattr( + response_usage.prompt_tokens_details, "cached_tokens", 0 + ) + ), + output_tokens_details=OutputTokensDetails( + reasoning_tokens=getattr( + response_usage.completion_tokens_details, "reasoning_tokens", 0 + ) + ), ) if response.usage else Usage() diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 89619f83..4465ff2f 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -9,6 +9,7 @@ from openai.types import ChatModel from openai.types.chat import ChatCompletion, ChatCompletionChunk from openai.types.responses import Response +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from .. import _debug from ..agent_output import AgentOutputSchemaBase @@ -83,6 +84,18 @@ async def get_response( input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.completion_tokens, total_tokens=response.usage.total_tokens, + input_tokens_details=InputTokensDetails( + cached_tokens=getattr( + response.usage.prompt_tokens_details, "cached_tokens", 0 + ) + or 0, + ), + output_tokens_details=OutputTokensDetails( + reasoning_tokens=getattr( + response.usage.completion_tokens_details, "reasoning_tokens", 0 + ) + or 0, + ), ) if response.usage else Usage() @@ -252,7 +265,7 @@ async def _fetch_response( stream_options=self._non_null_or_not_given(stream_options), store=self._non_null_or_not_given(store), reasoning_effort=self._non_null_or_not_given(reasoning_effort), - extra_headers={ **HEADERS, **(model_settings.extra_headers or {}) }, + extra_headers={**HEADERS, **(model_settings.extra_headers or {})}, extra_query=model_settings.extra_query, extra_body=model_settings.extra_body, metadata=self._non_null_or_not_given(model_settings.metadata), diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index c1ff85b9..6ec8f8f7 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -98,6 +98,8 @@ async def get_response( input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens, total_tokens=response.usage.total_tokens, + input_tokens_details=response.usage.input_tokens_details, + output_tokens_details=response.usage.output_tokens_details, ) if response.usage else Usage() diff --git a/src/agents/run.py b/src/agents/run.py index 849da7bf..b196c3bf 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -689,6 +689,8 @@ async def _run_single_turn_streamed( input_tokens=event.response.usage.input_tokens, output_tokens=event.response.usage.output_tokens, total_tokens=event.response.usage.total_tokens, + input_tokens_details=event.response.usage.input_tokens_details, + output_tokens_details=event.response.usage.output_tokens_details, ) if event.response.usage else Usage() diff --git a/src/agents/usage.py b/src/agents/usage.py index 23d989b4..843f6293 100644 --- a/src/agents/usage.py +++ b/src/agents/usage.py @@ -1,4 +1,6 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field + +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails @dataclass @@ -9,9 +11,18 @@ class Usage: input_tokens: int = 0 """Total input tokens sent, across all requests.""" + input_tokens_details: InputTokensDetails = field( + default_factory=lambda: InputTokensDetails(cached_tokens=0) + ) + """Details about the input tokens, matching responses API usage details.""" output_tokens: int = 0 """Total output tokens received, across all requests.""" + output_tokens_details: OutputTokensDetails = field( + default_factory=lambda: OutputTokensDetails(reasoning_tokens=0) + ) + """Details about the output tokens, matching responses API usage details.""" + total_tokens: int = 0 """Total tokens sent and received, across all requests.""" @@ -20,3 +31,12 @@ def add(self, other: "Usage") -> None: self.input_tokens += other.input_tokens if other.input_tokens else 0 self.output_tokens += other.output_tokens if other.output_tokens else 0 self.total_tokens += other.total_tokens if other.total_tokens else 0 + self.input_tokens_details = InputTokensDetails( + cached_tokens=self.input_tokens_details.cached_tokens + + other.input_tokens_details.cached_tokens + ) + + self.output_tokens_details = OutputTokensDetails( + reasoning_tokens=self.output_tokens_details.reasoning_tokens + + other.output_tokens_details.reasoning_tokens + ) diff --git a/tests/models/test_litellm_chatcompletions_stream.py b/tests/models/test_litellm_chatcompletions_stream.py index 80bd8ea2..06e46b39 100644 --- a/tests/models/test_litellm_chatcompletions_stream.py +++ b/tests/models/test_litellm_chatcompletions_stream.py @@ -8,7 +8,11 @@ ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction, ) -from openai.types.completion_usage import CompletionUsage +from openai.types.completion_usage import ( + CompletionTokensDetails, + CompletionUsage, + PromptTokensDetails, +) from openai.types.responses import ( Response, ResponseFunctionToolCall, @@ -46,7 +50,13 @@ async def test_stream_response_yields_events_for_text_content(monkeypatch) -> No model="fake", object="chat.completion.chunk", choices=[Choice(index=0, delta=ChoiceDelta(content="llo"))], - usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + usage=CompletionUsage( + completion_tokens=5, + prompt_tokens=7, + total_tokens=12, + completion_tokens_details=CompletionTokensDetails(reasoning_tokens=2), + prompt_tokens_details=PromptTokensDetails(cached_tokens=6), + ), ) async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: @@ -112,6 +122,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert completed_resp.usage.input_tokens == 7 assert completed_resp.usage.output_tokens == 5 assert completed_resp.usage.total_tokens == 12 + assert completed_resp.usage.input_tokens_details.cached_tokens == 6 + assert completed_resp.usage.output_tokens_details.reasoning_tokens == 2 @pytest.mark.allow_call_model_methods diff --git a/tests/test_extra_headers.py b/tests/test_extra_headers.py index f29c2540..a6af3007 100644 --- a/tests/test_extra_headers.py +++ b/tests/test_extra_headers.py @@ -1,6 +1,7 @@ import pytest from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents import ModelSettings, ModelTracing, OpenAIChatCompletionsModel, OpenAIResponsesModel @@ -17,21 +18,29 @@ class DummyResponses: async def create(self, **kwargs): nonlocal called_kwargs called_kwargs = kwargs + class DummyResponse: id = "dummy" output = [] usage = type( - "Usage", (), {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} + "Usage", + (), + { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "input_tokens_details": InputTokensDetails(cached_tokens=0), + "output_tokens_details": OutputTokensDetails(reasoning_tokens=0), + }, )() + return DummyResponse() class DummyClient: def __init__(self): self.responses = DummyResponses() - - - model = OpenAIResponsesModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + model = OpenAIResponsesModel(model="gpt-4", openai_client=DummyClient()) # type: ignore extra_headers = {"X-Test-Header": "test-value"} await model.get_response( system_instructions=None, @@ -47,7 +56,6 @@ def __init__(self): assert called_kwargs["extra_headers"]["X-Test-Header"] == "test-value" - @pytest.mark.allow_call_model_methods @pytest.mark.asyncio async def test_extra_headers_passed_to_openai_client(): @@ -76,7 +84,7 @@ def __init__(self): self.chat = type("_Chat", (), {"completions": DummyCompletions()})() self.base_url = "https://api.openai.com" - model = OpenAIChatCompletionsModel(model="gpt-4", openai_client=DummyClient()) # type: ignore + model = OpenAIChatCompletionsModel(model="gpt-4", openai_client=DummyClient()) # type: ignore extra_headers = {"X-Test-Header": "test-value"} await model.get_response( system_instructions=None, diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index ba3ec68d..ba4605d0 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -13,7 +13,10 @@ ChatCompletionMessageToolCall, Function, ) -from openai.types.completion_usage import CompletionUsage +from openai.types.completion_usage import ( + CompletionUsage, + PromptTokensDetails, +) from openai.types.responses import ( Response, ResponseFunctionToolCall, @@ -51,7 +54,13 @@ async def test_get_response_with_text_message(monkeypatch) -> None: model="fake", object="chat.completion", choices=[choice], - usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + usage=CompletionUsage( + completion_tokens=5, + prompt_tokens=7, + total_tokens=12, + # completion_tokens_details left blank to test default + prompt_tokens_details=PromptTokensDetails(cached_tokens=3), + ), ) async def patched_fetch_response(self, *args, **kwargs): @@ -81,6 +90,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert resp.usage.input_tokens == 7 assert resp.usage.output_tokens == 5 assert resp.usage.total_tokens == 12 + assert resp.usage.input_tokens_details.cached_tokens == 3 + assert resp.usage.output_tokens_details.reasoning_tokens == 0 assert resp.response_id is None @@ -127,6 +138,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert resp.usage.requests == 0 assert resp.usage.input_tokens == 0 assert resp.usage.output_tokens == 0 + assert resp.usage.input_tokens_details.cached_tokens == 0 + assert resp.usage.output_tokens_details.reasoning_tokens == 0 @pytest.mark.allow_call_model_methods diff --git a/tests/test_openai_chatcompletions_stream.py b/tests/test_openai_chatcompletions_stream.py index b82f2430..5c8bb9e3 100644 --- a/tests/test_openai_chatcompletions_stream.py +++ b/tests/test_openai_chatcompletions_stream.py @@ -8,7 +8,11 @@ ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction, ) -from openai.types.completion_usage import CompletionUsage +from openai.types.completion_usage import ( + CompletionTokensDetails, + CompletionUsage, + PromptTokensDetails, +) from openai.types.responses import ( Response, ResponseFunctionToolCall, @@ -46,7 +50,13 @@ async def test_stream_response_yields_events_for_text_content(monkeypatch) -> No model="fake", object="chat.completion.chunk", choices=[Choice(index=0, delta=ChoiceDelta(content="llo"))], - usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12), + usage=CompletionUsage( + completion_tokens=5, + prompt_tokens=7, + total_tokens=12, + prompt_tokens_details=PromptTokensDetails(cached_tokens=2), + completion_tokens_details=CompletionTokensDetails(reasoning_tokens=3), + ), ) async def fake_stream() -> AsyncIterator[ChatCompletionChunk]: @@ -112,6 +122,8 @@ async def patched_fetch_response(self, *args, **kwargs): assert completed_resp.usage.input_tokens == 7 assert completed_resp.usage.output_tokens == 5 assert completed_resp.usage.total_tokens == 12 + assert completed_resp.usage.input_tokens_details.cached_tokens == 2 + assert completed_resp.usage.output_tokens_details.reasoning_tokens == 3 @pytest.mark.allow_call_model_methods diff --git a/tests/test_responses_tracing.py b/tests/test_responses_tracing.py index 0bc97a95..dfac74bb 100644 --- a/tests/test_responses_tracing.py +++ b/tests/test_responses_tracing.py @@ -1,7 +1,10 @@ +from typing import Optional + import pytest from inline_snapshot import snapshot from openai import AsyncOpenAI from openai.types.responses import ResponseCompletedEvent +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from agents import ModelSettings, ModelTracing, OpenAIResponsesModel, trace from agents.tracing.span_data import ResponseSpanData @@ -16,10 +19,25 @@ def is_disabled(self): class DummyUsage: - def __init__(self, input_tokens=1, output_tokens=1, total_tokens=2): + def __init__( + self, + input_tokens: int = 1, + input_tokens_details: Optional[InputTokensDetails] = None, + output_tokens: int = 1, + output_tokens_details: Optional[OutputTokensDetails] = None, + total_tokens: int = 2, + ): self.input_tokens = input_tokens self.output_tokens = output_tokens self.total_tokens = total_tokens + self.input_tokens_details = ( + input_tokens_details if input_tokens_details else InputTokensDetails(cached_tokens=0) + ) + self.output_tokens_details = ( + output_tokens_details + if output_tokens_details + else OutputTokensDetails(reasoning_tokens=0) + ) class DummyResponse: diff --git a/tests/test_usage.py b/tests/test_usage.py new file mode 100644 index 00000000..405f99dd --- /dev/null +++ b/tests/test_usage.py @@ -0,0 +1,52 @@ +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails + +from agents.usage import Usage + + +def test_usage_add_aggregates_all_fields(): + u1 = Usage( + requests=1, + input_tokens=10, + input_tokens_details=InputTokensDetails(cached_tokens=3), + output_tokens=20, + output_tokens_details=OutputTokensDetails(reasoning_tokens=5), + total_tokens=30, + ) + u2 = Usage( + requests=2, + input_tokens=7, + input_tokens_details=InputTokensDetails(cached_tokens=4), + output_tokens=8, + output_tokens_details=OutputTokensDetails(reasoning_tokens=6), + total_tokens=15, + ) + + u1.add(u2) + + assert u1.requests == 3 + assert u1.input_tokens == 17 + assert u1.output_tokens == 28 + assert u1.total_tokens == 45 + assert u1.input_tokens_details.cached_tokens == 7 + assert u1.output_tokens_details.reasoning_tokens == 11 + + +def test_usage_add_aggregates_with_none_values(): + u1 = Usage() + u2 = Usage( + requests=2, + input_tokens=7, + input_tokens_details=InputTokensDetails(cached_tokens=4), + output_tokens=8, + output_tokens_details=OutputTokensDetails(reasoning_tokens=6), + total_tokens=15, + ) + + u1.add(u2) + + assert u1.requests == 2 + assert u1.input_tokens == 7 + assert u1.output_tokens == 8 + assert u1.total_tokens == 15 + assert u1.input_tokens_details.cached_tokens == 4 + assert u1.output_tokens_details.reasoning_tokens == 6 From ce2e2a4571c2b176e8641c558fedaa7bc1692013 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 15:17:58 -0400 Subject: [PATCH 81/85] Upgrade openAI sdk version (#730) --- [//]: # (BEGIN SAPLING FOOTER) * #732 * #731 * __->__ #730 --- pyproject.toml | 2 +- src/agents/models/chatcmpl_stream_handler.py | 26 +++++++++++++++++++- src/agents/models/openai_responses.py | 16 ++++-------- tests/fake_model.py | 1 + tests/test_responses_tracing.py | 4 +++ tests/voice/test_workflow.py | 2 ++ uv.lock | 10 ++++---- 7 files changed, 43 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 672258c4..200ac248 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.9" license = "MIT" authors = [{ name = "OpenAI", email = "support@openai.com" }] dependencies = [ - "openai>=1.76.0", + "openai>=1.81.0", "pydantic>=2.10, <3", "griffe>=1.5.6, <2", "typing-extensions>=4.12.2, <5", diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py index c71adeb5..d18f5912 100644 --- a/src/agents/models/chatcmpl_stream_handler.py +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -38,6 +38,16 @@ class StreamingState: function_calls: dict[int, ResponseFunctionToolCall] = field(default_factory=dict) +class SequenceNumber: + def __init__(self): + self._sequence_number = 0 + + def get_and_increment(self) -> int: + num = self._sequence_number + self._sequence_number += 1 + return num + + class ChatCmplStreamHandler: @classmethod async def handle_stream( @@ -47,13 +57,14 @@ async def handle_stream( ) -> AsyncIterator[TResponseStreamEvent]: usage: CompletionUsage | None = None state = StreamingState() - + sequence_number = SequenceNumber() async for chunk in stream: if not state.started: state.started = True yield ResponseCreatedEvent( response=response, type="response.created", + sequence_number=sequence_number.get_and_increment(), ) # This is always set by the OpenAI API, but not by others e.g. LiteLLM @@ -89,6 +100,7 @@ async def handle_stream( item=assistant_item, output_index=0, type="response.output_item.added", + sequence_number=sequence_number.get_and_increment(), ) yield ResponseContentPartAddedEvent( content_index=state.text_content_index_and_output[0], @@ -100,6 +112,7 @@ async def handle_stream( annotations=[], ), type="response.content_part.added", + sequence_number=sequence_number.get_and_increment(), ) # Emit the delta for this segment of content yield ResponseTextDeltaEvent( @@ -108,6 +121,7 @@ async def handle_stream( item_id=FAKE_RESPONSES_ID, output_index=0, type="response.output_text.delta", + sequence_number=sequence_number.get_and_increment(), ) # Accumulate the text into the response part state.text_content_index_and_output[1].text += delta.content @@ -134,6 +148,7 @@ async def handle_stream( item=assistant_item, output_index=0, type="response.output_item.added", + sequence_number=sequence_number.get_and_increment(), ) yield ResponseContentPartAddedEvent( content_index=state.refusal_content_index_and_output[0], @@ -145,6 +160,7 @@ async def handle_stream( annotations=[], ), type="response.content_part.added", + sequence_number=sequence_number.get_and_increment(), ) # Emit the delta for this segment of refusal yield ResponseRefusalDeltaEvent( @@ -153,6 +169,7 @@ async def handle_stream( item_id=FAKE_RESPONSES_ID, output_index=0, type="response.refusal.delta", + sequence_number=sequence_number.get_and_increment(), ) # Accumulate the refusal string in the output part state.refusal_content_index_and_output[1].refusal += delta.refusal @@ -190,6 +207,7 @@ async def handle_stream( output_index=0, part=state.text_content_index_and_output[1], type="response.content_part.done", + sequence_number=sequence_number.get_and_increment(), ) if state.refusal_content_index_and_output: @@ -201,6 +219,7 @@ async def handle_stream( output_index=0, part=state.refusal_content_index_and_output[1], type="response.content_part.done", + sequence_number=sequence_number.get_and_increment(), ) # Actually send events for the function calls @@ -216,6 +235,7 @@ async def handle_stream( ), output_index=function_call_starting_index, type="response.output_item.added", + sequence_number=sequence_number.get_and_increment(), ) # Then, yield the args yield ResponseFunctionCallArgumentsDeltaEvent( @@ -223,6 +243,7 @@ async def handle_stream( item_id=FAKE_RESPONSES_ID, output_index=function_call_starting_index, type="response.function_call_arguments.delta", + sequence_number=sequence_number.get_and_increment(), ) # Finally, the ResponseOutputItemDone yield ResponseOutputItemDoneEvent( @@ -235,6 +256,7 @@ async def handle_stream( ), output_index=function_call_starting_index, type="response.output_item.done", + sequence_number=sequence_number.get_and_increment(), ) # Finally, send the Response completed event @@ -258,6 +280,7 @@ async def handle_stream( item=assistant_msg, output_index=0, type="response.output_item.done", + sequence_number=sequence_number.get_and_increment(), ) for function_call in state.function_calls.values(): @@ -289,4 +312,5 @@ async def handle_stream( yield ResponseCompletedEvent( response=final_response, type="response.completed", + sequence_number=sequence_number.get_and_increment(), ) diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 6ec8f8f7..cb656790 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -10,6 +10,7 @@ from openai.types.responses import ( Response, ResponseCompletedEvent, + ResponseIncludable, ResponseStreamEvent, ResponseTextConfigParam, ToolParam, @@ -36,13 +37,6 @@ _USER_AGENT = f"Agents/Python {__version__}" _HEADERS = {"User-Agent": _USER_AGENT} -# From the Responses API -IncludeLiteral = Literal[ - "file_search_call.results", - "message.input_image.image_url", - "computer_call_output.output.image_url", -] - class OpenAIResponsesModel(Model): """ @@ -273,7 +267,7 @@ def _get_client(self) -> AsyncOpenAI: @dataclass class ConvertedTools: tools: list[ToolParam] - includes: list[IncludeLiteral] + includes: list[ResponseIncludable] class Converter: @@ -330,7 +324,7 @@ def convert_tools( handoffs: list[Handoff[Any]], ) -> ConvertedTools: converted_tools: list[ToolParam] = [] - includes: list[IncludeLiteral] = [] + includes: list[ResponseIncludable] = [] computer_tools = [tool for tool in tools if isinstance(tool, ComputerTool)] if len(computer_tools) > 1: @@ -348,7 +342,7 @@ def convert_tools( return ConvertedTools(tools=converted_tools, includes=includes) @classmethod - def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, IncludeLiteral | None]: + def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, ResponseIncludable | None]: """Returns converted tool and includes""" if isinstance(tool, FunctionTool): @@ -359,7 +353,7 @@ def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, IncludeLiteral | None]: "type": "function", "description": tool.description, } - includes: IncludeLiteral | None = None + includes: ResponseIncludable | None = None elif isinstance(tool, WebSearchTool): ws: WebSearchToolParam = { "type": "web_search_preview", diff --git a/tests/fake_model.py b/tests/fake_model.py index 32f919ef..9f0c83a2 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -129,6 +129,7 @@ async def stream_response( yield ResponseCompletedEvent( type="response.completed", response=get_response_obj(output, usage=self.hardcoded_usage), + sequence_number=0, ) diff --git a/tests/test_responses_tracing.py b/tests/test_responses_tracing.py index dfac74bb..db24fe49 100644 --- a/tests/test_responses_tracing.py +++ b/tests/test_responses_tracing.py @@ -50,6 +50,7 @@ def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj(self.output), + sequence_number=0, ) @@ -201,6 +202,7 @@ async def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj([], "dummy-id-123"), + sequence_number=0, ) return DummyStream() @@ -253,6 +255,7 @@ async def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj([], "dummy-id-123"), + sequence_number=0, ) return DummyStream() @@ -304,6 +307,7 @@ async def __aiter__(self): yield ResponseCompletedEvent( type="response.completed", response=fake_model.get_response_obj([], "dummy-id-123"), + sequence_number=0, ) return DummyStream() diff --git a/tests/voice/test_workflow.py b/tests/voice/test_workflow.py index 2bdf2a65..035a05d5 100644 --- a/tests/voice/test_workflow.py +++ b/tests/voice/test_workflow.py @@ -81,11 +81,13 @@ async def stream_response( type="response.output_text.delta", output_index=0, item_id=item.id, + sequence_number=0, ) yield ResponseCompletedEvent( type="response.completed", response=get_response_obj(output), + sequence_number=1, ) diff --git a/uv.lock b/uv.lock index 6ccc1996..7a0cb1e6 100644 --- a/uv.lock +++ b/uv.lock @@ -1461,7 +1461,7 @@ wheels = [ [[package]] name = "openai" -version = "1.76.0" +version = "1.81.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1473,14 +1473,14 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/51/817969ec969b73d8ddad085670ecd8a45ef1af1811d8c3b8a177ca4d1309/openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2", size = 434660 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/89/a1e4f3fa7ca4f7fec90dbf47d93b7cd5ff65924926733af15044e302a192/openai-1.81.0.tar.gz", hash = "sha256:349567a8607e0bcffd28e02f96b5c2397d0d25d06732d90ab3ecbf97abf030f9", size = 456861 } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/aa/84e02ab500ca871eb8f62784426963a1c7c17a72fea3c7f268af4bbaafa5/openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a", size = 661201 }, + { url = "https://files.pythonhosted.org/packages/02/66/bcc7f9bf48e8610a33e3b5c96a5a644dad032d92404ea2a5e8b43ba067e8/openai-1.81.0-py3-none-any.whl", hash = "sha256:1c71572e22b43876c5d7d65ade0b7b516bb527c3d44ae94111267a09125f7bae", size = 717529 }, ] [[package]] name = "openai-agents" -version = "0.0.14" +version = "0.0.15" source = { editable = "." } dependencies = [ { name = "griffe" }, @@ -1536,7 +1536,7 @@ requires-dist = [ { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.67.4.post1,<2" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.8.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, - { name = "openai", specifier = ">=1.76.0" }, + { name = "openai", specifier = ">=1.81.0" }, { name = "pydantic", specifier = ">=2.10,<3" }, { name = "requests", specifier = ">=2.0,<3" }, { name = "types-requests", specifier = ">=2.0,<3" }, From 9fa5c39d69937a215a6f247883243fe38c5a39c2 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 15:21:37 -0400 Subject: [PATCH 82/85] Hosted MCP support (#731) --- [//]: # (BEGIN SAPLING FOOTER) * #732 * __->__ #731 --- examples/hosted_mcp/__init__.py | 0 examples/hosted_mcp/approvals.py | 61 ++++++++++++++ examples/hosted_mcp/simple.py | 47 +++++++++++ src/agents/__init__.py | 8 ++ src/agents/_run_impl.py | 114 ++++++++++++++++++++++++-- src/agents/items.py | 41 ++++++++- src/agents/models/openai_responses.py | 6 +- src/agents/stream_events.py | 2 + src/agents/tool.py | 64 ++++++++++++++- 9 files changed, 332 insertions(+), 11 deletions(-) create mode 100644 examples/hosted_mcp/__init__.py create mode 100644 examples/hosted_mcp/approvals.py create mode 100644 examples/hosted_mcp/simple.py diff --git a/examples/hosted_mcp/__init__.py b/examples/hosted_mcp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/hosted_mcp/approvals.py b/examples/hosted_mcp/approvals.py new file mode 100644 index 00000000..2cabb3ee --- /dev/null +++ b/examples/hosted_mcp/approvals.py @@ -0,0 +1,61 @@ +import argparse +import asyncio + +from agents import ( + Agent, + HostedMCPTool, + MCPToolApprovalFunctionResult, + MCPToolApprovalRequest, + Runner, +) + +"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with +approval callbacks.""" + + +def approval_callback(request: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult: + answer = input(f"Approve running the tool `{request.data.name}`? (y/n) ") + result: MCPToolApprovalFunctionResult = {"approve": answer == "y"} + if not result["approve"]: + result["reason"] = "User denied" + return result + + +async def main(verbose: bool, stream: bool): + agent = Agent( + name="Assistant", + tools=[ + HostedMCPTool( + tool_config={ + "type": "mcp", + "server_label": "gitmcp", + "server_url": "https://gitmcp.io/openai/codex", + "require_approval": "always", + }, + on_approval_request=approval_callback, + ) + ], + ) + + if stream: + result = Runner.run_streamed(agent, "Which language is this repo written in?") + async for event in result.stream_events(): + if event.type == "run_item_stream_event": + print(f"Got event of type {event.item.__class__.__name__}") + print(f"Done streaming; final result: {result.final_output}") + else: + res = await Runner.run(agent, "Which language is this repo written in?") + print(res.final_output) + + if verbose: + for item in result.new_items: + print(item) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--verbose", action="store_true", default=False) + parser.add_argument("--stream", action="store_true", default=False) + args = parser.parse_args() + + asyncio.run(main(args.verbose, args.stream)) diff --git a/examples/hosted_mcp/simple.py b/examples/hosted_mcp/simple.py new file mode 100644 index 00000000..508c3a7a --- /dev/null +++ b/examples/hosted_mcp/simple.py @@ -0,0 +1,47 @@ +import argparse +import asyncio + +from agents import Agent, HostedMCPTool, Runner + +"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with +approvals not required for any tools. You should only use this for trusted MCP servers.""" + + +async def main(verbose: bool, stream: bool): + agent = Agent( + name="Assistant", + tools=[ + HostedMCPTool( + tool_config={ + "type": "mcp", + "server_label": "gitmcp", + "server_url": "https://gitmcp.io/openai/codex", + "require_approval": "never", + } + ) + ], + ) + + if stream: + result = Runner.run_streamed(agent, "Which language is this repo written in?") + async for event in result.stream_events(): + if event.type == "run_item_stream_event": + print(f"Got event of type {event.item.__class__.__name__}") + print(f"Done streaming; final result: {result.final_output}") + else: + res = await Runner.run(agent, "Which language is this repo written in?") + print(res.final_output) + # The repository is primarily written in multiple languages, including Rust and TypeScript... + + if verbose: + for item in result.new_items: + print(item) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--verbose", action="store_true", default=False) + parser.add_argument("--stream", action="store_true", default=False) + args = parser.parse_args() + + asyncio.run(main(args.verbose, args.stream)) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 6d7c90b4..36c26b80 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -58,6 +58,10 @@ FileSearchTool, FunctionTool, FunctionToolResult, + HostedMCPTool, + MCPToolApprovalFunction, + MCPToolApprovalFunctionResult, + MCPToolApprovalRequest, Tool, WebSearchTool, default_tool_error_function, @@ -208,6 +212,10 @@ def enable_verbose_stdout_logging(): "FileSearchTool", "Tool", "WebSearchTool", + "HostedMCPTool", + "MCPToolApprovalFunction", + "MCPToolApprovalRequest", + "MCPToolApprovalFunctionResult", "function_tool", "Usage", "add_trace_processor", diff --git a/src/agents/_run_impl.py b/src/agents/_run_impl.py index b5a83685..ab1e7879 100644 --- a/src/agents/_run_impl.py +++ b/src/agents/_run_impl.py @@ -25,7 +25,8 @@ ActionType, ActionWait, ) -from openai.types.responses.response_input_param import ComputerCallOutput +from openai.types.responses.response_input_param import ComputerCallOutput, McpApprovalResponse +from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools from openai.types.responses.response_reasoning_item import ResponseReasoningItem from .agent import Agent, ToolsToFinalOutputResult @@ -38,6 +39,9 @@ HandoffCallItem, HandoffOutputItem, ItemHelpers, + MCPApprovalRequestItem, + MCPApprovalResponseItem, + MCPListToolsItem, MessageOutputItem, ModelResponse, ReasoningItem, @@ -52,7 +56,14 @@ from .models.interface import ModelTracing from .run_context import RunContextWrapper, TContext from .stream_events import RunItemStreamEvent, StreamEvent -from .tool import ComputerTool, FunctionTool, FunctionToolResult, Tool +from .tool import ( + ComputerTool, + FunctionTool, + FunctionToolResult, + HostedMCPTool, + MCPToolApprovalRequest, + Tool, +) from .tracing import ( SpanError, Trace, @@ -112,6 +123,12 @@ class ToolRunComputerAction: computer_tool: ComputerTool +@dataclass +class ToolRunMCPApprovalRequest: + request_item: McpApprovalRequest + mcp_tool: HostedMCPTool + + @dataclass class ProcessedResponse: new_items: list[RunItem] @@ -119,8 +136,9 @@ class ProcessedResponse: functions: list[ToolRunFunction] computer_actions: list[ToolRunComputerAction] tools_used: list[str] # Names of all tools used, including hosted tools + mcp_approval_requests: list[ToolRunMCPApprovalRequest] # Only requests with callbacks - def has_tools_to_run(self) -> bool: + def has_tools_or_approvals_to_run(self) -> bool: # Handoffs, functions and computer actions need local processing # Hosted tools have already run, so there's nothing to do. return any( @@ -128,6 +146,7 @@ def has_tools_to_run(self) -> bool: self.handoffs, self.functions, self.computer_actions, + self.mcp_approval_requests, ] ) @@ -226,7 +245,16 @@ async def execute_tools_and_side_effects( new_step_items.extend([result.run_item for result in function_results]) new_step_items.extend(computer_results) - # Second, check if there are any handoffs + # Next, run the MCP approval requests + if processed_response.mcp_approval_requests: + approval_results = await cls.execute_mcp_approval_requests( + agent=agent, + approval_requests=processed_response.mcp_approval_requests, + context_wrapper=context_wrapper, + ) + new_step_items.extend(approval_results) + + # Next, check if there are any handoffs if run_handoffs := processed_response.handoffs: return await cls.execute_handoffs( agent=agent, @@ -240,7 +268,7 @@ async def execute_tools_and_side_effects( run_config=run_config, ) - # Third, we'll check if the tool use should result in a final output + # Next, we'll check if the tool use should result in a final output check_tool_use = await cls._check_for_final_output_from_tools( agent=agent, tool_results=function_results, @@ -295,7 +323,7 @@ async def execute_tools_and_side_effects( ) elif ( not output_schema or output_schema.is_plain_text() - ) and not processed_response.has_tools_to_run(): + ) and not processed_response.has_tools_or_approvals_to_run(): return await cls.execute_final_output( agent=agent, original_input=original_input, @@ -343,10 +371,16 @@ def process_model_response( run_handoffs = [] functions = [] computer_actions = [] + mcp_approval_requests = [] tools_used: list[str] = [] handoff_map = {handoff.tool_name: handoff for handoff in handoffs} function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)} computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None) + hosted_mcp_server_map = { + tool.tool_config["server_label"]: tool + for tool in all_tools + if isinstance(tool, HostedMCPTool) + } for output in response.output: if isinstance(output, ResponseOutputMessage): @@ -375,6 +409,34 @@ def process_model_response( computer_actions.append( ToolRunComputerAction(tool_call=output, computer_tool=computer_tool) ) + elif isinstance(output, McpApprovalRequest): + items.append(MCPApprovalRequestItem(raw_item=output, agent=agent)) + if output.server_label not in hosted_mcp_server_map: + _error_tracing.attach_error_to_current_span( + SpanError( + message="MCP server label not found", + data={"server_label": output.server_label}, + ) + ) + raise ModelBehaviorError(f"MCP server label {output.server_label} not found") + else: + server = hosted_mcp_server_map[output.server_label] + if server.on_approval_request: + mcp_approval_requests.append( + ToolRunMCPApprovalRequest( + request_item=output, + mcp_tool=server, + ) + ) + else: + logger.warning( + f"MCP server {output.server_label} has no on_approval_request hook" + ) + elif isinstance(output, McpListTools): + items.append(MCPListToolsItem(raw_item=output, agent=agent)) + elif isinstance(output, McpCall): + items.append(ToolCallItem(raw_item=output, agent=agent)) + tools_used.append(output.name) elif not isinstance(output, ResponseFunctionToolCall): logger.warning(f"Unexpected output type, ignoring: {type(output)}") continue @@ -417,6 +479,7 @@ def process_model_response( functions=functions, computer_actions=computer_actions, tools_used=tools_used, + mcp_approval_requests=mcp_approval_requests, ) @classmethod @@ -643,6 +706,40 @@ async def execute_handoffs( next_step=NextStepHandoff(new_agent), ) + @classmethod + async def execute_mcp_approval_requests( + cls, + *, + agent: Agent[TContext], + approval_requests: list[ToolRunMCPApprovalRequest], + context_wrapper: RunContextWrapper[TContext], + ) -> list[RunItem]: + async def run_single_approval(approval_request: ToolRunMCPApprovalRequest) -> RunItem: + callback = approval_request.mcp_tool.on_approval_request + assert callback is not None, "Callback is required for MCP approval requests" + maybe_awaitable_result = callback( + MCPToolApprovalRequest(context_wrapper, approval_request.request_item) + ) + if inspect.isawaitable(maybe_awaitable_result): + result = await maybe_awaitable_result + else: + result = maybe_awaitable_result + reason = result.get("reason", None) + raw_item: McpApprovalResponse = { + "approval_request_id": approval_request.request_item.id, + "approve": result["approve"], + "type": "mcp_approval_response", + } + if not result["approve"] and reason: + raw_item["reason"] = reason + return MCPApprovalResponseItem( + raw_item=raw_item, + agent=agent, + ) + + tasks = [run_single_approval(approval_request) for approval_request in approval_requests] + return await asyncio.gather(*tasks) + @classmethod async def execute_final_output( cls, @@ -727,6 +824,11 @@ def stream_step_result_to_queue( event = RunItemStreamEvent(item=item, name="tool_output") elif isinstance(item, ReasoningItem): event = RunItemStreamEvent(item=item, name="reasoning_item_created") + elif isinstance(item, MCPApprovalRequestItem): + event = RunItemStreamEvent(item=item, name="mcp_approval_requested") + elif isinstance(item, MCPListToolsItem): + event = RunItemStreamEvent(item=item, name="mcp_list_tools") + else: logger.warning(f"Unexpected item type: {type(item)}") event = None diff --git a/src/agents/items.py b/src/agents/items.py index 8fb2b52a..65a91179 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -18,7 +18,12 @@ ResponseOutputText, ResponseStreamEvent, ) -from openai.types.responses.response_input_item_param import ComputerCallOutput, FunctionCallOutput +from openai.types.responses.response_input_item_param import ( + ComputerCallOutput, + FunctionCallOutput, + McpApprovalResponse, +) +from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools from openai.types.responses.response_reasoning_item import ResponseReasoningItem from pydantic import BaseModel from typing_extensions import TypeAlias @@ -108,6 +113,7 @@ class HandoffOutputItem(RunItemBase[TResponseInputItem]): ResponseComputerToolCall, ResponseFileSearchToolCall, ResponseFunctionWebSearch, + McpCall, ] """A type that represents a tool call item.""" @@ -147,6 +153,36 @@ class ReasoningItem(RunItemBase[ResponseReasoningItem]): type: Literal["reasoning_item"] = "reasoning_item" +@dataclass +class MCPListToolsItem(RunItemBase[McpListTools]): + """Represents a call to an MCP server to list tools.""" + + raw_item: McpListTools + """The raw MCP list tools call.""" + + type: Literal["mcp_list_tools_item"] = "mcp_list_tools_item" + + +@dataclass +class MCPApprovalRequestItem(RunItemBase[McpApprovalRequest]): + """Represents a request for MCP approval.""" + + raw_item: McpApprovalRequest + """The raw MCP approval request.""" + + type: Literal["mcp_approval_request_item"] = "mcp_approval_request_item" + + +@dataclass +class MCPApprovalResponseItem(RunItemBase[McpApprovalResponse]): + """Represents a response to an MCP approval request.""" + + raw_item: McpApprovalResponse + """The raw MCP approval response.""" + + type: Literal["mcp_approval_response_item"] = "mcp_approval_response_item" + + RunItem: TypeAlias = Union[ MessageOutputItem, HandoffCallItem, @@ -154,6 +190,9 @@ class ReasoningItem(RunItemBase[ResponseReasoningItem]): ToolCallItem, ToolCallOutputItem, ReasoningItem, + MCPListToolsItem, + MCPApprovalRequestItem, + MCPApprovalResponseItem, ] """An item generated by an agent.""" diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index cb656790..65a4f5ca 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -24,7 +24,7 @@ from ..handoffs import Handoff from ..items import ItemHelpers, ModelResponse, TResponseInputItem from ..logger import logger -from ..tool import ComputerTool, FileSearchTool, FunctionTool, Tool, WebSearchTool +from ..tool import ComputerTool, FileSearchTool, FunctionTool, HostedMCPTool, Tool, WebSearchTool from ..tracing import SpanError, response_span from ..usage import Usage from ..version import __version__ @@ -383,7 +383,9 @@ def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, ResponseIncludable | None "display_height": tool.computer.dimensions[1], } includes = None - + elif isinstance(tool, HostedMCPTool): + converted_tool = tool.tool_config + includes = None else: raise UserError(f"Unknown tool type: {type(tool)}, tool") diff --git a/src/agents/stream_events.py b/src/agents/stream_events.py index bd37d11f..111d0b95 100644 --- a/src/agents/stream_events.py +++ b/src/agents/stream_events.py @@ -35,6 +35,8 @@ class RunItemStreamEvent: "tool_called", "tool_output", "reasoning_item_created", + "mcp_approval_requested", + "mcp_list_tools", ] """The name of the event.""" diff --git a/src/agents/tool.py b/src/agents/tool.py index c1c16242..3bcd57c2 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -7,9 +7,11 @@ from typing import Any, Callable, Literal, Union, overload from openai.types.responses.file_search_tool_param import Filters, RankingOptions +from openai.types.responses.response_output_item import McpApprovalRequest +from openai.types.responses.tool_param import Mcp from openai.types.responses.web_search_tool_param import UserLocation from pydantic import ValidationError -from typing_extensions import Concatenate, ParamSpec +from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict from . import _debug from .computer import AsyncComputer, Computer @@ -130,7 +132,55 @@ def name(self): return "computer_use_preview" -Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool] +@dataclass +class MCPToolApprovalRequest: + """A request to approve a tool call.""" + + ctx_wrapper: RunContextWrapper[Any] + """The run context.""" + + data: McpApprovalRequest + """The data from the MCP tool approval request.""" + + +class MCPToolApprovalFunctionResult(TypedDict): + """The result of an MCP tool approval function.""" + + approve: bool + """Whether to approve the tool call.""" + + reason: NotRequired[str] + """An optional reason, if rejected.""" + + +MCPToolApprovalFunction = Callable[ + [MCPToolApprovalRequest], MaybeAwaitable[MCPToolApprovalFunctionResult] +] +"""A function that approves or rejects a tool call.""" + + +@dataclass +class HostedMCPTool: + """A tool that allows the LLM to use a remote MCP server. The LLM will automatically list and + call tools, without requiring a a round trip back to your code. + If you want to run MCP servers locally via stdio, in a VPC or other non-publicly-accessible + environment, or you just prefer to run tool calls locally, then you can instead use the servers + in `agents.mcp` and pass `Agent(mcp_servers=[...])` to the agent.""" + + tool_config: Mcp + """The MCP tool config, which includes the server URL and other settings.""" + + on_approval_request: MCPToolApprovalFunction | None = None + """An optional function that will be called if approval is requested for an MCP tool. If not + provided, you will need to manually add approvals/rejections to the input and call + `Runner.run(...)` again.""" + + @property + def name(self): + return "hosted_mcp" + + +Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool, HostedMCPTool] """A tool that can be used in an agent.""" @@ -308,3 +358,13 @@ def decorator(real_func: ToolFunction[...]) -> FunctionTool: return _create_function_tool(real_func) return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator + return decorator From 079764f0ab463fda9ecf397b0a5d8e466e87a86c Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 15:26:22 -0400 Subject: [PATCH 83/85] Add support for local shell, image generator, code interpreter tools (#732) --- examples/tools/code_interpreter.py | 34 +++++++ examples/tools/image_generator.py | 54 +++++++++++ src/agents/__init__.py | 10 +++ src/agents/_run_impl.py | 124 +++++++++++++++++++++++++- src/agents/items.py | 21 ++++- src/agents/models/openai_responses.py | 35 +++++++- src/agents/tool.py | 76 +++++++++++++--- 7 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 examples/tools/code_interpreter.py create mode 100644 examples/tools/image_generator.py diff --git a/examples/tools/code_interpreter.py b/examples/tools/code_interpreter.py new file mode 100644 index 00000000..a5843ce3 --- /dev/null +++ b/examples/tools/code_interpreter.py @@ -0,0 +1,34 @@ +import asyncio + +from agents import Agent, CodeInterpreterTool, Runner, trace + + +async def main(): + agent = Agent( + name="Code interpreter", + instructions="You love doing math.", + tools=[ + CodeInterpreterTool( + tool_config={"type": "code_interpreter", "container": {"type": "auto"}}, + ) + ], + ) + + with trace("Code interpreter example"): + print("Solving math problem...") + result = Runner.run_streamed(agent, "What is the square root of273 * 312821 plus 1782?") + async for event in result.stream_events(): + if ( + event.type == "run_item_stream_event" + and event.item.type == "tool_call_item" + and event.item.raw_item.type == "code_interpreter_call" + ): + print(f"Code interpreter code:\n```\n{event.item.raw_item.code}\n```\n") + elif event.type == "run_item_stream_event": + print(f"Other event: {event.item.type}") + + print(f"Final output: {result.final_output}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/tools/image_generator.py b/examples/tools/image_generator.py new file mode 100644 index 00000000..fd6fcc6b --- /dev/null +++ b/examples/tools/image_generator.py @@ -0,0 +1,54 @@ +import asyncio +import base64 +import os +import subprocess +import sys +import tempfile + +from agents import Agent, ImageGenerationTool, Runner, trace + + +def open_file(path: str) -> None: + if sys.platform.startswith("darwin"): + subprocess.run(["open", path], check=False) # macOS + elif os.name == "nt": # Windows + os.astartfile(path) # type: ignore + elif os.name == "posix": + subprocess.run(["xdg-open", path], check=False) # Linux/Unix + else: + print(f"Don't know how to open files on this platform: {sys.platform}") + + +async def main(): + agent = Agent( + name="Image generator", + instructions="You are a helpful agent.", + tools=[ + ImageGenerationTool( + tool_config={"type": "image_generation", "quality": "low"}, + ) + ], + ) + + with trace("Image generation example"): + print("Generating image, this may take a while...") + result = await Runner.run( + agent, "Create an image of a frog eating a pizza, comic book style." + ) + print(result.final_output) + for item in result.new_items: + if ( + item.type == "tool_call_item" + and item.raw_item.type == "image_generation_call" + and (img_result := item.raw_item.result) + ): + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + tmp.write(base64.b64decode(img_result)) + temp_path = tmp.name + + # Open the image + open_file(temp_path) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 36c26b80..58949157 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -54,11 +54,16 @@ StreamEvent, ) from .tool import ( + CodeInterpreterTool, ComputerTool, FileSearchTool, FunctionTool, FunctionToolResult, HostedMCPTool, + ImageGenerationTool, + LocalShellCommandRequest, + LocalShellExecutor, + LocalShellTool, MCPToolApprovalFunction, MCPToolApprovalFunctionResult, MCPToolApprovalRequest, @@ -210,6 +215,11 @@ def enable_verbose_stdout_logging(): "FunctionToolResult", "ComputerTool", "FileSearchTool", + "CodeInterpreterTool", + "ImageGenerationTool", + "LocalShellCommandRequest", + "LocalShellExecutor", + "LocalShellTool", "Tool", "WebSearchTool", "HostedMCPTool", diff --git a/src/agents/_run_impl.py b/src/agents/_run_impl.py index ab1e7879..2cfa270e 100644 --- a/src/agents/_run_impl.py +++ b/src/agents/_run_impl.py @@ -14,6 +14,9 @@ ResponseFunctionWebSearch, ResponseOutputMessage, ) +from openai.types.responses.response_code_interpreter_tool_call import ( + ResponseCodeInterpreterToolCall, +) from openai.types.responses.response_computer_tool_call import ( ActionClick, ActionDoubleClick, @@ -26,7 +29,12 @@ ActionWait, ) from openai.types.responses.response_input_param import ComputerCallOutput, McpApprovalResponse -from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools +from openai.types.responses.response_output_item import ( + ImageGenerationCall, + LocalShellCall, + McpApprovalRequest, + McpListTools, +) from openai.types.responses.response_reasoning_item import ResponseReasoningItem from .agent import Agent, ToolsToFinalOutputResult @@ -61,6 +69,8 @@ FunctionTool, FunctionToolResult, HostedMCPTool, + LocalShellCommandRequest, + LocalShellTool, MCPToolApprovalRequest, Tool, ) @@ -129,12 +139,19 @@ class ToolRunMCPApprovalRequest: mcp_tool: HostedMCPTool +@dataclass +class ToolRunLocalShellCall: + tool_call: LocalShellCall + local_shell_tool: LocalShellTool + + @dataclass class ProcessedResponse: new_items: list[RunItem] handoffs: list[ToolRunHandoff] functions: list[ToolRunFunction] computer_actions: list[ToolRunComputerAction] + local_shell_calls: list[ToolRunLocalShellCall] tools_used: list[str] # Names of all tools used, including hosted tools mcp_approval_requests: list[ToolRunMCPApprovalRequest] # Only requests with callbacks @@ -146,6 +163,7 @@ def has_tools_or_approvals_to_run(self) -> bool: self.handoffs, self.functions, self.computer_actions, + self.local_shell_calls, self.mcp_approval_requests, ] ) @@ -371,11 +389,15 @@ def process_model_response( run_handoffs = [] functions = [] computer_actions = [] + local_shell_calls = [] mcp_approval_requests = [] tools_used: list[str] = [] handoff_map = {handoff.tool_name: handoff for handoff in handoffs} function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)} computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None) + local_shell_tool = next( + (tool for tool in all_tools if isinstance(tool, LocalShellTool)), None + ) hosted_mcp_server_map = { tool.tool_config["server_label"]: tool for tool in all_tools @@ -434,9 +456,29 @@ def process_model_response( ) elif isinstance(output, McpListTools): items.append(MCPListToolsItem(raw_item=output, agent=agent)) - elif isinstance(output, McpCall): + elif isinstance(output, ImageGenerationCall): + items.append(ToolCallItem(raw_item=output, agent=agent)) + tools_used.append("image_generation") + elif isinstance(output, ResponseCodeInterpreterToolCall): items.append(ToolCallItem(raw_item=output, agent=agent)) - tools_used.append(output.name) + tools_used.append("code_interpreter") + elif isinstance(output, LocalShellCall): + items.append(ToolCallItem(raw_item=output, agent=agent)) + tools_used.append("local_shell") + if not local_shell_tool: + _error_tracing.attach_error_to_current_span( + SpanError( + message="Local shell tool not found", + data={}, + ) + ) + raise ModelBehaviorError( + "Model produced local shell call without a local shell tool." + ) + local_shell_calls.append( + ToolRunLocalShellCall(tool_call=output, local_shell_tool=local_shell_tool) + ) + elif not isinstance(output, ResponseFunctionToolCall): logger.warning(f"Unexpected output type, ignoring: {type(output)}") continue @@ -478,6 +520,7 @@ def process_model_response( handoffs=run_handoffs, functions=functions, computer_actions=computer_actions, + local_shell_calls=local_shell_calls, tools_used=tools_used, mcp_approval_requests=mcp_approval_requests, ) @@ -552,6 +595,30 @@ async def run_single_tool( for tool_run, result in zip(tool_runs, results) ] + @classmethod + async def execute_local_shell_calls( + cls, + *, + agent: Agent[TContext], + calls: list[ToolRunLocalShellCall], + context_wrapper: RunContextWrapper[TContext], + hooks: RunHooks[TContext], + config: RunConfig, + ) -> list[RunItem]: + results: list[RunItem] = [] + # Need to run these serially, because each call can affect the local shell state + for call in calls: + results.append( + await LocalShellAction.execute( + agent=agent, + call=call, + hooks=hooks, + context_wrapper=context_wrapper, + config=config, + ) + ) + return results + @classmethod async def execute_computer_actions( cls, @@ -1021,3 +1088,54 @@ async def _get_screenshot_async( await computer.wait() return await computer.screenshot() + + +class LocalShellAction: + @classmethod + async def execute( + cls, + *, + agent: Agent[TContext], + call: ToolRunLocalShellCall, + hooks: RunHooks[TContext], + context_wrapper: RunContextWrapper[TContext], + config: RunConfig, + ) -> RunItem: + await asyncio.gather( + hooks.on_tool_start(context_wrapper, agent, call.local_shell_tool), + ( + agent.hooks.on_tool_start(context_wrapper, agent, call.local_shell_tool) + if agent.hooks + else _coro.noop_coroutine() + ), + ) + + request = LocalShellCommandRequest( + ctx_wrapper=context_wrapper, + data=call.tool_call, + ) + output = call.local_shell_tool.executor(request) + if inspect.isawaitable(output): + result = await output + else: + result = output + + await asyncio.gather( + hooks.on_tool_end(context_wrapper, agent, call.local_shell_tool, result), + ( + agent.hooks.on_tool_end(context_wrapper, agent, call.local_shell_tool, result) + if agent.hooks + else _coro.noop_coroutine() + ), + ) + + return ToolCallOutputItem( + agent=agent, + output=output, + raw_item={ + "type": "local_shell_call_output", + "id": call.tool_call.call_id, + "output": result, + # "id": "out" + call.tool_call.id, # TODO remove this, it should be optional + }, + ) diff --git a/src/agents/items.py b/src/agents/items.py index 65a91179..64797ad2 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -18,12 +18,22 @@ ResponseOutputText, ResponseStreamEvent, ) +from openai.types.responses.response_code_interpreter_tool_call import ( + ResponseCodeInterpreterToolCall, +) from openai.types.responses.response_input_item_param import ( ComputerCallOutput, FunctionCallOutput, + LocalShellCallOutput, McpApprovalResponse, ) -from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools +from openai.types.responses.response_output_item import ( + ImageGenerationCall, + LocalShellCall, + McpApprovalRequest, + McpCall, + McpListTools, +) from openai.types.responses.response_reasoning_item import ResponseReasoningItem from pydantic import BaseModel from typing_extensions import TypeAlias @@ -114,6 +124,9 @@ class HandoffOutputItem(RunItemBase[TResponseInputItem]): ResponseFileSearchToolCall, ResponseFunctionWebSearch, McpCall, + ResponseCodeInterpreterToolCall, + ImageGenerationCall, + LocalShellCall, ] """A type that represents a tool call item.""" @@ -129,10 +142,12 @@ class ToolCallItem(RunItemBase[ToolCallItemTypes]): @dataclass -class ToolCallOutputItem(RunItemBase[Union[FunctionCallOutput, ComputerCallOutput]]): +class ToolCallOutputItem( + RunItemBase[Union[FunctionCallOutput, ComputerCallOutput, LocalShellCallOutput]] +): """Represents the output of a tool call.""" - raw_item: FunctionCallOutput | ComputerCallOutput + raw_item: FunctionCallOutput | ComputerCallOutput | LocalShellCallOutput """The raw item from the model.""" output: Any diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 65a4f5ca..86c8e69c 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -24,7 +24,17 @@ from ..handoffs import Handoff from ..items import ItemHelpers, ModelResponse, TResponseInputItem from ..logger import logger -from ..tool import ComputerTool, FileSearchTool, FunctionTool, HostedMCPTool, Tool, WebSearchTool +from ..tool import ( + CodeInterpreterTool, + ComputerTool, + FileSearchTool, + FunctionTool, + HostedMCPTool, + ImageGenerationTool, + LocalShellTool, + Tool, + WebSearchTool, +) from ..tracing import SpanError, response_span from ..usage import Usage from ..version import __version__ @@ -295,6 +305,18 @@ def convert_tool_choice( return { "type": "computer_use_preview", } + elif tool_choice == "image_generation": + return { + "type": "image_generation", + } + elif tool_choice == "code_interpreter": + return { + "type": "code_interpreter", + } + elif tool_choice == "mcp": + return { + "type": "mcp", + } else: return { "type": "function", @@ -386,6 +408,17 @@ def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, ResponseIncludable | None elif isinstance(tool, HostedMCPTool): converted_tool = tool.tool_config includes = None + elif isinstance(tool, ImageGenerationTool): + converted_tool = tool.tool_config + includes = None + elif isinstance(tool, CodeInterpreterTool): + converted_tool = tool.tool_config + includes = None + elif isinstance(tool, LocalShellTool): + converted_tool = { + "type": "local_shell", + } + includes = None else: raise UserError(f"Unknown tool type: {type(tool)}, tool") diff --git a/src/agents/tool.py b/src/agents/tool.py index 3bcd57c2..fd5a21c8 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -7,8 +7,8 @@ from typing import Any, Callable, Literal, Union, overload from openai.types.responses.file_search_tool_param import Filters, RankingOptions -from openai.types.responses.response_output_item import McpApprovalRequest -from openai.types.responses.tool_param import Mcp +from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest +from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp from openai.types.responses.web_search_tool_param import UserLocation from pydantic import ValidationError from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict @@ -180,7 +180,67 @@ def name(self): return "hosted_mcp" -Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool, HostedMCPTool] +@dataclass +class CodeInterpreterTool: + """A tool that allows the LLM to execute code in a sandboxed environment.""" + + tool_config: CodeInterpreter + """The tool config, which includes the container and other settings.""" + + @property + def name(self): + return "code_interpreter" + + +@dataclass +class ImageGenerationTool: + """A tool that allows the LLM to generate images.""" + + tool_config: ImageGeneration + """The tool config, which image generation settings.""" + + @property + def name(self): + return "image_generation" + + +@dataclass +class LocalShellCommandRequest: + """A request to execute a command on a shell.""" + + ctx_wrapper: RunContextWrapper[Any] + """The run context.""" + + data: LocalShellCall + """The data from the local shell tool call.""" + + +LocalShellExecutor = Callable[[LocalShellCommandRequest], MaybeAwaitable[str]] +"""A function that executes a command on a shell.""" + + +@dataclass +class LocalShellTool: + """A tool that allows the LLM to execute commands on a shell.""" + + executor: LocalShellExecutor + """A function that executes a command on a shell.""" + + @property + def name(self): + return "local_shell" + + +Tool = Union[ + FunctionTool, + FileSearchTool, + WebSearchTool, + ComputerTool, + HostedMCPTool, + LocalShellTool, + ImageGenerationTool, + CodeInterpreterTool, +] """A tool that can be used in an agent.""" @@ -358,13 +418,3 @@ def decorator(real_func: ToolFunction[...]) -> FunctionTool: return _create_function_tool(real_func) return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator - return decorator From 1992be3e8d1746164f0f47f3e2001de4ab4059b9 Mon Sep 17 00:00:00 2001 From: Rohan Mehta Date: Wed, 21 May 2025 16:06:13 -0400 Subject: [PATCH 84/85] v0.0.16 (#733) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 200ac248..38a2f2b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.0.15" +version = "0.0.16" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.9" diff --git a/uv.lock b/uv.lock index 7a0cb1e6..6f2f3f84 100644 --- a/uv.lock +++ b/uv.lock @@ -1480,7 +1480,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.0.15" +version = "0.0.16" source = { editable = "." } dependencies = [ { name = "griffe" }, From 1364f4408e8e0d8bec1d6ac8337f68b3b888a4a7 Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Wed, 21 May 2025 14:59:47 -0700 Subject: [PATCH 85/85] fix Gemini token validation issue with LiteLLM (#735) Fix for #734 --- src/agents/extensions/models/litellm_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index ffb2c3c1..49e2d42d 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -111,12 +111,12 @@ async def get_response( input_tokens_details=InputTokensDetails( cached_tokens=getattr( response_usage.prompt_tokens_details, "cached_tokens", 0 - ) + ) or 0 ), output_tokens_details=OutputTokensDetails( reasoning_tokens=getattr( response_usage.completion_tokens_details, "reasoning_tokens", 0 - ) + ) or 0 ), ) if response.usage