agno 0.1.2__py3-none-any.whl → 2.3.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/__init__.py +8 -0
- agno/agent/__init__.py +44 -5
- agno/agent/agent.py +10531 -2975
- agno/api/agent.py +14 -53
- agno/api/api.py +7 -46
- agno/api/evals.py +22 -0
- agno/api/os.py +17 -0
- agno/api/routes.py +6 -25
- agno/api/schemas/__init__.py +9 -0
- agno/api/schemas/agent.py +6 -9
- agno/api/schemas/evals.py +16 -0
- agno/api/schemas/os.py +14 -0
- agno/api/schemas/team.py +10 -10
- agno/api/schemas/utils.py +21 -0
- agno/api/schemas/workflows.py +16 -0
- agno/api/settings.py +53 -0
- agno/api/team.py +22 -26
- agno/api/workflow.py +28 -0
- agno/cloud/aws/base.py +214 -0
- agno/cloud/aws/s3/__init__.py +2 -0
- agno/cloud/aws/s3/api_client.py +43 -0
- agno/cloud/aws/s3/bucket.py +195 -0
- agno/cloud/aws/s3/object.py +57 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/__init__.py +24 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +946 -0
- agno/db/dynamo/__init__.py +3 -0
- agno/db/dynamo/dynamo.py +2781 -0
- agno/db/dynamo/schemas.py +442 -0
- agno/db/dynamo/utils.py +743 -0
- agno/db/firestore/__init__.py +3 -0
- agno/db/firestore/firestore.py +2379 -0
- agno/db/firestore/schemas.py +181 -0
- agno/db/firestore/utils.py +376 -0
- agno/db/gcs_json/__init__.py +3 -0
- agno/db/gcs_json/gcs_json_db.py +1791 -0
- agno/db/gcs_json/utils.py +228 -0
- agno/db/in_memory/__init__.py +3 -0
- agno/db/in_memory/in_memory_db.py +1312 -0
- agno/db/in_memory/utils.py +230 -0
- agno/db/json/__init__.py +3 -0
- agno/db/json/json_db.py +1777 -0
- agno/db/json/utils.py +230 -0
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +635 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +17 -0
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +2597 -0
- agno/db/mongo/schemas.py +119 -0
- agno/db/mongo/utils.py +276 -0
- agno/db/mysql/__init__.py +4 -0
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +2923 -0
- agno/db/mysql/schemas.py +186 -0
- agno/db/mysql/utils.py +488 -0
- agno/db/postgres/__init__.py +4 -0
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +2870 -0
- agno/db/postgres/schemas.py +187 -0
- agno/db/postgres/utils.py +442 -0
- agno/db/redis/__init__.py +3 -0
- agno/db/redis/redis.py +2141 -0
- agno/db/redis/schemas.py +159 -0
- agno/db/redis/utils.py +346 -0
- agno/db/schemas/__init__.py +4 -0
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +34 -0
- agno/db/schemas/knowledge.py +40 -0
- agno/db/schemas/memory.py +61 -0
- agno/db/singlestore/__init__.py +3 -0
- agno/db/singlestore/schemas.py +179 -0
- agno/db/singlestore/singlestore.py +2877 -0
- agno/db/singlestore/utils.py +384 -0
- agno/db/sqlite/__init__.py +4 -0
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +181 -0
- agno/db/sqlite/sqlite.py +2908 -0
- agno/db/sqlite/utils.py +429 -0
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +118 -0
- agno/eval/__init__.py +24 -0
- agno/eval/accuracy.py +666 -276
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +779 -0
- agno/eval/reliability.py +241 -62
- agno/eval/utils.py +120 -0
- agno/exceptions.py +143 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/__init__.py +3 -0
- agno/integrations/discord/client.py +203 -0
- agno/knowledge/__init__.py +5 -1
- agno/{document → knowledge}/chunking/agentic.py +22 -14
- agno/{document → knowledge}/chunking/document.py +2 -2
- agno/{document → knowledge}/chunking/fixed.py +7 -6
- agno/knowledge/chunking/markdown.py +151 -0
- agno/{document → knowledge}/chunking/recursive.py +15 -3
- agno/knowledge/chunking/row.py +39 -0
- agno/knowledge/chunking/semantic.py +91 -0
- agno/knowledge/chunking/strategy.py +165 -0
- agno/knowledge/content.py +74 -0
- agno/knowledge/document/__init__.py +5 -0
- agno/{document → knowledge/document}/base.py +12 -2
- agno/knowledge/embedder/__init__.py +5 -0
- agno/knowledge/embedder/aws_bedrock.py +343 -0
- agno/knowledge/embedder/azure_openai.py +210 -0
- agno/{embedder → knowledge/embedder}/base.py +8 -0
- agno/knowledge/embedder/cohere.py +323 -0
- agno/knowledge/embedder/fastembed.py +62 -0
- agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
- agno/knowledge/embedder/google.py +258 -0
- agno/knowledge/embedder/huggingface.py +94 -0
- agno/knowledge/embedder/jina.py +182 -0
- agno/knowledge/embedder/langdb.py +22 -0
- agno/knowledge/embedder/mistral.py +206 -0
- agno/knowledge/embedder/nebius.py +13 -0
- agno/knowledge/embedder/ollama.py +154 -0
- agno/knowledge/embedder/openai.py +195 -0
- agno/knowledge/embedder/sentence_transformer.py +63 -0
- agno/{embedder → knowledge/embedder}/together.py +1 -1
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +165 -0
- agno/knowledge/knowledge.py +3006 -0
- agno/knowledge/reader/__init__.py +7 -0
- agno/knowledge/reader/arxiv_reader.py +81 -0
- agno/knowledge/reader/base.py +95 -0
- agno/knowledge/reader/csv_reader.py +164 -0
- agno/knowledge/reader/docx_reader.py +82 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/firecrawl_reader.py +201 -0
- agno/knowledge/reader/json_reader.py +88 -0
- agno/knowledge/reader/markdown_reader.py +137 -0
- agno/knowledge/reader/pdf_reader.py +431 -0
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +313 -0
- agno/knowledge/reader/s3_reader.py +89 -0
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +127 -0
- agno/knowledge/reader/web_search_reader.py +325 -0
- agno/knowledge/reader/website_reader.py +455 -0
- agno/knowledge/reader/wikipedia_reader.py +91 -0
- agno/knowledge/reader/youtube_reader.py +78 -0
- agno/knowledge/remote_content/remote_content.py +88 -0
- agno/knowledge/reranker/__init__.py +3 -0
- agno/{reranker → knowledge/reranker}/base.py +1 -1
- agno/{reranker → knowledge/reranker}/cohere.py +2 -2
- agno/knowledge/reranker/infinity.py +195 -0
- agno/knowledge/reranker/sentence_transformer.py +54 -0
- agno/knowledge/types.py +39 -0
- agno/knowledge/utils.py +234 -0
- agno/media.py +439 -95
- agno/memory/__init__.py +16 -3
- agno/memory/manager.py +1474 -123
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/__init__.py +5 -0
- agno/models/aimlapi/aimlapi.py +62 -0
- agno/models/anthropic/__init__.py +4 -0
- agno/models/anthropic/claude.py +960 -496
- agno/models/aws/__init__.py +15 -0
- agno/models/aws/bedrock.py +686 -451
- agno/models/aws/claude.py +190 -183
- agno/models/azure/__init__.py +18 -1
- agno/models/azure/ai_foundry.py +489 -0
- agno/models/azure/openai_chat.py +89 -40
- agno/models/base.py +2477 -550
- agno/models/cerebras/__init__.py +12 -0
- agno/models/cerebras/cerebras.py +565 -0
- agno/models/cerebras/cerebras_openai.py +131 -0
- agno/models/cohere/__init__.py +4 -0
- agno/models/cohere/chat.py +306 -492
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +74 -0
- agno/models/dashscope/__init__.py +5 -0
- agno/models/dashscope/dashscope.py +90 -0
- agno/models/deepinfra/__init__.py +5 -0
- agno/models/deepinfra/deepinfra.py +45 -0
- agno/models/deepseek/__init__.py +4 -0
- agno/models/deepseek/deepseek.py +110 -9
- agno/models/fireworks/__init__.py +4 -0
- agno/models/fireworks/fireworks.py +19 -22
- agno/models/google/__init__.py +3 -7
- agno/models/google/gemini.py +1717 -662
- agno/models/google/utils.py +22 -0
- agno/models/groq/__init__.py +4 -0
- agno/models/groq/groq.py +391 -666
- agno/models/huggingface/__init__.py +4 -0
- agno/models/huggingface/huggingface.py +266 -538
- agno/models/ibm/__init__.py +5 -0
- agno/models/ibm/watsonx.py +432 -0
- agno/models/internlm/__init__.py +3 -0
- agno/models/internlm/internlm.py +20 -3
- agno/models/langdb/__init__.py +1 -0
- agno/models/langdb/langdb.py +60 -0
- agno/models/litellm/__init__.py +14 -0
- agno/models/litellm/chat.py +503 -0
- agno/models/litellm/litellm_openai.py +42 -0
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/lmstudio/__init__.py +5 -0
- agno/models/lmstudio/lmstudio.py +25 -0
- agno/models/message.py +361 -39
- agno/models/meta/__init__.py +12 -0
- agno/models/meta/llama.py +502 -0
- agno/models/meta/llama_openai.py +79 -0
- agno/models/metrics.py +120 -0
- agno/models/mistral/__init__.py +4 -0
- agno/models/mistral/mistral.py +293 -393
- agno/models/nebius/__init__.py +3 -0
- agno/models/nebius/nebius.py +53 -0
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/__init__.py +4 -0
- agno/models/nvidia/nvidia.py +22 -3
- agno/models/ollama/__init__.py +4 -2
- agno/models/ollama/chat.py +257 -492
- agno/models/openai/__init__.py +7 -0
- agno/models/openai/chat.py +725 -770
- agno/models/openai/like.py +16 -2
- agno/models/openai/responses.py +1121 -0
- agno/models/openrouter/__init__.py +4 -0
- agno/models/openrouter/openrouter.py +62 -5
- agno/models/perplexity/__init__.py +5 -0
- agno/models/perplexity/perplexity.py +203 -0
- agno/models/portkey/__init__.py +3 -0
- agno/models/portkey/portkey.py +82 -0
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +69 -0
- agno/models/response.py +177 -7
- agno/models/sambanova/__init__.py +4 -0
- agno/models/sambanova/sambanova.py +23 -4
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +42 -0
- agno/models/together/__init__.py +4 -0
- agno/models/together/together.py +21 -164
- agno/models/utils.py +266 -0
- agno/models/vercel/__init__.py +3 -0
- agno/models/vercel/v0.py +43 -0
- agno/models/vertexai/__init__.py +0 -1
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/__init__.py +3 -0
- agno/models/vllm/vllm.py +83 -0
- agno/models/xai/__init__.py +2 -0
- agno/models/xai/xai.py +111 -7
- agno/os/__init__.py +3 -0
- agno/os/app.py +1027 -0
- agno/os/auth.py +244 -0
- agno/os/config.py +126 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +249 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/__init__.py +3 -0
- agno/os/interfaces/agui/agui.py +47 -0
- agno/os/interfaces/agui/router.py +147 -0
- agno/os/interfaces/agui/utils.py +574 -0
- agno/os/interfaces/base.py +25 -0
- agno/os/interfaces/slack/__init__.py +3 -0
- agno/os/interfaces/slack/router.py +148 -0
- agno/os/interfaces/slack/security.py +30 -0
- agno/os/interfaces/slack/slack.py +47 -0
- agno/os/interfaces/whatsapp/__init__.py +3 -0
- agno/os/interfaces/whatsapp/router.py +210 -0
- agno/os/interfaces/whatsapp/security.py +55 -0
- agno/os/interfaces/whatsapp/whatsapp.py +36 -0
- agno/os/mcp.py +293 -0
- agno/os/middleware/__init__.py +9 -0
- agno/os/middleware/jwt.py +797 -0
- agno/os/router.py +258 -0
- agno/os/routers/__init__.py +3 -0
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/__init__.py +3 -0
- agno/os/routers/evals/evals.py +450 -0
- agno/os/routers/evals/schemas.py +174 -0
- agno/os/routers/evals/utils.py +231 -0
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/__init__.py +3 -0
- agno/os/routers/knowledge/knowledge.py +1008 -0
- agno/os/routers/knowledge/schemas.py +178 -0
- agno/os/routers/memory/__init__.py +3 -0
- agno/os/routers/memory/memory.py +661 -0
- agno/os/routers/memory/schemas.py +88 -0
- agno/os/routers/metrics/__init__.py +3 -0
- agno/os/routers/metrics/metrics.py +190 -0
- agno/os/routers/metrics/schemas.py +47 -0
- agno/os/routers/session/__init__.py +3 -0
- agno/os/routers/session/session.py +997 -0
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +534 -0
- agno/os/scopes.py +469 -0
- agno/{playground → os}/settings.py +7 -15
- agno/os/utils.py +973 -0
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +67 -0
- agno/reasoning/deepseek.py +63 -0
- agno/reasoning/default.py +97 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +71 -0
- agno/reasoning/helpers.py +24 -1
- agno/reasoning/ollama.py +67 -0
- agno/reasoning/openai.py +86 -0
- agno/reasoning/step.py +2 -1
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +822 -0
- agno/run/base.py +247 -0
- agno/run/cancel.py +81 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +767 -0
- agno/run/workflow.py +708 -0
- agno/session/__init__.py +10 -0
- agno/session/agent.py +260 -0
- agno/session/summary.py +265 -0
- agno/session/team.py +342 -0
- agno/session/workflow.py +501 -0
- agno/table.py +10 -0
- agno/team/__init__.py +37 -0
- agno/team/team.py +9536 -0
- agno/tools/__init__.py +7 -0
- agno/tools/agentql.py +120 -0
- agno/tools/airflow.py +22 -12
- agno/tools/api.py +122 -0
- agno/tools/apify.py +276 -83
- agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
- agno/tools/aws_lambda.py +28 -7
- agno/tools/aws_ses.py +66 -0
- agno/tools/baidusearch.py +11 -4
- agno/tools/bitbucket.py +292 -0
- agno/tools/brandfetch.py +213 -0
- agno/tools/bravesearch.py +106 -0
- agno/tools/brightdata.py +367 -0
- agno/tools/browserbase.py +209 -0
- agno/tools/calcom.py +32 -23
- agno/tools/calculator.py +24 -37
- agno/tools/cartesia.py +187 -0
- agno/tools/{clickup_tool.py → clickup.py} +17 -28
- agno/tools/confluence.py +91 -26
- agno/tools/crawl4ai.py +139 -43
- agno/tools/csv_toolkit.py +28 -22
- agno/tools/dalle.py +36 -22
- agno/tools/daytona.py +475 -0
- agno/tools/decorator.py +169 -14
- agno/tools/desi_vocal.py +23 -11
- agno/tools/discord.py +32 -29
- agno/tools/docker.py +716 -0
- agno/tools/duckdb.py +76 -81
- agno/tools/duckduckgo.py +43 -40
- agno/tools/e2b.py +703 -0
- agno/tools/eleven_labs.py +65 -54
- agno/tools/email.py +13 -5
- agno/tools/evm.py +129 -0
- agno/tools/exa.py +324 -42
- agno/tools/fal.py +39 -35
- agno/tools/file.py +196 -30
- agno/tools/file_generation.py +356 -0
- agno/tools/financial_datasets.py +288 -0
- agno/tools/firecrawl.py +108 -33
- agno/tools/function.py +960 -122
- agno/tools/giphy.py +34 -12
- agno/tools/github.py +1294 -97
- agno/tools/gmail.py +922 -0
- agno/tools/google_bigquery.py +117 -0
- agno/tools/google_drive.py +271 -0
- agno/tools/google_maps.py +253 -0
- agno/tools/googlecalendar.py +607 -107
- agno/tools/googlesheets.py +377 -0
- agno/tools/hackernews.py +20 -12
- agno/tools/jina.py +24 -14
- agno/tools/jira.py +48 -19
- agno/tools/knowledge.py +218 -0
- agno/tools/linear.py +82 -43
- agno/tools/linkup.py +58 -0
- agno/tools/local_file_system.py +15 -7
- agno/tools/lumalab.py +41 -26
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +193 -0
- agno/tools/memory.py +419 -0
- agno/tools/mlx_transcribe.py +11 -9
- agno/tools/models/azure_openai.py +190 -0
- agno/tools/models/gemini.py +203 -0
- agno/tools/models/groq.py +158 -0
- agno/tools/models/morph.py +186 -0
- agno/tools/models/nebius.py +124 -0
- agno/tools/models_labs.py +163 -82
- agno/tools/moviepy_video.py +18 -13
- agno/tools/nano_banana.py +151 -0
- agno/tools/neo4j.py +134 -0
- agno/tools/newspaper.py +15 -4
- agno/tools/newspaper4k.py +19 -6
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +181 -17
- agno/tools/openbb.py +27 -20
- agno/tools/opencv.py +321 -0
- agno/tools/openweather.py +233 -0
- agno/tools/oxylabs.py +385 -0
- agno/tools/pandas.py +25 -15
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +238 -185
- agno/tools/pubmed.py +125 -13
- agno/tools/python.py +48 -35
- agno/tools/reasoning.py +283 -0
- agno/tools/reddit.py +207 -29
- agno/tools/redshift.py +406 -0
- agno/tools/replicate.py +69 -26
- agno/tools/resend.py +11 -6
- agno/tools/scrapegraph.py +179 -19
- agno/tools/searxng.py +23 -31
- agno/tools/serpapi.py +15 -10
- agno/tools/serper.py +255 -0
- agno/tools/shell.py +23 -12
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +56 -14
- agno/tools/sleep.py +8 -6
- agno/tools/spider.py +35 -11
- agno/tools/spotify.py +919 -0
- agno/tools/sql.py +34 -19
- agno/tools/tavily.py +158 -8
- agno/tools/telegram.py +18 -8
- agno/tools/todoist.py +218 -0
- agno/tools/toolkit.py +134 -9
- agno/tools/trafilatura.py +388 -0
- agno/tools/trello.py +25 -28
- agno/tools/twilio.py +18 -9
- agno/tools/user_control_flow.py +78 -0
- agno/tools/valyu.py +228 -0
- agno/tools/visualization.py +467 -0
- agno/tools/webbrowser.py +28 -0
- agno/tools/webex.py +76 -0
- agno/tools/website.py +23 -19
- agno/tools/webtools.py +45 -0
- agno/tools/whatsapp.py +286 -0
- agno/tools/wikipedia.py +28 -19
- agno/tools/workflow.py +285 -0
- agno/tools/{twitter.py → x.py} +142 -46
- agno/tools/yfinance.py +41 -39
- agno/tools/youtube.py +34 -17
- agno/tools/zendesk.py +15 -5
- agno/tools/zep.py +454 -0
- agno/tools/zoom.py +86 -37
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/audio.py +37 -1
- agno/utils/certs.py +27 -0
- agno/utils/code_execution.py +11 -0
- agno/utils/common.py +103 -20
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +700 -0
- agno/utils/functions.py +107 -37
- agno/utils/gemini.py +426 -0
- agno/utils/hooks.py +171 -0
- agno/utils/http.py +185 -0
- agno/utils/json_schema.py +159 -37
- agno/utils/knowledge.py +36 -0
- agno/utils/location.py +19 -0
- agno/utils/log.py +221 -8
- agno/utils/mcp.py +214 -0
- agno/utils/media.py +335 -14
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +77 -2
- agno/utils/models/ai_foundry.py +50 -0
- agno/utils/models/claude.py +373 -0
- agno/utils/models/cohere.py +94 -0
- agno/utils/models/llama.py +85 -0
- agno/utils/models/mistral.py +100 -0
- agno/utils/models/openai_responses.py +140 -0
- agno/utils/models/schema_utils.py +153 -0
- agno/utils/models/watsonx.py +41 -0
- agno/utils/openai.py +257 -0
- agno/utils/pickle.py +1 -1
- agno/utils/pprint.py +124 -8
- agno/utils/print_response/agent.py +930 -0
- agno/utils/print_response/team.py +1914 -0
- agno/utils/print_response/workflow.py +1668 -0
- agno/utils/prompts.py +111 -0
- agno/utils/reasoning.py +108 -0
- agno/utils/response.py +163 -0
- agno/utils/serialize.py +32 -0
- agno/utils/shell.py +4 -4
- agno/utils/streamlit.py +487 -0
- agno/utils/string.py +204 -51
- agno/utils/team.py +139 -0
- agno/utils/timer.py +9 -2
- agno/utils/tokens.py +657 -0
- agno/utils/tools.py +19 -1
- agno/utils/whatsapp.py +305 -0
- agno/utils/yaml_io.py +3 -3
- agno/vectordb/__init__.py +2 -0
- agno/vectordb/base.py +87 -9
- agno/vectordb/cassandra/__init__.py +5 -1
- agno/vectordb/cassandra/cassandra.py +383 -27
- agno/vectordb/chroma/__init__.py +4 -0
- agno/vectordb/chroma/chromadb.py +748 -83
- agno/vectordb/clickhouse/__init__.py +7 -1
- agno/vectordb/clickhouse/clickhousedb.py +554 -53
- agno/vectordb/couchbase/__init__.py +3 -0
- agno/vectordb/couchbase/couchbase.py +1446 -0
- agno/vectordb/lancedb/__init__.py +5 -0
- agno/vectordb/lancedb/lance_db.py +730 -98
- agno/vectordb/langchaindb/__init__.py +5 -0
- agno/vectordb/langchaindb/langchaindb.py +163 -0
- agno/vectordb/lightrag/__init__.py +5 -0
- agno/vectordb/lightrag/lightrag.py +388 -0
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +166 -0
- agno/vectordb/milvus/__init__.py +3 -0
- agno/vectordb/milvus/milvus.py +966 -78
- agno/vectordb/mongodb/__init__.py +9 -1
- agno/vectordb/mongodb/mongodb.py +1175 -172
- agno/vectordb/pgvector/__init__.py +8 -0
- agno/vectordb/pgvector/pgvector.py +599 -115
- agno/vectordb/pineconedb/__init__.py +5 -1
- agno/vectordb/pineconedb/pineconedb.py +406 -43
- agno/vectordb/qdrant/__init__.py +4 -0
- agno/vectordb/qdrant/qdrant.py +914 -61
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/__init__.py +8 -1
- agno/vectordb/singlestore/singlestore.py +771 -0
- agno/vectordb/surrealdb/__init__.py +3 -0
- agno/vectordb/surrealdb/surrealdb.py +663 -0
- agno/vectordb/upstashdb/__init__.py +5 -0
- agno/vectordb/upstashdb/upstashdb.py +718 -0
- agno/vectordb/weaviate/__init__.py +8 -0
- agno/vectordb/weaviate/index.py +15 -0
- agno/vectordb/weaviate/weaviate.py +1009 -0
- agno/workflow/__init__.py +23 -1
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +759 -0
- agno/workflow/loop.py +756 -0
- agno/workflow/parallel.py +853 -0
- agno/workflow/router.py +723 -0
- agno/workflow/step.py +1564 -0
- agno/workflow/steps.py +613 -0
- agno/workflow/types.py +556 -0
- agno/workflow/workflow.py +4327 -514
- agno-2.3.13.dist-info/METADATA +639 -0
- agno-2.3.13.dist-info/RECORD +613 -0
- {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
- agno-2.3.13.dist-info/licenses/LICENSE +201 -0
- agno/api/playground.py +0 -91
- agno/api/schemas/playground.py +0 -22
- agno/api/schemas/user.py +0 -22
- agno/api/schemas/workspace.py +0 -46
- agno/api/user.py +0 -160
- agno/api/workspace.py +0 -151
- agno/cli/auth_server.py +0 -118
- agno/cli/config.py +0 -275
- agno/cli/console.py +0 -88
- agno/cli/credentials.py +0 -23
- agno/cli/entrypoint.py +0 -571
- agno/cli/operator.py +0 -355
- agno/cli/settings.py +0 -85
- agno/cli/ws/ws_cli.py +0 -817
- agno/constants.py +0 -13
- agno/document/__init__.py +0 -1
- agno/document/chunking/semantic.py +0 -47
- agno/document/chunking/strategy.py +0 -31
- agno/document/reader/__init__.py +0 -1
- agno/document/reader/arxiv_reader.py +0 -41
- agno/document/reader/base.py +0 -22
- agno/document/reader/csv_reader.py +0 -84
- agno/document/reader/docx_reader.py +0 -46
- agno/document/reader/firecrawl_reader.py +0 -99
- agno/document/reader/json_reader.py +0 -43
- agno/document/reader/pdf_reader.py +0 -219
- agno/document/reader/s3/pdf_reader.py +0 -46
- agno/document/reader/s3/text_reader.py +0 -51
- agno/document/reader/text_reader.py +0 -41
- agno/document/reader/website_reader.py +0 -175
- agno/document/reader/youtube_reader.py +0 -50
- agno/embedder/__init__.py +0 -1
- agno/embedder/azure_openai.py +0 -86
- agno/embedder/cohere.py +0 -72
- agno/embedder/fastembed.py +0 -37
- agno/embedder/google.py +0 -73
- agno/embedder/huggingface.py +0 -54
- agno/embedder/mistral.py +0 -80
- agno/embedder/ollama.py +0 -57
- agno/embedder/openai.py +0 -74
- agno/embedder/sentence_transformer.py +0 -38
- agno/embedder/voyageai.py +0 -64
- agno/eval/perf.py +0 -201
- agno/file/__init__.py +0 -1
- agno/file/file.py +0 -16
- agno/file/local/csv.py +0 -32
- agno/file/local/txt.py +0 -19
- agno/infra/app.py +0 -240
- agno/infra/base.py +0 -144
- agno/infra/context.py +0 -20
- agno/infra/db_app.py +0 -52
- agno/infra/resource.py +0 -205
- agno/infra/resources.py +0 -55
- agno/knowledge/agent.py +0 -230
- agno/knowledge/arxiv.py +0 -22
- agno/knowledge/combined.py +0 -22
- agno/knowledge/csv.py +0 -28
- agno/knowledge/csv_url.py +0 -19
- agno/knowledge/document.py +0 -20
- agno/knowledge/docx.py +0 -30
- agno/knowledge/json.py +0 -28
- agno/knowledge/langchain.py +0 -71
- agno/knowledge/llamaindex.py +0 -66
- agno/knowledge/pdf.py +0 -28
- agno/knowledge/pdf_url.py +0 -26
- agno/knowledge/s3/base.py +0 -60
- agno/knowledge/s3/pdf.py +0 -21
- agno/knowledge/s3/text.py +0 -23
- agno/knowledge/text.py +0 -30
- agno/knowledge/website.py +0 -88
- agno/knowledge/wikipedia.py +0 -31
- agno/knowledge/youtube.py +0 -22
- agno/memory/agent.py +0 -392
- agno/memory/classifier.py +0 -104
- agno/memory/db/__init__.py +0 -1
- agno/memory/db/base.py +0 -42
- agno/memory/db/mongodb.py +0 -189
- agno/memory/db/postgres.py +0 -203
- agno/memory/db/sqlite.py +0 -193
- agno/memory/memory.py +0 -15
- agno/memory/row.py +0 -36
- agno/memory/summarizer.py +0 -192
- agno/memory/summary.py +0 -19
- agno/memory/workflow.py +0 -38
- agno/models/google/gemini_openai.py +0 -26
- agno/models/ollama/hermes.py +0 -221
- agno/models/ollama/tools.py +0 -362
- agno/models/vertexai/gemini.py +0 -595
- agno/playground/__init__.py +0 -3
- agno/playground/async_router.py +0 -421
- agno/playground/deploy.py +0 -249
- agno/playground/operator.py +0 -92
- agno/playground/playground.py +0 -91
- agno/playground/schemas.py +0 -76
- agno/playground/serve.py +0 -55
- agno/playground/sync_router.py +0 -405
- agno/reasoning/agent.py +0 -68
- agno/run/response.py +0 -112
- agno/storage/agent/__init__.py +0 -0
- agno/storage/agent/base.py +0 -38
- agno/storage/agent/dynamodb.py +0 -350
- agno/storage/agent/json.py +0 -92
- agno/storage/agent/mongodb.py +0 -228
- agno/storage/agent/postgres.py +0 -367
- agno/storage/agent/session.py +0 -79
- agno/storage/agent/singlestore.py +0 -303
- agno/storage/agent/sqlite.py +0 -357
- agno/storage/agent/yaml.py +0 -93
- agno/storage/workflow/__init__.py +0 -0
- agno/storage/workflow/base.py +0 -40
- agno/storage/workflow/mongodb.py +0 -233
- agno/storage/workflow/postgres.py +0 -366
- agno/storage/workflow/session.py +0 -60
- agno/storage/workflow/sqlite.py +0 -359
- agno/tools/googlesearch.py +0 -88
- agno/utils/defaults.py +0 -57
- agno/utils/filesystem.py +0 -39
- agno/utils/git.py +0 -52
- agno/utils/json_io.py +0 -30
- agno/utils/load_env.py +0 -19
- agno/utils/py_io.py +0 -19
- agno/utils/pyproject.py +0 -18
- agno/utils/resource_filter.py +0 -31
- agno/vectordb/singlestore/s2vectordb.py +0 -390
- agno/vectordb/singlestore/s2vectordb2.py +0 -355
- agno/workspace/__init__.py +0 -0
- agno/workspace/config.py +0 -325
- agno/workspace/enums.py +0 -6
- agno/workspace/helpers.py +0 -48
- agno/workspace/operator.py +0 -758
- agno/workspace/settings.py +0 -63
- agno-0.1.2.dist-info/LICENSE +0 -375
- agno-0.1.2.dist-info/METADATA +0 -502
- agno-0.1.2.dist-info/RECORD +0 -352
- agno-0.1.2.dist-info/entry_points.txt +0 -3
- /agno/{cli → db/migrations}/__init__.py +0 -0
- /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
- /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
- /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
- /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
- /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
- /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
- /agno/{reranker → utils/models}/__init__.py +0 -0
- /agno/{storage → utils/print_response}/__init__.py +0 -0
- {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Tuple, Type, Union
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from typing_extensions import Literal
|
|
8
|
+
|
|
9
|
+
from agno.exceptions import ModelAuthenticationError, ModelProviderError
|
|
10
|
+
from agno.media import File
|
|
11
|
+
from agno.models.base import Model
|
|
12
|
+
from agno.models.message import Citations, Message, UrlCitation
|
|
13
|
+
from agno.models.metrics import Metrics
|
|
14
|
+
from agno.models.response import ModelResponse
|
|
15
|
+
from agno.run.agent import RunOutput
|
|
16
|
+
from agno.utils.http import get_default_async_client, get_default_sync_client
|
|
17
|
+
from agno.utils.log import log_debug, log_error, log_warning
|
|
18
|
+
from agno.utils.models.openai_responses import images_to_message
|
|
19
|
+
from agno.utils.models.schema_utils import get_response_schema_for_provider
|
|
20
|
+
from agno.utils.tokens import count_schema_tokens
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from openai import APIConnectionError, APIStatusError, AsyncOpenAI, OpenAI, RateLimitError
|
|
24
|
+
from openai.types.responses import Response, ResponseReasoningItem, ResponseStreamEvent, ResponseUsage
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
raise ImportError("`openai` not installed. Please install using `pip install openai -U`") from e
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class OpenAIResponses(Model):
|
|
31
|
+
"""
|
|
32
|
+
A class for interacting with OpenAI models using the Responses API.
|
|
33
|
+
|
|
34
|
+
For more information, see: https://platform.openai.com/docs/api-reference/responses
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
id: str = "gpt-4o"
|
|
38
|
+
name: str = "OpenAIResponses"
|
|
39
|
+
provider: str = "OpenAI"
|
|
40
|
+
supports_native_structured_outputs: bool = True
|
|
41
|
+
|
|
42
|
+
# Request parameters
|
|
43
|
+
include: Optional[List[str]] = None
|
|
44
|
+
max_output_tokens: Optional[int] = None
|
|
45
|
+
max_tool_calls: Optional[int] = None
|
|
46
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
47
|
+
parallel_tool_calls: Optional[bool] = None
|
|
48
|
+
reasoning: Optional[Dict[str, Any]] = None
|
|
49
|
+
verbosity: Optional[Literal["low", "medium", "high"]] = None
|
|
50
|
+
reasoning_effort: Optional[Literal["minimal", "low", "medium", "high"]] = None
|
|
51
|
+
reasoning_summary: Optional[Literal["auto", "concise", "detailed"]] = None
|
|
52
|
+
store: Optional[bool] = None
|
|
53
|
+
temperature: Optional[float] = None
|
|
54
|
+
top_p: Optional[float] = None
|
|
55
|
+
truncation: Optional[Literal["auto", "disabled"]] = None
|
|
56
|
+
user: Optional[str] = None
|
|
57
|
+
service_tier: Optional[Literal["auto", "default", "flex", "priority"]] = None
|
|
58
|
+
strict_output: bool = True # When True, guarantees schema adherence for structured outputs. When False, attempts to follow schema as a guide but may occasionally deviate
|
|
59
|
+
extra_headers: Optional[Any] = None
|
|
60
|
+
extra_query: Optional[Any] = None
|
|
61
|
+
extra_body: Optional[Any] = None
|
|
62
|
+
request_params: Optional[Dict[str, Any]] = None
|
|
63
|
+
|
|
64
|
+
# Client parameters
|
|
65
|
+
api_key: Optional[str] = None
|
|
66
|
+
organization: Optional[str] = None
|
|
67
|
+
base_url: Optional[Union[str, httpx.URL]] = None
|
|
68
|
+
timeout: Optional[float] = None
|
|
69
|
+
max_retries: Optional[int] = None
|
|
70
|
+
default_headers: Optional[Dict[str, str]] = None
|
|
71
|
+
default_query: Optional[Dict[str, str]] = None
|
|
72
|
+
http_client: Optional[Union[httpx.Client, httpx.AsyncClient]] = None
|
|
73
|
+
client_params: Optional[Dict[str, Any]] = None
|
|
74
|
+
|
|
75
|
+
# Parameters affecting built-in tools
|
|
76
|
+
vector_store_name: str = "knowledge_base"
|
|
77
|
+
|
|
78
|
+
# OpenAI clients
|
|
79
|
+
client: Optional[OpenAI] = None
|
|
80
|
+
async_client: Optional[AsyncOpenAI] = None
|
|
81
|
+
|
|
82
|
+
# The role to map the message role to.
|
|
83
|
+
role_map: Dict[str, str] = field(
|
|
84
|
+
default_factory=lambda: {
|
|
85
|
+
"system": "developer",
|
|
86
|
+
"user": "user",
|
|
87
|
+
"assistant": "assistant",
|
|
88
|
+
"tool": "tool",
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _using_reasoning_model(self) -> bool:
|
|
93
|
+
"""Return True if the contextual used model is a known reasoning model."""
|
|
94
|
+
return self.id.startswith("o3") or self.id.startswith("o4-mini") or self.id.startswith("gpt-5")
|
|
95
|
+
|
|
96
|
+
def _set_reasoning_request_param(self, base_params: Dict[str, Any]) -> Dict[str, Any]:
|
|
97
|
+
"""Set the reasoning request parameter."""
|
|
98
|
+
base_params["reasoning"] = self.reasoning or {}
|
|
99
|
+
|
|
100
|
+
if self.reasoning_effort is not None:
|
|
101
|
+
base_params["reasoning"]["effort"] = self.reasoning_effort
|
|
102
|
+
|
|
103
|
+
if self.reasoning_summary is not None:
|
|
104
|
+
base_params["reasoning"]["summary"] = self.reasoning_summary
|
|
105
|
+
|
|
106
|
+
return base_params
|
|
107
|
+
|
|
108
|
+
def _get_client_params(self) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Get client parameters for API requests.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dict[str, Any]: Client parameters
|
|
114
|
+
"""
|
|
115
|
+
from os import getenv
|
|
116
|
+
|
|
117
|
+
# Fetch API key from env if not already set
|
|
118
|
+
if not self.api_key:
|
|
119
|
+
self.api_key = getenv("OPENAI_API_KEY")
|
|
120
|
+
if not self.api_key:
|
|
121
|
+
raise ModelAuthenticationError(
|
|
122
|
+
message="OPENAI_API_KEY not set. Please set the OPENAI_API_KEY environment variable.",
|
|
123
|
+
model_name=self.name,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Define base client params
|
|
127
|
+
base_params = {
|
|
128
|
+
"api_key": self.api_key,
|
|
129
|
+
"organization": self.organization,
|
|
130
|
+
"base_url": self.base_url,
|
|
131
|
+
"timeout": self.timeout,
|
|
132
|
+
"max_retries": self.max_retries,
|
|
133
|
+
"default_headers": self.default_headers,
|
|
134
|
+
"default_query": self.default_query,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Create client_params dict with non-None values
|
|
138
|
+
client_params = {k: v for k, v in base_params.items() if v is not None}
|
|
139
|
+
|
|
140
|
+
# Add additional client params if provided
|
|
141
|
+
if self.client_params:
|
|
142
|
+
client_params.update(self.client_params)
|
|
143
|
+
|
|
144
|
+
return client_params
|
|
145
|
+
|
|
146
|
+
def get_client(self) -> OpenAI:
|
|
147
|
+
"""
|
|
148
|
+
Returns an OpenAI client. Caches the client to avoid recreating it on every request.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
OpenAI: An instance of the OpenAI client.
|
|
152
|
+
"""
|
|
153
|
+
if self.client and not self.client.is_closed():
|
|
154
|
+
return self.client
|
|
155
|
+
|
|
156
|
+
client_params: Dict[str, Any] = self._get_client_params()
|
|
157
|
+
if self.http_client is not None:
|
|
158
|
+
client_params["http_client"] = self.http_client
|
|
159
|
+
else:
|
|
160
|
+
# Use global sync client when no custom http_client is provided
|
|
161
|
+
client_params["http_client"] = get_default_sync_client()
|
|
162
|
+
|
|
163
|
+
self.client = OpenAI(**client_params)
|
|
164
|
+
return self.client
|
|
165
|
+
|
|
166
|
+
def get_async_client(self) -> AsyncOpenAI:
|
|
167
|
+
"""
|
|
168
|
+
Returns an asynchronous OpenAI client. Caches the client to avoid recreating it on every request.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
AsyncOpenAI: An instance of the asynchronous OpenAI client.
|
|
172
|
+
"""
|
|
173
|
+
if self.async_client and not self.async_client.is_closed():
|
|
174
|
+
return self.async_client
|
|
175
|
+
|
|
176
|
+
client_params: Dict[str, Any] = self._get_client_params()
|
|
177
|
+
if self.http_client and isinstance(self.http_client, httpx.AsyncClient):
|
|
178
|
+
client_params["http_client"] = self.http_client
|
|
179
|
+
else:
|
|
180
|
+
# Use global async client when no custom http_client is provided
|
|
181
|
+
client_params["http_client"] = get_default_async_client()
|
|
182
|
+
|
|
183
|
+
self.async_client = AsyncOpenAI(**client_params)
|
|
184
|
+
return self.async_client
|
|
185
|
+
|
|
186
|
+
def get_request_params(
|
|
187
|
+
self,
|
|
188
|
+
messages: Optional[List[Message]] = None,
|
|
189
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
190
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
191
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
192
|
+
) -> Dict[str, Any]:
|
|
193
|
+
"""
|
|
194
|
+
Returns keyword arguments for API requests.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dict[str, Any]: A dictionary of keyword arguments for API requests.
|
|
198
|
+
"""
|
|
199
|
+
# Define base request parameters
|
|
200
|
+
base_params: Dict[str, Any] = {
|
|
201
|
+
"include": self.include,
|
|
202
|
+
"max_output_tokens": self.max_output_tokens,
|
|
203
|
+
"max_tool_calls": self.max_tool_calls,
|
|
204
|
+
"metadata": self.metadata,
|
|
205
|
+
"parallel_tool_calls": self.parallel_tool_calls,
|
|
206
|
+
"store": self.store,
|
|
207
|
+
"temperature": self.temperature,
|
|
208
|
+
"top_p": self.top_p,
|
|
209
|
+
"truncation": self.truncation,
|
|
210
|
+
"user": self.user,
|
|
211
|
+
"service_tier": self.service_tier,
|
|
212
|
+
"extra_headers": self.extra_headers,
|
|
213
|
+
"extra_query": self.extra_query,
|
|
214
|
+
"extra_body": self.extra_body,
|
|
215
|
+
}
|
|
216
|
+
# Populate the reasoning parameter
|
|
217
|
+
base_params = self._set_reasoning_request_param(base_params)
|
|
218
|
+
|
|
219
|
+
# Build text parameter
|
|
220
|
+
text_params: Dict[str, Any] = {}
|
|
221
|
+
|
|
222
|
+
# Add verbosity if specified
|
|
223
|
+
if self.verbosity is not None:
|
|
224
|
+
text_params["verbosity"] = self.verbosity
|
|
225
|
+
|
|
226
|
+
# Set the response format
|
|
227
|
+
if response_format is not None:
|
|
228
|
+
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
|
|
229
|
+
schema = get_response_schema_for_provider(response_format, "openai")
|
|
230
|
+
text_params["format"] = {
|
|
231
|
+
"type": "json_schema",
|
|
232
|
+
"name": response_format.__name__,
|
|
233
|
+
"schema": schema,
|
|
234
|
+
"strict": self.strict_output,
|
|
235
|
+
}
|
|
236
|
+
else:
|
|
237
|
+
# JSON mode
|
|
238
|
+
text_params["format"] = {"type": "json_object"}
|
|
239
|
+
|
|
240
|
+
# Add text parameter if there are any text-level params
|
|
241
|
+
if text_params:
|
|
242
|
+
base_params["text"] = text_params
|
|
243
|
+
|
|
244
|
+
# Filter out None values
|
|
245
|
+
request_params: Dict[str, Any] = {k: v for k, v in base_params.items() if v is not None}
|
|
246
|
+
|
|
247
|
+
# Deep research models require web_search_preview tool or MCP tool
|
|
248
|
+
if "deep-research" in self.id:
|
|
249
|
+
if tools is None:
|
|
250
|
+
tools = []
|
|
251
|
+
|
|
252
|
+
# Check if web_search_preview tool is already present
|
|
253
|
+
has_web_search = any(tool.get("type") == "web_search_preview" for tool in tools)
|
|
254
|
+
|
|
255
|
+
# Add web_search_preview if not present - this enables the model to search
|
|
256
|
+
# the web for current information and provide citations
|
|
257
|
+
if not has_web_search:
|
|
258
|
+
web_search_tool = {"type": "web_search_preview"}
|
|
259
|
+
tools.insert(0, web_search_tool)
|
|
260
|
+
log_debug(f"Added web_search_preview tool for deep research model: {self.id}")
|
|
261
|
+
|
|
262
|
+
if tools:
|
|
263
|
+
request_params["tools"] = self._format_tool_params(messages=messages, tools=tools) # type: ignore
|
|
264
|
+
|
|
265
|
+
if tool_choice is not None:
|
|
266
|
+
request_params["tool_choice"] = tool_choice
|
|
267
|
+
|
|
268
|
+
# Handle reasoning tools for o3 and o4-mini models
|
|
269
|
+
if self._using_reasoning_model() and messages is not None:
|
|
270
|
+
if self.store is False:
|
|
271
|
+
request_params["store"] = False
|
|
272
|
+
|
|
273
|
+
# Add encrypted reasoning content to include if not already present
|
|
274
|
+
include_list = request_params.get("include", []) or []
|
|
275
|
+
if "reasoning.encrypted_content" not in include_list:
|
|
276
|
+
include_list.append("reasoning.encrypted_content")
|
|
277
|
+
if request_params.get("include") is None:
|
|
278
|
+
request_params["include"] = include_list
|
|
279
|
+
elif isinstance(request_params["include"], list):
|
|
280
|
+
request_params["include"].extend(include_list)
|
|
281
|
+
|
|
282
|
+
else:
|
|
283
|
+
request_params["store"] = True
|
|
284
|
+
|
|
285
|
+
# Check if the last assistant message has a previous_response_id to continue from
|
|
286
|
+
previous_response_id = None
|
|
287
|
+
for msg in reversed(messages):
|
|
288
|
+
if (
|
|
289
|
+
msg.role == "assistant"
|
|
290
|
+
and hasattr(msg, "provider_data")
|
|
291
|
+
and msg.provider_data
|
|
292
|
+
and "response_id" in msg.provider_data
|
|
293
|
+
):
|
|
294
|
+
previous_response_id = msg.provider_data["response_id"]
|
|
295
|
+
log_debug(f"Using previous_response_id: {previous_response_id}")
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
if previous_response_id:
|
|
299
|
+
request_params["previous_response_id"] = previous_response_id
|
|
300
|
+
|
|
301
|
+
# Add additional request params if provided
|
|
302
|
+
if self.request_params:
|
|
303
|
+
request_params.update(self.request_params)
|
|
304
|
+
|
|
305
|
+
if request_params:
|
|
306
|
+
log_debug(f"Calling {self.provider} with request parameters: {request_params}", log_level=2)
|
|
307
|
+
return request_params
|
|
308
|
+
|
|
309
|
+
def _upload_file(self, file: File) -> Optional[str]:
|
|
310
|
+
"""Upload a file to the OpenAI vector database."""
|
|
311
|
+
from pathlib import Path
|
|
312
|
+
from urllib.parse import urlparse
|
|
313
|
+
|
|
314
|
+
if file.url is not None:
|
|
315
|
+
file_content_tuple = file.file_url_content
|
|
316
|
+
if file_content_tuple is not None:
|
|
317
|
+
file_content = file_content_tuple[0]
|
|
318
|
+
else:
|
|
319
|
+
return None
|
|
320
|
+
file_name = Path(urlparse(file.url).path).name or "file"
|
|
321
|
+
file_tuple = (file_name, file_content)
|
|
322
|
+
result = self.get_client().files.create(file=file_tuple, purpose="assistants")
|
|
323
|
+
return result.id
|
|
324
|
+
elif file.filepath is not None:
|
|
325
|
+
import mimetypes
|
|
326
|
+
|
|
327
|
+
file_path = file.filepath if isinstance(file.filepath, Path) else Path(file.filepath)
|
|
328
|
+
if file_path.exists() and file_path.is_file():
|
|
329
|
+
file_name = file_path.name
|
|
330
|
+
file_content = file_path.read_bytes() # type: ignore
|
|
331
|
+
content_type = mimetypes.guess_type(file_path)[0]
|
|
332
|
+
result = self.get_client().files.create(
|
|
333
|
+
file=(file_name, file_content, content_type),
|
|
334
|
+
purpose="assistants", # type: ignore
|
|
335
|
+
)
|
|
336
|
+
return result.id
|
|
337
|
+
else:
|
|
338
|
+
raise ValueError(f"File not found: {file_path}")
|
|
339
|
+
elif file.content is not None:
|
|
340
|
+
result = self.get_client().files.create(file=file.content, purpose="assistants")
|
|
341
|
+
return result.id
|
|
342
|
+
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
def _create_vector_store(self, file_ids: List[str]) -> str:
|
|
346
|
+
"""Create a vector store for the files."""
|
|
347
|
+
vector_store = self.get_client().vector_stores.create(name=self.vector_store_name)
|
|
348
|
+
for file_id in file_ids:
|
|
349
|
+
self.get_client().vector_stores.files.create(vector_store_id=vector_store.id, file_id=file_id)
|
|
350
|
+
while True:
|
|
351
|
+
uploaded_files = self.get_client().vector_stores.files.list(vector_store_id=vector_store.id)
|
|
352
|
+
all_completed = True
|
|
353
|
+
failed = False
|
|
354
|
+
for file in uploaded_files:
|
|
355
|
+
if file.status == "failed":
|
|
356
|
+
log_error(f"File {file.id} failed to upload.")
|
|
357
|
+
failed = True
|
|
358
|
+
break
|
|
359
|
+
if file.status != "completed":
|
|
360
|
+
all_completed = False
|
|
361
|
+
if all_completed or failed:
|
|
362
|
+
break
|
|
363
|
+
time.sleep(1)
|
|
364
|
+
return vector_store.id
|
|
365
|
+
|
|
366
|
+
def _format_tool_params(
|
|
367
|
+
self, messages: List[Message], tools: Optional[List[Dict[str, Any]]] = None
|
|
368
|
+
) -> List[Dict[str, Any]]:
|
|
369
|
+
"""Format the tool parameters for the OpenAI Responses API."""
|
|
370
|
+
formatted_tools = []
|
|
371
|
+
if tools:
|
|
372
|
+
for _tool in tools:
|
|
373
|
+
if _tool.get("type") == "function":
|
|
374
|
+
_tool_dict = _tool.get("function", {})
|
|
375
|
+
_tool_dict["type"] = "function"
|
|
376
|
+
for prop in _tool_dict.get("parameters", {}).get("properties", {}).values():
|
|
377
|
+
if isinstance(prop.get("type", ""), list):
|
|
378
|
+
prop["type"] = prop["type"][0]
|
|
379
|
+
|
|
380
|
+
formatted_tools.append(_tool_dict)
|
|
381
|
+
else:
|
|
382
|
+
formatted_tools.append(_tool)
|
|
383
|
+
|
|
384
|
+
# Find files to upload to the OpenAI vector database
|
|
385
|
+
file_ids = []
|
|
386
|
+
for message in messages:
|
|
387
|
+
# Upload any attached files to the OpenAI vector database
|
|
388
|
+
if message.files is not None and len(message.files) > 0:
|
|
389
|
+
for file in message.files:
|
|
390
|
+
file_id = self._upload_file(file)
|
|
391
|
+
if file_id is not None:
|
|
392
|
+
file_ids.append(file_id)
|
|
393
|
+
|
|
394
|
+
vector_store_id = self._create_vector_store(file_ids) if file_ids else None
|
|
395
|
+
|
|
396
|
+
# Add the file IDs to the tool parameters
|
|
397
|
+
for _tool in formatted_tools:
|
|
398
|
+
if _tool["type"] == "file_search" and vector_store_id is not None:
|
|
399
|
+
_tool["vector_store_ids"] = [vector_store_id]
|
|
400
|
+
|
|
401
|
+
return formatted_tools
|
|
402
|
+
|
|
403
|
+
def _format_messages(
|
|
404
|
+
self, messages: List[Message], compress_tool_results: bool = False
|
|
405
|
+
) -> List[Union[Dict[str, Any], ResponseReasoningItem]]:
|
|
406
|
+
"""
|
|
407
|
+
Format a message into the format expected by OpenAI.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
messages (List[Message]): The message to format.
|
|
411
|
+
compress_tool_results: Whether to compress tool results.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Dict[str, Any]: The formatted message.
|
|
415
|
+
"""
|
|
416
|
+
formatted_messages: List[Union[Dict[str, Any], ResponseReasoningItem]] = []
|
|
417
|
+
|
|
418
|
+
messages_to_format = messages
|
|
419
|
+
previous_response_id: Optional[str] = None
|
|
420
|
+
|
|
421
|
+
if self._using_reasoning_model() and self.store is not False:
|
|
422
|
+
# Detect whether we're chaining via previous_response_id. If so, we should NOT
|
|
423
|
+
# re-send prior function_call items; the Responses API already has the state and
|
|
424
|
+
# expects only the corresponding function_call_output items.
|
|
425
|
+
|
|
426
|
+
for msg in reversed(messages):
|
|
427
|
+
if (
|
|
428
|
+
msg.role == "assistant"
|
|
429
|
+
and hasattr(msg, "provider_data")
|
|
430
|
+
and msg.provider_data
|
|
431
|
+
and "response_id" in msg.provider_data
|
|
432
|
+
):
|
|
433
|
+
previous_response_id = msg.provider_data["response_id"]
|
|
434
|
+
msg_index = messages.index(msg)
|
|
435
|
+
|
|
436
|
+
# Include messages after this assistant message
|
|
437
|
+
messages_to_format = messages[msg_index + 1 :]
|
|
438
|
+
|
|
439
|
+
break
|
|
440
|
+
|
|
441
|
+
# Build a mapping from function_call id (fc_*) → call_id (call_*) from prior assistant tool_calls
|
|
442
|
+
fc_id_to_call_id: Dict[str, str] = {}
|
|
443
|
+
for msg in messages:
|
|
444
|
+
tool_calls = getattr(msg, "tool_calls", None)
|
|
445
|
+
if tool_calls:
|
|
446
|
+
for tc in tool_calls:
|
|
447
|
+
fc_id = tc.get("id")
|
|
448
|
+
call_id = tc.get("call_id") or fc_id
|
|
449
|
+
if isinstance(fc_id, str) and isinstance(call_id, str):
|
|
450
|
+
fc_id_to_call_id[fc_id] = call_id
|
|
451
|
+
|
|
452
|
+
for message in messages_to_format:
|
|
453
|
+
if message.role in ["user", "system"]:
|
|
454
|
+
message_dict: Dict[str, Any] = {
|
|
455
|
+
"role": self.role_map[message.role],
|
|
456
|
+
"content": message.get_content(use_compressed_content=compress_tool_results),
|
|
457
|
+
}
|
|
458
|
+
message_dict = {k: v for k, v in message_dict.items() if v is not None}
|
|
459
|
+
|
|
460
|
+
# Ignore non-string message content
|
|
461
|
+
# because we assume that the images/audio are already added to the message
|
|
462
|
+
if message.images is not None and len(message.images) > 0:
|
|
463
|
+
# Ignore non-string message content
|
|
464
|
+
# because we assume that the images/audio are already added to the message
|
|
465
|
+
if isinstance(message.content, str):
|
|
466
|
+
message_dict["content"] = [{"type": "input_text", "text": message.content}]
|
|
467
|
+
if message.images is not None:
|
|
468
|
+
message_dict["content"].extend(images_to_message(images=message.images))
|
|
469
|
+
|
|
470
|
+
if message.audio is not None and len(message.audio) > 0:
|
|
471
|
+
log_warning("Audio input is currently unsupported.")
|
|
472
|
+
|
|
473
|
+
if message.videos is not None and len(message.videos) > 0:
|
|
474
|
+
log_warning("Video input is currently unsupported.")
|
|
475
|
+
|
|
476
|
+
formatted_messages.append(message_dict)
|
|
477
|
+
|
|
478
|
+
# Tool call result
|
|
479
|
+
elif message.role == "tool":
|
|
480
|
+
tool_result = message.get_content(use_compressed_content=compress_tool_results)
|
|
481
|
+
|
|
482
|
+
if message.tool_call_id and tool_result is not None:
|
|
483
|
+
function_call_id = message.tool_call_id
|
|
484
|
+
# Normalize: if a fc_* id was provided, translate to its corresponding call_* id
|
|
485
|
+
if isinstance(function_call_id, str) and function_call_id in fc_id_to_call_id:
|
|
486
|
+
call_id_value = fc_id_to_call_id[function_call_id]
|
|
487
|
+
else:
|
|
488
|
+
call_id_value = function_call_id
|
|
489
|
+
formatted_messages.append(
|
|
490
|
+
{"type": "function_call_output", "call_id": call_id_value, "output": tool_result}
|
|
491
|
+
)
|
|
492
|
+
# Tool Calls
|
|
493
|
+
elif message.tool_calls is not None and len(message.tool_calls) > 0:
|
|
494
|
+
# Only skip re-sending prior function_call items when we have a previous_response_id
|
|
495
|
+
# (reasoning models). For non-reasoning models, we must include the prior function_call
|
|
496
|
+
# so the API can associate the subsequent function_call_output by call_id.
|
|
497
|
+
if self._using_reasoning_model() and previous_response_id is not None:
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
for tool_call in message.tool_calls:
|
|
501
|
+
formatted_messages.append(
|
|
502
|
+
{
|
|
503
|
+
"type": "function_call",
|
|
504
|
+
"id": tool_call.get("id"),
|
|
505
|
+
"call_id": tool_call.get("call_id", tool_call.get("id")),
|
|
506
|
+
"name": tool_call["function"]["name"],
|
|
507
|
+
"arguments": tool_call["function"]["arguments"],
|
|
508
|
+
"status": "completed",
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
elif message.role == "assistant":
|
|
512
|
+
# Handle null content by converting to empty string
|
|
513
|
+
content = message.content if message.content is not None else ""
|
|
514
|
+
formatted_messages.append({"role": self.role_map[message.role], "content": content})
|
|
515
|
+
|
|
516
|
+
if self.store is False and hasattr(message, "provider_data") and message.provider_data is not None:
|
|
517
|
+
if message.provider_data.get("reasoning_output") is not None:
|
|
518
|
+
reasoning_output = ResponseReasoningItem.model_validate(
|
|
519
|
+
message.provider_data["reasoning_output"]
|
|
520
|
+
)
|
|
521
|
+
formatted_messages.append(reasoning_output)
|
|
522
|
+
return formatted_messages
|
|
523
|
+
|
|
524
|
+
def count_tokens(
|
|
525
|
+
self,
|
|
526
|
+
messages: List[Message],
|
|
527
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
528
|
+
output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
529
|
+
) -> int:
|
|
530
|
+
try:
|
|
531
|
+
formatted_input = self._format_messages(messages, compress_tool_results=True)
|
|
532
|
+
formatted_tools = self._format_tool_params(messages, tools) if tools else None
|
|
533
|
+
|
|
534
|
+
response = self.get_client().responses.input_tokens.count(
|
|
535
|
+
model=self.id,
|
|
536
|
+
input=formatted_input, # type: ignore
|
|
537
|
+
instructions=self.instructions, # type: ignore
|
|
538
|
+
tools=formatted_tools, # type: ignore
|
|
539
|
+
)
|
|
540
|
+
return response.input_tokens + count_schema_tokens(output_schema, self.id)
|
|
541
|
+
except Exception as e:
|
|
542
|
+
log_warning(f"Failed to count tokens via API: {e}")
|
|
543
|
+
return super().count_tokens(messages, tools, output_schema)
|
|
544
|
+
|
|
545
|
+
async def acount_tokens(
|
|
546
|
+
self,
|
|
547
|
+
messages: List[Message],
|
|
548
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
549
|
+
output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
550
|
+
) -> int:
|
|
551
|
+
"""Async version of count_tokens using the async client."""
|
|
552
|
+
try:
|
|
553
|
+
formatted_input = self._format_messages(messages, compress_tool_results=True)
|
|
554
|
+
formatted_tools = self._format_tool_params(messages, tools) if tools else None
|
|
555
|
+
|
|
556
|
+
response = await self.get_async_client().responses.input_tokens.count(
|
|
557
|
+
model=self.id,
|
|
558
|
+
input=formatted_input, # type: ignore
|
|
559
|
+
instructions=self.instructions, # type: ignore
|
|
560
|
+
tools=formatted_tools, # type: ignore
|
|
561
|
+
)
|
|
562
|
+
return response.input_tokens + count_schema_tokens(output_schema, self.id)
|
|
563
|
+
except Exception as e:
|
|
564
|
+
log_warning(f"Failed to count tokens via API: {e}")
|
|
565
|
+
return await super().acount_tokens(messages, tools, output_schema)
|
|
566
|
+
|
|
567
|
+
def invoke(
|
|
568
|
+
self,
|
|
569
|
+
messages: List[Message],
|
|
570
|
+
assistant_message: Message,
|
|
571
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
572
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
573
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
574
|
+
run_response: Optional[RunOutput] = None,
|
|
575
|
+
compress_tool_results: bool = False,
|
|
576
|
+
) -> ModelResponse:
|
|
577
|
+
"""
|
|
578
|
+
Send a request to the OpenAI Responses API.
|
|
579
|
+
"""
|
|
580
|
+
try:
|
|
581
|
+
request_params = self.get_request_params(
|
|
582
|
+
messages=messages, response_format=response_format, tools=tools, tool_choice=tool_choice
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if run_response and run_response.metrics:
|
|
586
|
+
run_response.metrics.set_time_to_first_token()
|
|
587
|
+
|
|
588
|
+
assistant_message.metrics.start_timer()
|
|
589
|
+
|
|
590
|
+
provider_response = self.get_client().responses.create(
|
|
591
|
+
model=self.id,
|
|
592
|
+
input=self._format_messages(messages, compress_tool_results), # type: ignore
|
|
593
|
+
**request_params,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
assistant_message.metrics.stop_timer()
|
|
597
|
+
|
|
598
|
+
model_response = self._parse_provider_response(provider_response, response_format=response_format)
|
|
599
|
+
|
|
600
|
+
return model_response
|
|
601
|
+
|
|
602
|
+
except RateLimitError as exc:
|
|
603
|
+
log_error(f"Rate limit error from OpenAI API: {exc}")
|
|
604
|
+
error_message = exc.response.json().get("error", {})
|
|
605
|
+
error_message = (
|
|
606
|
+
error_message.get("message", "Unknown model error")
|
|
607
|
+
if isinstance(error_message, dict)
|
|
608
|
+
else error_message
|
|
609
|
+
)
|
|
610
|
+
raise ModelProviderError(
|
|
611
|
+
message=error_message,
|
|
612
|
+
status_code=exc.response.status_code,
|
|
613
|
+
model_name=self.name,
|
|
614
|
+
model_id=self.id,
|
|
615
|
+
) from exc
|
|
616
|
+
except APIConnectionError as exc:
|
|
617
|
+
log_error(f"API connection error from OpenAI API: {exc}")
|
|
618
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
619
|
+
except APIStatusError as exc:
|
|
620
|
+
log_error(f"API status error from OpenAI API: {exc}")
|
|
621
|
+
error_message = exc.response.json().get("error", {})
|
|
622
|
+
error_message = (
|
|
623
|
+
error_message.get("message", "Unknown model error")
|
|
624
|
+
if isinstance(error_message, dict)
|
|
625
|
+
else error_message
|
|
626
|
+
)
|
|
627
|
+
raise ModelProviderError(
|
|
628
|
+
message=error_message,
|
|
629
|
+
status_code=exc.response.status_code,
|
|
630
|
+
model_name=self.name,
|
|
631
|
+
model_id=self.id,
|
|
632
|
+
) from exc
|
|
633
|
+
except ModelAuthenticationError as exc:
|
|
634
|
+
log_error(f"Model authentication error from OpenAI API: {exc}")
|
|
635
|
+
raise exc
|
|
636
|
+
except Exception as exc:
|
|
637
|
+
log_error(f"Error from OpenAI API: {exc}")
|
|
638
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
639
|
+
|
|
640
|
+
async def ainvoke(
|
|
641
|
+
self,
|
|
642
|
+
messages: List[Message],
|
|
643
|
+
assistant_message: Message,
|
|
644
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
645
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
646
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
647
|
+
run_response: Optional[RunOutput] = None,
|
|
648
|
+
compress_tool_results: bool = False,
|
|
649
|
+
) -> ModelResponse:
|
|
650
|
+
"""
|
|
651
|
+
Sends an asynchronous request to the OpenAI Responses API.
|
|
652
|
+
"""
|
|
653
|
+
try:
|
|
654
|
+
request_params = self.get_request_params(
|
|
655
|
+
messages=messages, response_format=response_format, tools=tools, tool_choice=tool_choice
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
if run_response and run_response.metrics:
|
|
659
|
+
run_response.metrics.set_time_to_first_token()
|
|
660
|
+
|
|
661
|
+
assistant_message.metrics.start_timer()
|
|
662
|
+
|
|
663
|
+
provider_response = await self.get_async_client().responses.create(
|
|
664
|
+
model=self.id,
|
|
665
|
+
input=self._format_messages(messages, compress_tool_results), # type: ignore
|
|
666
|
+
**request_params,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
assistant_message.metrics.stop_timer()
|
|
670
|
+
|
|
671
|
+
model_response = self._parse_provider_response(provider_response, response_format=response_format)
|
|
672
|
+
|
|
673
|
+
return model_response
|
|
674
|
+
|
|
675
|
+
except RateLimitError as exc:
|
|
676
|
+
log_error(f"Rate limit error from OpenAI API: {exc}")
|
|
677
|
+
error_message = exc.response.json().get("error", {})
|
|
678
|
+
error_message = (
|
|
679
|
+
error_message.get("message", "Unknown model error")
|
|
680
|
+
if isinstance(error_message, dict)
|
|
681
|
+
else error_message
|
|
682
|
+
)
|
|
683
|
+
raise ModelProviderError(
|
|
684
|
+
message=error_message,
|
|
685
|
+
status_code=exc.response.status_code,
|
|
686
|
+
model_name=self.name,
|
|
687
|
+
model_id=self.id,
|
|
688
|
+
) from exc
|
|
689
|
+
except APIConnectionError as exc:
|
|
690
|
+
log_error(f"API connection error from OpenAI API: {exc}")
|
|
691
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
692
|
+
except APIStatusError as exc:
|
|
693
|
+
log_error(f"API status error from OpenAI API: {exc}")
|
|
694
|
+
error_message = exc.response.json().get("error", {})
|
|
695
|
+
error_message = (
|
|
696
|
+
error_message.get("message", "Unknown model error")
|
|
697
|
+
if isinstance(error_message, dict)
|
|
698
|
+
else error_message
|
|
699
|
+
)
|
|
700
|
+
raise ModelProviderError(
|
|
701
|
+
message=error_message,
|
|
702
|
+
status_code=exc.response.status_code,
|
|
703
|
+
model_name=self.name,
|
|
704
|
+
model_id=self.id,
|
|
705
|
+
) from exc
|
|
706
|
+
except ModelAuthenticationError as exc:
|
|
707
|
+
log_error(f"Model authentication error from OpenAI API: {exc}")
|
|
708
|
+
raise exc
|
|
709
|
+
except Exception as exc:
|
|
710
|
+
log_error(f"Error from OpenAI API: {exc}")
|
|
711
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
712
|
+
|
|
713
|
+
def invoke_stream(
|
|
714
|
+
self,
|
|
715
|
+
messages: List[Message],
|
|
716
|
+
assistant_message: Message,
|
|
717
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
718
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
719
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
720
|
+
run_response: Optional[RunOutput] = None,
|
|
721
|
+
compress_tool_results: bool = False,
|
|
722
|
+
) -> Iterator[ModelResponse]:
|
|
723
|
+
"""
|
|
724
|
+
Send a streaming request to the OpenAI Responses API.
|
|
725
|
+
"""
|
|
726
|
+
try:
|
|
727
|
+
request_params = self.get_request_params(
|
|
728
|
+
messages=messages, response_format=response_format, tools=tools, tool_choice=tool_choice
|
|
729
|
+
)
|
|
730
|
+
tool_use: Dict[str, Any] = {}
|
|
731
|
+
|
|
732
|
+
if run_response and run_response.metrics:
|
|
733
|
+
run_response.metrics.set_time_to_first_token()
|
|
734
|
+
|
|
735
|
+
assistant_message.metrics.start_timer()
|
|
736
|
+
|
|
737
|
+
for chunk in self.get_client().responses.create(
|
|
738
|
+
model=self.id,
|
|
739
|
+
input=self._format_messages(messages, compress_tool_results), # type: ignore
|
|
740
|
+
stream=True,
|
|
741
|
+
**request_params,
|
|
742
|
+
):
|
|
743
|
+
model_response, tool_use = self._parse_provider_response_delta(
|
|
744
|
+
stream_event=chunk, # type: ignore
|
|
745
|
+
assistant_message=assistant_message,
|
|
746
|
+
tool_use=tool_use, # type: ignore
|
|
747
|
+
)
|
|
748
|
+
yield model_response
|
|
749
|
+
|
|
750
|
+
assistant_message.metrics.stop_timer()
|
|
751
|
+
|
|
752
|
+
except RateLimitError as exc:
|
|
753
|
+
log_error(f"Rate limit error from OpenAI API: {exc}")
|
|
754
|
+
error_message = exc.response.json().get("error", {})
|
|
755
|
+
error_message = (
|
|
756
|
+
error_message.get("message", "Unknown model error")
|
|
757
|
+
if isinstance(error_message, dict)
|
|
758
|
+
else error_message
|
|
759
|
+
)
|
|
760
|
+
raise ModelProviderError(
|
|
761
|
+
message=error_message,
|
|
762
|
+
status_code=exc.response.status_code,
|
|
763
|
+
model_name=self.name,
|
|
764
|
+
model_id=self.id,
|
|
765
|
+
) from exc
|
|
766
|
+
except APIConnectionError as exc:
|
|
767
|
+
log_error(f"API connection error from OpenAI API: {exc}")
|
|
768
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
769
|
+
except APIStatusError as exc:
|
|
770
|
+
log_error(f"API status error from OpenAI API: {exc}")
|
|
771
|
+
error_message = exc.response.json().get("error", {})
|
|
772
|
+
error_message = (
|
|
773
|
+
error_message.get("message", "Unknown model error")
|
|
774
|
+
if isinstance(error_message, dict)
|
|
775
|
+
else error_message
|
|
776
|
+
)
|
|
777
|
+
raise ModelProviderError(
|
|
778
|
+
message=error_message,
|
|
779
|
+
status_code=exc.response.status_code,
|
|
780
|
+
model_name=self.name,
|
|
781
|
+
model_id=self.id,
|
|
782
|
+
) from exc
|
|
783
|
+
except ModelAuthenticationError as exc:
|
|
784
|
+
log_error(f"Model authentication error from OpenAI API: {exc}")
|
|
785
|
+
raise exc
|
|
786
|
+
except Exception as exc:
|
|
787
|
+
log_error(f"Error from OpenAI API: {exc}")
|
|
788
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
789
|
+
|
|
790
|
+
async def ainvoke_stream(
|
|
791
|
+
self,
|
|
792
|
+
messages: List[Message],
|
|
793
|
+
assistant_message: Message,
|
|
794
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
795
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
796
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
797
|
+
run_response: Optional[RunOutput] = None,
|
|
798
|
+
compress_tool_results: bool = False,
|
|
799
|
+
) -> AsyncIterator[ModelResponse]:
|
|
800
|
+
"""
|
|
801
|
+
Sends an asynchronous streaming request to the OpenAI Responses API.
|
|
802
|
+
"""
|
|
803
|
+
try:
|
|
804
|
+
request_params = self.get_request_params(
|
|
805
|
+
messages=messages, response_format=response_format, tools=tools, tool_choice=tool_choice
|
|
806
|
+
)
|
|
807
|
+
tool_use: Dict[str, Any] = {}
|
|
808
|
+
|
|
809
|
+
if run_response and run_response.metrics:
|
|
810
|
+
run_response.metrics.set_time_to_first_token()
|
|
811
|
+
|
|
812
|
+
assistant_message.metrics.start_timer()
|
|
813
|
+
|
|
814
|
+
async_stream = await self.get_async_client().responses.create(
|
|
815
|
+
model=self.id,
|
|
816
|
+
input=self._format_messages(messages, compress_tool_results), # type: ignore
|
|
817
|
+
stream=True,
|
|
818
|
+
**request_params,
|
|
819
|
+
)
|
|
820
|
+
async for chunk in async_stream: # type: ignore
|
|
821
|
+
model_response, tool_use = self._parse_provider_response_delta(chunk, assistant_message, tool_use) # type: ignore
|
|
822
|
+
yield model_response
|
|
823
|
+
|
|
824
|
+
assistant_message.metrics.stop_timer()
|
|
825
|
+
|
|
826
|
+
except RateLimitError as exc:
|
|
827
|
+
log_error(f"Rate limit error from OpenAI API: {exc}")
|
|
828
|
+
error_message = exc.response.json().get("error", {})
|
|
829
|
+
error_message = (
|
|
830
|
+
error_message.get("message", "Unknown model error")
|
|
831
|
+
if isinstance(error_message, dict)
|
|
832
|
+
else error_message
|
|
833
|
+
)
|
|
834
|
+
raise ModelProviderError(
|
|
835
|
+
message=error_message,
|
|
836
|
+
status_code=exc.response.status_code,
|
|
837
|
+
model_name=self.name,
|
|
838
|
+
model_id=self.id,
|
|
839
|
+
) from exc
|
|
840
|
+
except APIConnectionError as exc:
|
|
841
|
+
log_error(f"API connection error from OpenAI API: {exc}")
|
|
842
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
843
|
+
except APIStatusError as exc:
|
|
844
|
+
log_error(f"API status error from OpenAI API: {exc}")
|
|
845
|
+
error_message = exc.response.json().get("error", {})
|
|
846
|
+
error_message = (
|
|
847
|
+
error_message.get("message", "Unknown model error")
|
|
848
|
+
if isinstance(error_message, dict)
|
|
849
|
+
else error_message
|
|
850
|
+
)
|
|
851
|
+
raise ModelProviderError(
|
|
852
|
+
message=error_message,
|
|
853
|
+
status_code=exc.response.status_code,
|
|
854
|
+
model_name=self.name,
|
|
855
|
+
model_id=self.id,
|
|
856
|
+
) from exc
|
|
857
|
+
except ModelAuthenticationError as exc:
|
|
858
|
+
log_error(f"Model authentication error from OpenAI API: {exc}")
|
|
859
|
+
raise exc
|
|
860
|
+
except Exception as exc:
|
|
861
|
+
log_error(f"Error from OpenAI API: {exc}")
|
|
862
|
+
raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
|
|
863
|
+
|
|
864
|
+
def format_function_call_results(
|
|
865
|
+
self,
|
|
866
|
+
messages: List[Message],
|
|
867
|
+
function_call_results: List[Message],
|
|
868
|
+
tool_call_ids: List[str],
|
|
869
|
+
compress_tool_results: bool = False,
|
|
870
|
+
) -> None:
|
|
871
|
+
"""
|
|
872
|
+
Handle the results of function calls.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
messages (List[Message]): The list of conversation messages.
|
|
876
|
+
function_call_results (List[Message]): The results of the function calls.
|
|
877
|
+
tool_ids (List[str]): The tool ids.
|
|
878
|
+
compress_tool_results (bool): Whether to compress tool results.
|
|
879
|
+
"""
|
|
880
|
+
if len(function_call_results) > 0:
|
|
881
|
+
for _fc_message_index, _fc_message in enumerate(function_call_results):
|
|
882
|
+
_fc_message.tool_call_id = tool_call_ids[_fc_message_index]
|
|
883
|
+
messages.append(_fc_message)
|
|
884
|
+
|
|
885
|
+
def _parse_provider_response(self, response: Response, **kwargs) -> ModelResponse:
|
|
886
|
+
"""
|
|
887
|
+
Parse the OpenAI response into a ModelResponse.
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
response: Response from invoke() method
|
|
891
|
+
|
|
892
|
+
Returns:
|
|
893
|
+
ModelResponse: Parsed response data
|
|
894
|
+
"""
|
|
895
|
+
model_response = ModelResponse()
|
|
896
|
+
|
|
897
|
+
if response.error:
|
|
898
|
+
raise ModelProviderError(
|
|
899
|
+
message=response.error.message,
|
|
900
|
+
model_name=self.name,
|
|
901
|
+
model_id=self.id,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# Store the response ID for continuity
|
|
905
|
+
if response.id:
|
|
906
|
+
if model_response.provider_data is None:
|
|
907
|
+
model_response.provider_data = {}
|
|
908
|
+
model_response.provider_data["response_id"] = response.id
|
|
909
|
+
|
|
910
|
+
# Add role
|
|
911
|
+
model_response.role = "assistant"
|
|
912
|
+
reasoning_summary: Optional[str] = None
|
|
913
|
+
|
|
914
|
+
for output in response.output:
|
|
915
|
+
# Add content
|
|
916
|
+
if output.type == "message":
|
|
917
|
+
model_response.content = response.output_text
|
|
918
|
+
|
|
919
|
+
# Add citations
|
|
920
|
+
citations = Citations()
|
|
921
|
+
for content in output.content:
|
|
922
|
+
if content.type == "output_text" and content.annotations:
|
|
923
|
+
citations.raw = [annotation.model_dump() for annotation in content.annotations]
|
|
924
|
+
for annotation in content.annotations:
|
|
925
|
+
if annotation.type == "url_citation":
|
|
926
|
+
if citations.urls is None:
|
|
927
|
+
citations.urls = []
|
|
928
|
+
citations.urls.append(UrlCitation(url=annotation.url, title=annotation.title))
|
|
929
|
+
if citations.urls or citations.documents:
|
|
930
|
+
model_response.citations = citations
|
|
931
|
+
|
|
932
|
+
# Add tool calls
|
|
933
|
+
elif output.type == "function_call":
|
|
934
|
+
if model_response.tool_calls is None:
|
|
935
|
+
model_response.tool_calls = []
|
|
936
|
+
model_response.tool_calls.append(
|
|
937
|
+
{
|
|
938
|
+
"id": output.id,
|
|
939
|
+
# Store additional call_id from OpenAI responses
|
|
940
|
+
"call_id": output.call_id or output.id,
|
|
941
|
+
"type": "function",
|
|
942
|
+
"function": {
|
|
943
|
+
"name": output.name,
|
|
944
|
+
"arguments": output.arguments,
|
|
945
|
+
},
|
|
946
|
+
}
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
model_response.extra = model_response.extra or {}
|
|
950
|
+
model_response.extra.setdefault("tool_call_ids", []).append(output.call_id)
|
|
951
|
+
|
|
952
|
+
# Handle reasoning output items
|
|
953
|
+
elif output.type == "reasoning":
|
|
954
|
+
# Save encrypted reasoning content for ZDR mode
|
|
955
|
+
if self.store is False:
|
|
956
|
+
if model_response.provider_data is None:
|
|
957
|
+
model_response.provider_data = {}
|
|
958
|
+
model_response.provider_data["reasoning_output"] = output.model_dump(exclude_none=True)
|
|
959
|
+
|
|
960
|
+
if reasoning_summaries := getattr(output, "summary", None):
|
|
961
|
+
for summary in reasoning_summaries:
|
|
962
|
+
if isinstance(summary, dict):
|
|
963
|
+
summary_text = summary.get("text")
|
|
964
|
+
else:
|
|
965
|
+
summary_text = getattr(summary, "text", None)
|
|
966
|
+
if summary_text:
|
|
967
|
+
reasoning_summary = (reasoning_summary or "") + summary_text
|
|
968
|
+
|
|
969
|
+
# Add reasoning content
|
|
970
|
+
if reasoning_summary is not None:
|
|
971
|
+
model_response.reasoning_content = reasoning_summary
|
|
972
|
+
elif self.reasoning is not None:
|
|
973
|
+
model_response.reasoning_content = response.output_text
|
|
974
|
+
|
|
975
|
+
# Add metrics
|
|
976
|
+
if response.usage is not None:
|
|
977
|
+
model_response.response_usage = self._get_metrics(response.usage)
|
|
978
|
+
|
|
979
|
+
return model_response
|
|
980
|
+
|
|
981
|
+
def _parse_provider_response_delta(
|
|
982
|
+
self, stream_event: ResponseStreamEvent, assistant_message: Message, tool_use: Dict[str, Any]
|
|
983
|
+
) -> Tuple[ModelResponse, Dict[str, Any]]:
|
|
984
|
+
"""
|
|
985
|
+
Parse the streaming response from the model provider into a ModelResponse object.
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
response: Raw response chunk from the model provider
|
|
989
|
+
|
|
990
|
+
Returns:
|
|
991
|
+
ModelResponse: Parsed response delta
|
|
992
|
+
"""
|
|
993
|
+
model_response = ModelResponse()
|
|
994
|
+
|
|
995
|
+
# 1. Add response ID
|
|
996
|
+
if stream_event.type == "response.created":
|
|
997
|
+
if stream_event.response.id:
|
|
998
|
+
if model_response.provider_data is None:
|
|
999
|
+
model_response.provider_data = {}
|
|
1000
|
+
model_response.provider_data["response_id"] = stream_event.response.id
|
|
1001
|
+
if not assistant_message.metrics.time_to_first_token:
|
|
1002
|
+
assistant_message.metrics.set_time_to_first_token()
|
|
1003
|
+
|
|
1004
|
+
# 2. Add citations
|
|
1005
|
+
elif stream_event.type == "response.output_text.annotation.added":
|
|
1006
|
+
if model_response.citations is None:
|
|
1007
|
+
model_response.citations = Citations(raw=[stream_event.annotation])
|
|
1008
|
+
else:
|
|
1009
|
+
model_response.citations.raw.append(stream_event.annotation) # type: ignore
|
|
1010
|
+
|
|
1011
|
+
if isinstance(stream_event.annotation, dict):
|
|
1012
|
+
if stream_event.annotation.get("type") == "url_citation":
|
|
1013
|
+
if model_response.citations.urls is None:
|
|
1014
|
+
model_response.citations.urls = []
|
|
1015
|
+
model_response.citations.urls.append(
|
|
1016
|
+
UrlCitation(url=stream_event.annotation.get("url"), title=stream_event.annotation.get("title"))
|
|
1017
|
+
)
|
|
1018
|
+
else:
|
|
1019
|
+
if stream_event.annotation.type == "url_citation": # type: ignore
|
|
1020
|
+
if model_response.citations.urls is None:
|
|
1021
|
+
model_response.citations.urls = []
|
|
1022
|
+
model_response.citations.urls.append(
|
|
1023
|
+
UrlCitation(url=stream_event.annotation.url, title=stream_event.annotation.title) # type: ignore
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
# 3. Add content
|
|
1027
|
+
elif stream_event.type == "response.output_text.delta":
|
|
1028
|
+
model_response.content = stream_event.delta
|
|
1029
|
+
|
|
1030
|
+
# Treat the output_text deltas as reasoning content if the reasoning summary is not requested.
|
|
1031
|
+
if self.reasoning is not None and self.reasoning_summary is None:
|
|
1032
|
+
model_response.reasoning_content = stream_event.delta
|
|
1033
|
+
|
|
1034
|
+
# 4. Add tool calls information
|
|
1035
|
+
|
|
1036
|
+
# 4.1 Add starting tool call
|
|
1037
|
+
elif stream_event.type == "response.output_item.added":
|
|
1038
|
+
item = stream_event.item
|
|
1039
|
+
if item.type == "function_call":
|
|
1040
|
+
tool_use = {
|
|
1041
|
+
"id": getattr(item, "id", None),
|
|
1042
|
+
"call_id": getattr(item, "call_id", None) or getattr(item, "id", None),
|
|
1043
|
+
"type": "function",
|
|
1044
|
+
"function": {
|
|
1045
|
+
"name": item.name,
|
|
1046
|
+
"arguments": item.arguments,
|
|
1047
|
+
},
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
# 4.2 Add tool call arguments
|
|
1051
|
+
elif stream_event.type == "response.function_call_arguments.delta":
|
|
1052
|
+
tool_use["function"]["arguments"] += stream_event.delta
|
|
1053
|
+
|
|
1054
|
+
# 4.3 Add tool call completion data
|
|
1055
|
+
elif stream_event.type == "response.output_item.done" and tool_use:
|
|
1056
|
+
model_response.tool_calls = [tool_use]
|
|
1057
|
+
if assistant_message.tool_calls is None:
|
|
1058
|
+
assistant_message.tool_calls = []
|
|
1059
|
+
assistant_message.tool_calls.append(tool_use)
|
|
1060
|
+
|
|
1061
|
+
model_response.extra = model_response.extra or {}
|
|
1062
|
+
model_response.extra.setdefault("tool_call_ids", []).append(tool_use["call_id"])
|
|
1063
|
+
tool_use = {}
|
|
1064
|
+
|
|
1065
|
+
# 5. Add metrics
|
|
1066
|
+
elif stream_event.type == "response.completed":
|
|
1067
|
+
model_response = ModelResponse()
|
|
1068
|
+
|
|
1069
|
+
# Handle reasoning output items
|
|
1070
|
+
if self.reasoning_summary is not None or self.store is False:
|
|
1071
|
+
summary_text: str = ""
|
|
1072
|
+
for out in getattr(stream_event.response, "output", []) or []:
|
|
1073
|
+
if getattr(out, "type", None) == "reasoning":
|
|
1074
|
+
# In ZDR mode (store=False), store reasoning data for next request
|
|
1075
|
+
if self.store is False and hasattr(out, "encrypted_content"):
|
|
1076
|
+
if model_response.provider_data is None:
|
|
1077
|
+
model_response.provider_data = {}
|
|
1078
|
+
# Store the complete output item
|
|
1079
|
+
model_response.provider_data["reasoning_output"] = out.model_dump(exclude_none=True)
|
|
1080
|
+
if self.reasoning_summary is not None:
|
|
1081
|
+
summaries = getattr(out, "summary", None)
|
|
1082
|
+
if summaries:
|
|
1083
|
+
for s in summaries:
|
|
1084
|
+
text_val = s.get("text") if isinstance(s, dict) else getattr(s, "text", None)
|
|
1085
|
+
if text_val:
|
|
1086
|
+
if summary_text:
|
|
1087
|
+
summary_text += "\n\n"
|
|
1088
|
+
summary_text += text_val
|
|
1089
|
+
|
|
1090
|
+
if summary_text:
|
|
1091
|
+
model_response.reasoning_content = summary_text
|
|
1092
|
+
|
|
1093
|
+
# Add metrics
|
|
1094
|
+
if stream_event.response.usage is not None:
|
|
1095
|
+
model_response.response_usage = self._get_metrics(stream_event.response.usage)
|
|
1096
|
+
|
|
1097
|
+
return model_response, tool_use
|
|
1098
|
+
|
|
1099
|
+
def _get_metrics(self, response_usage: ResponseUsage) -> Metrics:
|
|
1100
|
+
"""
|
|
1101
|
+
Parse the given OpenAI-specific usage into an Agno Metrics object.
|
|
1102
|
+
|
|
1103
|
+
Args:
|
|
1104
|
+
response: The response from the provider.
|
|
1105
|
+
|
|
1106
|
+
Returns:
|
|
1107
|
+
Metrics: Parsed metrics data
|
|
1108
|
+
"""
|
|
1109
|
+
metrics = Metrics()
|
|
1110
|
+
|
|
1111
|
+
metrics.input_tokens = response_usage.input_tokens or 0
|
|
1112
|
+
metrics.output_tokens = response_usage.output_tokens or 0
|
|
1113
|
+
metrics.total_tokens = response_usage.total_tokens or 0
|
|
1114
|
+
|
|
1115
|
+
if input_tokens_details := response_usage.input_tokens_details:
|
|
1116
|
+
metrics.cache_read_tokens = input_tokens_details.cached_tokens
|
|
1117
|
+
|
|
1118
|
+
if output_tokens_details := response_usage.output_tokens_details:
|
|
1119
|
+
metrics.reasoning_tokens = output_tokens_details.reasoning_tokens
|
|
1120
|
+
|
|
1121
|
+
return metrics
|