diff --git a/temporalio/contrib/openai_agents/__init__.py b/temporalio/contrib/openai_agents/__init__.py new file mode 100644 index 000000000..4b4120d4e --- /dev/null +++ b/temporalio/contrib/openai_agents/__init__.py @@ -0,0 +1 @@ +"""Support for running OpenAI agents as part of Temporal workflows.""" diff --git a/temporalio/contrib/openai_agents/_heartbeat_decorator.py b/temporalio/contrib/openai_agents/_heartbeat_decorator.py new file mode 100644 index 000000000..9e046f471 --- /dev/null +++ b/temporalio/contrib/openai_agents/_heartbeat_decorator.py @@ -0,0 +1,37 @@ +import asyncio +from functools import wraps +from typing import Any, Awaitable, Callable, TypeVar, cast + +from temporalio import activity + +F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) + + +def _auto_heartbeater(fn: F) -> F: + # We want to ensure that the type hints from the original callable are + # available via our wrapper, so we use the functools wraps decorator + @wraps(fn) + async def wrapper(*args, **kwargs): + heartbeat_timeout = activity.info().heartbeat_timeout + heartbeat_task = None + if heartbeat_timeout: + # Heartbeat twice as often as the timeout + heartbeat_task = asyncio.create_task( + heartbeat_every(heartbeat_timeout.total_seconds() / 2) + ) + try: + return await fn(*args, **kwargs) + finally: + if heartbeat_task: + heartbeat_task.cancel() + # Wait for heartbeat cancellation to complete + await asyncio.wait([heartbeat_task]) + + return cast(F, wrapper) + + +async def heartbeat_every(delay: float, *details: Any) -> None: + """Heartbeat every so often while not cancelled""" + while True: + await asyncio.sleep(delay) + activity.heartbeat(*details) diff --git a/temporalio/contrib/openai_agents/_openai_runner.py b/temporalio/contrib/openai_agents/_openai_runner.py new file mode 100644 index 000000000..1c08bdb44 --- /dev/null +++ b/temporalio/contrib/openai_agents/_openai_runner.py @@ -0,0 +1,126 @@ +from dataclasses import replace + +from agents import ( + Agent, + RunConfig, + RunHooks, + Runner, + RunResult, + RunResultStreaming, + TContext, + TResponseInputItem, +) +from agents.run import DEFAULT_MAX_TURNS, DEFAULT_RUNNER, DefaultRunner + +from temporalio import workflow +from temporalio.contrib.openai_agents._temporal_model_stub import _TemporalModelStub + +# TODO: Uncomment when Agent.tools type accepts Callable +# def _activities_as_tools(tools: list[Tool]) -> list[Tool]: +# """Convert activities to tools.""" +# return [activity_as_tool(tool) if isinstance(tool, Callable) else tool for tool in tools] + + +class TemporalOpenAIRunner(Runner): + """Temporal Runner for OpenAI agents. + + Forwards model calls to a Temporal activity. + + TODO: Implement original runner forwarding + """ + + def __init__(self): + """Initialize the Temporal OpenAI Runner.""" + self._runner = DEFAULT_RUNNER or DefaultRunner() + + async def _run_impl( + self, + starting_agent: Agent[TContext], + input: str | list[TResponseInputItem], + *, + context: TContext | None = None, + max_turns: int = DEFAULT_MAX_TURNS, + hooks: RunHooks[TContext] | None = None, + run_config: RunConfig | None = None, + previous_response_id: str | None = None, + ) -> RunResult: + """Run the agent in a Temporal workflow.""" + if not workflow.in_workflow(): + return await self._runner._run_impl( + starting_agent, + input, + context=context, + max_turns=max_turns, + hooks=hooks, + run_config=run_config, + previous_response_id=previous_response_id, + ) + if run_config is None: + run_config = RunConfig() + + if run_config.model is not None and not isinstance(run_config.model, str): + raise ValueError( + "Temporal workflows require a model name to be a string in the run config." + ) + updated_run_config = replace( + run_config, model=_TemporalModelStub(run_config.model) + ) + + # TODO: Uncomment when Agent.tools type accepts Callable + # tools = _activities_as_tools(starting_agent.tools) if starting_agent.tools else None + # updated_starting_agent = replace(starting_agent, tools=tools) + + return await self._runner._run_impl( + starting_agent=starting_agent, + input=input, + context=context, + max_turns=max_turns, + hooks=hooks, + run_config=updated_run_config, + previous_response_id=previous_response_id, + ) + + def _run_sync_impl( + self, + starting_agent: Agent[TContext], + input: str | list[TResponseInputItem], + *, + context: TContext | None = None, + max_turns: int = DEFAULT_MAX_TURNS, + hooks: RunHooks[TContext] | None = None, + run_config: RunConfig | None = None, + previous_response_id: str | None = None, + ) -> RunResult: + if not workflow.in_workflow(): + return self._runner._run_sync_impl( + starting_agent, + input, + context=context, + max_turns=max_turns, + hooks=hooks, + run_config=run_config, + previous_response_id=previous_response_id, + ) + raise RuntimeError("Temporal workflows do not support synchronous model calls.") + + def _run_streamed_impl( + self, + starting_agent: Agent[TContext], + input: str | list[TResponseInputItem], + context: TContext | None = None, + max_turns: int = DEFAULT_MAX_TURNS, + hooks: RunHooks[TContext] | None = None, + run_config: RunConfig | None = None, + previous_response_id: str | None = None, + ) -> RunResultStreaming: + if not workflow.in_workflow(): + return self._runner._run_streamed_impl( + starting_agent, + input, + context=context, + max_turns=max_turns, + hooks=hooks, + run_config=run_config, + previous_response_id=previous_response_id, + ) + raise RuntimeError("Temporal workflows do not support streaming.") diff --git a/temporalio/contrib/openai_agents/_temporal_model_stub.py b/temporalio/contrib/openai_agents/_temporal_model_stub.py new file mode 100644 index 000000000..7accfe583 --- /dev/null +++ b/temporalio/contrib/openai_agents/_temporal_model_stub.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from temporalio import workflow + +with workflow.unsafe.imports_passed_through(): + from datetime import timedelta + from typing import Any, AsyncIterator, Sequence, cast + + from agents import ( + AgentOutputSchema, + AgentOutputSchemaBase, + ComputerTool, + FileSearchTool, + FunctionTool, + Handoff, + Model, + ModelResponse, + ModelSettings, + ModelTracing, + Tool, + TResponseInputItem, + WebSearchTool, + ) + from agents.items import TResponseStreamEvent + + from temporalio.contrib.openai_agents.invoke_model_activity import ( + ActivityModelInput, + AgentOutputSchemaInput, + FunctionToolInput, + HandoffInput, + ModelTracingInput, + ToolInput, + invoke_model_activity, + ) + + +class _TemporalModelStub(Model): + """A stub that allows invoking models as Temporal activities.""" + + def __init__(self, model_name: str | None) -> None: + self.model_name = model_name + + async def get_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + *, + previous_response_id: str | None, + ) -> ModelResponse: + def get_summary(input: str | list[TResponseInputItem]) -> str: + ### Activity summary shown in the UI + try: + max_size = 100 + if isinstance(input, str): + return input[:max_size] + elif isinstance(input, list): + seq_input = cast(Sequence[Any], input) + last_item = seq_input[-1] + if isinstance(last_item, dict): + return last_item.get("content", "")[:max_size] + elif hasattr(last_item, "content"): + return str(getattr(last_item, "content"))[:max_size] + return str(last_item)[:max_size] + elif isinstance(input, dict): + return input.get("content", "")[:max_size] + except Exception as e: + print(f"Error getting summary: {e}") + return "" + + def make_tool_info(tool: Tool) -> ToolInput: + if isinstance(tool, FileSearchTool): + return cast(FileSearchTool, tool) + elif isinstance(tool, WebSearchTool): + return cast(WebSearchTool, tool) + elif isinstance(tool, ComputerTool): + raise NotImplementedError( + "Computer search preview is not supported in Temporal model" + ) + elif isinstance(tool, FunctionTool): + t = cast(FunctionToolInput, tool) + return FunctionToolInput( + name=t.name, + description=t.description, + params_json_schema=t.params_json_schema, + strict_json_schema=t.strict_json_schema, + ) + else: + raise ValueError(f"Unknown tool type: {tool.name}") + + tool_infos = [make_tool_info(x) for x in tools] + handoff_infos = [ + HandoffInput( + tool_name=x.tool_name, + tool_description=x.tool_description, + input_json_schema=x.input_json_schema, + agent_name=x.agent_name, + strict_json_schema=x.strict_json_schema, + ) + for x in handoffs + ] + if output_schema is not None and not isinstance( + output_schema, AgentOutputSchema + ): + raise TypeError( + f"Only AgentOutputSchema is supported by Temporal Model, got {type(output_schema).__name__}" + ) + agent_output_schema = cast(AgentOutputSchema, output_schema) + output_schema_input = ( + None + if agent_output_schema is None + else AgentOutputSchemaInput( + output_type_name=agent_output_schema.name(), + is_wrapped=agent_output_schema._is_wrapped, + output_schema=agent_output_schema.json_schema() + if not agent_output_schema.is_plain_text() + else None, + strict_json_schema=agent_output_schema.is_strict_json_schema(), + ) + ) + + activity_input = ActivityModelInput( + model_name=self.model_name, + system_instructions=system_instructions, + input=input, + model_settings=model_settings, + tools=tool_infos, + output_schema=output_schema_input, + handoffs=handoff_infos, + tracing=ModelTracingInput(tracing.value), + previous_response_id=previous_response_id, + ) + return await workflow.execute_activity( + invoke_model_activity, + activity_input, + start_to_close_timeout=timedelta(seconds=60), + heartbeat_timeout=timedelta(seconds=10), + summary=get_summary(input), + ) + + def stream_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + *, + previous_response_id: str | None, + ) -> AsyncIterator[TResponseStreamEvent]: + raise NotImplementedError("Temporal model doesn't support streams yet") diff --git a/temporalio/contrib/openai_agents/_temporal_trace_provider.py b/temporalio/contrib/openai_agents/_temporal_trace_provider.py new file mode 100644 index 000000000..e628a18b4 --- /dev/null +++ b/temporalio/contrib/openai_agents/_temporal_trace_provider.py @@ -0,0 +1,175 @@ +import uuid +from typing import Any + +from agents import SpanData, Trace, TracingProcessor +from agents.tracing import ( # TODO: TraceProvider is not declared in __all__ + TraceProvider, # pyright: ignore[reportPrivateImportUsage] + get_trace_provider, # pyright: ignore[reportPrivateImportUsage] +) +from agents.tracing.spans import Span + +from temporalio import workflow +from temporalio.workflow import ReadOnlyContextError + + +class ActivitySpanData(SpanData): + """Captures fields from ActivityTaskScheduledEventAttributes for tracing.""" + + def __init__( + self, + activity_id: str, + activity_type: str, + task_queue: str, + schedule_to_close_timeout: float | None = None, + schedule_to_start_timeout: float | None = None, + start_to_close_timeout: float | None = None, + heartbeat_timeout: float | None = None, + ): + """Initialize an ActivitySpanData instance.""" + self.activity_id = activity_id + self.activity_type = activity_type + self.task_queue = task_queue + self.schedule_to_close_timeout = schedule_to_close_timeout + self.schedule_to_start_timeout = schedule_to_start_timeout + self.start_to_close_timeout = start_to_close_timeout + self.heartbeat_timeout = heartbeat_timeout + + @property + def type(self) -> str: + """Return the type of this span data.""" + return "temporal-activity" + + def export(self) -> dict[str, Any]: + """Export the span data as a dictionary.""" + return { + "type": self.type, + "activity_id": self.activity_id, + "activity_type": self.activity_type, + "task_queue": self.task_queue, + "schedule_to_close_timeout": self.schedule_to_close_timeout, + "schedule_to_start_timeout": self.schedule_to_start_timeout, + "start_to_close_timeout": self.start_to_close_timeout, + "heartbeat_timeout": self.heartbeat_timeout, + } + + +def activity_span( + activity_id: str, + activity_type: str, + task_queue: str, + # schedule_to_close_timeout: float, + # schedule_to_start_timeout: float, + start_to_close_timeout: float, + # heartbeat_timeout: float, +) -> Span[ActivitySpanData]: + """Create a trace span for a Temporal activity.""" + return get_trace_provider().create_span( + span_data=ActivitySpanData( + activity_id=activity_id, + activity_type=activity_type, + task_queue=task_queue, + # schedule_to_close_timeout=schedule_to_close_timeout, + # schedule_to_start_timeout=schedule_to_start_timeout, + start_to_close_timeout=start_to_close_timeout, + # heartbeat_timeout=heartbeat_timeout, + ), + # span_id=span_id, + # parent=parent, + # disabled=disabled, + ) + + +class _TemporalTracingProcessor(TracingProcessor): + def __init__(self, impl: TracingProcessor): + super().__init__() + self._impl = impl + + def on_trace_start(self, trace: Trace) -> None: + if workflow.in_workflow() and workflow.unsafe.is_replaying(): + # In replay mode, don't report + return + + self._impl.on_trace_start(trace) + + def on_trace_end(self, trace: Trace) -> None: + if workflow.in_workflow() and workflow.unsafe.is_replaying(): + # In replay mode, don't report + return + + self._impl.on_trace_end(trace) + + def on_span_start(self, span: Span[Any]) -> None: + if workflow.in_workflow() and workflow.unsafe.is_replaying(): + # In replay mode, don't report + return + + self._impl.on_span_start(span) + + def on_span_end(self, span: Span[Any]) -> None: + if workflow.in_workflow() and workflow.unsafe.is_replaying(): + # In replay mode, don't report + return + self._impl.on_span_end(span) + + def shutdown(self) -> None: + self._impl.shutdown() + + def force_flush(self) -> None: + self._impl.force_flush() + + +class TemporalTraceProvider(TraceProvider): + """A trace provider that integrates with Temporal workflows.""" + + def __init__(self): + """Initialize the TemporalTraceProvider.""" + super().__init__() + self._original_provider = get_trace_provider() + self._multi_processor = _TemporalTracingProcessor( # type: ignore[assignment] + self._original_provider._multi_processor + ) + + def time_iso(self) -> str: + """Return the current deterministic time in ISO 8601 format.""" + if workflow.in_workflow(): + return workflow.now().isoformat() + return super().time_iso() + + def gen_trace_id(self) -> str: + """Generate a new trace ID.""" + if workflow.in_workflow(): + try: + """Generate a new trace ID.""" + return f"trace_{workflow.uuid4().hex}" + except ReadOnlyContextError: + return f"trace_{uuid.uuid4().hex}" + return super().gen_trace_id() + + def gen_span_id(self) -> str: + """Generate a span ID.""" + if workflow.in_workflow(): + try: + """Generate a deterministic span ID.""" + return f"span_{workflow.uuid4().hex[:24]}" + except ReadOnlyContextError: + return f"span_{uuid.uuid4().hex[:24]}" + return super().gen_span_id() + + def gen_group_id(self) -> str: + """Generate a group ID.""" + if workflow.in_workflow(): + try: + """Generate a deterministic group ID.""" + return f"group_{workflow.uuid4().hex[:24]}" + except ReadOnlyContextError: + return f"group_{uuid.uuid4().hex[:24]}" + return super().gen_group_id() + + def __enter__(self): + """Enter the context of the Temporal trace provider.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit the context of the Temporal trace provider.""" + # cleanup code + self._multi_processor.shutdown() diff --git a/temporalio/contrib/openai_agents/invoke_model_activity.py b/temporalio/contrib/openai_agents/invoke_model_activity.py new file mode 100644 index 000000000..2dcce26e9 --- /dev/null +++ b/temporalio/contrib/openai_agents/invoke_model_activity.py @@ -0,0 +1,175 @@ +"""A temporal activity that invokes a LLM model. + +Implements mapping of OpenAI datastructures to Pydantic friendly types. +""" + +import enum +import json +from dataclasses import dataclass +from typing import Any, Optional, Required, TypedDict, Union, cast + +from agents import ( + AgentOutputSchemaBase, + ComputerTool, + FileSearchTool, + FunctionTool, + Handoff, + ModelResponse, + ModelSettings, + ModelTracing, + RunContextWrapper, + Tool, + TResponseInputItem, + UserError, + WebSearchTool, +) +from agents.models.multi_provider import MultiProvider + +from temporalio import activity +from temporalio.contrib.openai_agents._heartbeat_decorator import _auto_heartbeater + + +@dataclass +class HandoffInput: + """Data conversion friendly representation of a Handoff.""" + + tool_name: str + tool_description: str + input_json_schema: dict[str, Any] + agent_name: str + # input_filter: HandoffInputFilter | None = None + strict_json_schema: bool = True + + +@dataclass +class FunctionToolInput: + """Data conversion friendly representation of a FunctionTool.""" + + name: str + description: str + params_json_schema: dict[str, Any] + # on_invoke_tool: Callable[[RunContextWrapper[Any], str], Awaitable[Any]] + strict_json_schema: bool = True + + +ToolInput = Union[FunctionToolInput, FileSearchTool, WebSearchTool] + + +@dataclass +class AgentOutputSchemaInput(AgentOutputSchemaBase): + """Data conversion friendly representation of AgentOutputSchema.""" + + output_type_name: str | None + is_wrapped: bool + output_schema: dict[str, Any] | None + strict_json_schema: bool + + def is_plain_text(self) -> bool: + """Whether the output type is plain text (versus a JSON object).""" + return self.output_type_name is None or self.output_type_name == "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") + if self.output_schema is None: + raise UserError("Output schema is not defined") + return self.output_schema + + def validate_json(self, json_str: str) -> Any: + """Validate the JSON string against the schema.""" + raise NotImplementedError() + + def name(self) -> str: + """Get the name of the output type.""" + if self.output_type_name is None: + raise ValueError("output_type_name is None") + return self.output_type_name + + +class ModelTracingInput(enum.IntEnum): + """Conversion friendly representation of ModelTracing. + + Needed as ModelTracing is enum.Enum instead of IntEnum + """ + + DISABLED = 0 + ENABLED = 1 + ENABLED_WITHOUT_DATA = 2 + + +class ActivityModelInput(TypedDict, total=False): + """Input for the invoke_model_activity activity.""" + + model_name: Optional[str] + system_instructions: Optional[str] + input: Required[str | list[TResponseInputItem]] + model_settings: Required[ModelSettings] + tools: list[ToolInput] + output_schema: Optional[AgentOutputSchemaInput] + handoffs: list[HandoffInput] + tracing: Required[ModelTracingInput] + previous_response_id: Optional[str] + + +@activity.defn +@_auto_heartbeater +async def invoke_model_activity(input: ActivityModelInput) -> ModelResponse: + """Activity that invokes a model with the given input.""" + # TODO: Is model caching needed here? + model = MultiProvider().get_model(input.get("model_name")) + + async def empty_on_invoke_tool(ctx: RunContextWrapper[Any], input: str) -> str: + return "" + + async def empty_on_invoke_handoff(ctx: RunContextWrapper[Any], input: str) -> Any: + return None + + # workaround for https://github.com/pydantic/pydantic/issues/9541 + # ValidatorIterator returned + input_json = json.dumps(input["input"], default=lambda o: str(o)) + input_input = json.loads(input_json) + + def make_tool(tool: ToolInput) -> Tool: + if isinstance(tool, FileSearchTool): + return cast(FileSearchTool, tool) + elif isinstance(tool, WebSearchTool): + return cast(WebSearchTool, tool) + elif isinstance(tool, FunctionToolInput): + t = cast(FunctionToolInput, tool) + return FunctionTool( + name=t.name, + description=t.description, + params_json_schema=t.params_json_schema, + on_invoke_tool=empty_on_invoke_tool, + strict_json_schema=t.strict_json_schema, + ) + else: + raise UserError(f"Unknown tool type: {tool.name}") + + tools = [make_tool(x) for x in input.get("tools", [])] + handoffs = [ + Handoff( + tool_name=x.tool_name, + tool_description=x.tool_description, + input_json_schema=x.input_json_schema, + agent_name=x.agent_name, + strict_json_schema=x.strict_json_schema, + on_invoke_handoff=empty_on_invoke_handoff, + ) + for x in input.get("handoffs", []) + ] + return await model.get_response( + system_instructions=input.get("system_instructions"), + input=input_input, + model_settings=input["model_settings"], + tools=tools, + output_schema=input.get("output_schema"), + handoffs=handoffs, + tracing=ModelTracing(input["tracing"]), + previous_response_id=input.get("previous_response_id"), + ) diff --git a/temporalio/contrib/openai_agents/open_ai_data_converter.py b/temporalio/contrib/openai_agents/open_ai_data_converter.py new file mode 100644 index 000000000..01f9a6283 --- /dev/null +++ b/temporalio/contrib/openai_agents/open_ai_data_converter.py @@ -0,0 +1,104 @@ +"""DataConverter that supports conversion of types used by OpenAI Agents SDK. + +These are mostly Pydantic types. Some of them should be explicitly imported. +""" + +from __future__ import annotations + +from typing import Any, Optional, Type, TypeVar + +from agents import Usage +from agents.items import TResponseOutputItem +from openai import NOT_GIVEN, BaseModel +from pydantic import RootModel, TypeAdapter + +import temporalio.api.common.v1 +from temporalio.converter import ( + CompositePayloadConverter, + DataConverter, + DefaultPayloadConverter, + EncodingPayloadConverter, + JSONPlainPayloadConverter, +) + +T = TypeVar("T", bound=BaseModel) + + +class _WrapperModel(RootModel[T]): + model_config = { + "arbitrary_types_allowed": True, + } + + +class _OpenAIJSONPlainPayloadConverter(EncodingPayloadConverter): + """Pydantic JSON payload converter. + + Supports conversion of all types supported by Pydantic to and from JSON. + + In addition to Pydantic models, these include all `json.dump`-able types, + various non-`json.dump`-able standard library types such as dataclasses, + types from the datetime module, sets, UUID, etc, and custom types composed + of any of these. + + See https://docs.pydantic.dev/latest/api/standard_library_types/ + """ + + @property + def encoding(self) -> str: + """See base class.""" + return "json/plain" + + def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: + """See base class. + Needs _WrapperModel configure arbitrary_types_allowed=True + """ + wrapper = _WrapperModel[Any](root=value) + data = wrapper.model_dump_json().encode() + + return temporalio.api.common.v1.Payload( + metadata={"encoding": self.encoding.encode()}, data=data + ) + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: Optional[Type] = None, + ) -> Any: + _type_hint = type_hint if type_hint is not None else Any + wrapper = _WrapperModel[_type_hint] # type: ignore[valid-type] + # Needed due to + # if TYPE_CHECKING: + # from .agent import Agent + # + # in the agents/items.py + wrapper.model_rebuild( + _types_namespace={ + "TResponseOutputItem": TResponseOutputItem, + "Usage": Usage, + } + ) + return TypeAdapter(wrapper).validate_json(payload.data.decode()).root + + +class OpenAIPayloadConverter(CompositePayloadConverter): + """Payload converter for payloads containing pydantic model instances. + + JSON conversion is replaced with a converter that uses + :py:class:`PydanticJSONPlainPayloadConverter`. + """ + + def __init__(self) -> None: + """Initialize object""" + json_payload_converter = _OpenAIJSONPlainPayloadConverter() + super().__init__( + *( + c + if not isinstance(c, JSONPlainPayloadConverter) + else json_payload_converter + for c in DefaultPayloadConverter.default_encoding_payload_converters + ) + ) + + +open_ai_data_converter = DataConverter(payload_converter_class=OpenAIPayloadConverter) +"""Open AI Agent library types data converter""" diff --git a/temporalio/contrib/openai_agents/temporal_openai_agents.py b/temporalio/contrib/openai_agents/temporal_openai_agents.py new file mode 100644 index 000000000..0e3a1a0d6 --- /dev/null +++ b/temporalio/contrib/openai_agents/temporal_openai_agents.py @@ -0,0 +1,25 @@ +"""Initialize Temporal OpenAI Agents overrides.""" + +from agents import set_default_runner, set_trace_provider + +from temporalio.contrib.openai_agents._openai_runner import TemporalOpenAIRunner +from temporalio.contrib.openai_agents._temporal_trace_provider import ( + TemporalTraceProvider, +) + + +def set_open_ai_agent_temporal_overrides() -> TemporalTraceProvider: + """Should be called in the main entry point of the application. + + The intended usage is: + + with set_open_ai_agent_temporal_overrides(): + # Initialize the Temporal client and start the worker. + + TODO: Revert runner on __exit__ to the previous one. + TODO: Consider wrapping the worker instead of this method. + """ + set_default_runner(TemporalOpenAIRunner()) + provider = TemporalTraceProvider() + set_trace_provider(provider) + return provider diff --git a/temporalio/contrib/openai_agents/temporal_tools.py b/temporalio/contrib/openai_agents/temporal_tools.py new file mode 100644 index 000000000..4c8bbc3bf --- /dev/null +++ b/temporalio/contrib/openai_agents/temporal_tools.py @@ -0,0 +1,46 @@ +"""Support for using Temporal activities as OpenAI agents tools.""" + +from temporalio.workflow import unsafe + +with unsafe.imports_passed_through(): + from datetime import timedelta + from typing import Any, Callable + + from agents import FunctionTool, RunContextWrapper, Tool + from agents.function_schema import function_schema + + from temporalio import activity, workflow + from temporalio.exceptions import ApplicationError + + +def activities_as_tools(*tools: Callable) -> list[Tool]: + """Convert Temporal activities to OpenAI agents tools.""" + return [activity_as_tool(tool) for tool in tools] + + +def activity_as_tool(fn: Callable) -> Tool: + """Convert a Temporal activity function to an OpenAI agents tool.""" + ret = activity._Definition.from_callable(fn) + if not ret: + raise ApplicationError( + "Bare function without tool and activity decorators is not supported", + "invalid_tool", + ) + + async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any: + return str( + await workflow.execute_activity( + fn, + input, + start_to_close_timeout=timedelta(seconds=10), + ) + ) + + schema = function_schema(fn) + return FunctionTool( + name=schema.name, + description=schema.description or "", + params_json_schema=schema.params_json_schema, + on_invoke_tool=run_activity, + strict_json_schema=True, + ) diff --git a/temporalio/contrib/openai_agents/trace_interceptor.py b/temporalio/contrib/openai_agents/trace_interceptor.py new file mode 100644 index 000000000..3d2cd6936 --- /dev/null +++ b/temporalio/contrib/openai_agents/trace_interceptor.py @@ -0,0 +1,321 @@ +"""Adds OpenAI Agents traces and spans to Temporal workflows and activities.""" + +from __future__ import annotations + +from contextlib import contextmanager +from typing import Any, Mapping, Protocol, Type + +from agents import CustomSpanData, custom_span, get_current_span, trace +from agents.tracing import ( + get_trace_provider, # pyright: ignore[reportPrivateImportUsage] +) +from agents.tracing.spans import NoOpSpan, SpanImpl + +import temporalio.activity +import temporalio.api.common.v1 +import temporalio.client +import temporalio.converter +import temporalio.worker +import temporalio.workflow +from temporalio import activity, workflow + +HEADER_KEY = "__openai_span" + + +class _InputWithHeaders(Protocol): + headers: Mapping[str, temporalio.api.common.v1.Payload] + + +def set_header_from_context( + input: _InputWithHeaders, payload_converter: temporalio.converter.PayloadConverter +) -> None: + """Inserts the OpenAI Agents trace/span data in the input header.""" + current = get_current_span() + if current is None or isinstance(current, NoOpSpan): + return + + trace = get_trace_provider().get_current_trace() + input.headers = { + **input.headers, + HEADER_KEY: payload_converter.to_payload( + { + "traceName": trace.name if trace else "Unknown Workflow", + "spanId": current.span_id, + "traceId": current.trace_id, + } + ), + } + + +@contextmanager +def context_from_header( + span_name: str, + input: _InputWithHeaders, + payload_converter: temporalio.converter.PayloadConverter, +): + """Extracts and initializes trace information the input header.""" + payload = input.headers.get(HEADER_KEY) + span_info = payload_converter.from_payload(payload) if payload else None + if span_info is None: + yield + else: + span = SpanImpl( + trace_id=str(span_info["traceId"]), + span_id=span_info["spanId"], + parent_id=None, + span_data=CustomSpanData( + name="Parent Temporal Span", + data={}, + ), + processor=get_trace_provider()._multi_processor, + ) + workflow_type = ( + activity.info().workflow_type + if activity.in_activity() + else workflow.info().workflow_type + ) + data = ( + {"activityId": activity.info().activity_id} + if activity.in_activity() + else None + ) + if get_trace_provider().get_current_trace() is None: + metadata = { + "temporal:workflowId": activity.info().workflow_id + if activity.in_activity() + else workflow.info().workflow_id, + "temporal:runId": activity.info().workflow_run_id + if activity.in_activity() + else workflow.info().run_id, + "temporal:workflowType": workflow_type, + } + with trace( + span_info["traceName"], + trace_id=span_info["traceId"], + metadata=metadata, + ): + with custom_span(name=span_name, parent=span, data=data): + yield + else: + with custom_span(name=span_name, parent=span, data=data): + yield + + +class OpenAIAgentsTracingInterceptor( + temporalio.client.Interceptor, temporalio.worker.Interceptor +): + """Interceptor that can serialize/deserialize contexts.""" + + def __init__( + self, + payload_converter: temporalio.converter.PayloadConverter = temporalio.converter.default().payload_converter, + ) -> None: + """Initialize the interceptor with a payload converter.""" + self._payload_converter = payload_converter + + def intercept_client( + self, next: temporalio.client.OutboundInterceptor + ) -> temporalio.client.OutboundInterceptor: + """Intercepts client calls to propagate context.""" + return _ContextPropagationClientOutboundInterceptor( + next, self._payload_converter + ) + + def intercept_activity( + self, next: temporalio.worker.ActivityInboundInterceptor + ) -> temporalio.worker.ActivityInboundInterceptor: + """Intercepts activity calls to propagate context.""" + return _ContextPropagationActivityInboundInterceptor(next) + + def workflow_interceptor_class( + self, input: temporalio.worker.WorkflowInterceptorClassInput + ) -> Type[_ContextPropagationWorkflowInboundInterceptor]: + """Returns the workflow interceptor class to propagate context.""" + return _ContextPropagationWorkflowInboundInterceptor + + +class _ContextPropagationClientOutboundInterceptor( + temporalio.client.OutboundInterceptor +): + def __init__( + self, + next: temporalio.client.OutboundInterceptor, + payload_converter: temporalio.converter.PayloadConverter, + ) -> None: + super().__init__(next) + self._payload_converter = payload_converter + + async def start_workflow( + self, input: temporalio.client.StartWorkflowInput + ) -> temporalio.client.WorkflowHandle[Any, Any]: + metadata = { + "temporal:workflowType": input.workflow, + **({"temporal:workflowId": input.id} if input.id else {}), + } + data = {"workflowId": input.id} if input.id else None + span_name = f"temporal:startWorkflow" + if get_trace_provider().get_current_trace() is None: + with trace( + span_name + ":" + input.workflow, metadata=metadata, group_id=input.id + ): + with custom_span(name=span_name + ":" + input.workflow, data=data): + set_header_from_context(input, self._payload_converter) + return await super().start_workflow(input) + else: + with custom_span(name=span_name, data=data): + set_header_from_context(input, self._payload_converter) + return await super().start_workflow(input) + + async def query_workflow(self, input: temporalio.client.QueryWorkflowInput) -> Any: + metadata = { + "temporal:queryWorkflow": input.query, + **({"temporal:workflowId": input.id} if input.id else {}), + } + data = {"workflowId": input.id, "query": input.query} + span_name = f"temporal:queryWorkflow" + if get_trace_provider().get_current_trace() is None: + with trace(span_name, metadata=metadata, group_id=input.id): + with custom_span(name=span_name, data=data): + set_header_from_context(input, self._payload_converter) + return await super().query_workflow(input) + else: + with custom_span(name=span_name, data=data): + set_header_from_context(input, self._payload_converter) + return await super().query_workflow(input) + + async def signal_workflow( + self, input: temporalio.client.SignalWorkflowInput + ) -> None: + metadata = { + "temporal:signalWorkflow": input.signal, + **({"temporal:workflowId": input.id} if input.id else {}), + } + data = {"workflowId": input.id, "signal": input.signal} + span_name = f"temporal:signalWorkflow" + if get_trace_provider().get_current_trace() is None: + with trace(span_name, metadata=metadata, group_id=input.id): + with custom_span(name=span_name, data=data): + set_header_from_context(input, self._payload_converter) + await super().signal_workflow(input) + else: + with custom_span(name=span_name, data=data): + set_header_from_context(input, self._payload_converter) + await super().signal_workflow(input) + + async def start_workflow_update( + self, input: temporalio.client.StartWorkflowUpdateInput + ) -> temporalio.client.WorkflowUpdateHandle[Any]: + metadata = { + "temporal:updateWorkflow": input.update, + **({"temporal:workflowId": input.id} if input.id else {}), + } + data = { + **({"workflowId": input.id} if input.id else {}), + "update": input.update, + } + span_name = "temporal:updateWorkflow" + if get_trace_provider().get_current_trace() is None: + with trace(span_name, metadata=metadata, group_id=input.id): + with custom_span(name=span_name, data=data): + set_header_from_context(input, self._payload_converter) + return await self.next.start_workflow_update(input) + else: + with custom_span(name=span_name, data=data): + set_header_from_context(input, self._payload_converter) + return await self.next.start_workflow_update(input) + + +class _ContextPropagationActivityInboundInterceptor( + temporalio.worker.ActivityInboundInterceptor +): + async def execute_activity( + self, input: temporalio.worker.ExecuteActivityInput + ) -> Any: + with context_from_header( + "temporal:executeActivity", input, temporalio.activity.payload_converter() + ): + return await self.next.execute_activity(input) + + +class _ContextPropagationWorkflowInboundInterceptor( + temporalio.worker.WorkflowInboundInterceptor +): + def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: + self.next.init(_ContextPropagationWorkflowOutboundInterceptor(outbound)) + + async def execute_workflow( + self, input: temporalio.worker.ExecuteWorkflowInput + ) -> Any: + with context_from_header( + "temporal:executeWorkflow", input, temporalio.workflow.payload_converter() + ): + return await self.next.execute_workflow(input) + + async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> None: + with context_from_header( + "temporal:handleSignal", input, temporalio.workflow.payload_converter() + ): + return await self.next.handle_signal(input) + + async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: + with context_from_header( + "temporal:handleQuery", input, temporalio.workflow.payload_converter() + ): + return await self.next.handle_query(input) + + def handle_update_validator( + self, input: temporalio.worker.HandleUpdateInput + ) -> None: + with context_from_header( + "temporal:handleUpdateValidator", + input, + temporalio.workflow.payload_converter(), + ): + self.next.handle_update_validator(input) + + async def handle_update_handler( + self, input: temporalio.worker.HandleUpdateInput + ) -> Any: + with context_from_header( + "temporal:handleUpdateHandler", + input, + temporalio.workflow.payload_converter(), + ): + return await self.next.handle_update_handler(input) + + +class _ContextPropagationWorkflowOutboundInterceptor( + temporalio.worker.WorkflowOutboundInterceptor +): + async def signal_child_workflow( + self, input: temporalio.worker.SignalChildWorkflowInput + ) -> None: + set_header_from_context(input, temporalio.workflow.payload_converter()) + return await self.next.signal_child_workflow(input) + + async def signal_external_workflow( + self, input: temporalio.worker.SignalExternalWorkflowInput + ) -> None: + set_header_from_context(input, temporalio.workflow.payload_converter()) + return await self.next.signal_external_workflow(input) + + def start_activity( + self, input: temporalio.worker.StartActivityInput + ) -> temporalio.workflow.ActivityHandle: + with custom_span( + name=f"temporal:startActivity:{input.activity}", + ): + set_header_from_context(input, temporalio.workflow.payload_converter()) + return self.next.start_activity(input) + + async def start_child_workflow( + self, input: temporalio.worker.StartChildWorkflowInput + ) -> temporalio.workflow.ChildWorkflowHandle: + set_header_from_context(input, temporalio.workflow.payload_converter()) + return await self.next.start_child_workflow(input) + + def start_local_activity( + self, input: temporalio.worker.StartLocalActivityInput + ) -> temporalio.workflow.ActivityHandle: + set_header_from_context(input, temporalio.workflow.payload_converter()) + return self.next.start_local_activity(input) diff --git a/uv.lock b/uv.lock index f814c2c2c..6668b9d85 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,9 @@ version = 1 -revision = 1 requires-python = ">=3.9, <4" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] [[package]] name = "annotated-types" @@ -11,6 +14,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + [[package]] name = "attrs" version = "25.1.0" @@ -229,6 +247,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/de/67bc9e8972717710a6faf814b7613653ab9efe7507015ca0ead4eba0f8cf/cibuildwheel-2.22.0-py3-none-any.whl", hash = "sha256:c40bb7ac7b57fed8195fca624cc9bd68334375d32b75bea6fa8330ac1cd902c4", size = 91689 }, ] +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -314,6 +344,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +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 = "docutils" version = "0.21.2" @@ -341,6 +380,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, ] +[[package]] +name = "griffe" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303 }, +] + [[package]] name = "grpcio" version = "1.70.0" @@ -452,6 +503,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/66/7c1a552545a9597fbd33d77c817f1f0cc56736ca64aa0821948f945118d6/grpcio_tools-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:840ec536ab933db2ef8d5acaa6b712d0e9e8f397f62907c852ec50a3f69cdb78", size = 1119339 }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +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 = "hyperlink" version = "21.0.0" @@ -552,6 +649,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, ] +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215 }, + { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814 }, + { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999 }, + { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109 }, + { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608 }, + { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454 }, + { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833 }, + { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646 }, + { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735 }, + { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747 }, + { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484 }, + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, + { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262 }, + { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124 }, + { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330 }, + { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670 }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057 }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372 }, + { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038 }, + { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538 }, + { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557 }, + { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202 }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781 }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176 }, + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617 }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947 }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618 }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829 }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034 }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529 }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671 }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864 }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989 }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495 }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074 }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225 }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235 }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278 }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866 }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772 }, + { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534 }, + { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087 }, + { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694 }, + { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992 }, + { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723 }, + { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215 }, + { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762 }, + { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427 }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127 }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527 }, + { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213 }, + { url = "https://files.pythonhosted.org/packages/98/fd/aced428e2bd3c6c1132f67c5a708f9e7fd161d0ca8f8c5862b17b93cdf0a/jiter-0.10.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bd6292a43c0fc09ce7c154ec0fa646a536b877d1e8f2f96c19707f65355b5a4d", size = 317665 }, + { url = "https://files.pythonhosted.org/packages/b6/2e/47d42f15d53ed382aef8212a737101ae2720e3697a954f9b95af06d34e89/jiter-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39de429dcaeb6808d75ffe9effefe96a4903c6a4b376b2f6d08d77c1aaee2f18", size = 312152 }, + { url = "https://files.pythonhosted.org/packages/7b/02/aae834228ef4834fc18718724017995ace8da5f70aa1ec225b9bc2b2d7aa/jiter-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ce124f13a7a616fad3bb723f2bfb537d78239d1f7f219566dc52b6f2a9e48d", size = 346708 }, + { url = "https://files.pythonhosted.org/packages/35/d4/6ff39dee2d0a9abd69d8a3832ce48a3aa644eed75e8515b5ff86c526ca9a/jiter-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:166f3606f11920f9a1746b2eea84fa2c0a5d50fd313c38bdea4edc072000b0af", size = 371360 }, + { url = "https://files.pythonhosted.org/packages/a9/67/c749d962b4eb62445867ae4e64a543cbb5d63cc7d78ada274ac515500a7f/jiter-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28dcecbb4ba402916034fc14eba7709f250c4d24b0c43fc94d187ee0580af181", size = 492105 }, + { url = "https://files.pythonhosted.org/packages/f6/d3/8fe1b1bae5161f27b1891c256668f598fa4c30c0a7dacd668046a6215fca/jiter-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86c5aa6910f9bebcc7bc4f8bc461aff68504388b43bfe5e5c0bd21efa33b52f4", size = 389577 }, + { url = "https://files.pythonhosted.org/packages/ef/28/ecb19d789b4777898a4252bfaac35e3f8caf16c93becd58dcbaac0dc24ad/jiter-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceeb52d242b315d7f1f74b441b6a167f78cea801ad7c11c36da77ff2d42e8a28", size = 353849 }, + { url = "https://files.pythonhosted.org/packages/77/69/261f798f84790da6482ebd8c87ec976192b8c846e79444d0a2e0d33ebed8/jiter-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ff76d8887c8c8ee1e772274fcf8cc1071c2c58590d13e33bd12d02dc9a560397", size = 392029 }, + { url = "https://files.pythonhosted.org/packages/cb/08/b8d15140d4d91f16faa2f5d416c1a71ab1bbe2b66c57197b692d04c0335f/jiter-0.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a9be4d0fa2b79f7222a88aa488bd89e2ae0a0a5b189462a12def6ece2faa45f1", size = 524386 }, + { url = "https://files.pythonhosted.org/packages/9b/1d/23c41765cc95c0e23ac492a88450d34bf0fd87a37218d1b97000bffe0f53/jiter-0.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab7fd8738094139b6c1ab1822d6f2000ebe41515c537235fd45dabe13ec9324", size = 515234 }, + { url = "https://files.pythonhosted.org/packages/9f/14/381d8b151132e79790579819c3775be32820569f23806769658535fe467f/jiter-0.10.0-cp39-cp39-win32.whl", hash = "sha256:5f51e048540dd27f204ff4a87f5d79294ea0aa3aa552aca34934588cf27023cf", size = 211436 }, + { url = "https://files.pythonhosted.org/packages/59/66/f23ae51dea8ee8ce429027b60008ca895d0fa0704f0c7fe5f09014a6cffb/jiter-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b28302349dc65703a9e4ead16f163b1c339efffbe1049c30a44b001a2a4fff9", size = 208777 }, +] + [[package]] name = "keyring" version = "25.6.0" @@ -614,6 +795,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/df/3641646696277249407c923795825176403c208a6553e0fd21b6764038b5/maturin-1.8.2-py3-none-win_arm64.whl", hash = "sha256:4232c2380faf61862d27269c6acf14e1d542c4ba64086a3f5c356d6e5e4823e7", size = 6656754 }, ] +[[package]] +name = "mcp" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { 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' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/df/8fefc0c6c7a5c66914763e3ff3893f9a03435628f6625d5e3b0dc45d73db/mcp-1.9.3.tar.gz", hash = "sha256:587ba38448e81885e5d1b84055cfcc0ca56d35cd0c58f50941cab01109405388", size = 333045 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/45/823ad05504bea55cb0feb7470387f151252127ad5c72f8882e8fe6cf5c0e/mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9", size = 131063 }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -786,6 +987,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] +[[package]] +name = "openai" +version = "1.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/a3/128caf24e116f48fad3e4d5122cdf84db06c5127911849d51663c66158c8/openai-1.84.0.tar.gz", hash = "sha256:4caa43bdab262cc75680ce1a2322cfc01626204074f7e8d9939ab372acf61698", size = 467066 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/10/f245db006a860dbc1f2e2c8382e0a1762c7753e7971ba43a1dc3f3ec1404/openai-1.84.0-py3-none-any.whl", hash = "sha256:7ec4436c3c933d68dc0f5a0cef0cb3dbc0864a54d62bddaf2ed5f3d521844711", size = 725512 }, +] + +[[package]] +name = "openai-agents" +version = "0.0.15" +source = { git = "https://github.com/openai/openai-agents-python.git?rev=pakrym%2Frunner-class#27646ee1c31dc191f1cf1268563de8ca6c6f111b" } +dependencies = [ + { name = "griffe" }, + { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] + [[package]] name = "opentelemetry-api" version = "1.30.0" @@ -1012,6 +1246,20 @@ wheels = [ { 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 }, ] +[[package]] +name = "pydantic-settings" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, +] + [[package]] name = "pydocstyle" version = "6.3.0" @@ -1119,6 +1367,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +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/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 = "pywin32-ctypes" version = "0.2.3" @@ -1248,6 +1514,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -1257,6 +1532,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, ] +[[package]] +name = "sse-starlette" +version = "2.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606 }, +] + +[[package]] +name = "starlette" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/d0/0332bd8a25779a0e2082b0e179805ad39afad642938b371ae0882e7f880d/starlette-0.47.0.tar.gz", hash = "sha256:1f64887e94a447fed5f23309fb6890ef23349b7e478faa7b24a851cd4eb844af", size = 2582856 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/81/c60b35fe9674f63b38a8feafc414fca0da378a9dbd5fa1e0b8d23fcc7a9b/starlette-0.47.0-py3-none-any.whl", hash = "sha256:9d052d4933683af40ffd47c7465433570b4949dc937e20ad1d73b34e72f10c37", size = 72796 }, +] + [[package]] name = "temporalio" version = "1.11.0" @@ -1272,6 +1571,9 @@ dependencies = [ grpc = [ { name = "grpcio" }, ] +openai-agents = [ + { name = "openai-agents" }, +] opentelemetry = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, @@ -1302,6 +1604,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.48.2,<2" }, + { name = "openai-agents", marker = "extra == 'openai-agents'", git = "https://github.com/openai/openai-agents-python.git?rev=pakrym%2Frunner-class" }, { name = "opentelemetry-api", marker = "extra == 'opentelemetry'", specifier = ">=1.11.1,<2" }, { name = "opentelemetry-sdk", marker = "extra == 'opentelemetry'", specifier = ">=1.11.1,<2" }, { name = "protobuf", specifier = ">=3.20" }, @@ -1310,7 +1613,6 @@ requires-dist = [ { name = "types-protobuf", specifier = ">=3.20" }, { name = "typing-extensions", specifier = ">=4.2.0,<5" }, ] -provides-extras = ["grpc", "opentelemetry", "pydantic"] [package.metadata.requires-dev] dev = [ @@ -1379,6 +1681,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + [[package]] name = "twine" version = "4.0.2" @@ -1426,6 +1740,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/e9/095c36efdc9dbb1df75ce42f37c0de6a6879aad937a08f0b97942b7c5968/types_protobuf-5.29.1.20250208-py3-none-any.whl", hash = "sha256:c5f8bfb4afdc1b5cbca1848f2c8b361a2090add7401f410b22b599ef647bf483", size = 73925 }, ] +[[package]] +name = "types-requests" +version = "2.32.0.20250602" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/b0/5321e6eeba5d59e4347fcf9bf06a5052f085c3aa0f4876230566d6a4dc97/types_requests-2.32.0.20250602.tar.gz", hash = "sha256:ee603aeefec42051195ae62ca7667cd909a2f8128fdf8aad9e8a5219ecfab3bf", size = 23042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/18/9b782980e575c6581d5c0c1c99f4c6f89a1d7173dad072ee96b2756c02e6/types_requests-2.32.0.20250602-py3-none-any.whl", hash = "sha256:f4f335f87779b47ce10b8b8597b409130299f6971ead27fead4fe7ba6ea3e726", size = 20638 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -1435,6 +1761,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + [[package]] name = "urllib3" version = "2.3.0" @@ -1444,6 +1782,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] +[[package]] +name = "uvicorn" +version = "0.34.3" +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/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431 }, +] + [[package]] name = "wrapt" version = "1.17.2"