From fb695893cf38e0aa6cb184e49567e8cb08c1d099 Mon Sep 17 00:00:00 2001 From: Nightingalelyy Date: Tue, 18 Aug 2026 02:45:57 +0800 Subject: [PATCH] fix(examples): validate OpenAI, OpenAI Agents, and OpenInference OTel 2.x spans --- python/tracing/openai-agents-sdk/README.md | 55 +- .../agent_patterns/agents_as_tools_test.py | 8 +- .../agent_patterns/deterministic_test.py | 16 +- .../agent_patterns/input_guardrails_test.py | 17 +- .../agent_patterns/llm_as_a_judge_test.py | 30 +- .../agent_patterns/output_guardrails_test.py | 16 +- .../agent_patterns/parallelization_test.py | 9 +- .../agent_patterns/routing_test.py | 22 +- .../basic/dynamic_system_prompt_test.py | 16 +- .../basic/hello_world_test.py | 15 +- .../basic/lifecycle_with_hook_example_test.py | 27 +- .../basic/stream_items_test.py | 10 +- .../basic/stream_text_test.py | 20 +- .../openai-agents-sdk/bridge_contract.py | 64 + .../complex_edge_cases_test.py | 89 +- python/tracing/openai-agents-sdk/conftest.py | 66 +- .../contract_scenarios_test.py | 166 +++ .../customer_service/main_test.py | 45 +- .../handoffs/handsoff_test.py | 15 +- .../handoffs/message_filter_streaming_test.py | 33 +- .../handoffs/message_filter_test.py | 21 +- python/tracing/openai-agents-sdk/poetry.lock | 1152 ++++++++++++++--- .../tracing/openai-agents-sdk/pyproject.toml | 22 +- .../openai-agents-sdk/requirements.txt | 8 + .../research_bot/agents/planner_agent.py | 3 +- .../research_bot/agents/writer_agent.py | 3 +- .../research_bot/main_test.py | 18 +- .../openai-agents-sdk/research_bot/manager.py | 17 +- .../openai-agents-sdk/research_bot/printer.py | 6 +- .../respan_exporter_openai_agents.py | 99 +- python/tracing/openai-agents-sdk/run_all.py | 84 ++ .../openai-agents-sdk/runner_contract.py | 26 + .../tools/computer_use_test.py | 33 +- .../tools/file_search_test.py | 17 +- .../openai-agents-sdk/tools/functions_test.py | 11 +- .../tools/web_search_test.py | 19 +- python/tracing/openai-sdk/.env.example | 8 +- python/tracing/openai-sdk/README.md | 46 + python/tracing/openai-sdk/_batch_data.py | 55 + python/tracing/openai-sdk/_shared.py | 411 ++++++ python/tracing/openai-sdk/async_parallel.py | 69 +- python/tracing/openai-sdk/attributes.py | 58 +- python/tracing/openai-sdk/batch.py | 136 +- python/tracing/openai-sdk/batch_async.py | 140 +- python/tracing/openai-sdk/decorators.py | 67 +- python/tracing/openai-sdk/hello_world.py | 58 +- python/tracing/openai-sdk/multi_turn.py | 67 +- python/tracing/openai-sdk/prompt.py | 66 +- .../tracing/openai-sdk/prompt_multi_turn.py | 68 +- python/tracing/openai-sdk/requirements.txt | 6 + .../openai-sdk/responses_hello_world.py | 48 +- .../openai-sdk/responses_multi_turn.py | 76 +- python/tracing/openai-sdk/responses_prompt.py | 78 +- .../tracing/openai-sdk/responses_streaming.py | 54 +- .../openai-sdk/responses_structured_output.py | 52 +- .../openai-sdk/responses_tool_calls.py | 97 +- python/tracing/openai-sdk/run_all.py | 71 + python/tracing/openai-sdk/streaming.py | 64 +- .../tracing/openai-sdk/structured_output.py | 58 +- .../openai-sdk/test_example_contract.py | 146 +++ python/tracing/openai-sdk/tool_calls.py | 94 +- .../tracing/openinference/01_chat_provider.py | 56 + python/tracing/openinference/02_tool_call.py | 86 ++ python/tracing/openinference/03_embedding.py | 45 + .../openinference/04_expected_failure.py | 52 + .../openinference/05_streaming_privacy.py | 67 + python/tracing/openinference/README.md | 48 + python/tracing/openinference/_shared.py | 111 ++ python/tracing/openinference/requirements.txt | 3 + python/tracing/openinference/run_all.py | 60 + .../openinference/test_example_contract.py | 92 ++ 71 files changed, 3834 insertions(+), 1157 deletions(-) create mode 100644 python/tracing/openai-agents-sdk/bridge_contract.py create mode 100644 python/tracing/openai-agents-sdk/contract_scenarios_test.py create mode 100644 python/tracing/openai-agents-sdk/requirements.txt create mode 100644 python/tracing/openai-agents-sdk/run_all.py create mode 100644 python/tracing/openai-agents-sdk/runner_contract.py create mode 100644 python/tracing/openai-sdk/README.md create mode 100644 python/tracing/openai-sdk/_batch_data.py create mode 100644 python/tracing/openai-sdk/_shared.py create mode 100644 python/tracing/openai-sdk/requirements.txt create mode 100644 python/tracing/openai-sdk/run_all.py create mode 100644 python/tracing/openai-sdk/test_example_contract.py create mode 100644 python/tracing/openinference/01_chat_provider.py create mode 100644 python/tracing/openinference/02_tool_call.py create mode 100644 python/tracing/openinference/03_embedding.py create mode 100644 python/tracing/openinference/04_expected_failure.py create mode 100644 python/tracing/openinference/05_streaming_privacy.py create mode 100644 python/tracing/openinference/README.md create mode 100644 python/tracing/openinference/_shared.py create mode 100644 python/tracing/openinference/requirements.txt create mode 100644 python/tracing/openinference/run_all.py create mode 100644 python/tracing/openinference/test_example_contract.py diff --git a/python/tracing/openai-agents-sdk/README.md b/python/tracing/openai-agents-sdk/README.md index 7908f01..03a9dbb 100644 --- a/python/tracing/openai-agents-sdk/README.md +++ b/python/tracing/openai-agents-sdk/README.md @@ -1,19 +1,56 @@ -# OpenAI Agents SDK Respan Examples +# OpenAI Agents SDK tracing examples -These examples use the current `respan-instrumentation-openai-agents` package. Older files still import `respan_exporter_openai_agents.RespanTraceProcessor`; the local compatibility bridge maps that import to the active instrumentation processor and initializes the unified Respan OTEL exporter. +These examples exercise `respan-instrumentation-openai-agents` against OpenAI Agents SDK `0.20.x`. The suite includes deterministic current-framework traces for success, failure, function tools, handoffs, guardrails, and streaming, plus the upstream-style live examples. -## Setup +## Install + +From this directory: + +```bash +python -m pip install -r requirements.txt +``` + +For repository development, install the local packages after the registry dependencies so validation uses the current checkout: ```bash -cd python/tracing/openai-agents-sdk -pip install openai-agents respan-ai respan-instrumentation-openai-agents python-dotenv pytest pytest-asyncio +python -m pip install -e ../../../../respan/python-sdks/respan-sdk +python -m pip install -e ../../../../respan/python-sdks/respan-tracing +python -m pip install -e ../../../../respan/python-sdks/respan +python -m pip install -e ../../../../respan/python-sdks/instrumentations/respan-instrumentation-openai-agents ``` -Use the repository root `.env` values. OpenAI Agents 0.17 uses the Responses API by default. Because the current Respan OpenAI gateway covers chat-compatible routes, the local bridge forces `chat_completions` and routes model calls to `RESPAN_GATEWAY_BASE_URL` when `RESPAN_GATEWAY_API_KEY` is present. Set `RESPAN_OPENAI_AGENTS_USE_OPENAI=1` to use a direct `OPENAI_API_KEY`/Responses run instead. Traces always go to `RESPAN_BASE_URL` with `RESPAN_API_KEY`. +The suite reads the repository `.env` without overriding variables supplied by the shell. Set `RESPAN_API_KEY` and `RESPAN_BASE_URL` for trace export. Gateway-compatible live examples use that Respan credential and Chat Completions route. + +The compatibility bridge disables Respan's direct OpenAI auto-instrumentation because the explicit Agents trace processor owns these provider calls; this keeps each model call to one canonical chat span even when both instrumentation packages are installed. -Run a focused example: +Hosted tools are intentionally not converted to Chat Completions. Web Search and the research bot require `RESPAN_OPENAI_AGENTS_USE_OPENAI=1` plus a direct `OPENAI_API_KEY`. File Search additionally requires `OPENAI_VECTOR_STORE_ID`. Computer Use additionally requires `RESPAN_OPENAI_AGENTS_ENABLE_COMPUTER=1` and an installed Playwright Chromium runtime: ```bash -pytest basic/hello_world_test.py -q -pytest tools/functions_test.py -q +python -m playwright install chromium ``` + +When those settings are absent, only the affected hosted examples are reported as explicit skips. + +## Run + +Run every collected example and the three legacy direct-run demos under one exact marker: + +```bash +RESPAN_EXAMPLE_RUN_ID=otel2-openai-agents-check python run_all.py +``` + +Run only deterministic structural coverage: + +```bash +RESPAN_EXAMPLE_RUN_ID=otel2-openai-agents-contract \ + python -m pytest contract_scenarios_test.py -q -s +``` + +Nested handoff files are directly executable without a manual `PYTHONPATH` adjustment: + +```bash +python handoffs/message_filter_test.py +python handoffs/message_filter_streaming_test.py +``` + +Every pytest case flushes after completion, every process performs an explicit final Respan shutdown, and the same `RESPAN_EXAMPLE_RUN_ID` is attached to every emitted record. diff --git a/python/tracing/openai-agents-sdk/agent_patterns/agents_as_tools_test.py b/python/tracing/openai-agents-sdk/agent_patterns/agents_as_tools_test.py index 343d291..77f4d4a 100644 --- a/python/tracing/openai-agents-sdk/agent_patterns/agents_as_tools_test.py +++ b/python/tracing/openai-agents-sdk/agent_patterns/agents_as_tools_test.py @@ -1,12 +1,14 @@ from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) import asyncio import os + import pytest from agents import Agent, ItemHelpers, MessageOutputItem, Runner, trace from agents.tracing import set_trace_processors + from respan_exporter_openai_agents import RespanTraceProcessor set_trace_processors( @@ -69,6 +71,7 @@ instructions="You inspect translations, correct them if needed, and produce a final concatenated response.", ) + @pytest.mark.asyncio async def test_main(): msg = "english to spanish" @@ -90,7 +93,6 @@ async def test_main(): print(f"\n\nFinal response:\n{synthesizer_result.final_output}") - if __name__ == "__main__": # For interactive use - asyncio.run(test_main()) \ No newline at end of file + asyncio.run(test_main()) diff --git a/python/tracing/openai-agents-sdk/agent_patterns/deterministic_test.py b/python/tracing/openai-agents-sdk/agent_patterns/deterministic_test.py index ce994b1..5e22926 100644 --- a/python/tracing/openai-agents-sdk/agent_patterns/deterministic_test.py +++ b/python/tracing/openai-agents-sdk/agent_patterns/deterministic_test.py @@ -1,13 +1,17 @@ from dotenv import load_dotenv -load_dotenv(override=True) + +load_dotenv(override=False) # ==========copy past below========== import asyncio import os -from pydantic import BaseModel +import sys + import pytest from agents import Agent, Runner, trace from agents.tracing import set_trace_processors +from pydantic import BaseModel + from respan_exporter_openai_agents import RespanTraceProcessor set_trace_processors( @@ -75,13 +79,15 @@ async def test_main(): assert isinstance(outline_checker_result.final_output, OutlineCheckerOutput) if not outline_checker_result.final_output.good_quality: print("Outline is not good quality, so we stop here.") - exit(0) + sys.exit(0) if not outline_checker_result.final_output.is_scifi: print("Outline is not a scifi story, so we stop here.") - exit(0) + sys.exit(0) - print("Outline is good quality and a scifi story, so we continue to write the story.") + print( + "Outline is good quality and a scifi story, so we continue to write the story." + ) # 4. Write the story story_result = await Runner.run( diff --git a/python/tracing/openai-agents-sdk/agent_patterns/input_guardrails_test.py b/python/tracing/openai-agents-sdk/agent_patterns/input_guardrails_test.py index d068b88..0038a96 100644 --- a/python/tracing/openai-agents-sdk/agent_patterns/input_guardrails_test.py +++ b/python/tracing/openai-agents-sdk/agent_patterns/input_guardrails_test.py @@ -1,13 +1,13 @@ from __future__ import annotations + from dotenv import load_dotenv -load_dotenv(override=True) + +load_dotenv(override=False) import asyncio import os -import pytest -from typing import Union -from pydantic import BaseModel +import pytest from agents import ( Agent, GuardrailFunctionOutput, @@ -17,8 +17,11 @@ TResponseInputItem, input_guardrail, ) -from respan_exporter_openai_agents import RespanTraceProcessor from agents.tracing import set_trace_processors, trace +from pydantic import BaseModel + +from respan_exporter_openai_agents import RespanTraceProcessor + set_trace_processors( [ RespanTraceProcessor( @@ -57,7 +60,9 @@ class MathHomeworkOutput(BaseModel): @input_guardrail async def math_guardrail( - context: RunContextWrapper[None], agent: Agent, input: Union[str, list[TResponseInputItem]] + context: RunContextWrapper[None], + agent: Agent, + input: str | list[TResponseInputItem], ) -> GuardrailFunctionOutput: """This is an input guardrail function, which happens to call an agent to check if the input is a math homework question. diff --git a/python/tracing/openai-agents-sdk/agent_patterns/llm_as_a_judge_test.py b/python/tracing/openai-agents-sdk/agent_patterns/llm_as_a_judge_test.py index af1fdc2..4583102 100644 --- a/python/tracing/openai-agents-sdk/agent_patterns/llm_as_a_judge_test.py +++ b/python/tracing/openai-agents-sdk/agent_patterns/llm_as_a_judge_test.py @@ -1,18 +1,19 @@ from __future__ import annotations + from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) import asyncio import os -from typing import Literal, Union -import pytest -from pydantic import BaseModel +from typing import Literal +import pytest from agents import Agent, ItemHelpers, Runner, TResponseInputItem, trace -from respan_exporter_openai_agents import RespanTraceProcessor from agents.tracing import set_trace_processors +from pydantic import BaseModel +from respan_exporter_openai_agents import RespanTraceProcessor set_trace_processors( [ @@ -64,11 +65,11 @@ async def test_main() -> StoryEvaluationResult: msg = "Sci fi" input_items: list[TResponseInputItem] = [{"content": msg, "role": "user"}] - latest_outline: Union[str, None] = None + latest_outline: str | None = None iterations = 0 max_iterations = 2 final_score = "" - + # We'll run the entire workflow in a single trace with trace("LLM as a judge"): while True: @@ -82,7 +83,9 @@ async def test_main() -> StoryEvaluationResult: ) input_items = story_outline_result.to_input_list() - latest_outline = ItemHelpers.text_message_outputs(story_outline_result.new_items) + latest_outline = ItemHelpers.text_message_outputs( + story_outline_result.new_items + ) evaluator_result = await Runner.run(evaluator, input_items) result: EvaluationFeedback = evaluator_result.final_output @@ -91,15 +94,20 @@ async def test_main() -> StoryEvaluationResult: if result.score == "pass": break - input_items.append({"content": f"Feedback: {result.feedback}", "role": "user"}) + input_items.append( + {"content": f"Feedback: {result.feedback}", "role": "user"} + ) return StoryEvaluationResult( final_outline=latest_outline or "", iterations=iterations, - final_score=final_score + final_score=final_score, ) + if __name__ == "__main__": result = asyncio.run(test_main()) - print(f"Final story outline after {result.iterations} iterations (score: {result.final_score}):") + print( + f"Final story outline after {result.iterations} iterations (score: {result.final_score}):" + ) print(result.final_outline) diff --git a/python/tracing/openai-agents-sdk/agent_patterns/output_guardrails_test.py b/python/tracing/openai-agents-sdk/agent_patterns/output_guardrails_test.py index a95bb77..2e6449b 100644 --- a/python/tracing/openai-agents-sdk/agent_patterns/output_guardrails_test.py +++ b/python/tracing/openai-agents-sdk/agent_patterns/output_guardrails_test.py @@ -1,13 +1,13 @@ from __future__ import annotations + from dotenv import load_dotenv -load_dotenv(override=True) -import pytest +load_dotenv(override=False) import asyncio import json +import os -from pydantic import BaseModel, Field - +import pytest from agents import ( Agent, GuardrailFunctionOutput, @@ -16,12 +16,12 @@ Runner, output_guardrail, ) +from agents.tracing import set_trace_processors, trace +from pydantic import BaseModel, Field + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from typing import Union -from agents.tracing import set_trace_processors, trace -import os set_trace_processors( [ @@ -50,7 +50,7 @@ class MessageOutput(BaseModel): description="Thoughts on how to respond to the user's message" ) response: str = Field(description="The response to the user's message") - user_name: Union[str, None] = Field( + user_name: str | None = Field( description="The name of the user who sent the message, if known" ) diff --git a/python/tracing/openai-agents-sdk/agent_patterns/parallelization_test.py b/python/tracing/openai-agents-sdk/agent_patterns/parallelization_test.py index 2b4dbf3..c45b0a3 100644 --- a/python/tracing/openai-agents-sdk/agent_patterns/parallelization_test.py +++ b/python/tracing/openai-agents-sdk/agent_patterns/parallelization_test.py @@ -1,14 +1,17 @@ import asyncio + from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) +import os + import pytest from agents import Agent, ItemHelpers, Runner, trace +from agents.tracing import set_trace_processors + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors -import os set_trace_processors( [ diff --git a/python/tracing/openai-agents-sdk/agent_patterns/routing_test.py b/python/tracing/openai-agents-sdk/agent_patterns/routing_test.py index df9acc2..fef3e9d 100644 --- a/python/tracing/openai-agents-sdk/agent_patterns/routing_test.py +++ b/python/tracing/openai-agents-sdk/agent_patterns/routing_test.py @@ -1,18 +1,18 @@ from dotenv import load_dotenv -load_dotenv(override=True) -import pytest +load_dotenv(override=False) import asyncio +import os import uuid +import pytest +from agents import Agent, RawResponsesStreamEvent, Runner, TResponseInputItem, trace +from agents.tracing import set_trace_processors from openai.types.responses import ResponseContentPartDoneEvent, ResponseTextDeltaEvent -from agents import Agent, RawResponsesStreamEvent, Runner, TResponseInputItem, trace from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors -import os set_trace_processors( [ @@ -55,8 +55,15 @@ async def test_main(): conversation_id = str(uuid.uuid4().hex[:16]) agent = triage_agent - inputs: list[TResponseInputItem] = [{"content": "Can you help me with my math homework?", "role": "user"}] - questions = ["Can you help me with my math homework?", "Yeah, how to solve for x: 2x + 5 = 11?", "What's the capital of France?", ""] + inputs: list[TResponseInputItem] = [ + {"content": "Can you help me with my math homework?", "role": "user"} + ] + questions = [ + "Can you help me with my math homework?", + "Yeah, how to solve for x: 2x + 5 = 11?", + "What's the capital of France?", + "", + ] with trace("Routing example", group_id=conversation_id): for question in questions: @@ -83,5 +90,6 @@ async def test_main(): inputs.append({"content": question, "role": "user"}) agent = result.current_agent + if __name__ == "__main__": asyncio.run(test_main()) diff --git a/python/tracing/openai-agents-sdk/basic/dynamic_system_prompt_test.py b/python/tracing/openai-agents-sdk/basic/dynamic_system_prompt_test.py index c54f868..4ddeaff 100644 --- a/python/tracing/openai-agents-sdk/basic/dynamic_system_prompt_test.py +++ b/python/tracing/openai-agents-sdk/basic/dynamic_system_prompt_test.py @@ -1,20 +1,26 @@ from dotenv import load_dotenv -load_dotenv(override=True) -import pytest -import os +load_dotenv(override=False) import asyncio +import os import random from typing import Literal +import pytest from agents import Agent, RunContextWrapper, Runner +from agents.tracing import set_trace_processors + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors set_trace_processors( - [RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"))] + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] ) diff --git a/python/tracing/openai-agents-sdk/basic/hello_world_test.py b/python/tracing/openai-agents-sdk/basic/hello_world_test.py index 167ae50..3c8f2b1 100644 --- a/python/tracing/openai-agents-sdk/basic/hello_world_test.py +++ b/python/tracing/openai-agents-sdk/basic/hello_world_test.py @@ -1,14 +1,16 @@ from dotenv import load_dotenv -load_dotenv(override=True) -from openai import AsyncOpenAI -import pytest +load_dotenv(override=False) # ==========copy paste below========== import asyncio import os -from agents import Agent, Runner, set_default_openai_client + +import pytest +from agents import Agent, Runner from agents.tracing import set_trace_processors, trace + from respan_exporter_openai_agents import RespanTraceProcessor + API_KEY = os.getenv("RESPAN_API_KEY") ENDPOINT = os.getenv("RESPAN_OAIA_TRACING_ENDPOINT") BASE_URL = os.getenv("RESPAN_BASE_URL") @@ -27,10 +29,7 @@ @pytest.mark.asyncio async def test_main(): - agent = Agent( - name="Assistant", - instructions="You only respond in haikus." - ) + agent = Agent(name="Assistant", instructions="You only respond in haikus.") with trace("Hello world test"): result = await Runner.run(agent, "Tell me about recursion in programming.") diff --git a/python/tracing/openai-agents-sdk/basic/lifecycle_with_hook_example_test.py b/python/tracing/openai-agents-sdk/basic/lifecycle_with_hook_example_test.py index 9243526..03072a1 100644 --- a/python/tracing/openai-agents-sdk/basic/lifecycle_with_hook_example_test.py +++ b/python/tracing/openai-agents-sdk/basic/lifecycle_with_hook_example_test.py @@ -1,19 +1,27 @@ from dotenv import load_dotenv -load_dotenv(override=True) -import pytest -import os +load_dotenv(override=False) import asyncio +import os import random from typing import Any +import pytest +from agents import ( + Agent, + RunContextWrapper, + RunHooks, + Runner, + Tool, + Usage, + function_tool, +) +from agents.tracing import set_trace_processors, trace from pydantic import BaseModel -from agents import Agent, RunContextWrapper, RunHooks, Runner, Tool, Usage, function_tool from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors, trace set_trace_processors( [ @@ -24,6 +32,7 @@ ] ) + class ExampleHooks(RunHooks): def __init__(self): self.event_counter = 0 @@ -37,13 +46,17 @@ async def on_agent_start(self, context: RunContextWrapper, agent: Agent) -> None f"### {self.event_counter}: Agent {agent.name} started. Usage: {self._usage_to_str(context.usage)}" ) - async def on_agent_end(self, context: RunContextWrapper, agent: Agent, output: Any) -> None: + async def on_agent_end( + self, context: RunContextWrapper, agent: Agent, output: Any + ) -> None: self.event_counter += 1 print( f"### {self.event_counter}: Agent {agent.name} ended with output {output}. Usage: {self._usage_to_str(context.usage)}" ) - async def on_tool_start(self, context: RunContextWrapper, agent: Agent, tool: Tool) -> None: + async def on_tool_start( + self, context: RunContextWrapper, agent: Agent, tool: Tool + ) -> None: self.event_counter += 1 print( f"### {self.event_counter}: Tool {tool.name} started. Usage: {self._usage_to_str(context.usage)}" diff --git a/python/tracing/openai-agents-sdk/basic/stream_items_test.py b/python/tracing/openai-agents-sdk/basic/stream_items_test.py index 96d537e..879cd00 100644 --- a/python/tracing/openai-agents-sdk/basic/stream_items_test.py +++ b/python/tracing/openai-agents-sdk/basic/stream_items_test.py @@ -1,17 +1,17 @@ from dotenv import load_dotenv -load_dotenv(override=True) -import pytest -import os - +load_dotenv(override=False) import asyncio +import os import random +import pytest from agents import Agent, ItemHelpers, Runner, function_tool +from agents.tracing import set_trace_processors, trace + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors, trace set_trace_processors( [ diff --git a/python/tracing/openai-agents-sdk/basic/stream_text_test.py b/python/tracing/openai-agents-sdk/basic/stream_text_test.py index b9b37d2..a987f85 100644 --- a/python/tracing/openai-agents-sdk/basic/stream_text_test.py +++ b/python/tracing/openai-agents-sdk/basic/stream_text_test.py @@ -1,26 +1,26 @@ from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) endpoint = "http://localhost:8000/api/openai/v1/traces/ingest" -import pytest -import time -import os import asyncio +import os - -from openai.types.responses import ResponseTextDeltaEvent - +import pytest from agents import Agent, Runner +from agents.tracing import set_trace_processors, trace +from openai.types.responses import ResponseTextDeltaEvent from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors, trace set_trace_processors( [ - RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT")), + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ), ] ) @@ -42,4 +42,4 @@ async def test_main(): if __name__ == "__main__": - asyncio.run(test_main()) \ No newline at end of file + asyncio.run(test_main()) diff --git a/python/tracing/openai-agents-sdk/bridge_contract.py b/python/tracing/openai-agents-sdk/bridge_contract.py new file mode 100644 index 0000000..55ff0e4 --- /dev/null +++ b/python/tracing/openai-agents-sdk/bridge_contract.py @@ -0,0 +1,64 @@ +"""Focused lifecycle contract for the shared OpenAI Agents example bridge.""" + +from __future__ import annotations + +import asyncio + +import pytest + +import respan_exporter_openai_agents as bridge + + +def test_bridge_disables_direct_openai_auto_instrumentation(monkeypatch): + captured = None + + class FakeRespan: + def __init__(self, **kwargs): + nonlocal captured + captured = kwargs + + monkeypatch.setattr(bridge, "Respan", FakeRespan) + monkeypatch.setattr(bridge, "_RESPAN", None) + + instance = bridge._ensure_respan("test-key", "https://example.invalid/api") + + assert instance is not None + assert captured is not None + assert captured["is_auto_instrument"] is False + bridge._RESPAN = None + + +@pytest.mark.asyncio +async def test_gateway_client_is_retained_and_closed_on_its_owner_loop(monkeypatch): + owner_loop = asyncio.get_running_loop() + closed_on = None + + class FakeAsyncOpenAI: + def __init__(self, **_kwargs): + pass + + async def close(self): + nonlocal closed_on + closed_on = asyncio.get_running_loop() + + monkeypatch.delenv("RESPAN_OPENAI_AGENTS_USE_OPENAI", raising=False) + monkeypatch.setattr(bridge, "AsyncOpenAI", FakeAsyncOpenAI) + monkeypatch.setattr(bridge, "set_default_openai_api", lambda _value: None) + monkeypatch.setattr(bridge, "set_default_openai_client", lambda _client: None) + monkeypatch.setattr(bridge, "_GATEWAY_CLIENT", None) + monkeypatch.setattr(bridge, "_GATEWAY_CLIENT_CONFIGURED", False) + monkeypatch.setattr(bridge, "_GATEWAY_CLIENT_OWNER_LOOP", None) + + bridge._configure_gateway_client("test-key") + retained = bridge._GATEWAY_CLIENT + bridge._claim_gateway_client_loop() + + assert retained is not None + assert bridge._GATEWAY_CLIENT_OWNER_LOOP is owner_loop + + await bridge.shutdown_respan_async() + + assert closed_on is owner_loop + assert bridge._GATEWAY_CLIENT is None + assert bridge._GATEWAY_CLIENT_OWNER_LOOP is None + assert bridge._GATEWAY_CLIENT_CONFIGURED is False diff --git a/python/tracing/openai-agents-sdk/complex_edge_cases_test.py b/python/tracing/openai-agents-sdk/complex_edge_cases_test.py index b6d5e3a..c3c61c5 100644 --- a/python/tracing/openai-agents-sdk/complex_edge_cases_test.py +++ b/python/tracing/openai-agents-sdk/complex_edge_cases_test.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Complex Edge-Case Tracing Example — stress-tests the Respan OpenAI Agents SDK exporter. @@ -18,11 +17,6 @@ import json import os import time -from typing import Union - -from dotenv import load_dotenv, find_dotenv -from openai import AsyncOpenAI -from pydantic import BaseModel, Field from agents import ( Agent, @@ -35,36 +29,36 @@ function_tool, input_guardrail, output_guardrail, - set_default_openai_client, ) from agents.tracing import set_trace_processors, trace -from respan_exporter_openai_agents import RespanTraceProcessor +from dotenv import find_dotenv, load_dotenv +from pydantic import BaseModel, Field -load_dotenv(find_dotenv(), override=True) +from respan_exporter_openai_agents import RespanTraceProcessor, shutdown_respan_async + +load_dotenv(find_dotenv(), override=False) # ── Configuration ────────────────────────────────────────────────────────── RESPAN_BASE_URL = os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api").rstrip("/") RESPAN_API_KEY = os.getenv("RESPAN_API_KEY") -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") RESPAN_MODEL = os.getenv("RESPAN_MODEL", "gpt-4o") -# ── Gateway: route all OpenAI calls through Respan ───────────────────────── -client = AsyncOpenAI(api_key=OPENAI_API_KEY) -set_default_openai_client(client) - # ── Tracing: export spans to Respan ──────────────────────────────────────── -set_trace_processors([ - RespanTraceProcessor( - api_key=RESPAN_API_KEY, - default_model=RESPAN_MODEL, - ), -]) +set_trace_processors( + [ + RespanTraceProcessor( + api_key=RESPAN_API_KEY, + default_model=RESPAN_MODEL, + ), + ] +) # ═══════════════════════════════════════════════════════════════════════════ # TOOLS — each one probes a different serialization / error edge case # ═══════════════════════════════════════════════════════════════════════════ + @function_tool def get_weather(city: str) -> str: """Get weather for a city — returns normal string.""" @@ -83,8 +77,16 @@ def get_city_stats(city: str) -> str: "population": 13_960_000, "density_per_km2": 6_363, "districts": [ - {"name": "Shibuya", "pop": 230_000, "landmarks": ["Hachiko", "Scramble Crossing"]}, - {"name": "Shinjuku", "pop": 346_000, "landmarks": ["Kabukicho", "Gyoen"]}, + { + "name": "Shibuya", + "pop": 230_000, + "landmarks": ["Hachiko", "Scramble Crossing"], + }, + { + "name": "Shinjuku", + "pop": 346_000, + "landmarks": ["Kabukicho", "Gyoen"], + }, ], }, "coordinates": {"lat": 35.6762, "lon": 139.6503}, @@ -119,7 +121,7 @@ def get_localized_greeting(language: str) -> str: "japanese": "こんにちは世界!🌸 東京タワー\n\t— with tabs and newlines —", "arabic": "مرحبا بالعالم 🌍 — RTL text mixed with LTR", "emoji": "👨‍👩‍👧‍👦 Family emoji + 🏳️‍🌈 flag + 🇯🇵 regional indicators", - "special": 'Quotes: "double" \'single\' `backtick` — Slashes: \\ / — Angle: <>& — Tabs:\t\tEnd', + "special": "Quotes: \"double\" 'single' `backtick` — Slashes: \\ / — Angle: <>& — Tabs:\t\tEnd", } return greetings.get(language, f"Hello from {language}!") @@ -142,7 +144,9 @@ async def slow_database_query(query: str) -> str: @function_tool def get_secret_data(classification: str) -> str: """Always raises — tests that errored tool spans are captured.""" - raise PermissionError(f"Access denied: '{classification}' requires LEVEL-5 clearance") + raise PermissionError( + f"Access denied: '{classification}' requires LEVEL-5 clearance" + ) # EDGE CASE: Tool with extremely large output. @@ -151,9 +155,7 @@ def get_secret_data(classification: str) -> str: @function_tool def generate_large_report(topic: str) -> str: """Generate a ~50KB report to stress payload size limits.""" - paragraph = ( - f"Analysis of {topic}: " + "Lorem ipsum dolor sit amet, " * 50 + "\n" - ) + paragraph = f"Analysis of {topic}: " + "Lorem ipsum dolor sit amet, " * 50 + "\n" return paragraph * 30 # ~50KB @@ -161,6 +163,7 @@ def generate_large_report(topic: str) -> str: # GUARDRAILS — test both triggered and non-triggered paths # ═══════════════════════════════════════════════════════════════════════════ + class ContentCheckOutput(BaseModel): is_appropriate: bool reasoning: str @@ -183,7 +186,7 @@ class ContentCheckOutput(BaseModel): async def content_safety_guardrail( context: RunContextWrapper[None], agent: Agent, - input: Union[str, list[TResponseInputItem]], + input: str | list[TResponseInputItem], ) -> GuardrailFunctionOutput: """Guardrail that internally runs a sub-agent — tests nested span trees.""" result = await Runner.run(guardrail_checker, input, context=context.context) @@ -325,7 +328,13 @@ async def quality_gate_guardrail( "- Report generation → Report Agent\n" "NEVER answer directly — ALWAYS hand off." ), - handoffs=[weather_router, research_agent, analysis_agent, resilience_agent, report_agent], + handoffs=[ + weather_router, + research_agent, + analysis_agent, + resilience_agent, + report_agent, + ], input_guardrails=[content_safety_guardrail], ) @@ -369,6 +378,7 @@ async def quality_gate_guardrail( # SCENARIO RUNNERS # ═══════════════════════════════════════════════════════════════════════════ + async def run_scenario(name: str, coro): """Run a scenario with error isolation so one failure doesn't kill the rest.""" print(f"\n{'─' * 60}") @@ -379,8 +389,8 @@ async def run_scenario(name: str, coro): print(f" ✓ {name} completed") except (InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered) as e: print(f" ⚠ {name} — guardrail tripped (expected): {type(e).__name__}") - except Exception as e: - print(f" ✗ {name} — error (testing resilience): {type(e).__name__}: {e}") + except Exception as error: # noqa: BLE001 - every scenario must continue. + print(f" ✗ {name} — error (testing resilience): {type(error).__name__}") async def scenario_handoff_chain(): @@ -466,8 +476,8 @@ async def scenario_rapid_sequential_runs(): instructions="Reply with exactly one word.", ) for i in range(5): - result = await Runner.run(simple, f"Word #{i+1}: give me a color name") - print(f" Run {i+1}: {result.final_output}") + result = await Runner.run(simple, f"Word #{i + 1}: give me a color name") + print(f" Run {i + 1}: {result.final_output}") async def scenario_concurrent_sub_traces(): @@ -548,6 +558,7 @@ async def scenario_zero_duration_spans(): # MAIN # ═══════════════════════════════════════════════════════════════════════════ + async def main(): print("=" * 60) print(" COMPLEX EDGE-CASE TRACING EXAMPLE") @@ -560,7 +571,6 @@ async def main(): start = time.time() with trace("Edge Case Stress Test"): - # ── Handoff chain (Handoff + Agent + Response + Generation) ──── await run_scenario( "Three-level handoff chain", @@ -636,12 +646,19 @@ async def main(): elapsed = time.time() - start print(f"\n{'=' * 60}") print(f" ALL SCENARIOS COMPLETE — {elapsed:.1f}s elapsed") - print(f" Waiting 5s for batch processor to flush...") + print(" Waiting 5s for batch processor to flush...") print(f"{'=' * 60}") await asyncio.sleep(5) print("\n Done! Check your Respan dashboard for the trace.") +async def _main_and_shutdown(): + try: + await main() + finally: + await shutdown_respan_async() + + if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(_main_and_shutdown()) diff --git a/python/tracing/openai-agents-sdk/conftest.py b/python/tracing/openai-agents-sdk/conftest.py index 792df4e..f45e84d 100644 --- a/python/tracing/openai-agents-sdk/conftest.py +++ b/python/tracing/openai-agents-sdk/conftest.py @@ -1,8 +1,70 @@ -"""Pytest import helpers for nested OpenAI Agents example files.""" +"""Shared import, marker, and lifecycle helpers for the example suite.""" -from pathlib import Path +import os import sys +from pathlib import Path + +import pytest +import pytest_asyncio ROOT = Path(__file__).resolve().parent if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) + +# These legacy entry points are executed once as standalone processes by +# ``run_all.py`` so their module-global setup and explicit shutdown are tested. +# Excluding them here prevents duplicate traces under the same exact marker. +collect_ignore = [ + "complex_edge_cases_test.py", + "handoffs/message_filter_streaming_test.py", + "handoffs/message_filter_test.py", +] + +_CONTRACT_ONLY_FILES = { + "bridge_contract.py", + "contract_scenarios_test.py", + "runner_contract.py", +} + + +@pytest.fixture(autouse=True) +def _flush_after_example(): + yield + from respan_exporter_openai_agents import flush_respan + + flush_respan() + + +@pytest_asyncio.fixture(scope="session", autouse=True, loop_scope="session") +async def _shutdown_clients_after_session(): + yield + from respan_exporter_openai_agents import shutdown_respan_async + + await shutdown_respan_async() + + +def pytest_collection_modifyitems(items): + from respan_exporter_openai_agents import has_direct_responses_credentials + + direct_responses = has_direct_responses_credentials() + for item in items: + relative = item.path.relative_to(ROOT).as_posix() + if relative not in _CONTRACT_ONLY_FILES: + item.add_marker(pytest.mark.live) + + reason = None + eligible = direct_responses + if relative in {"tools/web_search_test.py", "research_bot/main_test.py"}: + reason = "requires a direct OpenAI Responses API credential" + elif relative == "tools/file_search_test.py": + reason = "requires direct Responses credentials and OPENAI_VECTOR_STORE_ID" + eligible = eligible and bool(os.getenv("OPENAI_VECTOR_STORE_ID")) + elif relative == "tools/computer_use_test.py": + reason = "requires direct Responses credentials and an enabled Playwright browser" + eligible = eligible and ( + os.getenv("RESPAN_OPENAI_AGENTS_ENABLE_COMPUTER") == "1" + ) + if reason: + item.add_marker(pytest.mark.hosted) + if not eligible: + item.add_marker(pytest.mark.skip(reason=reason)) diff --git a/python/tracing/openai-agents-sdk/contract_scenarios_test.py b/python/tracing/openai-agents-sdk/contract_scenarios_test.py new file mode 100644 index 0000000..dd7e49b --- /dev/null +++ b/python/tracing/openai-agents-sdk/contract_scenarios_test.py @@ -0,0 +1,166 @@ +"""Deterministic current-SDK traces for content-level instrumentation checks.""" + +# ruff: noqa: SIM117 - nesting is the span-tree contract under test. + +from __future__ import annotations + +import os + +from agents.tracing import ( + SpanError, + agent_span, + function_span, + generation_span, + guardrail_span, + handoff_span, + set_trace_processors, + task_span, + trace, + turn_span, +) + +from respan_exporter_openai_agents import RespanTraceProcessor + +set_trace_processors( + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] +) + + +def _metadata(scenario: str) -> dict[str, str]: + marker = os.environ["RESPAN_EXAMPLE_RUN_ID"] + return {"example_run_id": marker, "scenario": scenario} + + +def test_success_structure(): + with trace("openai-agents.success", metadata=_metadata("success")): + with task_span("successful agent run"): + with agent_span("Assistant", output_type="str"): + with turn_span(1, "Assistant"): + with generation_span( + input=[{"role": "user", "content": "Say hello."}], + output=[{"role": "assistant", "content": "Hello!"}], + model="gpt-4o-mini", + usage={ + "input_tokens": 4, + "output_tokens": 2, + "total_tokens": 6, + }, + ): + pass + + +def test_function_tool_structure(): + with trace("openai-agents.tool", metadata=_metadata("tool")): + with task_span("tool agent run"): + with agent_span("Weather Agent", tools=["get_weather"]): + with turn_span(1, "Weather Agent"): + with generation_span( + input=[ + { + "role": "user", + "content": "What is the weather in Tokyo?", + } + ], + output=[ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_weather_1", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"city":"Tokyo"}', + }, + } + ], + } + ], + model="gpt-4o-mini", + usage={"input_tokens": 10, "output_tokens": 5}, + ): + pass + with function_span( + "get_weather", + input='{"city":"Tokyo"}', + output='{"condition":"sunny","temperature_c":22}', + ): + pass + + +def test_handoff_structure(): + with trace("openai-agents.handoff", metadata=_metadata("handoff")): + with task_span("handoff agent run"): + with agent_span("Triage", handoffs=["Spanish Assistant"]): + with turn_span(1, "Triage"): + with generation_span( + input=[{"role": "user", "content": "Hola"}], + output=[ + { + "type": "function_call", + "call_id": "call_handoff_1", + "name": "transfer_to_spanish_assistant", + "arguments": "{}", + } + ], + model="gpt-4o-mini", + ): + pass + with handoff_span("Triage", "Spanish Assistant"): + pass + with agent_span("Spanish Assistant"): + with turn_span(2, "Spanish Assistant"): + with generation_span( + input=[{"role": "user", "content": "Hola"}], + output=[{"role": "assistant", "content": "¡Hola!"}], + model="gpt-4o-mini", + ): + pass + + +def test_guardrail_and_provider_failure_structure(): + with trace("openai-agents.failure", metadata=_metadata("failure")): + with task_span("failed agent run"): + with agent_span("Guarded Agent"): + with guardrail_span("safe_input", triggered=True): + pass + with ( + turn_span(1, "Guarded Agent"), + generation_span( + input=[{"role": "user", "content": "Trigger failure"}], + model="gpt-4o-mini", + ) as failed, + ): + failed.set_error( + SpanError( + message="Provider temporarily unavailable", + data={"status_code": 503}, + ) + ) + + +def test_streaming_structure(): + with trace("openai-agents.stream", metadata=_metadata("stream")): + with task_span("streaming agent run"): + with agent_span("Streaming Agent"): + with turn_span(1, "Streaming Agent"): + with generation_span( + input=[{"role": "user", "content": "Stream a greeting."}], + output=[ + {"role": "assistant", "content": "Hello from a stream!"} + ], + model="gpt-4o-mini", + model_config={"stream": True}, + usage={ + "input_tokens": 5, + "output_tokens": 5, + "total_tokens": 10, + }, + ): + pass diff --git a/python/tracing/openai-agents-sdk/customer_service/main_test.py b/python/tracing/openai-agents-sdk/customer_service/main_test.py index 5b30738..04b229d 100644 --- a/python/tracing/openai-agents-sdk/customer_service/main_test.py +++ b/python/tracing/openai-agents-sdk/customer_service/main_test.py @@ -1,16 +1,15 @@ from __future__ import annotations as _annotations + from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) endpoint = "http://localhost:8000/api/openai/v1/traces/ingest" -import pytest -import os import asyncio +import os import random import uuid -from pydantic import BaseModel - +import pytest from agents import ( Agent, HandoffOutputItem, @@ -26,32 +25,40 @@ trace, ) from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX +from agents.tracing import set_trace_processors +from pydantic import BaseModel + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors -from typing import Union -load_dotenv(override=True) + +load_dotenv(override=False) set_trace_processors( - [RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"))] + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] ) ### CONTEXT class AirlineAgentContext(BaseModel): - passenger_name: Union[str, None] = None - confirmation_number: Union[str, None] = None - seat_number: Union[str, None] = None - flight_number: Union[str, None] = None + passenger_name: str | None = None + confirmation_number: str | None = None + seat_number: str | None = None + flight_number: str | None = None ### TOOLS @function_tool( - name_override="faq_lookup_tool", description_override="Lookup frequently asked questions." + name_override="faq_lookup_tool", + description_override="Lookup frequently asked questions.", ) async def faq_lookup_tool(question: str) -> str: if "bag" in question or "baggage" in question: @@ -73,7 +80,9 @@ async def faq_lookup_tool(question: str) -> str: @function_tool async def update_seat( - context: RunContextWrapper[AirlineAgentContext], confirmation_number: str, new_seat: str + context: RunContextWrapper[AirlineAgentContext], + confirmation_number: str, + new_seat: str, ) -> str: """ Update the seat for a given confirmation number. @@ -93,7 +102,9 @@ async def update_seat( ### HOOKS -async def on_seat_booking_handoff(context: RunContextWrapper[AirlineAgentContext]) -> None: +async def on_seat_booking_handoff( + context: RunContextWrapper[AirlineAgentContext], +) -> None: flight_number = f"FLT-{random.randint(100, 999)}" context.context.flight_number = flight_number @@ -158,7 +169,6 @@ async def test_main(): conversation_id = uuid.uuid4().hex[:16] questions = ["I need to change my seat", "Can you confirm my flight number?", ""] with trace("Customer service", group_id=conversation_id): - for question in questions: user_input = question if question == "": @@ -184,6 +194,5 @@ async def test_main(): current_agent = result.last_agent - if __name__ == "__main__": asyncio.run(test_main()) diff --git a/python/tracing/openai-agents-sdk/handoffs/handsoff_test.py b/python/tracing/openai-agents-sdk/handoffs/handsoff_test.py index 2f8286d..4c6e2cf 100644 --- a/python/tracing/openai-agents-sdk/handoffs/handsoff_test.py +++ b/python/tracing/openai-agents-sdk/handoffs/handsoff_test.py @@ -1,13 +1,17 @@ from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) +import asyncio +import os + import pytest + # ==========copy the below========== from agents import Agent, Runner -import asyncio -from respan_exporter_openai_agents import RespanTraceProcessor from agents.tracing import set_trace_processors, trace -import os + +from respan_exporter_openai_agents import RespanTraceProcessor + set_trace_processors( [ RespanTraceProcessor( @@ -41,5 +45,6 @@ async def test_main(): print(result.final_output) # ¡Hola! Estoy bien, gracias por preguntar. ¿Y tú, cómo estás? + if __name__ == "__main__": - asyncio.run(test_main()) \ No newline at end of file + asyncio.run(test_main()) diff --git a/python/tracing/openai-agents-sdk/handoffs/message_filter_streaming_test.py b/python/tracing/openai-agents-sdk/handoffs/message_filter_streaming_test.py index 2ff9f41..8304b11 100644 --- a/python/tracing/openai-agents-sdk/handoffs/message_filter_streaming_test.py +++ b/python/tracing/openai-agents-sdk/handoffs/message_filter_streaming_test.py @@ -1,23 +1,37 @@ from __future__ import annotations + from dotenv import load_dotenv -load_dotenv(override=True) -import pytest +load_dotenv(override=False) + endpoint = "http://localhost:8000/api/openai/v1/traces/ingest" -import os import json +import os import random +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) from agents import Agent, HandoffInputData, Runner, function_tool, handoff, trace from agents.extensions import handoff_filters +from agents.tracing import set_trace_processors + from respan_exporter_openai_agents import ( RespanTraceProcessor, + shutdown_respan_async, ) -from agents.tracing import set_trace_processors set_trace_processors( - [RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"))] + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] ) @@ -191,7 +205,14 @@ async def main(): """ +async def _main_and_shutdown(): + try: + await main() + finally: + await shutdown_respan_async() + + if __name__ == "__main__": import asyncio - asyncio.run(main()) + asyncio.run(_main_and_shutdown()) diff --git a/python/tracing/openai-agents-sdk/handoffs/message_filter_test.py b/python/tracing/openai-agents-sdk/handoffs/message_filter_test.py index 2b34df0..13d53dd 100644 --- a/python/tracing/openai-agents-sdk/handoffs/message_filter_test.py +++ b/python/tracing/openai-agents-sdk/handoffs/message_filter_test.py @@ -1,17 +1,25 @@ from __future__ import annotations + from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) # =============Only copy the below for docs============= # from __future__ import annotations import os import random +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + from agents import Agent, HandoffInputData, Runner, function_tool, handoff, trace from agents.extensions import handoff_filters -from respan_exporter_openai_agents import RespanTraceProcessor from agents.tracing import set_trace_processors +from respan_exporter_openai_agents import RespanTraceProcessor, shutdown_respan_async set_trace_processors( [ @@ -193,9 +201,14 @@ async def main(): """ -import time +async def _main_and_shutdown(): + try: + await main() + finally: + await shutdown_respan_async() + if __name__ == "__main__": import asyncio - asyncio.run(main()) + asyncio.run(_main_and_shutdown()) diff --git a/python/tracing/openai-agents-sdk/poetry.lock b/python/tracing/openai-agents-sdk/poetry.lock index c12e2cd..362fc80 100644 --- a/python/tracing/openai-agents-sdk/poetry.lock +++ b/python/tracing/openai-agents-sdk/poetry.lock @@ -60,6 +60,118 @@ files = [ {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ] +[[package]] +name = "cffi" +version = "2.1.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.10" +files = [ + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "charset-normalizer" version = "3.4.3" @@ -173,6 +285,67 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "50.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.9" +files = [ + {file = "cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef"}, + {file = "cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30"}, + {file = "cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95"}, + {file = "cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269"}, + {file = "cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7"}, + {file = "cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9"}, + {file = "cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +ssh = ["bcrypt (>=3.1.5)"] + [[package]] name = "dateparser" version = "1.2.2" @@ -207,18 +380,110 @@ files = [ ] [[package]] -name = "griffe" -version = "1.13.0" +name = "greenlet" +version = "3.5.5" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.10" +files = [ + {file = "greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638"}, + {file = "greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0"}, + {file = "greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4"}, + {file = "greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc"}, + {file = "greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f"}, + {file = "greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db"}, + {file = "greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39"}, + {file = "greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53"}, + {file = "greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5"}, + {file = "greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759"}, + {file = "greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b"}, + {file = "greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2"}, + {file = "greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18"}, + {file = "greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52"}, + {file = "greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b"}, + {file = "greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537"}, + {file = "greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e"}, + {file = "greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd"}, + {file = "greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc"}, + {file = "greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53"}, + {file = "greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc"}, + {file = "greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9"}, + {file = "greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1"}, + {file = "greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07"}, + {file = "greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41"}, + {file = "greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874"}, + {file = "greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71"}, + {file = "greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0"}, + {file = "greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474"}, + {file = "greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007"}, + {file = "greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773"}, + {file = "greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e"}, + {file = "greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769"}, + {file = "greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f"}, + {file = "greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0"}, + {file = "greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5"}, + {file = "greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8"}, + {file = "greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b"}, + {file = "greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + +[[package]] +name = "griffelib" +version = "2.2.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559"}, - {file = "griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0"}, + {file = "griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4"}, + {file = "griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c"}, ] -[package.dependencies] -colorama = ">=0.4" +[package.extras] +pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"] [[package]] name = "h11" @@ -434,60 +699,53 @@ files = [ referencing = ">=0.31.0" [[package]] -name = "keywordsai-exporter-openai-agents" -version = "0.1.3" -description = "Exporter for Keywords AI openai Agents SDK" +name = "markdown-it-py" +version = "4.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = "<3.13,>3.10" +python-versions = ">=3.10" files = [ - {file = "keywordsai_exporter_openai_agents-0.1.3-py3-none-any.whl", hash = "sha256:e8decb27dc3d14f5aca4a9463b36374f8c393655b80447b3975edd62453b6b17"}, - {file = "keywordsai_exporter_openai_agents-0.1.3.tar.gz", hash = "sha256:b7535724ef94b0df86e502cc2f6950c8f7fc901905bd2c36e64a57e4cac0e68d"}, + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, ] [package.dependencies] -keywordsai-sdk = ">=0.4.39,<0.5.0" -openai-agents = ">=0.0.18,<0.0.19" - -[[package]] -name = "keywordsai-sdk" -version = "0.4.84" -description = "Keywords AI SDK allows you to interact with the Keywords AI API smoothly" -optional = false -python-versions = "<4.0,>3.9" -files = [ - {file = "keywordsai_sdk-0.4.84-py3-none-any.whl", hash = "sha256:81444ff3a8893099f0fd93d0efa34e380d46caf816fd368582393b741c17409e"}, - {file = "keywordsai_sdk-0.4.84.tar.gz", hash = "sha256:a152f2f4f608e4402f3b2ff07fb27429afd35af31dd9f4336d9f5f1a67d93f1b"}, -] +mdurl = ">=0.1,<1.0" -[package.dependencies] -dateparser = ">=1.2.0,<2.0.0" -openai = "1.99.5" -pydantic = ">=2.10.3,<3.0.0" -requests = ">=2.31.0,<3.0.0" -typing-extensions = ">=4.12.2,<5.0.0" +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] [[package]] name = "mcp" -version = "1.13.1" +version = "1.29.0" description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" files = [ - {file = "mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df"}, - {file = "mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5"}, + {file = "mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7"}, + {file = "mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36"}, ] [package.dependencies] anyio = ">=4.5" -httpx = ">=0.27.1" +httpx = ">=0.27.1,<1.0.0" httpx-sse = ">=0.4" jsonschema = ">=4.20.0" -pydantic = ">=2.11.0,<3.0.0" +pydantic = {version = ">=2.11.0,<3.0.0", markers = "python_version < \"3.14\""} pydantic-settings = ">=2.5.2" +pyjwt = {version = ">=2.10.1", extras = ["crypto"]} python-multipart = ">=0.0.9" -pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""} +pywin32 = {version = ">=310", markers = "sys_platform == \"win32\" and python_version < \"3.14\""} sse-starlette = ">=1.6.1" -starlette = ">=0.27" +starlette = {version = ">=0.27", markers = "python_version < \"3.14\""} +typing-extensions = ">=4.9.0" +typing-inspection = ">=0.4.1" uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""} [package.extras] @@ -495,57 +753,183 @@ cli = ["python-dotenv (>=1.0.0)", "typer (>=0.16.0)"] rich = ["rich (>=13.9.4)"] ws = ["websockets (>=15.0.1)"] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "openai" -version = "1.99.5" +version = "2.54.0" description = "The official Python library for the openai API" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "openai-1.99.5-py3-none-any.whl", hash = "sha256:4e870f9501b7c36132e2be13313ce3c4d6915a837e7a299c483aab6a6d4412e9"}, - {file = "openai-1.99.5.tar.gz", hash = "sha256:aa97ac3326cac7949c5e4ac0274c454c1d19c939760107ae0d3948fc26a924ca"}, + {file = "openai-2.54.0-py3-none-any.whl", hash = "sha256:89089789197ccdb87f173a03145ed1598d00795220c93e96cf712b1cbf5e5f2b"}, + {file = "openai-2.54.0.tar.gz", hash = "sha256:e3e6f8bc1ba30ddf381ace1a14340eed381cb984a1a59bd0f34b5be3b5d49cfa"}, ] [package.dependencies] anyio = ">=3.5.0,<5" distro = ">=1.7.0,<2" httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" +jiter = ">=0.10.0,<1" pydantic = ">=1.9.0,<3" sniffio = "*" tqdm = ">4" -typing-extensions = ">=4.11,<5" +typing-extensions = ">=4.14,<5" [package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] +aiohttp = ["aiohttp (>=3.14.1)", "httpx-aiohttp (>=0.1.9)"] +bedrock = ["botocore (>=1.40.0,<2)"] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] +httpx2 = ["anyio (>=4.10.0,<5)", "httpx (>=0.25.1,<1)", "httpx2 (>=2.7.0,<3)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "openai-agents" -version = "0.0.18" +version = "0.20.0" description = "OpenAI Agents SDK" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "openai_agents-0.0.18-py3-none-any.whl", hash = "sha256:2c0aba4cbe63f42d424f2ae3ee545d96f746beeaa87b13541aa8de8cc4b48476"}, - {file = "openai_agents-0.0.18.tar.gz", hash = "sha256:a8faf48c7fd9b6646f7afd83097d78f1575177482014b7d4d6d951b6f37dee51"}, + {file = "openai_agents-0.20.0-py3-none-any.whl", hash = "sha256:aaff662b802fa90762ad539e131b9ea387e12e3664b87bc75157ad1b3fc88850"}, + {file = "openai_agents-0.20.0.tar.gz", hash = "sha256:a6b37954877868ac233876a27dab1e2a461c53ecbd5fcf6e19e2d276d3e848cd"}, ] [package.dependencies] -griffe = ">=1.5.6,<2" -mcp = {version = ">=1.9.4,<2", markers = "python_version >= \"3.10\""} -openai = ">=1.87.0" -pydantic = ">=2.10,<3" +griffelib = ">=2,<3" +mcp = {version = ">=1.19.0,<3", markers = "python_version >= \"3.10\""} +openai = ">=2.45.0,<3" +pydantic = ">=2.12.2,<3" requests = ">=2.0,<3" -types-requests = ">=2.0,<3" typing-extensions = ">=4.12.2,<5" +websockets = ">=15.0,<17" [package.extras] -litellm = ["litellm (>=1.67.4.post1,<2)"] +any-llm = ["any-llm-sdk (>=1.11.0,<2)"] +blaxel = ["aiohttp (>=3.12,<4)", "blaxel (>=0.2.50)"] +cloudflare = ["aiohttp (>=3.12,<4)"] +dapr = ["dapr (>=1.16.0)", "grpcio (>=1.60.0)"] +daytona = ["daytona (>=0.155.0)"] +docker = ["docker (>=6.1)"] +e2b = ["e2b (==2.31.0)", "e2b-code-interpreter (==2.8.1)"] +encrypt = ["cryptography (>=45.0,<46)"] +litellm = ["litellm (>=1.83.0)"] +modal = ["modal (==1.4.3)"] +mongodb = ["pymongo (>=4.14)"] +realtime = ["websockets (>=15.0,<17)"] +redis = ["redis (>=7)"] +runloop = ["runloop-api-client (>=1.16.0,<2.0.0)"] +s3 = ["boto3 (>=1.34)"] +sqlalchemy = ["asyncpg (>=0.29.0)", "sqlalchemy (>=2.0)"] +temporal = ["temporalio (==1.26.0)", "textual (>=8.2.3,<8.3)"] +vercel = ["vercel (>=0.5.6,<0.6)"] viz = ["graphviz (>=0.17)"] -voice = ["numpy (>=2.2.0,<3)", "websockets (>=15.0,<16)"] +voice = ["numpy (>=2.2.0,<3)", "websockets (>=15.0,<17)"] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef"}, + {file = "opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a"}, +] + +[package.dependencies] +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.65b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137"}, + {file = "opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.65b0" +packaging = ">=18.0" +wrapt = ">=1.0.0,<3.0.0" + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.65b0" +description = "Thread context propagation support for OpenTelemetry" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_instrumentation_threading-0.65b0-py3-none-any.whl", hash = "sha256:d8a1a1f35418a32769d469ef2d7e8401935e097a8553abcfba833da9c74736ce"}, + {file = "opentelemetry_instrumentation_threading-0.65b0.tar.gz", hash = "sha256:aefd23eb16c5e7a7c6c6eacdcb6c6f269ed8ed3a1b458bf5dd555b878bf58937"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.65b0" +wrapt = ">=1.0.0,<3.0.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad"}, + {file = "opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b"}, +] + +[package.dependencies] +opentelemetry-api = "1.44.0" +opentelemetry-semantic-conventions = "0.65b0" +typing-extensions = ">=4.5.0" + +[package.extras] +file-configuration = ["opentelemetry-configuration (==0.65b0)"] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb"}, + {file = "opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60"}, +] + +[package.dependencies] +opentelemetry-api = "1.44.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.5.1" +description = "OpenTelemetry Semantic Conventions Extension for Large Language Models" +optional = false +python-versions = "<4,>=3.9" +files = [ + {file = "opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4"}, + {file = "opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c"}, +] + +[package.dependencies] +opentelemetry-sdk = ">=1.38.0,<2" +opentelemetry-semantic-conventions = ">=0.59b0" [[package]] name = "packaging" @@ -558,6 +942,27 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "playwright" +version = "1.62.0" +description = "A high-level API to automate web browsers" +optional = false +python-versions = ">=3.10" +files = [ + {file = "playwright-1.62.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f"}, + {file = "playwright-1.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034"}, + {file = "playwright-1.62.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da"}, + {file = "playwright-1.62.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3"}, + {file = "playwright-1.62.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff"}, + {file = "playwright-1.62.0-py3-none-win32.whl", hash = "sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c"}, + {file = "playwright-1.62.0-py3-none-win_amd64.whl", hash = "sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1"}, + {file = "playwright-1.62.0-py3-none-win_arm64.whl", hash = "sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1"}, +] + +[package.dependencies] +greenlet = ">=3.1.1,<4.0.0" +pyee = ">=13,<14" + [[package]] name = "pluggy" version = "1.6.0" @@ -573,22 +978,33 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + [[package]] name = "pydantic" -version = "2.11.7" +version = "2.13.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, - {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.33.2" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" +pydantic-core = "2.46.4" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -596,114 +1012,135 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, ] [package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +typing-extensions = ">=4.14.1" [[package]] name = "pydantic-settings" @@ -728,6 +1165,23 @@ gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] +[[package]] +name = "pyee" +version = "13.0.1" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228"}, + {file = "pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "mypy", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] + [[package]] name = "pygments" version = "2.19.2" @@ -742,6 +1196,23 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjwt" +version = "2.13.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, + {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] + [[package]] name = "pytest" version = "8.4.2" @@ -763,6 +1234,25 @@ pygments = ">=2.7.2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, +] + +[package.dependencies] +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -975,6 +1465,92 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "respan-ai" +version = "4.1.0" +description = "Unified entry point for Respan tracing and instrumentation plugins" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "respan_ai-4.1.0-py3-none-any.whl", hash = "sha256:aa011d8620404eaa0e47414537f58205ff964029d88a6b8bff64b73ff17fcfde"}, + {file = "respan_ai-4.1.0.tar.gz", hash = "sha256:c6dbc95d35279345eae0159ca85f3108c54d20b215d50a253e155055cfbb0f08"}, +] + +[package.dependencies] +respan-tracing = ">=2.3.0" + +[[package]] +name = "respan-instrumentation-openai-agents" +version = "1.1.2" +description = "Respan instrumentation plugin for the OpenAI Agents SDK" +optional = false +python-versions = "<3.14,>=3.11" +files = [ + {file = "respan_instrumentation_openai_agents-1.1.2-py3-none-any.whl", hash = "sha256:4929811b634334ec496ecbeec82282cc406cf2c8c52b222cd872cb74f1153d8b"}, + {file = "respan_instrumentation_openai_agents-1.1.2.tar.gz", hash = "sha256:28b1506a37ff57a40748663bc9f1f8a79123af3d69c646f97ffc7522dd0bab11"}, +] + +[package.dependencies] +openai-agents = ">=0.3.1" +opentelemetry-semantic-conventions-ai = ">=0.4.1" +respan-sdk = ">=2.5.0" +respan-tracing = ">=2.3.0" + +[[package]] +name = "respan-sdk" +version = "2.7.5" +description = "Respan SDK allows you to interact with the Respan API smoothly" +optional = false +python-versions = "<4.0,>3.9" +files = [ + {file = "respan_sdk-2.7.5-py3-none-any.whl", hash = "sha256:63ec386efd98a5cbdbda556ed10b8af022c26d94725d6551100e9467d616e880"}, + {file = "respan_sdk-2.7.5.tar.gz", hash = "sha256:a3286374646151f888912d5191c9d1ac6395ff8a6085ef48e5c99da8dce37f96"}, +] + +[package.dependencies] +dateparser = ">=1.2.0,<2.0.0" +pydantic = ">=2.10.3,<3.0.0" +requests = ">=2.31.0,<3.0.0" +typing-extensions = ">=4.12.2,<5.0.0" + +[[package]] +name = "respan-tracing" +version = "2.19.2" +description = "Respan SDK allows you to interact with the Respan API smoothly" +optional = false +python-versions = "<3.14,>=3.11" +files = [ + {file = "respan_tracing-2.19.2-py3-none-any.whl", hash = "sha256:a02eda77380823f735f3ac3dd5dec0a3b99b0c36382556eac1584b2e911720ab"}, + {file = "respan_tracing-2.19.2.tar.gz", hash = "sha256:b5555cf3b2cea2e50771c2a717ba91b85424b13cacd59848274d4c2292326a68"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.29.0,<2.0.0" +opentelemetry-instrumentation-threading = ">=0.55b1" +opentelemetry-sdk = ">=1.29.0,<2.0.0" +opentelemetry-semantic-conventions-ai = ">=0.4.1" +requests = ">=2.28.0" +respan-sdk = ">=2.6.1" +typing-extensions = ">=4.12.2,<5.0.0" + +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "rpds-py" version = "0.27.1" @@ -1220,20 +1796,6 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] -[[package]] -name = "types-requests" -version = "2.32.4.20250809" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.9" -files = [ - {file = "types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163"}, - {file = "types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3"}, -] - -[package.dependencies] -urllib3 = ">=2" - [[package]] name = "typing-extensions" version = "4.15.0" @@ -1247,17 +1809,17 @@ files = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.4" description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, ] [package.dependencies] -typing-extensions = ">=4.12.0" +typing-extensions = ">=4.15.0" [[package]] name = "tzdata" @@ -1322,7 +1884,227 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "websockets" +version = "16.1.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a"}, + {file = "websockets-16.1.1-cp310-cp310-win32.whl", hash = "sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9"}, + {file = "websockets-16.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81"}, + {file = "websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57"}, + {file = "websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3"}, + {file = "websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747"}, + {file = "websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87"}, + {file = "websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea"}, + {file = "websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9"}, + {file = "websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328"}, + {file = "websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737"}, + {file = "websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7"}, + {file = "websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5"}, + {file = "websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3"}, + {file = "websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57"}, +] + +[[package]] +name = "wrapt" +version = "2.3.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.9" +files = [ + {file = "wrapt-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7"}, + {file = "wrapt-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023"}, + {file = "wrapt-2.3.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276"}, + {file = "wrapt-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199"}, + {file = "wrapt-2.3.0-cp310-cp310-win32.whl", hash = "sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35"}, + {file = "wrapt-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0"}, + {file = "wrapt-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760"}, + {file = "wrapt-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe"}, + {file = "wrapt-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c"}, + {file = "wrapt-2.3.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6"}, + {file = "wrapt-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd"}, + {file = "wrapt-2.3.0-cp311-cp311-win32.whl", hash = "sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14"}, + {file = "wrapt-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84"}, + {file = "wrapt-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98"}, + {file = "wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525"}, + {file = "wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb"}, + {file = "wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3"}, + {file = "wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d"}, + {file = "wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1"}, + {file = "wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8"}, + {file = "wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab"}, + {file = "wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f"}, + {file = "wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0"}, + {file = "wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae"}, + {file = "wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3"}, + {file = "wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f"}, + {file = "wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838"}, + {file = "wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579"}, + {file = "wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944"}, + {file = "wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a"}, + {file = "wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41"}, + {file = "wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4"}, + {file = "wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3"}, + {file = "wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98"}, + {file = "wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6"}, + {file = "wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc"}, + {file = "wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5"}, + {file = "wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f"}, + {file = "wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23"}, + {file = "wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b"}, + {file = "wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d"}, + {file = "wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab"}, + {file = "wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84"}, + {file = "wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c"}, + {file = "wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df"}, + {file = "wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109"}, + {file = "wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501"}, + {file = "wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5"}, + {file = "wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51"}, + {file = "wrapt-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb"}, + {file = "wrapt-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c"}, + {file = "wrapt-2.3.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2"}, + {file = "wrapt-2.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe"}, + {file = "wrapt-2.3.0-cp39-cp39-win32.whl", hash = "sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f"}, + {file = "wrapt-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636"}, + {file = "wrapt-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66"}, + {file = "wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2"}, + {file = "wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107"}, +] + +[package.extras] +dev = ["pytest", "setuptools"] + [metadata] lock-version = "2.0" -python-versions = ">=3.11,<3.13" -content-hash = "c9e822d7507d3221139c921d6f5342f623c853622923c2cbdb79333cd511e230" +python-versions = ">=3.11,<3.14" +content-hash = "a02e15855c06e206e88ca9be72df461ac07a088a59ec77dd56c36dc1e1f94b4f" diff --git a/python/tracing/openai-agents-sdk/pyproject.toml b/python/tracing/openai-agents-sdk/pyproject.toml index 0ca4500..b5f1039 100644 --- a/python/tracing/openai-agents-sdk/pyproject.toml +++ b/python/tracing/openai-agents-sdk/pyproject.toml @@ -7,14 +7,26 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.11,<3.14" -openai-agents = ">=0.17.7" -respan-ai = ">=2.6.0" +openai-agents = ">=0.20.0,<0.21.0" +respan-ai = ">=4.1.0,<5.0.0" respan-instrumentation-openai-agents = ">=1.1.1" -python-dotenv = "^1.0.0" +python-dotenv = ">=1.0.0,<2.0.0" +rich = ">=13.0.0,<16.0.0" +playwright = ">=1.50.0,<2.0.0" [tool.poetry.group.dev.dependencies] -pytest = "^8.4.2" -pytest-asyncio = "^0.24.0" +pytest = ">=8.4.0,<10.0.0" +pytest-asyncio = ">=0.24.0,<2.0.0" + +[tool.pytest.ini_options] +asyncio_default_test_loop_scope = "session" +markers = [ + "hosted: requires a direct OpenAI Responses API credential or hosted resource", + "live: performs a configured provider or gateway request", +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["N999"] [build-system] requires = ["poetry-core"] diff --git a/python/tracing/openai-agents-sdk/requirements.txt b/python/tracing/openai-agents-sdk/requirements.txt new file mode 100644 index 0000000..b008afb --- /dev/null +++ b/python/tracing/openai-agents-sdk/requirements.txt @@ -0,0 +1,8 @@ +openai-agents>=0.20.0,<0.21.0 +playwright>=1.50.0,<2.0.0 +pytest>=8.4.0,<10.0.0 +pytest-asyncio>=0.24.0,<2.0.0 +python-dotenv>=1.0.0,<2.0.0 +respan-ai>=4.1.0,<5.0.0 +respan-instrumentation-openai-agents>=1.1.1 +rich>=13.0.0,<16.0.0 diff --git a/python/tracing/openai-agents-sdk/research_bot/agents/planner_agent.py b/python/tracing/openai-agents-sdk/research_bot/agents/planner_agent.py index e80a8e6..3c2929c 100644 --- a/python/tracing/openai-agents-sdk/research_bot/agents/planner_agent.py +++ b/python/tracing/openai-agents-sdk/research_bot/agents/planner_agent.py @@ -1,6 +1,5 @@ -from pydantic import BaseModel - from agents import Agent +from pydantic import BaseModel PROMPT = ( "You are a helpful research assistant. Given a query, come up with a set of web searches " diff --git a/python/tracing/openai-agents-sdk/research_bot/agents/writer_agent.py b/python/tracing/openai-agents-sdk/research_bot/agents/writer_agent.py index 7b7d01a..1f2e6db 100644 --- a/python/tracing/openai-agents-sdk/research_bot/agents/writer_agent.py +++ b/python/tracing/openai-agents-sdk/research_bot/agents/writer_agent.py @@ -1,7 +1,6 @@ # Agent used to synthesize a final report from the individual summaries. -from pydantic import BaseModel - from agents import Agent +from pydantic import BaseModel PROMPT = ( "You are a senior researcher tasked with writing a cohesive report for a research query. " diff --git a/python/tracing/openai-agents-sdk/research_bot/main_test.py b/python/tracing/openai-agents-sdk/research_bot/main_test.py index 052b1c7..30751a5 100644 --- a/python/tracing/openai-agents-sdk/research_bot/main_test.py +++ b/python/tracing/openai-agents-sdk/research_bot/main_test.py @@ -1,19 +1,25 @@ from dotenv import load_dotenv -loaded = load_dotenv(override=True) -import pytest -import os +loaded = load_dotenv(override=False) import asyncio +import os - -from .manager import ResearchManager +import pytest from agents import set_trace_processors + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) +from .manager import ResearchManager + set_trace_processors( - [RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"))] + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] ) diff --git a/python/tracing/openai-agents-sdk/research_bot/manager.py b/python/tracing/openai-agents-sdk/research_bot/manager.py index c2bba9f..47f5118 100644 --- a/python/tracing/openai-agents-sdk/research_bot/manager.py +++ b/python/tracing/openai-agents-sdk/research_bot/manager.py @@ -3,15 +3,14 @@ import asyncio import time -from rich.console import Console - from agents import Runner, custom_span, gen_trace_id, trace +from rich.console import Console from .agents.planner_agent import WebSearchItem, WebSearchPlan, planner_agent from .agents.search_agent import search_agent from .agents.writer_agent import ReportData, writer_agent from .printer import Printer -from typing import Union + class ResearchManager: def __init__(self): @@ -65,21 +64,21 @@ async def _plan_searches(self, query: str) -> WebSearchPlan: async def _perform_searches(self, search_plan: WebSearchPlan) -> list[str]: with custom_span("Search the web"): self.printer.update_item("searching", "Searching...") - num_completed = 0 - tasks = [asyncio.create_task(self._search(item)) for item in search_plan.searches] + tasks = [ + asyncio.create_task(self._search(item)) for item in search_plan.searches + ] results = [] - for task in asyncio.as_completed(tasks): + for num_completed, task in enumerate(asyncio.as_completed(tasks), start=1): result = await task if result is not None: results.append(result) - num_completed += 1 self.printer.update_item( "searching", f"Searching... {num_completed}/{len(tasks)} completed" ) self.printer.mark_item_done("searching") return results - async def _search(self, item: WebSearchItem) -> Union[str, None]: + async def _search(self, item: WebSearchItem) -> str | None: input = f"Search term: {item.query}\nReason for searching: {item.reason}" try: result = await Runner.run( @@ -87,7 +86,7 @@ async def _search(self, item: WebSearchItem) -> Union[str, None]: input, ) return str(result.final_output) - except Exception: + except Exception: # noqa: BLE001 - one failed search must not abort the report. return None async def _write_report(self, query: str, search_results: list[str]) -> ReportData: diff --git a/python/tracing/openai-agents-sdk/research_bot/printer.py b/python/tracing/openai-agents-sdk/research_bot/printer.py index e820c75..fa448e7 100644 --- a/python/tracing/openai-agents-sdk/research_bot/printer.py +++ b/python/tracing/openai-agents-sdk/research_bot/printer.py @@ -19,7 +19,11 @@ def hide_done_checkmark(self, item_id: str) -> None: self.hide_done_ids.add(item_id) def update_item( - self, item_id: str, content: str, is_done: bool = False, hide_checkmark: bool = False + self, + item_id: str, + content: str, + is_done: bool = False, + hide_checkmark: bool = False, ) -> None: self.items[item_id] = (content, is_done) if hide_checkmark: diff --git a/python/tracing/openai-agents-sdk/respan_exporter_openai_agents.py b/python/tracing/openai-agents-sdk/respan_exporter_openai_agents.py index d2e578e..b055e67 100644 --- a/python/tracing/openai-agents-sdk/respan_exporter_openai_agents.py +++ b/python/tracing/openai-agents-sdk/respan_exporter_openai_agents.py @@ -8,19 +8,25 @@ from __future__ import annotations +import asyncio import os from typing import Any +from agents import set_default_openai_api, set_default_openai_client from dotenv import find_dotenv, load_dotenv from openai import AsyncOpenAI -from agents import set_default_openai_api, set_default_openai_client from respan import Respan -from respan_instrumentation_openai_agents._instrumentation import _RespanTracingProcessor +from respan_instrumentation_openai_agents._instrumentation import ( + _install_stream_patches, + _RespanTracingProcessor, +) -load_dotenv(find_dotenv(), override=True) +load_dotenv(find_dotenv(), override=False) _RESPAN: Respan | None = None _GATEWAY_CLIENT_CONFIGURED = False +_GATEWAY_CLIENT: AsyncOpenAI | None = None +_GATEWAY_CLIENT_OWNER_LOOP: asyncio.AbstractEventLoop | None = None def _clean_url(value: str | None, default: str) -> str: @@ -28,7 +34,7 @@ def _clean_url(value: str | None, default: str) -> str: def _configure_gateway_client(api_key: str | None) -> None: - global _GATEWAY_CLIENT_CONFIGURED + global _GATEWAY_CLIENT, _GATEWAY_CLIENT_CONFIGURED if _GATEWAY_CLIENT_CONFIGURED: return @@ -47,25 +53,58 @@ def _configure_gateway_client(api_key: str | None) -> None: set_default_openai_api("chat_completions") os.environ["OPENAI_API_KEY"] = gateway_api_key os.environ["OPENAI_BASE_URL"] = gateway_base_url - set_default_openai_client( - AsyncOpenAI(api_key=gateway_api_key, base_url=os.environ["OPENAI_BASE_URL"]) + _GATEWAY_CLIENT = AsyncOpenAI( + api_key=gateway_api_key, base_url=os.environ["OPENAI_BASE_URL"] ) + set_default_openai_client(_GATEWAY_CLIENT) _GATEWAY_CLIENT_CONFIGURED = True +def _claim_gateway_client_loop() -> None: + """Bind the retained async client to the loop that first uses an Agent trace.""" + global _GATEWAY_CLIENT_OWNER_LOOP + if _GATEWAY_CLIENT is None: + return + try: + current_loop = asyncio.get_running_loop() + except RuntimeError: + return + if _GATEWAY_CLIENT_OWNER_LOOP is None: + _GATEWAY_CLIENT_OWNER_LOOP = current_loop + elif _GATEWAY_CLIENT_OWNER_LOOP is not current_loop: + raise RuntimeError( + "The shared OpenAI Agents client must stay on its owning event loop" + ) + + +def has_direct_responses_credentials() -> bool: + """Return whether hosted Responses tools can run without gateway coercion.""" + return os.getenv("RESPAN_OPENAI_AGENTS_USE_OPENAI") == "1" and bool( + os.getenv("OPENAI_API_KEY") + ) + + def _ensure_respan(api_key: str | None, base_url: str | None) -> Respan | None: global _RESPAN if _RESPAN is not None: return _RESPAN - resolved_api_key = api_key or os.getenv("RESPAN_API_KEY") or os.getenv("RESPAN_GATEWAY_API_KEY") + resolved_api_key = ( + api_key or os.getenv("RESPAN_API_KEY") or os.getenv("RESPAN_GATEWAY_API_KEY") + ) if not resolved_api_key: return None _RESPAN = Respan( app_name=os.getenv("RESPAN_APP_NAME", "openai-agents-sdk-examples"), api_key=resolved_api_key, - base_url=_clean_url(base_url or os.getenv("RESPAN_BASE_URL"), "https://api.respan.ai/api"), + base_url=_clean_url( + base_url or os.getenv("RESPAN_BASE_URL"), "https://api.respan.ai/api" + ), + # The explicit Agents trace processor owns provider-call coverage. + # Direct OpenAI auto-instrumentation would duplicate its chat spans. + is_auto_instrument=False, + log_level=os.getenv("RESPAN_LOG_LEVEL", "WARNING"), ) return _RESPAN @@ -81,14 +120,22 @@ def __init__( default_model: str | None = None, **_: Any, ) -> None: - super().__init__() + marker = os.getenv("RESPAN_EXAMPLE_RUN_ID") + super().__init__( + metadata={"example_run_id": marker} if marker else None, + ) self.api_key = api_key or os.getenv("RESPAN_API_KEY") self.endpoint = endpoint self.base_url = base_url self.default_model = default_model or os.getenv("RESPAN_MODEL") self._respan = _ensure_respan(self.api_key, base_url) + _install_stream_patches() _configure_gateway_client(self.api_key) + def on_trace_start(self, trace) -> None: + _claim_gateway_client_loop() + return super().on_trace_start(trace) + def shutdown(self) -> None: if self._respan is not None: self._respan.flush() @@ -96,3 +143,37 @@ def shutdown(self) -> None: def force_flush(self) -> None: if self._respan is not None: self._respan.flush() + + +def flush_respan() -> None: + """Flush all example spans without closing the shared test-session client.""" + if _RESPAN is not None: + _RESPAN.flush() + + +async def shutdown_respan_async() -> None: + """Close the exporter and retained gateway client on its owning event loop.""" + global _GATEWAY_CLIENT, _GATEWAY_CLIENT_CONFIGURED + global _GATEWAY_CLIENT_OWNER_LOOP, _RESPAN + + try: + if _RESPAN is not None: + _RESPAN.shutdown() + _RESPAN = None + finally: + client = _GATEWAY_CLIENT + if client is None: + _GATEWAY_CLIENT_CONFIGURED = False + else: + current_loop = asyncio.get_running_loop() + if ( + _GATEWAY_CLIENT_OWNER_LOOP is not None + and _GATEWAY_CLIENT_OWNER_LOOP is not current_loop + ): + raise RuntimeError( + "The shared OpenAI Agents client must close on its owning event loop" + ) + await client.close() + _GATEWAY_CLIENT = None + _GATEWAY_CLIENT_OWNER_LOOP = None + _GATEWAY_CLIENT_CONFIGURED = False diff --git a/python/tracing/openai-agents-sdk/run_all.py b/python/tracing/openai-agents-sdk/run_all.py new file mode 100644 index 0000000..342112f --- /dev/null +++ b/python/tracing/openai-agents-sdk/run_all.py @@ -0,0 +1,84 @@ +"""Run every collected and legacy direct OpenAI Agents example with one marker.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +from dotenv import find_dotenv, load_dotenv + +ROOT = Path(__file__).resolve().parent +load_dotenv(find_dotenv(), override=False) +DEFAULT_COMMAND_TIMEOUT_SECONDS = 300 + + +def _marker() -> str: + existing = os.getenv("RESPAN_EXAMPLE_RUN_ID") + if existing: + return existing + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + return f"otel2-openai-agents-{timestamp}" + + +def _timeout_seconds() -> int: + raw = os.getenv("RESPAN_EXAMPLE_COMMAND_TIMEOUT_SECONDS", "") + try: + value = int(raw) + except ValueError: + return DEFAULT_COMMAND_TIMEOUT_SECONDS + return value if value > 0 else DEFAULT_COMMAND_TIMEOUT_SECONDS + + +def _run_commands( + commands: list[list[str]], *, env: dict[str, str], timeout_seconds: int +) -> list[tuple[list[str], str]]: + failures: list[tuple[list[str], str]] = [] + for index, command in enumerate(commands, start=1): + print(f"[{index}/{len(commands)}] {' '.join(command)}", flush=True) + try: + completed = subprocess.run( + command, + cwd=ROOT, + env=env, + check=False, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired: + detail = f"timeout after {timeout_seconds}s" + print(detail, flush=True) + failures.append((command, detail)) + continue + print(f"exit={completed.returncode}", flush=True) + if completed.returncode: + failures.append((command, f"exit={completed.returncode}")) + return failures + + +def main() -> int: + marker = _marker() + env = { + **os.environ, + "PYTHONDONTWRITEBYTECODE": "1", + "RESPAN_EXAMPLE_RUN_ID": marker, + } + commands = [ + [sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"], + [sys.executable, "handoffs/message_filter_test.py"], + [sys.executable, "handoffs/message_filter_streaming_test.py"], + [sys.executable, "complex_edge_cases_test.py"], + ] + print(f"RESPAN_EXAMPLE_RUN_ID={marker}", flush=True) + failures = _run_commands(commands, env=env, timeout_seconds=_timeout_seconds()) + if failures: + print("OpenAI Agents example failures:", flush=True) + for command, detail in failures: + print(f" {detail}: {' '.join(command)}", flush=True) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/python/tracing/openai-agents-sdk/runner_contract.py b/python/tracing/openai-agents-sdk/runner_contract.py new file mode 100644 index 0000000..47e6609 --- /dev/null +++ b/python/tracing/openai-agents-sdk/runner_contract.py @@ -0,0 +1,26 @@ +"""Focused contract checks for the bounded all-example runner.""" + +from __future__ import annotations + +import subprocess + +import run_all + + +def test_timeout_is_reported_without_stopping_later_commands(monkeypatch, capsys): + calls: list[list[str]] = [] + + def fake_run(command, **_kwargs): + calls.append(command) + if len(calls) == 1: + raise subprocess.TimeoutExpired(command, timeout=7) + return subprocess.CompletedProcess(command, returncode=0) + + monkeypatch.setattr(run_all.subprocess, "run", fake_run) + commands = [["python", "slow.py"], ["python", "next.py"]] + + failures = run_all._run_commands(commands, env={}, timeout_seconds=7) + + assert calls == commands + assert failures == [(commands[0], "timeout after 7s")] + assert "[2/2] python next.py" in capsys.readouterr().out diff --git a/python/tracing/openai-agents-sdk/tools/computer_use_test.py b/python/tracing/openai-agents-sdk/tools/computer_use_test.py index 322d5c1..4359970 100644 --- a/python/tracing/openai-agents-sdk/tools/computer_use_test.py +++ b/python/tracing/openai-agents-sdk/tools/computer_use_test.py @@ -1,15 +1,12 @@ from dotenv import load_dotenv -load_dotenv(override=True) -import pytest -import os +load_dotenv(override=False) import asyncio import base64 -import logging -from typing import Literal, Union - -from playwright.async_api import Browser, Page, Playwright, async_playwright +import os +from typing import Literal +import pytest from agents import ( Agent, AsyncComputer, @@ -21,17 +18,21 @@ trace, ) from agents.tracing import set_trace_processors +from playwright.async_api import Browser, Page, Playwright, async_playwright + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) set_trace_processors( - [RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"))] + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] ) -logging.getLogger("openai.agents").setLevel(logging.DEBUG) -logging.getLogger("openai.agents").addHandler(logging.StreamHandler()) - @pytest.mark.asyncio async def test_main(): @@ -45,7 +46,9 @@ async def test_main(): model="computer-use-preview", model_settings=ModelSettings(truncation="auto"), ) - result = await Runner.run(agent, "Search who is the president of the United States now") + result = await Runner.run( + agent, "Search who is the president of the United States now" + ) print(result.final_output) @@ -82,9 +85,9 @@ class LocalPlaywrightComputer(AsyncComputer): """A computer, implemented using a local Playwright browser.""" def __init__(self): - self._playwright: Union[Playwright, None] = None - self._browser: Union[Browser, None] = None - self._page: Union[Page, None] = None + self._playwright: Playwright | None = None + self._browser: Browser | None = None + self._page: Page | None = None async def _get_browser_and_page(self) -> tuple[Browser, Page]: width, height = self.dimensions diff --git a/python/tracing/openai-agents-sdk/tools/file_search_test.py b/python/tracing/openai-agents-sdk/tools/file_search_test.py index 567dc33..722ac06 100644 --- a/python/tracing/openai-agents-sdk/tools/file_search_test.py +++ b/python/tracing/openai-agents-sdk/tools/file_search_test.py @@ -1,18 +1,24 @@ from dotenv import load_dotenv -load_dotenv(override=True) -import pytest -import os +load_dotenv(override=False) import asyncio +import os +import pytest from agents import Agent, FileSearchTool, Runner, trace from agents.tracing import set_trace_processors + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) set_trace_processors( - [RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"))] + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] ) @@ -24,7 +30,7 @@ async def test_main(): tools=[ FileSearchTool( max_num_results=3, - vector_store_ids=["vs_67d3bdd0c8888191adfa890a9e829480"], + vector_store_ids=[os.environ["OPENAI_VECTOR_STORE_ID"]], include_search_results=True, ) ], @@ -45,5 +51,6 @@ async def test_main(): {"id":"...", "queries":["Arrakis"], "results":[...]} """ + if __name__ == "__main__": asyncio.run(test_main()) diff --git a/python/tracing/openai-agents-sdk/tools/functions_test.py b/python/tracing/openai-agents-sdk/tools/functions_test.py index 9a4c25c..10b8285 100644 --- a/python/tracing/openai-agents-sdk/tools/functions_test.py +++ b/python/tracing/openai-agents-sdk/tools/functions_test.py @@ -1,15 +1,17 @@ from dotenv import load_dotenv -load_dotenv(override=True) -import pytest +load_dotenv(override=False) # ==========copy the below========== import asyncio +import os + +import pytest from agents import Agent, Runner, function_tool +from agents.tracing import set_trace_processors + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) -from agents.tracing import set_trace_processors -import os set_trace_processors( [ @@ -39,5 +41,6 @@ async def test_main(): print(result.final_output) # The weather in Tokyo is sunny. + if __name__ == "__main__": asyncio.run(test_main()) diff --git a/python/tracing/openai-agents-sdk/tools/web_search_test.py b/python/tracing/openai-agents-sdk/tools/web_search_test.py index 48d25c4..de3a905 100644 --- a/python/tracing/openai-agents-sdk/tools/web_search_test.py +++ b/python/tracing/openai-agents-sdk/tools/web_search_test.py @@ -1,19 +1,25 @@ from dotenv import load_dotenv -load_dotenv(override=True) +load_dotenv(override=False) -import os import asyncio -import pytest +import os +import pytest from agents import Agent, Runner, WebSearchTool, trace from agents.tracing import set_trace_processors + from respan_exporter_openai_agents import ( RespanTraceProcessor, ) set_trace_processors( - [RespanTraceProcessor(os.getenv("RESPAN_API_KEY"), endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"))] + [ + RespanTraceProcessor( + os.getenv("RESPAN_API_KEY"), + endpoint=os.getenv("RESPAN_OAIA_TRACING_ENDPOINT"), + ) + ] ) @@ -22,7 +28,9 @@ async def test_main(): agent = Agent( name="Web searcher", instructions="You are a helpful agent.", - tools=[WebSearchTool(user_location={"type": "approximate", "city": "New York"})], + tools=[ + WebSearchTool(user_location={"type": "approximate", "city": "New York"}) + ], ) with trace("Web search example"): @@ -33,5 +41,6 @@ async def test_main(): print(result.final_output) # The New York Giants are reportedly pursuing quarterback Aaron Rodgers after his ... + if __name__ == "__main__": asyncio.run(test_main()) diff --git a/python/tracing/openai-sdk/.env.example b/python/tracing/openai-sdk/.env.example index 1f3d605..8895857 100644 --- a/python/tracing/openai-sdk/.env.example +++ b/python/tracing/openai-sdk/.env.example @@ -2,6 +2,8 @@ RESPAN_API_KEY=your-api-key-here RESPAN_BASE_URL=https://api.respan.ai/api -# OpenAI — direct API key (NOT the Respan gateway key) -# Required for Batch API, file uploads, and other direct OpenAI endpoints -OPENAI_API_KEY=your-api-key-here +# Optional live-provider mode. The default examples are deterministic. +RESPAN_OPENAI_LIVE=0 +# OPENAI_API_KEY=your-direct-openai-key +# OPENAI_BASE_URL=https://api.openai.com/v1 +# RESPAN_PROMPT_ID=your-managed-prompt-id diff --git a/python/tracing/openai-sdk/README.md b/python/tracing/openai-sdk/README.md new file mode 100644 index 0000000..c0d5d53 --- /dev/null +++ b/python/tracing/openai-sdk/README.md @@ -0,0 +1,46 @@ +# OpenAI SDK tracing examples + +These 18 scripts exercise the real OpenAI 3.x sync and async client surfaces +while exporting traces through the local editable Respan packages. The default +mode is deterministic: an in-process `httpx2.MockTransport` supplies valid +OpenAI response payloads, so no provider credential or managed prompt is +required. Requests still pass through the official OpenAI resource, streaming, +SSE, Pydantic parse, and error classes. + +The examples cover Chat and Responses calls, sync/async structured parsing, +streaming, two-turn tools, embeddings, precise 401 handling, propagated +attributes, decorators, prompt-shaped requests, and deterministic batch result +logging. + +## Run + +From this directory, link the checked-out packages and run the full set with +one exact marker: + +```bash +python -m pip install -e ../../../../respan/python-sdks/respan-sdk \ + -e ../../../../respan/python-sdks/respan-tracing \ + -e ../../../../respan/python-sdks/instrumentations/respan-instrumentation-openai \ + -e ../../../../respan/python-sdks/respan +RESPAN_EXAMPLE_RUN_ID=openai-check-001 python run_all.py +``` + +`run_all.py` preserves a shell-supplied marker, runs every script independently, +and reports all failures after the set finishes. Every script flushes and shuts +down Respan explicitly. + +## Optional live provider + +Set the following only when you want to replace deterministic transport with a +real OpenAI-compatible endpoint: + +```bash +RESPAN_OPENAI_LIVE=1 +OPENAI_API_KEY=... +# Optional: +OPENAI_BASE_URL=https://api.openai.com/v1 +``` + +Managed-prompt-shaped examples use `RESPAN_PROMPT_ID` when supplied. Batch +examples intentionally use deterministic result payloads because provider-side +file upload and polling are independent of this instrumentation package. diff --git a/python/tracing/openai-sdk/_batch_data.py b/python/tracing/openai-sdk/_batch_data.py new file mode 100644 index 0000000..3a221b1 --- /dev/null +++ b/python/tracing/openai-sdk/_batch_data.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Any + +from _shared import model_name + + +def batch_requests() -> list[dict[str, Any]]: + return [ + { + "custom_id": f"topic-{index}", + "method": "POST", + "url": "/v1/chat/completions", + "body": { + "model": model_name(), + "messages": [{"role": "user", "content": f"Summarize {topic}."}], + }, + } + for index, topic in enumerate( + ("quantum computing", "blockchain", "edge computing") + ) + ] + + +def batch_results(requests: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [ + { + "custom_id": request["custom_id"], + "response": { + "status_code": 200, + "body": { + "id": f"chat_{request['custom_id']}", + "object": "chat.completion", + "created": 1_786_972_800 + index, + "model": model_name(), + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": f"Deterministic summary {index + 1}.", + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 5 + index, + "completion_tokens": 3, + "total_tokens": 8 + index, + }, + }, + }, + } + for index, request in enumerate(requests) + ] diff --git a/python/tracing/openai-sdk/_shared.py b/python/tracing/openai-sdk/_shared.py new file mode 100644 index 0000000..ca0388e --- /dev/null +++ b/python/tracing/openai-sdk/_shared.py @@ -0,0 +1,411 @@ +from __future__ import annotations + +import json +import os +from contextlib import contextmanager +from pathlib import Path +from typing import Any +from uuid import uuid4 + +import httpx2 +from dotenv import load_dotenv +from openai import AsyncOpenAI, OpenAI +from respan import Respan, propagate_attributes +from respan_instrumentation_openai import OpenAIInstrumentor + +PROJECT_ROOT = Path(__file__).resolve().parents[3] +DEFAULT_RESPAN_BASE_URL = "https://api.respan.ai/api" +DEFAULT_MODEL = "gpt-4.1-nano" +FAILURE_SENTINEL = "RESPAN_EXPECTED_401" + + +def load_root_env() -> None: + # A shell-supplied group marker must win over values in the repository file. + load_dotenv(PROJECT_ROOT / ".env", override=False) + + +def require_respan_api_key() -> str: + load_root_env() + api_key = os.getenv("RESPAN_API_KEY") + if not api_key: + raise RuntimeError("RESPAN_API_KEY must be set in the repository .env") + return api_key + + +def respan_base_url() -> str: + return os.getenv("RESPAN_BASE_URL", DEFAULT_RESPAN_BASE_URL).rstrip("/") + + +def model_name() -> str: + return os.getenv("RESPAN_OPENAI_MODEL", DEFAULT_MODEL) + + +def run_id() -> str: + marker = os.getenv("RESPAN_EXAMPLE_RUN_ID") + if marker: + return marker + marker = f"openai-sdk-{uuid4().hex[:12]}" + os.environ["RESPAN_EXAMPLE_RUN_ID"] = marker + return marker + + +def workflow_name(example_name: str) -> str: + return f"openai_{example_name.replace('-', '_')}" + + +def make_respan(example_name: str) -> Respan: + marker = run_id() + return Respan( + api_key=require_respan_api_key(), + base_url=respan_base_url(), + app_name="openai-sdk-examples", + environment=os.getenv("RESPAN_ENVIRONMENT", "example"), + instrumentations=[OpenAIInstrumentor()], + metadata={ + "integration": "openai", + "example": example_name, + "example_run_id": marker, + }, + ) + + +@contextmanager +def example_attributes(example_name: str): + marker = run_id() + current_workflow = workflow_name(example_name) + with propagate_attributes( + custom_identifier=f"{marker}:{example_name}", + trace_group_identifier=current_workflow, + metadata={ + "integration": "openai", + "example": example_name, + "example_run_id": marker, + "workflow_name": current_workflow, + }, + ): + yield marker + + +def live_enabled() -> bool: + return os.getenv("RESPAN_OPENAI_LIVE", "").lower() in {"1", "true", "yes"} + + +def client_mode() -> str: + return "live-openai" if live_enabled() else "deterministic-openai-transport" + + +def _live_client_kwargs() -> dict[str, Any]: + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise RuntimeError("RESPAN_OPENAI_LIVE=1 requires OPENAI_API_KEY") + kwargs: dict[str, Any] = {"api_key": api_key, "max_retries": 0} + base_url = os.getenv("OPENAI_BASE_URL") + if base_url: + kwargs["base_url"] = base_url + return kwargs + + +def make_sync_client() -> OpenAI: + load_root_env() + if live_enabled(): + return OpenAI(**_live_client_kwargs()) + return OpenAI( + api_key="deterministic-test-key", + base_url="https://openai.example.invalid/v1", + max_retries=0, + http_client=httpx2.Client(transport=httpx2.MockTransport(_sync_handler)), + ) + + +def make_async_client() -> AsyncOpenAI: + load_root_env() + if live_enabled(): + return AsyncOpenAI(**_live_client_kwargs()) + return AsyncOpenAI( + api_key="deterministic-test-key", + base_url="https://openai.example.invalid/v1", + max_retries=0, + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(_async_handler)), + ) + + +def finish_respan(respan: Respan) -> None: + try: + respan.flush() + finally: + respan.shutdown() + + +def print_result(example_name: str, text: str) -> None: + print(f"example={example_name}") + print(f"example_run_id={run_id()}") + print(f"workflow_name={workflow_name(example_name)}") + print(f"client_mode={client_mode()}") + print(text.strip()) + + +def _request_body(request: httpx2.Request) -> dict[str, Any]: + return json.loads(request.content or b"{}") + + +def _contains_failure(value: Any) -> bool: + return FAILURE_SENTINEL in json.dumps(value, default=str) + + +def _chat_response(body: dict[str, Any]) -> dict[str, Any]: + messages = body.get("messages") or [] + structured = bool(body.get("response_format")) + if structured: + content = json.dumps( + { + "title": "The Matrix", + "rating": 9, + "summary": "A deterministic science-fiction classic.", + "pros": ["visuals", "ideas"], + "cons": ["dense exposition"], + } + ) + finish_reason = "stop" + message: dict[str, Any] = {"role": "assistant", "content": content} + elif body.get("tools") and not any( + message.get("role") == "tool" + for message in messages + if isinstance(message, dict) + ): + finish_reason = "tool_calls" + message = { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_weather", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"city":"Paris"}', + }, + } + ], + } + elif body.get("tools"): + finish_reason = "stop" + message = { + "role": "assistant", + "content": "The deterministic tool reports sunny weather in Paris.", + } + elif not messages: + finish_reason = "stop" + message = { + "role": "assistant", + "content": "Milestone plan: design, implement, validate.", + } + else: + finish_reason = "stop" + message = { + "role": "assistant", + "content": "Deterministic OpenAI chat response.", + } + return { + "id": "chat_deterministic", + "object": "chat.completion", + "created": 1_786_972_800, + "model": body.get("model") or model_name(), + "choices": [ + { + "index": 0, + "message": message, + "finish_reason": finish_reason, + } + ], + "usage": {"prompt_tokens": 13, "completion_tokens": 7, "total_tokens": 20}, + } + + +def _responses_response(body: dict[str, Any]) -> dict[str, Any]: + structured = bool(body.get("text", {}).get("format")) + input_value = body.get("input") + has_tool_result = False + if isinstance(input_value, list): + has_tool_result = any( + isinstance(item, dict) and item.get("type") == "function_call_output" + for item in input_value + ) + if structured: + text = json.dumps( + { + "title": "The Matrix", + "rating": 9, + "summary": "A deterministic science-fiction classic.", + "pros": ["visuals", "ideas"], + "cons": ["dense exposition"], + } + ) + output = [_response_message(text)] + elif body.get("tools") and not has_tool_result: + output = [ + { + "id": "fc_weather", + "type": "function_call", + "call_id": "call_weather", + "name": "get_weather", + "arguments": '{"city":"Paris"}', + "status": "completed", + } + ] + else: + text = ( + "The deterministic tool reports sunny weather in Paris." + if has_tool_result + else "Deterministic OpenAI Responses output." + ) + output = [_response_message(text)] + return { + "id": "resp_deterministic", + "object": "response", + "created_at": 1_786_972_800, + "status": "completed", + "model": body.get("model") or model_name(), + "output": output, + "parallel_tool_calls": True, + "tool_choice": "auto", + "tools": [], + "temperature": 1, + "top_p": 1, + "usage": {"input_tokens": 11, "output_tokens": 6, "total_tokens": 17}, + "error": None, + "incomplete_details": None, + "instructions": body.get("instructions"), + "metadata": {}, + } + + +def _response_message(text: str) -> dict[str, Any]: + return { + "id": "msg_deterministic", + "type": "message", + "status": "completed", + "role": "assistant", + "content": [{"type": "output_text", "text": text, "annotations": []}], + } + + +def _sync_handler(request: httpx2.Request) -> httpx2.Response: + body = _request_body(request) + if _contains_failure(body): + return httpx2.Response( + 401, + json={ + "error": { + "message": "deterministic OpenAI credential rejected", + "type": "authentication_error", + } + }, + ) + if request.url.path.endswith("/chat/completions"): + if body.get("stream"): + return _chat_stream_response(body) + return httpx2.Response(200, json=_chat_response(body)) + if request.url.path.endswith("/responses"): + if body.get("stream"): + return _responses_stream_response(body) + return httpx2.Response(200, json=_responses_response(body)) + if request.url.path.endswith("/embeddings"): + return httpx2.Response( + 200, + json={ + "object": "list", + "model": body.get("model") or "text-embedding-3-small", + "data": [ + {"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]} + ], + "usage": {"prompt_tokens": 4, "total_tokens": 4}, + }, + ) + raise AssertionError(f"unhandled deterministic endpoint: {request.url.path}") + + +async def _async_handler(request: httpx2.Request) -> httpx2.Response: + return _sync_handler(request) + + +def _chat_stream_response(body: dict[str, Any]) -> httpx2.Response: + chunks = [ + { + "id": "chat_stream", + "object": "chat.completion.chunk", + "created": 1_786_972_800, + "model": body.get("model") or model_name(), + "choices": [ + { + "index": 0, + "delta": {"role": "assistant", "content": "Streaming "}, + "finish_reason": None, + } + ], + }, + { + "id": "chat_stream", + "object": "chat.completion.chunk", + "created": 1_786_972_800, + "model": body.get("model") or model_name(), + "choices": [ + { + "index": 0, + "delta": {"content": "OpenAI response."}, + "finish_reason": "stop", + } + ], + }, + { + "id": "chat_stream", + "object": "chat.completion.chunk", + "created": 1_786_972_800, + "model": body.get("model") or model_name(), + "choices": [], + "usage": {"prompt_tokens": 9, "completion_tokens": 4, "total_tokens": 13}, + }, + ] + payload = "".join(f"data: {json.dumps(chunk)}\n\n" for chunk in chunks) + payload += "data: [DONE]\n\n" + return httpx2.Response( + 200, + headers={"content-type": "text/event-stream"}, + content=payload.encode(), + ) + + +def _responses_stream_response(body: dict[str, Any]) -> httpx2.Response: + final_response = _responses_response(body) + final_response["output"] = [_response_message("Streaming Responses output.")] + events = [ + { + "type": "response.output_text.delta", + "sequence_number": 0, + "item_id": "msg_deterministic", + "output_index": 0, + "content_index": 0, + "delta": "Streaming ", + }, + { + "type": "response.output_text.delta", + "sequence_number": 1, + "item_id": "msg_deterministic", + "output_index": 0, + "content_index": 0, + "delta": "Responses output.", + }, + { + "type": "response.completed", + "sequence_number": 2, + "response": final_response, + }, + ] + payload = "".join( + f"event: {event['type']}\ndata: {json.dumps(event)}\n\n" for event in events + ) + payload += "data: [DONE]\n\n" + return httpx2.Response( + 200, + headers={"content-type": "text/event-stream"}, + content=payload.encode(), + ) diff --git a/python/tracing/openai-sdk/async_parallel.py b/python/tracing/openai-sdk/async_parallel.py index 7ce0958..2ebc311 100644 --- a/python/tracing/openai-sdk/async_parallel.py +++ b/python/tracing/openai-sdk/async_parallel.py @@ -1,42 +1,61 @@ -"""Async Parallel — Run multiple OpenAI calls concurrently, all auto-traced.""" +"""Concurrent async Chat calls plus async Responses structured parsing.""" -import os import asyncio -from dotenv import load_dotenv -load_dotenv(override=True) +from pydantic import BaseModel +from respan import task, workflow -from openai import AsyncOpenAI -from respan import Respan, workflow, task -from respan_instrumentation_openai import OpenAIInstrumentor +from _shared import ( + example_attributes, + finish_respan, + make_async_client, + make_respan, + model_name, + print_result, +) -respan = Respan(instrumentations=[OpenAIInstrumentor()]) +EXAMPLE = "async-parallel" +respan = make_respan(EXAMPLE) -client = AsyncOpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), -) + +class Review(BaseModel): + title: str + rating: int + summary: str + pros: list[str] + cons: list[str] @task(name="summarize") async def summarize(topic: str) -> str: response = await client.chat.completions.create( - model="gpt-4.1-nano", - messages=[ - {"role": "system", "content": "Summarize in one sentence."}, - {"role": "user", "content": f"What is {topic}?"}, - ], - + model=model_name(), + messages=[{"role": "user", "content": f"Summarize {topic}."}], ) - return response.choices[0].message.content + return response.choices[0].message.content or "" -@workflow(name="parallel_summaries") -async def run(): +@workflow(name="openai_async_parallel") +async def run() -> str: topics = ["quantum computing", "blockchain", "edge computing"] - results = await asyncio.gather(*[summarize(t) for t in topics]) - for topic, summary in zip(topics, results): - print(f"{topic}: {summary}\n") + summaries = await asyncio.gather(*(summarize(topic) for topic in topics)) + parsed = await client.responses.parse( + model=model_name(), input="Review The Matrix", text_format=Review + ) + return f"summaries={len(summaries)} parsed_rating={parsed.output_parsed.rating}" + + +async def main() -> None: + try: + global client + client = make_async_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, await run()) + finally: + await client.close() + finally: + finish_respan(respan) -asyncio.run(run()) +asyncio.run(main()) diff --git a/python/tracing/openai-sdk/attributes.py b/python/tracing/openai-sdk/attributes.py index 1fe372e..7e68084 100644 --- a/python/tracing/openai-sdk/attributes.py +++ b/python/tracing/openai-sdk/attributes.py @@ -1,39 +1,41 @@ -"""Attributes — Attach customer info and metadata to traces.""" +"""Customer, thread, and metadata propagation across OpenAI calls.""" -import os -from dotenv import load_dotenv +from respan import propagate_attributes, workflow -load_dotenv(override=True) - -from openai import OpenAI -from respan import Respan, workflow, propagate_attributes -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan( - instrumentations=[OpenAIInstrumentor()], - metadata={"service": "chat-api", "version": "1.0.0"}, +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), -) +EXAMPLE = "attributes" +respan = make_respan(EXAMPLE) -@workflow(name="handle_request") -def handle_request(user_id: str, question: str): +@workflow(name="openai_handle_request") +def handle_request(user_id: str, question: str) -> str: with propagate_attributes( customer_identifier=user_id, - thread_identifier="conv_001", - metadata={"plan": "pro"}, # merged with default metadata + thread_identifier="openai-conversation-001", + metadata={"plan": "pro"}, ): response = client.chat.completions.create( - model="gpt-4.1-nano", - messages=[{"role": "user", "content": question}], - + model=model_name(), messages=[{"role": "user", "content": question}] ) - print(f"[{user_id}] {response.choices[0].message.content}") - - -handle_request("user_alice", "What is an API gateway?") -handle_request("user_bob", "Explain rate limiting.") + return response.choices[0].message.content or "" + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + first = handle_request("user_alice", "What is an API gateway?") + second = handle_request("user_bob", "Explain rate limiting.") + print_result(EXAMPLE, f"first={first} second={second}") + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/batch.py b/python/tracing/openai-sdk/batch.py index 6c9ad4f..0cc1096 100644 --- a/python/tracing/openai-sdk/batch.py +++ b/python/tracing/openai-sdk/batch.py @@ -1,127 +1,29 @@ -"""Batch API — Submit multiple requests for async processing at 50% cost. +"""Deterministic OpenAI Batch result logging without provider-side polling.""" -The Batch API uses OpenAI's file and batch endpoints directly. -Respan decorators trace the batch workflow for observability. +from respan import task, workflow -Set OPENAI_API_KEY (direct OpenAI key) and RESPAN_API_KEY in .env: +from _batch_data import batch_requests, batch_results +from _shared import example_attributes, finish_respan, make_respan, print_result - RESPAN_API_KEY=your-respan-key - OPENAI_API_KEY=sk-proj-... -""" +EXAMPLE = "batch" +respan = make_respan(EXAMPLE) -import os -import json -import time -from dotenv import load_dotenv - -load_dotenv() - -from openai import OpenAI -from respan import Respan, workflow, task -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -# Batch API requires direct OpenAI access (not gateway). -# The .env has a direct OPENAI_API_KEY (not the Respan gateway key). -client = OpenAI( - api_key=os.getenv("OPENAI_API_KEY"), -) - -TOPICS = [ - "quantum computing", - "blockchain", - "edge computing", - "reinforcement learning", - "zero-knowledge proofs", -] - - -@task(name="create_batch_file") -def create_batch_file() -> str: - """Create a JSONL file with batch requests.""" - tasks = [] - for i, topic in enumerate(TOPICS): - tasks.append({ - "custom_id": f"topic-{i}", - "method": "POST", - "url": "/v1/chat/completions", - "body": { - "model": "gpt-4.1-nano", - "messages": [ - {"role": "system", "content": "Explain in one sentence."}, - {"role": "user", "content": f"What is {topic}?"}, - ], - }, - }) - - file_path = "/tmp/respan_batch_input.jsonl" - with open(file_path, "w") as f: - for t in tasks: - f.write(json.dumps(t) + "\n") - - print(f"Created {len(tasks)} tasks in {file_path}") - return file_path - - -@task(name="upload_and_submit") -def upload_and_submit(file_path: str) -> str: - """Upload the JSONL file and create a batch job.""" - batch_file = client.files.create( - file=open(file_path, "rb"), - purpose="batch", - ) - print(f"Uploaded file: {batch_file.id}") - - batch = client.batches.create( - input_file_id=batch_file.id, - endpoint="/v1/chat/completions", - completion_window="24h", - ) - print(f"Batch created: {batch.id} (status: {batch.status})") - return batch.id - - -@task(name="poll_batch") -def poll_batch(batch_id: str) -> str: - """Poll until the batch completes.""" - while True: - batch = client.batches.retrieve(batch_id) - print(f"Status: {batch.status} ({batch.request_counts.completed}/{batch.request_counts.total} done)") - - if batch.status == "completed": - return batch.output_file_id - elif batch.status in ("failed", "expired", "cancelled"): - raise RuntimeError(f"Batch {batch.status}: {batch.errors}") - - time.sleep(5) - - -@task(name="download_results") -def download_results(output_file_id: str): - """Download batch results and log each completion as a traced span.""" - content = client.files.content(output_file_id).content - results = [json.loads(line) for line in content.decode().strip().split("\n")] - - with open("/tmp/respan_batch_input.jsonl") as f: - requests = [json.loads(line) for line in f] - - # Log each batch result as an individual chat completion span +@task(name="log_batch_results") +def log_results() -> int: + requests = batch_requests() + results = batch_results(requests) respan.log_batch_results(requests, results) - - for r in results: - idx = int(r["custom_id"].split("-")[1]) - message = r["response"]["body"]["choices"][0]["message"]["content"] - print(f"{TOPICS[idx]}: {message}") + return len(results) -@workflow(name="batch_pipeline") -def run(): - file_path = create_batch_file() - batch_id = upload_and_submit(file_path) - output_file_id = poll_batch(batch_id) - download_results(output_file_id) +@workflow(name="openai_batch_pipeline") +def run() -> str: + return f"logged_results={log_results()}" -run() +try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/batch_async.py b/python/tracing/openai-sdk/batch_async.py index 7f7809d..9360896 100644 --- a/python/tracing/openai-sdk/batch_async.py +++ b/python/tracing/openai-sdk/batch_async.py @@ -1,129 +1,33 @@ -"""Async Batch — Submit a batch, come back later, log results into the original trace. +"""Log deterministic batch results after the submission workflow completes.""" -Simulates the real-world pattern where batch submission and result retrieval -happen at different times (or in different processes). +from respan import get_client, task, workflow - RESPAN_API_KEY=your-respan-key - OPENAI_API_KEY=sk-proj-... -""" +from _batch_data import batch_requests, batch_results +from _shared import example_attributes, finish_respan, make_respan, print_result -import os -import json -import time +EXAMPLE = "batch-async" +respan = make_respan(EXAMPLE) -from dotenv import load_dotenv - -load_dotenv() - -from openai import OpenAI -from respan import Respan, workflow, task, get_client -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) -client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) - -TOPICS = [ - "quantum computing", - "blockchain", - "edge computing", -] - - -# ── Phase 1: Submit ────────────────────────────────────────────────── -# In production this would be an API endpoint, a CLI command, etc. - -@task(name="create_and_submit") -def create_and_submit() -> dict: - """Build requests, upload, submit batch, return handles for later.""" - requests = [] - for i, topic in enumerate(TOPICS): - requests.append({ - "custom_id": f"topic-{i}", - "method": "POST", - "url": "/v1/chat/completions", - "body": { - "model": "gpt-4.1-nano", - "messages": [ - {"role": "system", "content": "Explain in one sentence."}, - {"role": "user", "content": f"What is {topic}?"}, - ], - }, - }) - - # Write + upload + submit - file_path = "/tmp/respan_batch_async.jsonl" - with open(file_path, "w") as f: - for r in requests: - f.write(json.dumps(r) + "\n") - - batch_file = client.files.create(file=open(file_path, "rb"), purpose="batch") - batch = client.batches.create( - input_file_id=batch_file.id, - endpoint="/v1/chat/completions", - completion_window="24h", - ) - print(f"Batch submitted: {batch.id} (status: {batch.status})") - - # Capture trace context — in production, save this to DB - rc = get_client() - trace_id = rc.get_current_trace_id() - print(f"Trace ID saved: {trace_id}") +@task(name="submit_batch") +def submit_batch() -> dict[str, str]: return { - "batch_id": batch.id, - "trace_id": trace_id, - "input_file": file_path, + "batch_id": "batch_deterministic", + "trace_id": get_client().get_current_trace_id(), } -@workflow(name="batch_submit") -def submit(): - return create_and_submit() - - -# ── Phase 2: Retrieve (simulates a separate job) ──────────────────── -# In production this would be a cron job, webhook handler, queue worker, etc. - -def retrieve_and_log(saved: dict): - """Check batch status, download results, log into original trace.""" - batch_id = saved["batch_id"] - trace_id = saved["trace_id"] - input_file = saved["input_file"] - - # Poll until done - while True: - batch = client.batches.retrieve(batch_id) - print(f"Status: {batch.status} ({batch.request_counts.completed}/{batch.request_counts.total} done)") - if batch.status == "completed": - break - elif batch.status in ("failed", "expired", "cancelled"): - raise RuntimeError(f"Batch {batch.status}: {batch.errors}") - time.sleep(5) - - # Download results - content = client.files.content(batch.output_file_id).content - results = [json.loads(line) for line in content.decode().strip().split("\n")] - - # Load original requests - with open(input_file) as f: - requests = [json.loads(line) for line in f] - - # Log completions back into the ORIGINAL trace - respan.log_batch_results(requests, results, trace_id=trace_id) - - for r in results: - idx = int(r["custom_id"].split("-")[1]) - message = r["response"]["body"]["choices"][0]["message"]["content"] - print(f"{TOPICS[idx]}: {message}") - - -# ── Run both phases ────────────────────────────────────────────────── +@workflow(name="openai_batch_submit") +def submit() -> dict[str, str]: + return submit_batch() -# Phase 1: submit -saved = submit() -print(f"\n--- Batch submitted. Waiting for results... ---\n") -# Phase 2: come back later and retrieve -# (In production, this would be a completely separate process/job) -retrieve_and_log(saved) -print("\nDone — check the trace in Respan!") +try: + with example_attributes(EXAMPLE): + saved = submit() + requests = batch_requests() + results = batch_results(requests) + respan.log_batch_results(requests, results, trace_id=saved["trace_id"]) + print_result(EXAMPLE, f"batch_id={saved['batch_id']} logged={len(results)}") +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/decorators.py b/python/tracing/openai-sdk/decorators.py index e84eaab..602371b 100644 --- a/python/tracing/openai-sdk/decorators.py +++ b/python/tracing/openai-sdk/decorators.py @@ -1,56 +1,47 @@ -"""Decorators — Use @workflow and @task to structure traces.""" +"""A two-step content workflow around traced OpenAI Chat calls.""" -import os -from dotenv import load_dotenv +from respan import task, workflow -load_dotenv(override=True) - -from openai import OpenAI -from respan import Respan, workflow, task -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) +EXAMPLE = "decorators" +respan = make_respan(EXAMPLE) + @task(name="generate_outline") def generate_outline(topic: str) -> str: response = client.chat.completions.create( - model="gpt-4.1-nano", - messages=[ - {"role": "system", "content": "Generate a 3-point outline. Be concise."}, - {"role": "user", "content": topic}, - ], - + model=model_name(), messages=[{"role": "user", "content": topic}] ) - return response.choices[0].message.content + return response.choices[0].message.content or "" @task(name="write_draft") def write_draft(outline: str) -> str: response = client.chat.completions.create( - model="gpt-4.1-nano", - messages=[ - {"role": "system", "content": "Write a short paragraph from this outline."}, - {"role": "user", "content": outline}, - ], - + model=model_name(), messages=[{"role": "user", "content": outline}] ) - return response.choices[0].message.content + return response.choices[0].message.content or "" -@workflow(name="content_pipeline") +@workflow(name="openai_content_pipeline") def run(topic: str) -> str: - outline = generate_outline(topic) - print(f"Outline:\n{outline}\n") - - draft = write_draft(outline) - print(f"Draft:\n{draft}") - return draft - - -run("Benefits of open-source software") + return write_draft(generate_outline(topic)) + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run("Benefits of open-source software")) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/hello_world.py b/python/tracing/openai-sdk/hello_world.py index bd2a8e0..e352370 100644 --- a/python/tracing/openai-sdk/hello_world.py +++ b/python/tracing/openai-sdk/hello_world.py @@ -1,24 +1,48 @@ -"""Hello World — Simplest possible: one OpenAI call, auto-traced.""" +"""Chat, embedding, and precise provider-error tracing with the OpenAI SDK.""" -import os -from dotenv import load_dotenv +from openai import AuthenticationError +from respan import workflow -load_dotenv(override=True) +from _shared import ( + FAILURE_SENTINEL, + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, +) -from openai import OpenAI -from respan import Respan -from respan_instrumentation_openai import OpenAIInstrumentor +EXAMPLE = "hello-world" +respan = make_respan(EXAMPLE) -respan = Respan(instrumentations=[OpenAIInstrumentor()]) -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), -) +@workflow(name="openai_hello_world") +def run() -> str: + response = client.chat.completions.create( + model=model_name(), + messages=[{"role": "user", "content": "Say hello in three languages."}], + ) + embedding = client.embeddings.create( + model="text-embedding-3-small", input="observable OpenAI request" + ) + try: + client.chat.completions.create( + model=model_name(), + messages=[{"role": "user", "content": FAILURE_SENTINEL}], + ) + except AuthenticationError as exc: + if exc.status_code != 401: + raise + return f"{response.choices[0].message.content} embedding_dim={len(embedding.data[0].embedding)}" -response = client.chat.completions.create( - model="gpt-4.1-nano", - messages=[{"role": "user", "content": "Say hello in three languages."}], -) -print(response.choices[0].message.content) +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/multi_turn.py b/python/tracing/openai-sdk/multi_turn.py index ed876a8..1843978 100644 --- a/python/tracing/openai-sdk/multi_turn.py +++ b/python/tracing/openai-sdk/multi_turn.py @@ -1,44 +1,43 @@ -"""Multi-Turn — Conversational chat with message history, auto-traced.""" +"""Three-turn Chat history with each provider call auto-traced.""" -import os -from dotenv import load_dotenv +from respan import workflow -load_dotenv(override=True) - -from openai import OpenAI -from respan import Respan, workflow -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) +EXAMPLE = "multi-turn" +respan = make_respan(EXAMPLE) -@workflow(name="conversation") -def chat(): - messages = [ + +@workflow(name="openai_conversation") +def run() -> str: + messages: list[dict[str, str]] = [ {"role": "system", "content": "You are a concise cooking assistant."} ] - questions = [ + for question in ( "What can I make with eggs and cheese?", - "How long does the omelette take?", - "Any tips to make it fluffy?", - ] - - for question in questions: + "How long does it take?", + "How can I make it fluffy?", + ): messages.append({"role": "user", "content": question}) - response = client.chat.completions.create( - model="gpt-4.1-nano", - messages=messages, - - ) - answer = response.choices[0].message.content + response = client.chat.completions.create(model=model_name(), messages=messages) + answer = response.choices[0].message.content or "" messages.append({"role": "assistant", "content": answer}) - print(f"User: {question}") - print(f"Bot: {answer}\n") - - -chat() + return messages[-1]["content"] + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/prompt.py b/python/tracing/openai-sdk/prompt.py index 990c1cb..5a9fda5 100644 --- a/python/tracing/openai-sdk/prompt.py +++ b/python/tracing/openai-sdk/prompt.py @@ -1,35 +1,43 @@ -"""Prompt — Use a Respan-managed prompt template with variables.""" +"""A managed-prompt-shaped Chat request using a configurable prompt ID.""" import os -from dotenv import load_dotenv -load_dotenv(override=True) +from respan import workflow -from openai import OpenAI -from respan import Respan -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), -) - -PROMPT_ID = "d767498c1cbb4951bb122eef423b5f76" - -response = client.chat.completions.create( - model="placeholder", # model is defined in the prompt config - messages=[], # messages are defined in the prompt template - extra_body={ - "prompt": { - "prompt_id": PROMPT_ID, - "schema_version": 2, - "variables": { - "feature_request": "Add a real-time notification system for order status updates", - }, - } - }, +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + print_result, ) -print(response.choices[0].message.content) +EXAMPLE = "prompt" +respan = make_respan(EXAMPLE) + + +@workflow(name="openai_managed_prompt") +def run() -> str: + response = client.chat.completions.create( + model="placeholder", + messages=[], + extra_body={ + "prompt": { + "prompt_id": os.getenv("RESPAN_PROMPT_ID", "deterministic-prompt"), + "schema_version": 2, + "variables": {"feature_request": "Add order notifications"}, + } + }, + ) + return response.choices[0].message.content or "" + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/prompt_multi_turn.py b/python/tracing/openai-sdk/prompt_multi_turn.py index 2f608fe..15b3cc4 100644 --- a/python/tracing/openai-sdk/prompt_multi_turn.py +++ b/python/tracing/openai-sdk/prompt_multi_turn.py @@ -1,56 +1,52 @@ -"""Prompt Multi-Turn — Continue a conversation using a managed prompt.""" +"""Managed-prompt-shaped first turn followed by a normal Chat turn.""" import os -from dotenv import load_dotenv -load_dotenv(override=True) +from respan import workflow -from openai import OpenAI -from respan import Respan, workflow -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) -PROMPT_ID = "d767498c1cbb4951bb122eef423b5f76" +EXAMPLE = "prompt-multi-turn" +respan = make_respan(EXAMPLE) -@workflow(name="prompt_conversation") -def chat(): - # Turn 1: Initial request using the prompt template - response = client.chat.completions.create( +@workflow(name="openai_prompt_conversation") +def run() -> str: + first = client.chat.completions.create( model="placeholder", messages=[], extra_body={ "prompt": { - "prompt_id": PROMPT_ID, + "prompt_id": os.getenv("RESPAN_PROMPT_ID", "deterministic-prompt"), "schema_version": 2, - "variables": { - "feature_request": "Add dark mode support to the dashboard", - }, + "variables": {"feature_request": "Add dark mode"}, } }, ) - plan = response.choices[0].message.content - print(f"=== Initial Plan ===\n{plan}\n") - - # Turn 2: Follow-up without the prompt template (regular chat) - follow_up = client.chat.completions.create( - model="gpt-4.1-nano", + plan = first.choices[0].message.content or "" + second = client.chat.completions.create( + model=model_name(), messages=[ - {"role": "system", "content": "You are a Lead Product Engineer."}, {"role": "assistant", "content": plan}, - { - "role": "user", - "content": "Can you estimate the effort for each milestone in story points?", - }, + {"role": "user", "content": "Estimate effort."}, ], ) - print(f"=== Follow-up ===\n{follow_up.choices[0].message.content}") - - -chat() + return second.choices[0].message.content or "" + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/requirements.txt b/python/tracing/openai-sdk/requirements.txt new file mode 100644 index 0000000..9b26ad2 --- /dev/null +++ b/python/tracing/openai-sdk/requirements.txt @@ -0,0 +1,6 @@ +httpx2>=2.10.0,<3.0.0 +openai>=3.0.0,<4.0.0 +pydantic>=2.12.0,<3.0.0 +python-dotenv>=1.0.0,<2.0.0 +respan-ai>=4.1.0,<5.0.0 +respan-instrumentation-openai>=1.2.0,<2.0.0 diff --git a/python/tracing/openai-sdk/responses_hello_world.py b/python/tracing/openai-sdk/responses_hello_world.py index e0e2f3d..a16493e 100644 --- a/python/tracing/openai-sdk/responses_hello_world.py +++ b/python/tracing/openai-sdk/responses_hello_world.py @@ -1,24 +1,36 @@ -"""Responses API — Simplest possible: one call, auto-traced.""" +"""One successful Responses API call inside a workflow.""" -import os -from dotenv import load_dotenv +from respan import workflow -load_dotenv(override=True) +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, +) -from openai import OpenAI -from respan import Respan -from respan_instrumentation_openai import OpenAIInstrumentor +EXAMPLE = "responses-hello-world" +respan = make_respan(EXAMPLE) -respan = Respan(instrumentations=[OpenAIInstrumentor()]) -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), -) +@workflow(name="openai_responses_hello_world") +def run() -> str: + response = client.responses.create( + model=model_name(), + instructions="You are a helpful assistant.", + input="Say hello in three languages.", + ) + return response.output_text -response = client.responses.create( - model="gpt-4.1-nano", - instructions="You are a helpful assistant.", - input="Say hello in three languages.", -) -print(response.output_text) + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/responses_multi_turn.py b/python/tracing/openai-sdk/responses_multi_turn.py index d4ce8ad..4a2c50c 100644 --- a/python/tracing/openai-sdk/responses_multi_turn.py +++ b/python/tracing/openai-sdk/responses_multi_turn.py @@ -1,50 +1,46 @@ -"""Responses API Multi-Turn — Continue a conversation using previous_response_id.""" +"""Three Responses calls linked by previous_response_id.""" -import os -from dotenv import load_dotenv +from respan import workflow -load_dotenv(override=True) - -from openai import OpenAI -from respan import Respan, workflow -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) +EXAMPLE = "responses-multi-turn" +respan = make_respan(EXAMPLE) -@workflow(name="conversation") -def chat(): - # Turn 1 - r1 = client.responses.create( - model="gpt-4.1-nano", - instructions="You are a helpful assistant. Be concise.", - input="What is the capital of France?", - store=True, - ) - print(f"Turn 1: {r1.output_text}\n") - # Turn 2 — uses previous_response_id to chain context - r2 = client.responses.create( - model="gpt-4.1-nano", - input="And what is its population?", - previous_response_id=r1.id, +@workflow(name="openai_responses_conversation") +def run() -> str: + first = client.responses.create( + model=model_name(), input="Capital of France?", store=True + ) + second = client.responses.create( + model=model_name(), + input="Population?", + previous_response_id=first.id, store=True, ) - print(f"Turn 2: {r2.output_text}\n") - - # Turn 3 - r3 = client.responses.create( - model="gpt-4.1-nano", - input="Name three famous landmarks there.", - previous_response_id=r2.id, + third = client.responses.create( + model=model_name(), + input="Three landmarks?", + previous_response_id=second.id, store=True, ) - print(f"Turn 3: {r3.output_text}") - - -chat() + return third.output_text + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/responses_prompt.py b/python/tracing/openai-sdk/responses_prompt.py index 7431174..5aaf994 100644 --- a/python/tracing/openai-sdk/responses_prompt.py +++ b/python/tracing/openai-sdk/responses_prompt.py @@ -1,46 +1,46 @@ -"""Responses API Prompt — Use a Respan-managed prompt with the Responses API. - -With schema_version 2, the prompt template becomes `instructions` and the -body `input` is preserved as the user turn. -""" +"""A managed-prompt-shaped request sent through the Responses API.""" import os -from dotenv import load_dotenv - -load_dotenv(override=True) - -from openai import OpenAI -from respan import Respan -from respan_instrumentation_openai import OpenAIInstrumentor -respan = Respan(instrumentations=[OpenAIInstrumentor()]) +from respan import workflow -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) -PROMPT_ID = "d767498c1cbb4951bb122eef423b5f76" - -response = client.responses.create( - model="gpt-4.1-nano", - input=[ - { - "role": "user", - "content": "Add a real-time notification system for order status updates", - } - ], - extra_body={ - "respan_params": { - "prompt": { - "prompt_id": PROMPT_ID, - "schema_version": 2, - "variables": { - "feature_request": "Add a real-time notification system for order status updates", - }, +EXAMPLE = "responses-prompt" +respan = make_respan(EXAMPLE) + + +@workflow(name="openai_responses_prompt") +def run() -> str: + response = client.responses.create( + model=model_name(), + input="Add order-status notifications.", + extra_body={ + "respan_params": { + "prompt": { + "prompt_id": os.getenv("RESPAN_PROMPT_ID", "deterministic-prompt"), + "schema_version": 2, + "variables": {"feature_request": "Add order notifications"}, + } } - } - }, -) - -print(response.output_text) + }, + ) + return response.output_text + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/responses_streaming.py b/python/tracing/openai-sdk/responses_streaming.py index eeaa1bd..3a2567e 100644 --- a/python/tracing/openai-sdk/responses_streaming.py +++ b/python/tracing/openai-sdk/responses_streaming.py @@ -1,29 +1,39 @@ -"""Responses API Streaming — Stream a response, auto-traced.""" +"""Responses API streaming with explicit source close and final flush.""" -import os -from dotenv import load_dotenv +from respan import workflow -load_dotenv(override=True) +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, +) -from openai import OpenAI -from respan import Respan -from respan_instrumentation_openai import OpenAIInstrumentor +EXAMPLE = "responses-streaming" +respan = make_respan(EXAMPLE) -respan = Respan(instrumentations=[OpenAIInstrumentor()]) -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), -) +@workflow(name="openai_responses_streaming") +def run() -> str: + parts: list[str] = [] + stream = client.responses.create( + model=model_name(), input="Write a Python haiku.", stream=True + ) + with stream: + for event in stream: + if event.type == "response.output_text.delta": + parts.append(event.delta) + return "".join(parts) -stream = client.responses.create( - model="gpt-4.1-nano", - instructions="You are a helpful assistant.", - input="Write a haiku about Python.", - stream=True, -) -for event in stream: - if event.type == "response.output_text.delta": - print(event.delta, end="", flush=True) -print() +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/responses_structured_output.py b/python/tracing/openai-sdk/responses_structured_output.py index b329d25..a5c87bb 100644 --- a/python/tracing/openai-sdk/responses_structured_output.py +++ b/python/tracing/openai-sdk/responses_structured_output.py @@ -1,22 +1,20 @@ -"""Responses API Structured Output — Pydantic model parsing, auto-traced.""" - -import os -from dotenv import load_dotenv - -load_dotenv(override=True) +"""Responses.parse structured output on the real OpenAI 3.x parse path.""" from pydantic import BaseModel -from openai import OpenAI -from respan import Respan, workflow -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from respan import workflow + +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) +EXAMPLE = "responses-structured-output" +respan = make_respan(EXAMPLE) + class MovieReview(BaseModel): title: str @@ -26,19 +24,21 @@ class MovieReview(BaseModel): cons: list[str] -@workflow(name="movie_review") -def review(movie: str) -> MovieReview: +@workflow(name="openai_responses_movie_review") +def run(movie: str) -> MovieReview: response = client.responses.parse( - model="gpt-4.1-nano", - instructions="You are a film critic. Rate movies 1-10.", - input=f"Review: {movie}", - text_format=MovieReview, + model=model_name(), input=f"Review: {movie}", text_format=MovieReview ) return response.output_parsed -result = review("The Matrix") -print(f"{result.title} — {result.rating}/10") -print(f"Summary: {result.summary}") -print(f"Pros: {', '.join(result.pros)}") -print(f"Cons: {', '.join(result.cons)}") +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + result = run("The Matrix") + print_result(EXAMPLE, f"{result.title} rating={result.rating}") + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/responses_tool_calls.py b/python/tracing/openai-sdk/responses_tool_calls.py index b1282bb..4eb06fb 100644 --- a/python/tracing/openai-sdk/responses_tool_calls.py +++ b/python/tracing/openai-sdk/responses_tool_calls.py @@ -1,23 +1,21 @@ -"""Responses API Tool Calls — Function calling with the Responses API, auto-traced.""" +"""Two-turn Responses function call with a canonical tool execution span.""" -import os import json -from dotenv import load_dotenv -load_dotenv(override=True) +from respan import tool, workflow -from openai import OpenAI -from respan import Respan, workflow, task -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) -tools = [ +EXAMPLE = "responses-tool-calls" +respan = make_respan(EXAMPLE) +TOOLS = [ { "type": "function", "name": "get_weather", @@ -33,49 +31,38 @@ ] -@task(name="get_weather") -def get_weather(city: str) -> str: - return f"Sunny, 72°F in {city}" - +@tool(name="get_weather") +def get_weather(city: str) -> dict[str, str]: + return {"city": city, "weather": "sunny", "temperature_c": "22"} -@workflow(name="weather_assistant") -def run(question: str): - response = client.responses.create( - model="gpt-4.1-nano", - instructions="You are a weather assistant.", - input=[{"role": "user", "content": question}], - tools=tools, - ) - # Check for function calls in output - tool_call = next( - (item for item in response.output if item.type == "function_call"), - None, +@workflow(name="openai_responses_weather_assistant") +def run(question: str) -> str: + first = client.responses.create(model=model_name(), input=question, tools=TOOLS) + call = next(item for item in first.output if item.type == "function_call") + result = get_weather(**json.loads(call.arguments)) + final = client.responses.create( + model=model_name(), + input=[ + {"role": "user", "content": question}, + *first.output, + { + "type": "function_call_output", + "call_id": call.call_id, + "output": json.dumps(result), + }, + ], + tools=TOOLS, ) - - if tool_call: - args = json.loads(tool_call.arguments) - result = get_weather(**args) - print(f"Tool: {tool_call.name}({args}) -> {result}") - - # Send tool result back - final = client.responses.create( - model="gpt-4.1-nano", - instructions="You are a weather assistant.", - input=[ - {"role": "user", "content": question}, - *response.output, - { - "type": "function_call_output", - "call_id": tool_call.call_id, - "output": result, - }, - ], - tools=tools, - ) - print(f"Answer: {final.output_text}") - else: - print(f"Answer: {response.output_text}") + return final.output_text -run("What's the weather in Paris?") +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run("Weather in Paris?")) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/run_all.py b/python/tracing/openai-sdk/run_all.py new file mode 100644 index 0000000..9b90b8c --- /dev/null +++ b/python/tracing/openai-sdk/run_all.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import os +import subprocess +import sys +from datetime import UTC, datetime +from pathlib import Path + +EXAMPLE_DIR = Path(__file__).resolve().parent +EXAMPLES = [ + "hello_world.py", + "async_parallel.py", + "attributes.py", + "batch.py", + "batch_async.py", + "decorators.py", + "multi_turn.py", + "prompt.py", + "prompt_multi_turn.py", + "responses_hello_world.py", + "responses_multi_turn.py", + "responses_prompt.py", + "responses_streaming.py", + "responses_structured_output.py", + "responses_tool_calls.py", + "streaming.py", + "structured_output.py", + "tool_calls.py", +] + + +def marker() -> str: + configured = os.getenv("RESPAN_EXAMPLE_RUN_ID") + if configured: + return configured + return f"openai-sdk-{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}" + + +def main() -> int: + shared_marker = marker() + environment = os.environ.copy() + environment["RESPAN_EXAMPLE_RUN_ID"] = shared_marker + environment["PYTHONDONTWRITEBYTECODE"] = "1" + failures: list[str] = [] + + print(f"RESPAN_EXAMPLE_RUN_ID={shared_marker}", flush=True) + for index, script_name in enumerate(EXAMPLES, start=1): + print(f"[{index:02d}/{len(EXAMPLES):02d}] {script_name}", flush=True) + try: + result = subprocess.run( + [sys.executable, str(EXAMPLE_DIR / script_name)], + cwd=EXAMPLE_DIR, + env=environment, + check=False, + timeout=120, + ) + except subprocess.TimeoutExpired: + failures.append(f"{script_name}:timeout") + continue + if result.returncode: + failures.append(f"{script_name}:{result.returncode}") + + print(f"completed={len(EXAMPLES) - len(failures)}/{len(EXAMPLES)}", flush=True) + if failures: + print(f"failures={','.join(failures)}", flush=True) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/python/tracing/openai-sdk/streaming.py b/python/tracing/openai-sdk/streaming.py index 940d8cb..fba1354 100644 --- a/python/tracing/openai-sdk/streaming.py +++ b/python/tracing/openai-sdk/streaming.py @@ -1,30 +1,42 @@ -"""Streaming — Stream a chat completion, auto-traced.""" +"""Chat Completions streaming with explicit close and exact usage.""" -import os -from dotenv import load_dotenv - -load_dotenv(override=True) - -from openai import OpenAI -from respan import Respan -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), -) - -stream = client.chat.completions.create( - model="gpt-4.1-nano", - messages=[{"role": "user", "content": "Write a haiku about Python."}], - stream=True, +from respan import workflow +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) -for chunk in stream: - content = chunk.choices[0].delta.content - if content: - print(content, end="", flush=True) -print() +EXAMPLE = "streaming" +respan = make_respan(EXAMPLE) + + +@workflow(name="openai_chat_streaming") +def run() -> str: + parts: list[str] = [] + stream = client.chat.completions.create( + model=model_name(), + messages=[{"role": "user", "content": "Write a Python haiku."}], + stream=True, + stream_options={"include_usage": True}, + ) + with stream: + for chunk in stream: + if chunk.choices and chunk.choices[0].delta.content: + parts.append(chunk.choices[0].delta.content) + return "".join(parts) + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run()) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/structured_output.py b/python/tracing/openai-sdk/structured_output.py index 49c2977..5152569 100644 --- a/python/tracing/openai-sdk/structured_output.py +++ b/python/tracing/openai-sdk/structured_output.py @@ -1,22 +1,20 @@ -"""Structured Output — JSON mode with Pydantic, auto-traced.""" - -import os -from dotenv import load_dotenv - -load_dotenv(override=True) +"""Chat Completions.parse structured output on OpenAI 3.x.""" from pydantic import BaseModel -from openai import OpenAI -from respan import Respan, workflow -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from respan import workflow + +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) +EXAMPLE = "structured-output" +respan = make_respan(EXAMPLE) + class MovieReview(BaseModel): title: str @@ -26,25 +24,23 @@ class MovieReview(BaseModel): cons: list[str] -@workflow(name="movie_review") -def review(movie: str) -> MovieReview: +@workflow(name="openai_chat_movie_review") +def run(movie: str) -> MovieReview: response = client.beta.chat.completions.parse( - model="gpt-4.1-nano", - messages=[ - { - "role": "system", - "content": "You are a film critic. Rate movies 1-10.", - }, - {"role": "user", "content": f"Review: {movie}"}, - ], + model=model_name(), + messages=[{"role": "user", "content": f"Review: {movie}"}], response_format=MovieReview, - ) return response.choices[0].message.parsed -result = review("The Matrix") -print(f"{result.title} — {result.rating}/10") -print(f"Summary: {result.summary}") -print(f"Pros: {', '.join(result.pros)}") -print(f"Cons: {', '.join(result.cons)}") +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + result = run("The Matrix") + print_result(EXAMPLE, f"{result.title} rating={result.rating}") + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openai-sdk/test_example_contract.py b/python/tracing/openai-sdk/test_example_contract.py new file mode 100644 index 0000000..ad6677d --- /dev/null +++ b/python/tracing/openai-sdk/test_example_contract.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import ast +import importlib.util +import subprocess +from pathlib import Path +from types import ModuleType + +EXAMPLE_DIR = Path(__file__).resolve().parent +PUBLIC_EXAMPLES = { + path.name + for path in EXAMPLE_DIR.glob("*.py") + if not path.name.startswith("_") + and path.name not in {"run_all.py", "test_example_contract.py"} +} +CLIENT_EXAMPLES = { + path.name + for path in EXAMPLE_DIR.glob("*.py") + if path.name in PUBLIC_EXAMPLES + and ( + "make_sync_client" in path.read_text() + or "make_async_client" in path.read_text() + ) +} + + +def _load_runner() -> ModuleType: + spec = importlib.util.spec_from_file_location( + "openai_example_runner", EXAMPLE_DIR / "run_all.py" + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_run_all_lists_every_public_example_once() -> None: + module = ast.parse((EXAMPLE_DIR / "run_all.py").read_text()) + assignment = next( + node + for node in module.body + if isinstance(node, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "EXAMPLES" + for target in node.targets + ) + ) + listed = ast.literal_eval(assignment.value) + assert len(listed) == 18 + assert len(set(listed)) == len(listed) + assert set(listed) == PUBLIC_EXAMPLES + + +def test_shared_loader_preserves_shell_marker() -> None: + source = (EXAMPLE_DIR / "_shared.py").read_text() + assert "override=False" in source + assert 'os.getenv("RESPAN_EXAMPLE_RUN_ID")' in source + assert '"example_run_id": marker' in source + + +def test_every_example_has_explicit_shutdown() -> None: + for script_name in PUBLIC_EXAMPLES: + source = (EXAMPLE_DIR / script_name).read_text() + assert "finish_respan(respan)" in source, script_name + + +def _contains_call(nodes: list[ast.stmt], name: str) -> bool: + for node in nodes: + for child in ast.walk(node): + if not isinstance(child, ast.Call): + continue + if isinstance(child.func, ast.Name) and child.func.id == name: + return True + if isinstance(child.func, ast.Attribute) and child.func.attr == name: + return True + return False + + +def test_client_examples_always_close_before_respan_shutdown() -> None: + for script_name in CLIENT_EXAMPLES: + module = ast.parse((EXAMPLE_DIR / script_name).read_text()) + scope = module.body + if script_name == "async_parallel.py": + main = next( + node + for node in module.body + if isinstance(node, ast.AsyncFunctionDef) and node.name == "main" + ) + scope = main.body + + outer = next(node for node in scope if isinstance(node, ast.Try)) + client_assignment = next( + node + for node in outer.body + if isinstance(node, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "client" + for target in node.targets + ) + ) + assert isinstance(client_assignment.value, ast.Call), script_name + assert isinstance(client_assignment.value.func, ast.Name), script_name + assert client_assignment.value.func.id in { + "make_sync_client", + "make_async_client", + }, script_name + + nested = next(node for node in outer.body if isinstance(node, ast.Try)) + assert _contains_call(nested.finalbody, "close"), script_name + assert _contains_call(outer.finalbody, "finish_respan"), script_name + + +def test_run_all_records_timeout_and_continues(monkeypatch, capsys) -> None: + runner = _load_runner() + runner.EXAMPLES = ["first.py", "second.py"] + calls: list[str] = [] + + def fake_run(command, **kwargs): + del kwargs + script = Path(command[-1]).name + calls.append(script) + if script == "first.py": + raise subprocess.TimeoutExpired(command, 120) + return subprocess.CompletedProcess(command, 0) + + monkeypatch.setattr(runner.subprocess, "run", fake_run) + assert runner.main() == 1 + assert calls == ["first.py", "second.py"] + output = capsys.readouterr().out + assert "completed=1/2" in output + assert "failures=first.py:timeout" in output + + +def test_run_all_disables_child_bytecode_caches(monkeypatch) -> None: + runner = _load_runner() + runner.EXAMPLES = ["example.py"] + captured_environment: dict[str, str] = {} + + def fake_run(command, **kwargs): + del command + captured_environment.update(kwargs["env"]) + return subprocess.CompletedProcess([], 0) + + monkeypatch.setattr(runner.subprocess, "run", fake_run) + assert runner.main() == 0 + assert captured_environment["PYTHONDONTWRITEBYTECODE"] == "1" diff --git a/python/tracing/openai-sdk/tool_calls.py b/python/tracing/openai-sdk/tool_calls.py index 8f4f695..1945c16 100644 --- a/python/tracing/openai-sdk/tool_calls.py +++ b/python/tracing/openai-sdk/tool_calls.py @@ -1,23 +1,21 @@ -"""Tool Calls — Function calling with OpenAI, auto-traced.""" +"""Two-turn Chat function call with a canonical tool execution span.""" -import os import json -from dotenv import load_dotenv -load_dotenv(override=True) +from respan import tool, workflow -from openai import OpenAI -from respan import Respan, workflow, task -from respan_instrumentation_openai import OpenAIInstrumentor - -respan = Respan(instrumentations=[OpenAIInstrumentor()]) - -client = OpenAI( - api_key=os.getenv("RESPAN_API_KEY"), - base_url=os.getenv("RESPAN_BASE_URL", "https://api.respan.ai/api"), +from _shared import ( + example_attributes, + finish_respan, + make_respan, + make_sync_client, + model_name, + print_result, ) -tools = [ +EXAMPLE = "tool-calls" +respan = make_respan(EXAMPLE) +TOOLS = [ { "type": "function", "function": { @@ -33,42 +31,40 @@ ] -@task(name="get_weather") -def get_weather(city: str) -> str: - return f"Sunny, 72F in {city}" - +@tool(name="get_weather") +def get_weather(city: str) -> dict[str, str]: + return {"city": city, "weather": "sunny", "temperature_c": "22"} -@workflow(name="weather_assistant") -def run(question: str): - messages = [{"role": "user", "content": question}] - - response = client.chat.completions.create( - model="gpt-4.1-nano", - messages=messages, - tools=tools, +@workflow(name="openai_chat_weather_assistant") +def run(question: str) -> str: + messages: list = [{"role": "user", "content": question}] + first = client.chat.completions.create( + model=model_name(), messages=messages, tools=TOOLS ) - message = response.choices[0].message - - if message.tool_calls: - messages.append(message) - for tc in message.tool_calls: - args = json.loads(tc.function.arguments) - result = get_weather(**args) - print(f"Tool: {tc.function.name}({args}) -> {result}") - messages.append( - {"role": "tool", "tool_call_id": tc.id, "content": result} - ) - - final = client.chat.completions.create( - model="gpt-4.1-nano", - messages=messages, - tools=tools, - + message = first.choices[0].message + messages.append(message) + for call in message.tool_calls or []: + result = get_weather(**json.loads(call.function.arguments)) + messages.append( + { + "role": "tool", + "tool_call_id": call.id, + "content": json.dumps(result), + } ) - print(f"Answer: {final.choices[0].message.content}") - else: - print(f"Answer: {message.content}") - - -run("What's the weather in Paris?") + final = client.chat.completions.create( + model=model_name(), messages=messages, tools=TOOLS + ) + return final.choices[0].message.content or "" + + +try: + client = make_sync_client() + try: + with example_attributes(EXAMPLE): + print_result(EXAMPLE, run("Weather in Paris?")) + finally: + client.close() +finally: + finish_respan(respan) diff --git a/python/tracing/openinference/01_chat_provider.py b/python/tracing/openinference/01_chat_provider.py new file mode 100644 index 0000000..1ad9d1f --- /dev/null +++ b/python/tracing/openinference/01_chat_provider.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import json + +from _shared import ( + example_attributes, + example_run_id, + finish_respan, + make_respan, + print_result, + tracer, + workflow_name, +) +from respan import workflow + +EXAMPLE_NAME = "chat-provider" + + +@workflow(name=workflow_name(EXAMPLE_NAME)) +def chat_provider_workflow(prompt: str) -> str: + response = "OpenInference attributes are translated into one canonical contract." + with tracer().start_as_current_span("openai.chat") as span: + span.set_attribute("openinference.span.kind", "LLM") + span.set_attribute("llm.model_name", "gpt-4.1-mini") + span.set_attribute("llm.system", "OpenAI") + span.set_attribute("llm.provider", "OpenAI") + span.set_attribute("llm.invocation_parameters", '{"temperature":0.1}') + span.set_attribute("llm.input_messages.0.message.role", "user") + span.set_attribute("llm.input_messages.0.message.content", prompt) + span.set_attribute("llm.output_messages.0.message.role", "assistant") + span.set_attribute("llm.output_messages.0.message.content", response) + span.set_attribute("llm.output_messages.0.message.finish_reason", "stop") + span.set_attribute("llm.token_count.prompt", 11) + span.set_attribute("llm.token_count.completion", 9) + span.set_attribute("llm.token_count.total", 20) + span.set_attribute("input.value", json.dumps({"prompt": prompt})) + span.set_attribute("output.value", json.dumps({"response": response})) + return response + + +def run() -> None: + run_id = example_run_id() + respan = make_respan(EXAMPLE_NAME, run_id) + result = "" + try: + with example_attributes(EXAMPLE_NAME, run_id): + result = chat_provider_workflow( + "Explain the OpenInference translation boundary in one sentence." + ) + finally: + finish_respan(respan) + print_result(EXAMPLE_NAME, run_id, result) + + +if __name__ == "__main__": + run() diff --git a/python/tracing/openinference/02_tool_call.py b/python/tracing/openinference/02_tool_call.py new file mode 100644 index 0000000..0ea5519 --- /dev/null +++ b/python/tracing/openinference/02_tool_call.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import json + +from _shared import ( + example_attributes, + example_run_id, + finish_respan, + make_respan, + print_result, + tracer, + workflow_name, +) +from respan import workflow + +EXAMPLE_NAME = "tool-call" +TOOL_DEFINITION = { + "type": "function", + "function": { + "name": "lookup_weather", + "description": "Return deterministic weather for a city.", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + }, +} + + +@workflow(name=workflow_name(EXAMPLE_NAME)) +def tool_call_workflow(city: str) -> str: + call_id = "call-weather-001" + arguments = json.dumps({"city": city}) + result = {"city": city, "temperature_c": 22, "condition": "sunny"} + + with tracer().start_as_current_span("openai.chat.tool_call") as chat_span: + chat_span.set_attribute("openinference.span.kind", "LLM") + chat_span.set_attribute("llm.model_name", "gpt-4.1-mini") + chat_span.set_attribute("llm.system", "openai") + chat_span.set_attribute("llm.provider", "openai") + chat_span.set_attribute("llm.tools", json.dumps([TOOL_DEFINITION])) + chat_span.set_attribute("llm.input_messages.0.message.role", "user") + chat_span.set_attribute( + "llm.input_messages.0.message.content", + f"What is the weather in {city}?", + ) + chat_span.set_attribute("llm.output_messages.0.message.role", "assistant") + chat_span.set_attribute( + "llm.output_messages.0.message.tool_calls.0.tool_call.id", + call_id, + ) + chat_span.set_attribute( + "llm.output_messages.0.message.tool_calls.0.tool_call.function.name", + "lookup_weather", + ) + chat_span.set_attribute( + "llm.output_messages.0.message.tool_calls.0.tool_call.function.arguments", + arguments, + ) + chat_span.set_attribute("llm.token_count.prompt", 18) + chat_span.set_attribute("llm.token_count.completion", 6) + chat_span.set_attribute("llm.token_count.total", 24) + + with tracer().start_as_current_span("lookup_weather") as tool_span: + tool_span.set_attribute("openinference.span.kind", "TOOL") + tool_span.set_attribute("tool.name", "lookup_weather") + tool_span.set_attribute("input.value", arguments) + tool_span.set_attribute("output.value", json.dumps(result)) + return json.dumps(result, sort_keys=True) + + +def run() -> None: + run_id = example_run_id() + respan = make_respan(EXAMPLE_NAME, run_id) + result = "" + try: + with example_attributes(EXAMPLE_NAME, run_id): + result = tool_call_workflow("Tokyo") + finally: + finish_respan(respan) + print_result(EXAMPLE_NAME, run_id, result) + + +if __name__ == "__main__": + run() diff --git a/python/tracing/openinference/03_embedding.py b/python/tracing/openinference/03_embedding.py new file mode 100644 index 0000000..64c6ceb --- /dev/null +++ b/python/tracing/openinference/03_embedding.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from _shared import ( + example_attributes, + example_run_id, + finish_respan, + make_respan, + print_result, + tracer, + workflow_name, +) +from respan import workflow + +EXAMPLE_NAME = "embedding" + + +@workflow(name=workflow_name(EXAMPLE_NAME)) +def embedding_workflow(text: str) -> str: + vector = (0.125, -0.25, 0.5, 0.75) + with tracer().start_as_current_span("openai.embedding") as span: + span.set_attribute("openinference.span.kind", "EMBEDDING") + span.set_attribute("embedding.model_name", "text-embedding-3-small") + span.set_attribute("embedding.embeddings.0.embedding.text", text) + span.set_attribute("embedding.embeddings.0.embedding.vector", vector) + span.set_attribute("llm.system", "openai") + span.set_attribute("llm.provider", "openai") + span.set_attribute("llm.token_count.prompt", 5) + span.set_attribute("llm.token_count.total", 5) + return f"captured deterministic vector with {len(vector)} dimensions" + + +def run() -> None: + run_id = example_run_id() + respan = make_respan(EXAMPLE_NAME, run_id) + result = "" + try: + with example_attributes(EXAMPLE_NAME, run_id): + result = embedding_workflow("OpenInference embedding contract") + finally: + finish_respan(respan) + print_result(EXAMPLE_NAME, run_id, result) + + +if __name__ == "__main__": + run() diff --git a/python/tracing/openinference/04_expected_failure.py b/python/tracing/openinference/04_expected_failure.py new file mode 100644 index 0000000..8998b86 --- /dev/null +++ b/python/tracing/openinference/04_expected_failure.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import json + +from _shared import ( + example_attributes, + example_run_id, + finish_respan, + make_respan, + print_result, + tracer, + workflow_name, +) +from opentelemetry.trace import Status, StatusCode +from respan import workflow + +EXAMPLE_NAME = "expected-failure" +ERROR_MESSAGE = "openinference deterministic provider failure" + + +@workflow(name=workflow_name(EXAMPLE_NAME)) +def expected_failure_workflow(operation: str) -> None: + error = RuntimeError(ERROR_MESSAGE) + with tracer().start_as_current_span("openai.chat.failure") as span: + span.set_attribute("openinference.span.kind", "LLM") + span.set_attribute("llm.model_name", "gpt-4.1-mini") + span.set_attribute("llm.system", "openai") + span.set_attribute("llm.provider", "openai") + span.set_attribute("input.value", json.dumps({"operation": operation})) + span.record_exception(error) + span.set_status(Status(StatusCode.ERROR, ERROR_MESSAGE)) + raise error + + +def run() -> None: + run_id = example_run_id() + respan = make_respan(EXAMPLE_NAME, run_id) + observed = "" + try: + with example_attributes(EXAMPLE_NAME, run_id): + expected_failure_workflow("validate provider error translation") + except RuntimeError as error: + if str(error) != ERROR_MESSAGE: + raise + observed = f"observed expected error: {error}" + finally: + finish_respan(respan) + print_result(EXAMPLE_NAME, run_id, observed) + + +if __name__ == "__main__": + run() diff --git a/python/tracing/openinference/05_streaming_privacy.py b/python/tracing/openinference/05_streaming_privacy.py new file mode 100644 index 0000000..d330a4f --- /dev/null +++ b/python/tracing/openinference/05_streaming_privacy.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import json + +from _shared import ( + example_attributes, + example_run_id, + finish_respan, + make_respan, + print_result, + tracer, + workflow_name, +) +from openinference.semconv.trace import SpanAttributes as OISpanAttributes +from respan import workflow + +EXAMPLE_NAME = "streaming-privacy" + + +@workflow(name=workflow_name(EXAMPLE_NAME)) +def streaming_privacy_workflow(description: str) -> str: + chunks = ("bounded ", "streaming ", "content") + response = "".join(chunks) + with tracer().start_as_current_span("openai.chat.stream") as span: + span.set_attribute("openinference.span.kind", "LLM") + span.set_attribute( + OISpanAttributes.LLM_INVOCATION_PARAMETERS, + json.dumps({"stream": True}), + ) + span.set_attribute("llm.model_name", "gpt-4.1-mini") + span.set_attribute("llm.system", "openai") + span.set_attribute("llm.provider", "openai") + span.set_attribute( + "input.value", + json.dumps( + { + "description": description, + "authorization": "Bearer example-secret-must-be-redacted", + "nested": {"api_key": "sk-example-secret-123456"}, + } + ), + ) + span.set_attribute("output.value", json.dumps({"response": response})) + span.set_attribute("llm.output_messages.0.message.role", "assistant") + span.set_attribute("llm.output_messages.0.message.content", response) + span.set_attribute("llm.token_count.prompt", 7) + span.set_attribute("llm.token_count.completion", 3) + span.set_attribute("llm.token_count.total", 10) + return response + + +def run() -> None: + run_id = example_run_id() + respan = make_respan(EXAMPLE_NAME, run_id) + result = "" + try: + with example_attributes(EXAMPLE_NAME, run_id): + result = streaming_privacy_workflow( + "validate a streaming span without exporting credentials" + ) + finally: + finish_respan(respan) + print_result(EXAMPLE_NAME, run_id, result) + + +if __name__ == "__main__": + run() diff --git a/python/tracing/openinference/README.md b/python/tracing/openinference/README.md new file mode 100644 index 0000000..1b9654f --- /dev/null +++ b/python/tracing/openinference/README.md @@ -0,0 +1,48 @@ +# OpenInference Contract Examples + +These deterministic examples validate the generic +`respan-instrumentation-openinference` translation boundary without depending +on a vendor API. They emit standard OpenInference attributes through the real +OpenTelemetry provider and Respan exporter, covering: + +- chat content, provider identity, model, and provider-reported usage; +- tool definitions, current-turn tool calls, and a connected tool execution; +- embedding input, model, usage, and vector output; +- an expected OTel `ERROR` with no synthetic usage; +- streaming metadata plus bounded, redacted input/output content. + +Each script creates one named workflow, inherits the exact +`RESPAN_EXAMPLE_RUN_ID` marker when supplied, loads credentials from the +repository `.env` without overriding the shell, and explicitly flushes and +shuts down Respan. + +## Install from the registry + +```bash +cd python/tracing/openinference +python -m venv .venv +source .venv/bin/activate +python -m pip install -r requirements.txt +``` + +## Validate a local `respan` checkout + +Keep `requirements.txt` registry-portable, then overlay editable local packages: + +```bash +python -m pip install -e ../../../../respan/python-sdks/respan-sdk +python -m pip install -e ../../../../respan/python-sdks/respan-tracing +python -m pip install -e ../../../../respan/python-sdks/respan +python -m pip install -e ../../../../respan/python-sdks/instrumentations/respan-instrumentation-openinference +``` + +## Run + +```bash +RESPAN_EXAMPLE_RUN_ID=otel2-openinference-check python run_all.py +``` + +The runner executes all five scripts, prints each exit code, and returns nonzero +if any process fails. Expected output is five traces containing 11 records: one +workflow root per script, five translated OpenInference operations, and one tool +execution child. diff --git a/python/tracing/openinference/_shared.py b/python/tracing/openinference/_shared.py new file mode 100644 index 0000000..3fa9638 --- /dev/null +++ b/python/tracing/openinference/_shared.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import os +from collections.abc import Iterator +from contextlib import contextmanager +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from dotenv import load_dotenv +from opentelemetry import trace +from opentelemetry.sdk.trace import SpanProcessor +from respan import Respan, propagate_attributes +from respan_instrumentation_openinference import OpenInferenceInstrumentor + +PROJECT_ROOT = Path(__file__).resolve().parents[3] +DEFAULT_RESPAN_BASE_URL = "https://api.respan.ai/api" +INTEGRATION = "openinference" + + +class ContractSourceProcessor(SpanProcessor): + """No-op source processor used to activate the generic OI translator.""" + + def on_start(self, span: Any, parent_context: Any = None) -> None: + del span, parent_context + + def on_end(self, span: Any) -> None: + del span + + def shutdown(self) -> None: + pass + + def force_flush(self, timeout_millis: int = 30_000) -> bool: + del timeout_millis + return True + + +def load_root_env() -> None: + # An exact shell marker must win over any value in the repository .env. + load_dotenv(PROJECT_ROOT / ".env", override=False) + + +def require_respan_api_key() -> str: + load_root_env() + api_key = os.getenv("RESPAN_API_KEY") + if not api_key: + raise RuntimeError("RESPAN_API_KEY must be set in the repo-root .env") + return api_key + + +def example_run_id() -> str: + load_root_env() + inherited = os.getenv("RESPAN_EXAMPLE_RUN_ID") + if inherited: + return inherited + return f"openinference-{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}" + + +def make_respan(example_name: str, run_id: str) -> Respan: + current_workflow = workflow_name(example_name) + return Respan( + api_key=require_respan_api_key(), + base_url=os.getenv("RESPAN_BASE_URL", DEFAULT_RESPAN_BASE_URL).rstrip("/"), + app_name="openinference-contract-examples", + environment=os.getenv("RESPAN_ENVIRONMENT", "example"), + metadata={ + "example_run_id": run_id, + "integration": INTEGRATION, + "example": example_name, + "workflow_name": current_workflow, + }, + instrumentations=[OpenInferenceInstrumentor(ContractSourceProcessor)], + ) + + +def workflow_name(example_name: str) -> str: + return f"openinference_{example_name.replace('-', '_')}" + + +@contextmanager +def example_attributes(example_name: str, run_id: str) -> Iterator[None]: + current_workflow = workflow_name(example_name) + with propagate_attributes( + custom_identifier=f"{INTEGRATION}-{example_name}-{run_id}", + trace_group_identifier=current_workflow, + metadata={ + "example_run_id": run_id, + "integration": INTEGRATION, + "example": example_name, + "workflow_name": current_workflow, + }, + ): + yield + + +def tracer(): + return trace.get_tracer("openinference.contract.examples") + + +def finish_respan(respan: Respan) -> None: + try: + respan.flush() + finally: + respan.shutdown() + + +def print_result(example_name: str, run_id: str, result: str) -> None: + print(f"example={example_name}") + print(f"example_run_id={run_id}") + print(f"workflow_name={workflow_name(example_name)}") + print(result) diff --git a/python/tracing/openinference/requirements.txt b/python/tracing/openinference/requirements.txt new file mode 100644 index 0000000..5e0c7a6 --- /dev/null +++ b/python/tracing/openinference/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv>=1.0.0,<2.0.0 +respan-ai>=4.1.0 +respan-instrumentation-openinference>=1.2.2 diff --git a/python/tracing/openinference/run_all.py b/python/tracing/openinference/run_all.py new file mode 100644 index 0000000..6b4ccf4 --- /dev/null +++ b/python/tracing/openinference/run_all.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import os +import subprocess +import sys +from datetime import UTC, datetime +from pathlib import Path + +EXAMPLE_SCRIPTS = ( + "01_chat_provider.py", + "02_tool_call.py", + "03_embedding.py", + "04_expected_failure.py", + "05_streaming_privacy.py", +) +EXAMPLE_TIMEOUT_SECONDS = 120 + + +def resolved_run_id() -> str: + return os.getenv("RESPAN_EXAMPLE_RUN_ID") or ( + f"openinference-{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}" + ) + + +def main() -> int: + directory = Path(__file__).resolve().parent + environment = os.environ.copy() + environment["RESPAN_EXAMPLE_RUN_ID"] = resolved_run_id() + failures: list[tuple[str, int | str]] = [] + + print(f"example_run_id={environment['RESPAN_EXAMPLE_RUN_ID']}", flush=True) + for script in EXAMPLE_SCRIPTS: + print(f"running={script}", flush=True) + try: + result = subprocess.run( + [sys.executable, str(directory / script)], + cwd=directory, + env=environment, + check=False, + timeout=EXAMPLE_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired: + print( + f"finished={script} timeout={EXAMPLE_TIMEOUT_SECONDS}s", + flush=True, + ) + failures.append((script, "timeout")) + continue + print(f"finished={script} exit={result.returncode}", flush=True) + if result.returncode: + failures.append((script, result.returncode)) + + if failures: + print(f"failures={failures}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/python/tracing/openinference/test_example_contract.py b/python/tracing/openinference/test_example_contract.py new file mode 100644 index 0000000..02b5447 --- /dev/null +++ b/python/tracing/openinference/test_example_contract.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import ast +import os +from pathlib import Path + +import _shared +import run_all +from _shared import example_run_id + + +def test_shell_marker_wins_over_repository_env(monkeypatch): + marker = "openinference-shell-marker" + monkeypatch.setenv("RESPAN_EXAMPLE_RUN_ID", marker) + assert example_run_id() == marker + assert run_all.resolved_run_id() == marker + + +def test_run_all_lists_every_example_once(): + directory = Path(__file__).resolve().parent + expected = tuple( + path.name + for path in sorted(directory.glob("[0-9][0-9]_*.py")) + if path.name != "test_example_contract.py" + ) + assert run_all.EXAMPLE_SCRIPTS == expected + assert len(set(run_all.EXAMPLE_SCRIPTS)) == len(run_all.EXAMPLE_SCRIPTS) + + +def test_common_metadata_and_streaming_source_uses_upstream_openinference(monkeypatch): + captured = {} + + class FakeRespan: + def __init__(self, **kwargs): + captured.update(kwargs) + + monkeypatch.setattr(_shared, "Respan", FakeRespan) + monkeypatch.setattr(_shared, "require_respan_api_key", lambda: "test-key") + + _shared.make_respan("streaming-privacy", "contract-test-marker") + + assert captured["metadata"] == { + "example_run_id": "contract-test-marker", + "integration": "openinference", + "example": "streaming-privacy", + "workflow_name": "openinference_streaming_privacy", + } + streaming_source = (Path(__file__).parent / "05_streaming_privacy.py").read_text() + assert "OISpanAttributes.LLM_INVOCATION_PARAMETERS" in streaming_source + assert '"stream": True' in streaming_source + assert '"gen_ai.is_streaming"' not in streaming_source + + +def test_every_public_example_finishes_respan_in_finally(): + directory = Path(__file__).resolve().parent + for script in run_all.EXAMPLE_SCRIPTS: + tree = ast.parse((directory / script).read_text()) + finally_calls = [ + node + for candidate in ast.walk(tree) + if isinstance(candidate, ast.Try) + for node in ast.walk(ast.Module(body=candidate.finalbody, type_ignores=[])) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "finish_respan" + ] + assert finally_calls, f"{script} must call finish_respan() from finally" + + +def test_run_all_aggregates_process_failures_and_timeouts(monkeypatch): + outcomes = iter((0, 2, "timeout", 3, 0)) + calls = [] + + class Result: + def __init__(self, returncode): + self.returncode = returncode + + def fake_run(*args, **kwargs): + calls.append((args, kwargs)) + outcome = next(outcomes) + if outcome == "timeout": + raise run_all.subprocess.TimeoutExpired(args[0], kwargs["timeout"]) + return Result(outcome) + + monkeypatch.setenv("RESPAN_EXAMPLE_RUN_ID", "contract-test-marker") + monkeypatch.setattr(run_all.subprocess, "run", fake_run) + assert run_all.main() == 1 + assert len(calls) == len(run_all.EXAMPLE_SCRIPTS) + assert all( + kwargs["timeout"] == run_all.EXAMPLE_TIMEOUT_SECONDS for _, kwargs in calls + ) + assert os.environ["RESPAN_EXAMPLE_RUN_ID"] == "contract-test-marker"