agno 2.2.13__py3-none-any.whl → 2.4.3__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/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- 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/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
agno/os/router.py
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import TYPE_CHECKING,
|
|
3
|
-
from uuid import uuid4
|
|
2
|
+
from typing import TYPE_CHECKING, List, cast
|
|
4
3
|
|
|
5
4
|
from fastapi import (
|
|
6
5
|
APIRouter,
|
|
7
6
|
Depends,
|
|
8
|
-
File,
|
|
9
|
-
Form,
|
|
10
7
|
HTTPException,
|
|
11
|
-
Request,
|
|
12
|
-
UploadFile,
|
|
13
8
|
WebSocket,
|
|
14
9
|
)
|
|
15
|
-
from fastapi.responses import JSONResponse, StreamingResponse
|
|
16
|
-
from pydantic import BaseModel
|
|
17
10
|
|
|
18
|
-
from agno.
|
|
19
|
-
from agno.exceptions import InputCheckError, OutputCheckError
|
|
20
|
-
from agno.media import Audio, Image, Video
|
|
21
|
-
from agno.media import File as FileMedia
|
|
11
|
+
from agno.exceptions import RemoteServerUnavailableError
|
|
22
12
|
from agno.os.auth import get_authentication_dependency, validate_websocket_token
|
|
13
|
+
from agno.os.managers import websocket_manager
|
|
14
|
+
from agno.os.routers.workflows.router import handle_workflow_subscription, handle_workflow_via_websocket
|
|
23
15
|
from agno.os.schema import (
|
|
24
|
-
AgentResponse,
|
|
25
16
|
AgentSummaryResponse,
|
|
26
17
|
BadRequestResponse,
|
|
27
18
|
ConfigResponse,
|
|
@@ -29,559 +20,18 @@ from agno.os.schema import (
|
|
|
29
20
|
InternalServerErrorResponse,
|
|
30
21
|
Model,
|
|
31
22
|
NotFoundResponse,
|
|
32
|
-
TeamResponse,
|
|
33
23
|
TeamSummaryResponse,
|
|
34
24
|
UnauthenticatedResponse,
|
|
35
25
|
ValidationErrorResponse,
|
|
36
|
-
WorkflowResponse,
|
|
37
26
|
WorkflowSummaryResponse,
|
|
38
27
|
)
|
|
39
28
|
from agno.os.settings import AgnoAPISettings
|
|
40
|
-
from agno.
|
|
41
|
-
get_agent_by_id,
|
|
42
|
-
get_team_by_id,
|
|
43
|
-
get_workflow_by_id,
|
|
44
|
-
process_audio,
|
|
45
|
-
process_document,
|
|
46
|
-
process_image,
|
|
47
|
-
process_video,
|
|
48
|
-
)
|
|
49
|
-
from agno.run.agent import RunErrorEvent, RunOutput, RunOutputEvent
|
|
50
|
-
from agno.run.team import RunErrorEvent as TeamRunErrorEvent
|
|
51
|
-
from agno.run.team import TeamRunOutputEvent
|
|
52
|
-
from agno.run.workflow import WorkflowErrorEvent, WorkflowRunOutput, WorkflowRunOutputEvent
|
|
53
|
-
from agno.team.team import Team
|
|
54
|
-
from agno.utils.log import log_debug, log_error, log_warning, logger
|
|
55
|
-
from agno.workflow.workflow import Workflow
|
|
29
|
+
from agno.utils.log import logger
|
|
56
30
|
|
|
57
31
|
if TYPE_CHECKING:
|
|
58
32
|
from agno.os.app import AgentOS
|
|
59
33
|
|
|
60
34
|
|
|
61
|
-
async def _get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
|
|
62
|
-
"""Given a Request and an endpoint function, return a dictionary with all extra form data fields.
|
|
63
|
-
Args:
|
|
64
|
-
request: The FastAPI Request object
|
|
65
|
-
endpoint_func: The function exposing the endpoint that received the request
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
A dictionary of kwargs
|
|
69
|
-
"""
|
|
70
|
-
import inspect
|
|
71
|
-
|
|
72
|
-
form_data = await request.form()
|
|
73
|
-
sig = inspect.signature(endpoint_func)
|
|
74
|
-
known_fields = set(sig.parameters.keys())
|
|
75
|
-
kwargs: Dict[str, Any] = {key: value for key, value in form_data.items() if key not in known_fields}
|
|
76
|
-
|
|
77
|
-
# Handle JSON parameters. They are passed as strings and need to be deserialized.
|
|
78
|
-
if session_state := kwargs.get("session_state"):
|
|
79
|
-
try:
|
|
80
|
-
if isinstance(session_state, str):
|
|
81
|
-
session_state_dict = json.loads(session_state) # type: ignore
|
|
82
|
-
kwargs["session_state"] = session_state_dict
|
|
83
|
-
except json.JSONDecodeError:
|
|
84
|
-
kwargs.pop("session_state")
|
|
85
|
-
log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
|
|
86
|
-
|
|
87
|
-
if dependencies := kwargs.get("dependencies"):
|
|
88
|
-
try:
|
|
89
|
-
if isinstance(dependencies, str):
|
|
90
|
-
dependencies_dict = json.loads(dependencies) # type: ignore
|
|
91
|
-
kwargs["dependencies"] = dependencies_dict
|
|
92
|
-
except json.JSONDecodeError:
|
|
93
|
-
kwargs.pop("dependencies")
|
|
94
|
-
log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
|
|
95
|
-
|
|
96
|
-
if metadata := kwargs.get("metadata"):
|
|
97
|
-
try:
|
|
98
|
-
if isinstance(metadata, str):
|
|
99
|
-
metadata_dict = json.loads(metadata) # type: ignore
|
|
100
|
-
kwargs["metadata"] = metadata_dict
|
|
101
|
-
except json.JSONDecodeError:
|
|
102
|
-
kwargs.pop("metadata")
|
|
103
|
-
log_warning(f"Invalid metadata parameter couldn't be loaded: {metadata}")
|
|
104
|
-
|
|
105
|
-
if knowledge_filters := kwargs.get("knowledge_filters"):
|
|
106
|
-
try:
|
|
107
|
-
if isinstance(knowledge_filters, str):
|
|
108
|
-
knowledge_filters_dict = json.loads(knowledge_filters) # type: ignore
|
|
109
|
-
|
|
110
|
-
# Try to deserialize FilterExpr objects
|
|
111
|
-
from agno.filters import from_dict
|
|
112
|
-
|
|
113
|
-
# Check if it's a single FilterExpr dict or a list of FilterExpr dicts
|
|
114
|
-
if isinstance(knowledge_filters_dict, dict) and "op" in knowledge_filters_dict:
|
|
115
|
-
# Single FilterExpr - convert to list format
|
|
116
|
-
kwargs["knowledge_filters"] = [from_dict(knowledge_filters_dict)]
|
|
117
|
-
elif isinstance(knowledge_filters_dict, list):
|
|
118
|
-
# List of FilterExprs or mixed content
|
|
119
|
-
deserialized = []
|
|
120
|
-
for item in knowledge_filters_dict:
|
|
121
|
-
if isinstance(item, dict) and "op" in item:
|
|
122
|
-
deserialized.append(from_dict(item))
|
|
123
|
-
else:
|
|
124
|
-
# Keep non-FilterExpr items as-is
|
|
125
|
-
deserialized.append(item)
|
|
126
|
-
kwargs["knowledge_filters"] = deserialized
|
|
127
|
-
else:
|
|
128
|
-
# Regular dict filter
|
|
129
|
-
kwargs["knowledge_filters"] = knowledge_filters_dict
|
|
130
|
-
except json.JSONDecodeError:
|
|
131
|
-
kwargs.pop("knowledge_filters")
|
|
132
|
-
log_warning(f"Invalid knowledge_filters parameter couldn't be loaded: {knowledge_filters}")
|
|
133
|
-
except ValueError as e:
|
|
134
|
-
# Filter deserialization failed
|
|
135
|
-
kwargs.pop("knowledge_filters")
|
|
136
|
-
log_warning(f"Invalid FilterExpr in knowledge_filters: {e}")
|
|
137
|
-
|
|
138
|
-
# Parse boolean and null values
|
|
139
|
-
for key, value in kwargs.items():
|
|
140
|
-
if isinstance(value, str) and value.lower() in ["true", "false"]:
|
|
141
|
-
kwargs[key] = value.lower() == "true"
|
|
142
|
-
elif isinstance(value, str) and value.lower() in ["null", "none"]:
|
|
143
|
-
kwargs[key] = None
|
|
144
|
-
|
|
145
|
-
return kwargs
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def format_sse_event(event: Union[RunOutputEvent, TeamRunOutputEvent, WorkflowRunOutputEvent]) -> str:
|
|
149
|
-
"""Parse JSON data into SSE-compliant format.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
event_dict: Dictionary containing the event data
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
SSE-formatted response:
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
event: EventName
|
|
159
|
-
data: { ... }
|
|
160
|
-
|
|
161
|
-
event: AnotherEventName
|
|
162
|
-
data: { ... }
|
|
163
|
-
```
|
|
164
|
-
"""
|
|
165
|
-
try:
|
|
166
|
-
# Parse the JSON to extract the event type
|
|
167
|
-
event_type = event.event or "message"
|
|
168
|
-
|
|
169
|
-
# Serialize to valid JSON with double quotes and no newlines
|
|
170
|
-
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
171
|
-
|
|
172
|
-
return f"event: {event_type}\ndata: {clean_json}\n\n"
|
|
173
|
-
except json.JSONDecodeError:
|
|
174
|
-
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
175
|
-
return f"event: message\ndata: {clean_json}\n\n"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
class WebSocketManager:
|
|
179
|
-
"""Manages WebSocket connections for workflow runs"""
|
|
180
|
-
|
|
181
|
-
active_connections: Dict[str, WebSocket] # {run_id: websocket}
|
|
182
|
-
authenticated_connections: Dict[WebSocket, bool] # {websocket: is_authenticated}
|
|
183
|
-
|
|
184
|
-
def __init__(
|
|
185
|
-
self,
|
|
186
|
-
active_connections: Optional[Dict[str, WebSocket]] = None,
|
|
187
|
-
):
|
|
188
|
-
# Store active connections: {run_id: websocket}
|
|
189
|
-
self.active_connections = active_connections or {}
|
|
190
|
-
# Track authentication state for each websocket
|
|
191
|
-
self.authenticated_connections = {}
|
|
192
|
-
|
|
193
|
-
async def connect(self, websocket: WebSocket, requires_auth: bool = True):
|
|
194
|
-
"""Accept WebSocket connection"""
|
|
195
|
-
await websocket.accept()
|
|
196
|
-
logger.debug("WebSocket connected")
|
|
197
|
-
|
|
198
|
-
# If auth is not required, mark as authenticated immediately
|
|
199
|
-
self.authenticated_connections[websocket] = not requires_auth
|
|
200
|
-
|
|
201
|
-
# Send connection confirmation with auth requirement info
|
|
202
|
-
await websocket.send_text(
|
|
203
|
-
json.dumps(
|
|
204
|
-
{
|
|
205
|
-
"event": "connected",
|
|
206
|
-
"message": (
|
|
207
|
-
"Connected to workflow events. Please authenticate to continue."
|
|
208
|
-
if requires_auth
|
|
209
|
-
else "Connected to workflow events. Authentication not required."
|
|
210
|
-
),
|
|
211
|
-
"requires_auth": requires_auth,
|
|
212
|
-
}
|
|
213
|
-
)
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
async def authenticate_websocket(self, websocket: WebSocket):
|
|
217
|
-
"""Mark a WebSocket connection as authenticated"""
|
|
218
|
-
self.authenticated_connections[websocket] = True
|
|
219
|
-
logger.debug("WebSocket authenticated")
|
|
220
|
-
|
|
221
|
-
# Send authentication confirmation
|
|
222
|
-
await websocket.send_text(
|
|
223
|
-
json.dumps(
|
|
224
|
-
{
|
|
225
|
-
"event": "authenticated",
|
|
226
|
-
"message": "Authentication successful. You can now send commands.",
|
|
227
|
-
}
|
|
228
|
-
)
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
def is_authenticated(self, websocket: WebSocket) -> bool:
|
|
232
|
-
"""Check if a WebSocket connection is authenticated"""
|
|
233
|
-
return self.authenticated_connections.get(websocket, False)
|
|
234
|
-
|
|
235
|
-
async def register_workflow_websocket(self, run_id: str, websocket: WebSocket):
|
|
236
|
-
"""Register a workflow run with its WebSocket connection"""
|
|
237
|
-
self.active_connections[run_id] = websocket
|
|
238
|
-
logger.debug(f"Registered WebSocket for run_id: {run_id}")
|
|
239
|
-
|
|
240
|
-
async def disconnect_by_run_id(self, run_id: str):
|
|
241
|
-
"""Remove WebSocket connection by run_id"""
|
|
242
|
-
if run_id in self.active_connections:
|
|
243
|
-
websocket = self.active_connections[run_id]
|
|
244
|
-
del self.active_connections[run_id]
|
|
245
|
-
# Clean up authentication state
|
|
246
|
-
if websocket in self.authenticated_connections:
|
|
247
|
-
del self.authenticated_connections[websocket]
|
|
248
|
-
logger.debug(f"WebSocket disconnected for run_id: {run_id}")
|
|
249
|
-
|
|
250
|
-
async def disconnect_websocket(self, websocket: WebSocket):
|
|
251
|
-
"""Remove WebSocket connection and clean up all associated state"""
|
|
252
|
-
# Remove from authenticated connections
|
|
253
|
-
if websocket in self.authenticated_connections:
|
|
254
|
-
del self.authenticated_connections[websocket]
|
|
255
|
-
|
|
256
|
-
# Remove from active connections
|
|
257
|
-
runs_to_remove = [run_id for run_id, ws in self.active_connections.items() if ws == websocket]
|
|
258
|
-
for run_id in runs_to_remove:
|
|
259
|
-
del self.active_connections[run_id]
|
|
260
|
-
|
|
261
|
-
logger.debug("WebSocket disconnected and cleaned up")
|
|
262
|
-
|
|
263
|
-
async def get_websocket_for_run(self, run_id: str) -> Optional[WebSocket]:
|
|
264
|
-
"""Get WebSocket connection for a workflow run"""
|
|
265
|
-
return self.active_connections.get(run_id)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
# Global manager instance
|
|
269
|
-
websocket_manager = WebSocketManager(
|
|
270
|
-
active_connections={},
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
async def agent_response_streamer(
|
|
275
|
-
agent: Agent,
|
|
276
|
-
message: str,
|
|
277
|
-
session_id: Optional[str] = None,
|
|
278
|
-
user_id: Optional[str] = None,
|
|
279
|
-
images: Optional[List[Image]] = None,
|
|
280
|
-
audio: Optional[List[Audio]] = None,
|
|
281
|
-
videos: Optional[List[Video]] = None,
|
|
282
|
-
files: Optional[List[FileMedia]] = None,
|
|
283
|
-
**kwargs: Any,
|
|
284
|
-
) -> AsyncGenerator:
|
|
285
|
-
try:
|
|
286
|
-
run_response = agent.arun(
|
|
287
|
-
input=message,
|
|
288
|
-
session_id=session_id,
|
|
289
|
-
user_id=user_id,
|
|
290
|
-
images=images,
|
|
291
|
-
audio=audio,
|
|
292
|
-
videos=videos,
|
|
293
|
-
files=files,
|
|
294
|
-
stream=True,
|
|
295
|
-
stream_events=True,
|
|
296
|
-
**kwargs,
|
|
297
|
-
)
|
|
298
|
-
async for run_response_chunk in run_response:
|
|
299
|
-
yield format_sse_event(run_response_chunk) # type: ignore
|
|
300
|
-
except (InputCheckError, OutputCheckError) as e:
|
|
301
|
-
error_response = RunErrorEvent(
|
|
302
|
-
content=str(e),
|
|
303
|
-
error_type=e.type,
|
|
304
|
-
error_id=e.error_id,
|
|
305
|
-
additional_data=e.additional_data,
|
|
306
|
-
)
|
|
307
|
-
yield format_sse_event(error_response)
|
|
308
|
-
except Exception as e:
|
|
309
|
-
import traceback
|
|
310
|
-
|
|
311
|
-
traceback.print_exc(limit=3)
|
|
312
|
-
error_response = RunErrorEvent(
|
|
313
|
-
content=str(e),
|
|
314
|
-
)
|
|
315
|
-
yield format_sse_event(error_response)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
async def agent_continue_response_streamer(
|
|
319
|
-
agent: Agent,
|
|
320
|
-
run_id: Optional[str] = None,
|
|
321
|
-
updated_tools: Optional[List] = None,
|
|
322
|
-
session_id: Optional[str] = None,
|
|
323
|
-
user_id: Optional[str] = None,
|
|
324
|
-
) -> AsyncGenerator:
|
|
325
|
-
try:
|
|
326
|
-
continue_response = agent.acontinue_run(
|
|
327
|
-
run_id=run_id,
|
|
328
|
-
updated_tools=updated_tools,
|
|
329
|
-
session_id=session_id,
|
|
330
|
-
user_id=user_id,
|
|
331
|
-
stream=True,
|
|
332
|
-
stream_events=True,
|
|
333
|
-
)
|
|
334
|
-
async for run_response_chunk in continue_response:
|
|
335
|
-
yield format_sse_event(run_response_chunk) # type: ignore
|
|
336
|
-
except (InputCheckError, OutputCheckError) as e:
|
|
337
|
-
error_response = RunErrorEvent(
|
|
338
|
-
content=str(e),
|
|
339
|
-
error_type=e.type,
|
|
340
|
-
error_id=e.error_id,
|
|
341
|
-
additional_data=e.additional_data,
|
|
342
|
-
)
|
|
343
|
-
yield format_sse_event(error_response)
|
|
344
|
-
|
|
345
|
-
except Exception as e:
|
|
346
|
-
import traceback
|
|
347
|
-
|
|
348
|
-
traceback.print_exc(limit=3)
|
|
349
|
-
error_response = RunErrorEvent(
|
|
350
|
-
content=str(e),
|
|
351
|
-
error_type=e.type if hasattr(e, "type") else None,
|
|
352
|
-
error_id=e.error_id if hasattr(e, "error_id") else None,
|
|
353
|
-
)
|
|
354
|
-
yield format_sse_event(error_response)
|
|
355
|
-
return
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
async def team_response_streamer(
|
|
359
|
-
team: Team,
|
|
360
|
-
message: str,
|
|
361
|
-
session_id: Optional[str] = None,
|
|
362
|
-
user_id: Optional[str] = None,
|
|
363
|
-
images: Optional[List[Image]] = None,
|
|
364
|
-
audio: Optional[List[Audio]] = None,
|
|
365
|
-
videos: Optional[List[Video]] = None,
|
|
366
|
-
files: Optional[List[FileMedia]] = None,
|
|
367
|
-
**kwargs: Any,
|
|
368
|
-
) -> AsyncGenerator:
|
|
369
|
-
"""Run the given team asynchronously and yield its response"""
|
|
370
|
-
try:
|
|
371
|
-
run_response = team.arun(
|
|
372
|
-
input=message,
|
|
373
|
-
session_id=session_id,
|
|
374
|
-
user_id=user_id,
|
|
375
|
-
images=images,
|
|
376
|
-
audio=audio,
|
|
377
|
-
videos=videos,
|
|
378
|
-
files=files,
|
|
379
|
-
stream=True,
|
|
380
|
-
stream_events=True,
|
|
381
|
-
**kwargs,
|
|
382
|
-
)
|
|
383
|
-
async for run_response_chunk in run_response:
|
|
384
|
-
yield format_sse_event(run_response_chunk) # type: ignore
|
|
385
|
-
except (InputCheckError, OutputCheckError) as e:
|
|
386
|
-
error_response = TeamRunErrorEvent(
|
|
387
|
-
content=str(e),
|
|
388
|
-
error_type=e.type,
|
|
389
|
-
error_id=e.error_id,
|
|
390
|
-
additional_data=e.additional_data,
|
|
391
|
-
)
|
|
392
|
-
yield format_sse_event(error_response)
|
|
393
|
-
|
|
394
|
-
except Exception as e:
|
|
395
|
-
import traceback
|
|
396
|
-
|
|
397
|
-
traceback.print_exc()
|
|
398
|
-
error_response = TeamRunErrorEvent(
|
|
399
|
-
content=str(e),
|
|
400
|
-
error_type=e.type if hasattr(e, "type") else None,
|
|
401
|
-
error_id=e.error_id if hasattr(e, "error_id") else None,
|
|
402
|
-
)
|
|
403
|
-
yield format_sse_event(error_response)
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os: "AgentOS"):
|
|
408
|
-
"""Handle workflow execution directly via WebSocket"""
|
|
409
|
-
try:
|
|
410
|
-
workflow_id = message.get("workflow_id")
|
|
411
|
-
session_id = message.get("session_id")
|
|
412
|
-
user_message = message.get("message", "")
|
|
413
|
-
user_id = message.get("user_id")
|
|
414
|
-
|
|
415
|
-
if not workflow_id:
|
|
416
|
-
await websocket.send_text(json.dumps({"event": "error", "error": "workflow_id is required"}))
|
|
417
|
-
return
|
|
418
|
-
|
|
419
|
-
# Get workflow from OS
|
|
420
|
-
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
421
|
-
if not workflow:
|
|
422
|
-
await websocket.send_text(json.dumps({"event": "error", "error": f"Workflow {workflow_id} not found"}))
|
|
423
|
-
return
|
|
424
|
-
|
|
425
|
-
# Generate session_id if not provided
|
|
426
|
-
# Use workflow's default session_id if not provided in message
|
|
427
|
-
if not session_id:
|
|
428
|
-
if workflow.session_id:
|
|
429
|
-
session_id = workflow.session_id
|
|
430
|
-
else:
|
|
431
|
-
session_id = str(uuid4())
|
|
432
|
-
|
|
433
|
-
# Execute workflow in background with streaming
|
|
434
|
-
workflow_result = await workflow.arun( # type: ignore
|
|
435
|
-
input=user_message,
|
|
436
|
-
session_id=session_id,
|
|
437
|
-
user_id=user_id,
|
|
438
|
-
stream=True,
|
|
439
|
-
stream_events=True,
|
|
440
|
-
background=True,
|
|
441
|
-
websocket=websocket,
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
workflow_run_output = cast(WorkflowRunOutput, workflow_result)
|
|
445
|
-
|
|
446
|
-
await websocket_manager.register_workflow_websocket(workflow_run_output.run_id, websocket) # type: ignore
|
|
447
|
-
|
|
448
|
-
except (InputCheckError, OutputCheckError) as e:
|
|
449
|
-
await websocket.send_text(
|
|
450
|
-
json.dumps(
|
|
451
|
-
{
|
|
452
|
-
"event": "error",
|
|
453
|
-
"error": str(e),
|
|
454
|
-
"error_type": e.type,
|
|
455
|
-
"error_id": e.error_id,
|
|
456
|
-
"additional_data": e.additional_data,
|
|
457
|
-
}
|
|
458
|
-
)
|
|
459
|
-
)
|
|
460
|
-
except Exception as e:
|
|
461
|
-
logger.error(f"Error executing workflow via WebSocket: {e}")
|
|
462
|
-
error_payload = {
|
|
463
|
-
"event": "error",
|
|
464
|
-
"error": str(e),
|
|
465
|
-
"error_type": e.type if hasattr(e, "type") else None,
|
|
466
|
-
"error_id": e.error_id if hasattr(e, "error_id") else None,
|
|
467
|
-
}
|
|
468
|
-
error_payload = {k: v for k, v in error_payload.items() if v is not None}
|
|
469
|
-
await websocket.send_text(json.dumps(error_payload))
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
async def workflow_response_streamer(
|
|
473
|
-
workflow: Workflow,
|
|
474
|
-
input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None,
|
|
475
|
-
session_id: Optional[str] = None,
|
|
476
|
-
user_id: Optional[str] = None,
|
|
477
|
-
**kwargs: Any,
|
|
478
|
-
) -> AsyncGenerator:
|
|
479
|
-
try:
|
|
480
|
-
run_response = workflow.arun(
|
|
481
|
-
input=input,
|
|
482
|
-
session_id=session_id,
|
|
483
|
-
user_id=user_id,
|
|
484
|
-
stream=True,
|
|
485
|
-
stream_events=True,
|
|
486
|
-
**kwargs,
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
async for run_response_chunk in run_response:
|
|
490
|
-
yield format_sse_event(run_response_chunk) # type: ignore
|
|
491
|
-
|
|
492
|
-
except (InputCheckError, OutputCheckError) as e:
|
|
493
|
-
error_response = WorkflowErrorEvent(
|
|
494
|
-
error=str(e),
|
|
495
|
-
error_type=e.type,
|
|
496
|
-
error_id=e.error_id,
|
|
497
|
-
additional_data=e.additional_data,
|
|
498
|
-
)
|
|
499
|
-
yield format_sse_event(error_response)
|
|
500
|
-
|
|
501
|
-
except Exception as e:
|
|
502
|
-
import traceback
|
|
503
|
-
|
|
504
|
-
traceback.print_exc()
|
|
505
|
-
error_response = WorkflowErrorEvent(
|
|
506
|
-
error=str(e),
|
|
507
|
-
error_type=e.type if hasattr(e, "type") else None,
|
|
508
|
-
error_id=e.error_id if hasattr(e, "error_id") else None,
|
|
509
|
-
)
|
|
510
|
-
yield format_sse_event(error_response)
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
def get_websocket_router(
|
|
515
|
-
os: "AgentOS",
|
|
516
|
-
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
517
|
-
) -> APIRouter:
|
|
518
|
-
"""
|
|
519
|
-
Create WebSocket router without HTTP authentication dependencies.
|
|
520
|
-
WebSocket endpoints handle authentication internally via message-based auth.
|
|
521
|
-
"""
|
|
522
|
-
ws_router = APIRouter()
|
|
523
|
-
|
|
524
|
-
@ws_router.websocket(
|
|
525
|
-
"/workflows/ws",
|
|
526
|
-
name="workflow_websocket",
|
|
527
|
-
)
|
|
528
|
-
async def workflow_websocket_endpoint(websocket: WebSocket):
|
|
529
|
-
"""WebSocket endpoint for receiving real-time workflow events"""
|
|
530
|
-
requires_auth = bool(settings.os_security_key)
|
|
531
|
-
await websocket_manager.connect(websocket, requires_auth=requires_auth)
|
|
532
|
-
|
|
533
|
-
try:
|
|
534
|
-
while True:
|
|
535
|
-
data = await websocket.receive_text()
|
|
536
|
-
message = json.loads(data)
|
|
537
|
-
action = message.get("action")
|
|
538
|
-
|
|
539
|
-
# Handle authentication first
|
|
540
|
-
if action == "authenticate":
|
|
541
|
-
token = message.get("token")
|
|
542
|
-
if not token:
|
|
543
|
-
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Token is required"}))
|
|
544
|
-
continue
|
|
545
|
-
|
|
546
|
-
if validate_websocket_token(token, settings):
|
|
547
|
-
await websocket_manager.authenticate_websocket(websocket)
|
|
548
|
-
else:
|
|
549
|
-
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Invalid token"}))
|
|
550
|
-
continue
|
|
551
|
-
|
|
552
|
-
# Check authentication for all other actions (only when required)
|
|
553
|
-
elif requires_auth and not websocket_manager.is_authenticated(websocket):
|
|
554
|
-
await websocket.send_text(
|
|
555
|
-
json.dumps(
|
|
556
|
-
{
|
|
557
|
-
"event": "auth_required",
|
|
558
|
-
"error": "Authentication required. Send authenticate action with valid token.",
|
|
559
|
-
}
|
|
560
|
-
)
|
|
561
|
-
)
|
|
562
|
-
continue
|
|
563
|
-
|
|
564
|
-
# Handle authenticated actions
|
|
565
|
-
elif action == "ping":
|
|
566
|
-
await websocket.send_text(json.dumps({"event": "pong"}))
|
|
567
|
-
|
|
568
|
-
elif action == "start-workflow":
|
|
569
|
-
# Handle workflow execution directly via WebSocket
|
|
570
|
-
await handle_workflow_via_websocket(websocket, message, os)
|
|
571
|
-
|
|
572
|
-
else:
|
|
573
|
-
await websocket.send_text(json.dumps({"event": "error", "error": f"Unknown action: {action}"}))
|
|
574
|
-
|
|
575
|
-
except Exception as e:
|
|
576
|
-
if "1012" not in str(e) and "1001" not in str(e):
|
|
577
|
-
logger.error(f"WebSocket error: {e}")
|
|
578
|
-
finally:
|
|
579
|
-
# Clean up the websocket connection
|
|
580
|
-
await websocket_manager.disconnect_websocket(websocket)
|
|
581
|
-
|
|
582
|
-
return ws_router
|
|
583
|
-
|
|
584
|
-
|
|
585
35
|
def get_base_router(
|
|
586
36
|
os: "AgentOS",
|
|
587
37
|
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
@@ -690,10 +140,32 @@ def get_base_router(
|
|
|
690
140
|
},
|
|
691
141
|
)
|
|
692
142
|
async def config() -> ConfigResponse:
|
|
143
|
+
try:
|
|
144
|
+
agent_summaries = []
|
|
145
|
+
if os.agents:
|
|
146
|
+
for agent in os.agents:
|
|
147
|
+
agent_summaries.append(AgentSummaryResponse.from_agent(agent))
|
|
148
|
+
|
|
149
|
+
team_summaries = []
|
|
150
|
+
if os.teams:
|
|
151
|
+
for team in os.teams:
|
|
152
|
+
team_summaries.append(TeamSummaryResponse.from_team(team))
|
|
153
|
+
|
|
154
|
+
workflow_summaries = []
|
|
155
|
+
if os.workflows:
|
|
156
|
+
for workflow in os.workflows:
|
|
157
|
+
workflow_summaries.append(WorkflowSummaryResponse.from_workflow(workflow))
|
|
158
|
+
except RemoteServerUnavailableError as e:
|
|
159
|
+
raise HTTPException(
|
|
160
|
+
status_code=502,
|
|
161
|
+
detail=f"Failed to fetch config from remote AgentOS: {e}",
|
|
162
|
+
)
|
|
163
|
+
|
|
693
164
|
return ConfigResponse(
|
|
694
165
|
os_id=os.id or "Unnamed OS",
|
|
695
166
|
description=os.description,
|
|
696
167
|
available_models=os.config.available_models if os.config else [],
|
|
168
|
+
os_database=os.db.id if os.db else None,
|
|
697
169
|
databases=list({db.id for db_id, dbs in os.dbs.items() for db in dbs}),
|
|
698
170
|
chat=os.config.chat if os.config else None,
|
|
699
171
|
session=os._get_session_config(),
|
|
@@ -701,9 +173,10 @@ def get_base_router(
|
|
|
701
173
|
knowledge=os._get_knowledge_config(),
|
|
702
174
|
evals=os._get_evals_config(),
|
|
703
175
|
metrics=os._get_metrics_config(),
|
|
704
|
-
agents=
|
|
705
|
-
teams=
|
|
706
|
-
workflows=
|
|
176
|
+
agents=agent_summaries,
|
|
177
|
+
teams=team_summaries,
|
|
178
|
+
workflows=workflow_summaries,
|
|
179
|
+
traces=os._get_traces_config(),
|
|
707
180
|
interfaces=[
|
|
708
181
|
InterfaceResponse(type=interface.type, version=interface.version, route=interface.prefix)
|
|
709
182
|
for interface in os.interfaces
|
|
@@ -737,1027 +210,101 @@ def get_base_router(
|
|
|
737
210
|
)
|
|
738
211
|
async def get_models() -> List[Model]:
|
|
739
212
|
"""Return the list of all models used by agents and teams in the contextual OS"""
|
|
740
|
-
|
|
213
|
+
unique_models = {}
|
|
214
|
+
|
|
215
|
+
# Collect models from local agents
|
|
741
216
|
if os.agents:
|
|
742
|
-
|
|
217
|
+
for agent in os.agents:
|
|
218
|
+
model = cast(Model, agent.model)
|
|
219
|
+
if model and model.id is not None and model.provider is not None:
|
|
220
|
+
key = (model.id, model.provider)
|
|
221
|
+
if key not in unique_models:
|
|
222
|
+
unique_models[key] = Model(id=model.id, provider=model.provider)
|
|
223
|
+
|
|
224
|
+
# Collect models from local teams
|
|
743
225
|
if os.teams:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
key = (model.id, model.provider)
|
|
751
|
-
if key not in unique_models:
|
|
752
|
-
unique_models[key] = Model(id=model.id, provider=model.provider)
|
|
226
|
+
for team in os.teams:
|
|
227
|
+
model = cast(Model, team.model)
|
|
228
|
+
if model and model.id is not None and model.provider is not None:
|
|
229
|
+
key = (model.id, model.provider)
|
|
230
|
+
if key not in unique_models:
|
|
231
|
+
unique_models[key] = Model(id=model.id, provider=model.provider)
|
|
753
232
|
|
|
754
233
|
return list(unique_models.values())
|
|
755
234
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
@router.post(
|
|
759
|
-
"/agents/{agent_id}/runs",
|
|
760
|
-
tags=["Agents"],
|
|
761
|
-
operation_id="create_agent_run",
|
|
762
|
-
response_model_exclude_none=True,
|
|
763
|
-
summary="Create Agent Run",
|
|
764
|
-
description=(
|
|
765
|
-
"Execute an agent with a message and optional media files. Supports both streaming and non-streaming responses.\n\n"
|
|
766
|
-
"**Features:**\n"
|
|
767
|
-
"- Text message input with optional session management\n"
|
|
768
|
-
"- Multi-media support: images (PNG, JPEG, WebP), audio (WAV, MP3), video (MP4, WebM, etc.)\n"
|
|
769
|
-
"- Document processing: PDF, CSV, DOCX, TXT, JSON\n"
|
|
770
|
-
"- Real-time streaming responses with Server-Sent Events (SSE)\n"
|
|
771
|
-
"- User and session context preservation\n\n"
|
|
772
|
-
"**Streaming Response:**\n"
|
|
773
|
-
"When `stream=true`, returns SSE events with `event` and `data` fields."
|
|
774
|
-
),
|
|
775
|
-
responses={
|
|
776
|
-
200: {
|
|
777
|
-
"description": "Agent run executed successfully",
|
|
778
|
-
"content": {
|
|
779
|
-
"text/event-stream": {
|
|
780
|
-
"examples": {
|
|
781
|
-
"event_stream": {
|
|
782
|
-
"summary": "Example event stream response",
|
|
783
|
-
"value": 'event: RunStarted\ndata: {"content": "Hello!", "run_id": "123..."}\n\n',
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
},
|
|
787
|
-
},
|
|
788
|
-
},
|
|
789
|
-
400: {"description": "Invalid request or unsupported file type", "model": BadRequestResponse},
|
|
790
|
-
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
791
|
-
},
|
|
792
|
-
)
|
|
793
|
-
async def create_agent_run(
|
|
794
|
-
agent_id: str,
|
|
795
|
-
request: Request,
|
|
796
|
-
message: str = Form(...),
|
|
797
|
-
stream: bool = Form(False),
|
|
798
|
-
session_id: Optional[str] = Form(None),
|
|
799
|
-
user_id: Optional[str] = Form(None),
|
|
800
|
-
files: Optional[List[UploadFile]] = File(None),
|
|
801
|
-
):
|
|
802
|
-
kwargs = await _get_request_kwargs(request, create_agent_run)
|
|
803
|
-
|
|
804
|
-
if hasattr(request.state, "user_id"):
|
|
805
|
-
if user_id:
|
|
806
|
-
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
807
|
-
user_id = request.state.user_id
|
|
808
|
-
if hasattr(request.state, "session_id"):
|
|
809
|
-
if session_id:
|
|
810
|
-
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
811
|
-
session_id = request.state.session_id
|
|
812
|
-
if hasattr(request.state, "session_state"):
|
|
813
|
-
session_state = request.state.session_state
|
|
814
|
-
if "session_state" in kwargs:
|
|
815
|
-
log_warning("Session state parameter passed in both request state and kwargs, using request state")
|
|
816
|
-
kwargs["session_state"] = session_state
|
|
817
|
-
if hasattr(request.state, "dependencies"):
|
|
818
|
-
dependencies = request.state.dependencies
|
|
819
|
-
if "dependencies" in kwargs:
|
|
820
|
-
log_warning("Dependencies parameter passed in both request state and kwargs, using request state")
|
|
821
|
-
kwargs["dependencies"] = dependencies
|
|
822
|
-
if hasattr(request.state, "metadata"):
|
|
823
|
-
metadata = request.state.metadata
|
|
824
|
-
if "metadata" in kwargs:
|
|
825
|
-
log_warning("Metadata parameter passed in both request state and kwargs, using request state")
|
|
826
|
-
kwargs["metadata"] = metadata
|
|
827
|
-
|
|
828
|
-
agent = get_agent_by_id(agent_id, os.agents)
|
|
829
|
-
if agent is None:
|
|
830
|
-
raise HTTPException(status_code=404, detail="Agent not found")
|
|
831
|
-
|
|
832
|
-
if session_id is None or session_id == "":
|
|
833
|
-
log_debug("Creating new session")
|
|
834
|
-
session_id = str(uuid4())
|
|
835
|
-
|
|
836
|
-
base64_images: List[Image] = []
|
|
837
|
-
base64_audios: List[Audio] = []
|
|
838
|
-
base64_videos: List[Video] = []
|
|
839
|
-
input_files: List[FileMedia] = []
|
|
840
|
-
|
|
841
|
-
if files:
|
|
842
|
-
for file in files:
|
|
843
|
-
if file.content_type in [
|
|
844
|
-
"image/png",
|
|
845
|
-
"image/jpeg",
|
|
846
|
-
"image/jpg",
|
|
847
|
-
"image/gif",
|
|
848
|
-
"image/webp",
|
|
849
|
-
"image/bmp",
|
|
850
|
-
"image/tiff",
|
|
851
|
-
"image/tif",
|
|
852
|
-
"image/avif",
|
|
853
|
-
]:
|
|
854
|
-
try:
|
|
855
|
-
base64_image = process_image(file)
|
|
856
|
-
base64_images.append(base64_image)
|
|
857
|
-
except Exception as e:
|
|
858
|
-
log_error(f"Error processing image {file.filename}: {e}")
|
|
859
|
-
continue
|
|
860
|
-
elif file.content_type in [
|
|
861
|
-
"audio/wav",
|
|
862
|
-
"audio/wave",
|
|
863
|
-
"audio/mp3",
|
|
864
|
-
"audio/mpeg",
|
|
865
|
-
"audio/ogg",
|
|
866
|
-
"audio/mp4",
|
|
867
|
-
"audio/m4a",
|
|
868
|
-
"audio/aac",
|
|
869
|
-
"audio/flac",
|
|
870
|
-
]:
|
|
871
|
-
try:
|
|
872
|
-
audio = process_audio(file)
|
|
873
|
-
base64_audios.append(audio)
|
|
874
|
-
except Exception as e:
|
|
875
|
-
log_error(f"Error processing audio {file.filename} with content type {file.content_type}: {e}")
|
|
876
|
-
continue
|
|
877
|
-
elif file.content_type in [
|
|
878
|
-
"video/x-flv",
|
|
879
|
-
"video/quicktime",
|
|
880
|
-
"video/mpeg",
|
|
881
|
-
"video/mpegs",
|
|
882
|
-
"video/mpgs",
|
|
883
|
-
"video/mpg",
|
|
884
|
-
"video/mpg",
|
|
885
|
-
"video/mp4",
|
|
886
|
-
"video/webm",
|
|
887
|
-
"video/wmv",
|
|
888
|
-
"video/3gpp",
|
|
889
|
-
]:
|
|
890
|
-
try:
|
|
891
|
-
base64_video = process_video(file)
|
|
892
|
-
base64_videos.append(base64_video)
|
|
893
|
-
except Exception as e:
|
|
894
|
-
log_error(f"Error processing video {file.filename}: {e}")
|
|
895
|
-
continue
|
|
896
|
-
elif file.content_type in [
|
|
897
|
-
"application/pdf",
|
|
898
|
-
"application/json",
|
|
899
|
-
"application/x-javascript",
|
|
900
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
901
|
-
"text/javascript",
|
|
902
|
-
"application/x-python",
|
|
903
|
-
"text/x-python",
|
|
904
|
-
"text/plain",
|
|
905
|
-
"text/html",
|
|
906
|
-
"text/css",
|
|
907
|
-
"text/md",
|
|
908
|
-
"text/csv",
|
|
909
|
-
"text/xml",
|
|
910
|
-
"text/rtf",
|
|
911
|
-
]:
|
|
912
|
-
# Process document files
|
|
913
|
-
try:
|
|
914
|
-
input_file = process_document(file)
|
|
915
|
-
if input_file is not None:
|
|
916
|
-
input_files.append(input_file)
|
|
917
|
-
except Exception as e:
|
|
918
|
-
log_error(f"Error processing file {file.filename}: {e}")
|
|
919
|
-
continue
|
|
920
|
-
else:
|
|
921
|
-
raise HTTPException(status_code=400, detail="Unsupported file type")
|
|
922
|
-
|
|
923
|
-
if stream:
|
|
924
|
-
return StreamingResponse(
|
|
925
|
-
agent_response_streamer(
|
|
926
|
-
agent,
|
|
927
|
-
message,
|
|
928
|
-
session_id=session_id,
|
|
929
|
-
user_id=user_id,
|
|
930
|
-
images=base64_images if base64_images else None,
|
|
931
|
-
audio=base64_audios if base64_audios else None,
|
|
932
|
-
videos=base64_videos if base64_videos else None,
|
|
933
|
-
files=input_files if input_files else None,
|
|
934
|
-
**kwargs,
|
|
935
|
-
),
|
|
936
|
-
media_type="text/event-stream",
|
|
937
|
-
)
|
|
938
|
-
else:
|
|
939
|
-
try:
|
|
940
|
-
run_response = cast(
|
|
941
|
-
RunOutput,
|
|
942
|
-
await agent.arun(
|
|
943
|
-
input=message,
|
|
944
|
-
session_id=session_id,
|
|
945
|
-
user_id=user_id,
|
|
946
|
-
images=base64_images if base64_images else None,
|
|
947
|
-
audio=base64_audios if base64_audios else None,
|
|
948
|
-
videos=base64_videos if base64_videos else None,
|
|
949
|
-
files=input_files if input_files else None,
|
|
950
|
-
stream=False,
|
|
951
|
-
**kwargs,
|
|
952
|
-
),
|
|
953
|
-
)
|
|
954
|
-
return run_response.to_dict()
|
|
955
|
-
|
|
956
|
-
except InputCheckError as e:
|
|
957
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
958
|
-
|
|
959
|
-
@router.post(
|
|
960
|
-
"/agents/{agent_id}/runs/{run_id}/cancel",
|
|
961
|
-
tags=["Agents"],
|
|
962
|
-
operation_id="cancel_agent_run",
|
|
963
|
-
response_model_exclude_none=True,
|
|
964
|
-
summary="Cancel Agent Run",
|
|
965
|
-
description=(
|
|
966
|
-
"Cancel a currently executing agent run. This will attempt to stop the agent's execution gracefully.\n\n"
|
|
967
|
-
"**Note:** Cancellation may not be immediate for all operations."
|
|
968
|
-
),
|
|
969
|
-
responses={
|
|
970
|
-
200: {},
|
|
971
|
-
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
972
|
-
500: {"description": "Failed to cancel run", "model": InternalServerErrorResponse},
|
|
973
|
-
},
|
|
974
|
-
)
|
|
975
|
-
async def cancel_agent_run(
|
|
976
|
-
agent_id: str,
|
|
977
|
-
run_id: str,
|
|
978
|
-
):
|
|
979
|
-
agent = get_agent_by_id(agent_id, os.agents)
|
|
980
|
-
if agent is None:
|
|
981
|
-
raise HTTPException(status_code=404, detail="Agent not found")
|
|
235
|
+
return router
|
|
982
236
|
|
|
983
|
-
if not agent.cancel_run(run_id=run_id):
|
|
984
|
-
raise HTTPException(status_code=500, detail="Failed to cancel run")
|
|
985
237
|
|
|
986
|
-
|
|
238
|
+
def get_websocket_router(
|
|
239
|
+
os: "AgentOS",
|
|
240
|
+
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
241
|
+
) -> APIRouter:
|
|
242
|
+
"""
|
|
243
|
+
Create WebSocket router without HTTP authentication dependencies.
|
|
244
|
+
WebSocket endpoints handle authentication internally via message-based auth.
|
|
245
|
+
"""
|
|
246
|
+
ws_router = APIRouter()
|
|
987
247
|
|
|
988
|
-
@
|
|
989
|
-
"/
|
|
990
|
-
|
|
991
|
-
operation_id="continue_agent_run",
|
|
992
|
-
response_model_exclude_none=True,
|
|
993
|
-
summary="Continue Agent Run",
|
|
994
|
-
description=(
|
|
995
|
-
"Continue a paused or incomplete agent run with updated tool results.\n\n"
|
|
996
|
-
"**Use Cases:**\n"
|
|
997
|
-
"- Resume execution after tool approval/rejection\n"
|
|
998
|
-
"- Provide manual tool execution results\n\n"
|
|
999
|
-
"**Tools Parameter:**\n"
|
|
1000
|
-
"JSON string containing array of tool execution objects with results."
|
|
1001
|
-
),
|
|
1002
|
-
responses={
|
|
1003
|
-
200: {
|
|
1004
|
-
"description": "Agent run continued successfully",
|
|
1005
|
-
"content": {
|
|
1006
|
-
"text/event-stream": {
|
|
1007
|
-
"example": 'event: RunContent\ndata: {"created_at": 1757348314, "run_id": "123..."}\n\n'
|
|
1008
|
-
},
|
|
1009
|
-
},
|
|
1010
|
-
},
|
|
1011
|
-
400: {"description": "Invalid JSON in tools field or invalid tool structure", "model": BadRequestResponse},
|
|
1012
|
-
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
1013
|
-
},
|
|
248
|
+
@ws_router.websocket(
|
|
249
|
+
"/workflows/ws",
|
|
250
|
+
name="workflow_websocket",
|
|
1014
251
|
)
|
|
1015
|
-
async def
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
tools: str = Form(...), # JSON string of tools
|
|
1020
|
-
session_id: Optional[str] = Form(None),
|
|
1021
|
-
user_id: Optional[str] = Form(None),
|
|
1022
|
-
stream: bool = Form(True),
|
|
1023
|
-
):
|
|
1024
|
-
if hasattr(request.state, "user_id"):
|
|
1025
|
-
user_id = request.state.user_id
|
|
1026
|
-
if hasattr(request.state, "session_id"):
|
|
1027
|
-
session_id = request.state.session_id
|
|
252
|
+
async def workflow_websocket_endpoint(websocket: WebSocket):
|
|
253
|
+
"""WebSocket endpoint for receiving real-time workflow events"""
|
|
254
|
+
requires_auth = bool(settings.os_security_key)
|
|
255
|
+
await websocket_manager.connect(websocket, requires_auth=requires_auth)
|
|
1028
256
|
|
|
1029
|
-
# Parse the JSON string manually
|
|
1030
257
|
try:
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
agent = get_agent_by_id(agent_id, os.agents)
|
|
1036
|
-
if agent is None:
|
|
1037
|
-
raise HTTPException(status_code=404, detail="Agent not found")
|
|
1038
|
-
|
|
1039
|
-
if session_id is None or session_id == "":
|
|
1040
|
-
log_warning(
|
|
1041
|
-
"Continuing run without session_id. This might lead to unexpected behavior if session context is important."
|
|
1042
|
-
)
|
|
1043
|
-
|
|
1044
|
-
# Convert tools dict to ToolExecution objects if provided
|
|
1045
|
-
updated_tools = None
|
|
1046
|
-
if tools_data:
|
|
1047
|
-
try:
|
|
1048
|
-
from agno.models.response import ToolExecution
|
|
1049
|
-
|
|
1050
|
-
updated_tools = [ToolExecution.from_dict(tool) for tool in tools_data]
|
|
1051
|
-
except Exception as e:
|
|
1052
|
-
raise HTTPException(status_code=400, detail=f"Invalid structure or content for tools: {str(e)}")
|
|
1053
|
-
|
|
1054
|
-
if stream:
|
|
1055
|
-
return StreamingResponse(
|
|
1056
|
-
agent_continue_response_streamer(
|
|
1057
|
-
agent,
|
|
1058
|
-
run_id=run_id, # run_id from path
|
|
1059
|
-
updated_tools=updated_tools,
|
|
1060
|
-
session_id=session_id,
|
|
1061
|
-
user_id=user_id,
|
|
1062
|
-
),
|
|
1063
|
-
media_type="text/event-stream",
|
|
1064
|
-
)
|
|
1065
|
-
else:
|
|
1066
|
-
try:
|
|
1067
|
-
run_response_obj = cast(
|
|
1068
|
-
RunOutput,
|
|
1069
|
-
await agent.acontinue_run(
|
|
1070
|
-
run_id=run_id, # run_id from path
|
|
1071
|
-
updated_tools=updated_tools,
|
|
1072
|
-
session_id=session_id,
|
|
1073
|
-
user_id=user_id,
|
|
1074
|
-
stream=False,
|
|
1075
|
-
),
|
|
1076
|
-
)
|
|
1077
|
-
return run_response_obj.to_dict()
|
|
1078
|
-
|
|
1079
|
-
except InputCheckError as e:
|
|
1080
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
1081
|
-
|
|
1082
|
-
@router.get(
|
|
1083
|
-
"/agents",
|
|
1084
|
-
response_model=List[AgentResponse],
|
|
1085
|
-
response_model_exclude_none=True,
|
|
1086
|
-
tags=["Agents"],
|
|
1087
|
-
operation_id="get_agents",
|
|
1088
|
-
summary="List All Agents",
|
|
1089
|
-
description=(
|
|
1090
|
-
"Retrieve a comprehensive list of all agents configured in this OS instance.\n\n"
|
|
1091
|
-
"**Returns:**\n"
|
|
1092
|
-
"- Agent metadata (ID, name, description)\n"
|
|
1093
|
-
"- Model configuration and capabilities\n"
|
|
1094
|
-
"- Available tools and their configurations\n"
|
|
1095
|
-
"- Session, knowledge, memory, and reasoning settings\n"
|
|
1096
|
-
"- Only meaningful (non-default) configurations are included"
|
|
1097
|
-
),
|
|
1098
|
-
responses={
|
|
1099
|
-
200: {
|
|
1100
|
-
"description": "List of agents retrieved successfully",
|
|
1101
|
-
"content": {
|
|
1102
|
-
"application/json": {
|
|
1103
|
-
"example": [
|
|
1104
|
-
{
|
|
1105
|
-
"id": "main-agent",
|
|
1106
|
-
"name": "Main Agent",
|
|
1107
|
-
"db_id": "c6bf0644-feb8-4930-a305-380dae5ad6aa",
|
|
1108
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1109
|
-
"tools": None,
|
|
1110
|
-
"sessions": {"session_table": "agno_sessions"},
|
|
1111
|
-
"knowledge": {"knowledge_table": "main_knowledge"},
|
|
1112
|
-
"system_message": {"markdown": True, "add_datetime_to_context": True},
|
|
1113
|
-
}
|
|
1114
|
-
]
|
|
1115
|
-
}
|
|
1116
|
-
},
|
|
1117
|
-
}
|
|
1118
|
-
},
|
|
1119
|
-
)
|
|
1120
|
-
async def get_agents() -> List[AgentResponse]:
|
|
1121
|
-
"""Return the list of all Agents present in the contextual OS"""
|
|
1122
|
-
if os.agents is None:
|
|
1123
|
-
return []
|
|
1124
|
-
|
|
1125
|
-
agents = []
|
|
1126
|
-
for agent in os.agents:
|
|
1127
|
-
agent_response = await AgentResponse.from_agent(agent=agent)
|
|
1128
|
-
agents.append(agent_response)
|
|
1129
|
-
|
|
1130
|
-
return agents
|
|
1131
|
-
|
|
1132
|
-
@router.get(
|
|
1133
|
-
"/agents/{agent_id}",
|
|
1134
|
-
response_model=AgentResponse,
|
|
1135
|
-
response_model_exclude_none=True,
|
|
1136
|
-
tags=["Agents"],
|
|
1137
|
-
operation_id="get_agent",
|
|
1138
|
-
summary="Get Agent Details",
|
|
1139
|
-
description=(
|
|
1140
|
-
"Retrieve detailed configuration and capabilities of a specific agent.\n\n"
|
|
1141
|
-
"**Returns comprehensive agent information including:**\n"
|
|
1142
|
-
"- Model configuration and provider details\n"
|
|
1143
|
-
"- Complete tool inventory and configurations\n"
|
|
1144
|
-
"- Session management settings\n"
|
|
1145
|
-
"- Knowledge base and memory configurations\n"
|
|
1146
|
-
"- Reasoning capabilities and settings\n"
|
|
1147
|
-
"- System prompts and response formatting options"
|
|
1148
|
-
),
|
|
1149
|
-
responses={
|
|
1150
|
-
200: {
|
|
1151
|
-
"description": "Agent details retrieved successfully",
|
|
1152
|
-
"content": {
|
|
1153
|
-
"application/json": {
|
|
1154
|
-
"example": {
|
|
1155
|
-
"id": "main-agent",
|
|
1156
|
-
"name": "Main Agent",
|
|
1157
|
-
"db_id": "9e064c70-6821-4840-a333-ce6230908a70",
|
|
1158
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1159
|
-
"tools": None,
|
|
1160
|
-
"sessions": {"session_table": "agno_sessions"},
|
|
1161
|
-
"knowledge": {"knowledge_table": "main_knowledge"},
|
|
1162
|
-
"system_message": {"markdown": True, "add_datetime_to_context": True},
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
},
|
|
1166
|
-
},
|
|
1167
|
-
404: {"description": "Agent not found", "model": NotFoundResponse},
|
|
1168
|
-
},
|
|
1169
|
-
)
|
|
1170
|
-
async def get_agent(agent_id: str) -> AgentResponse:
|
|
1171
|
-
agent = get_agent_by_id(agent_id, os.agents)
|
|
1172
|
-
if agent is None:
|
|
1173
|
-
raise HTTPException(status_code=404, detail="Agent not found")
|
|
1174
|
-
|
|
1175
|
-
return await AgentResponse.from_agent(agent)
|
|
1176
|
-
|
|
1177
|
-
# -- Team routes ---
|
|
1178
|
-
|
|
1179
|
-
@router.post(
|
|
1180
|
-
"/teams/{team_id}/runs",
|
|
1181
|
-
tags=["Teams"],
|
|
1182
|
-
operation_id="create_team_run",
|
|
1183
|
-
response_model_exclude_none=True,
|
|
1184
|
-
summary="Create Team Run",
|
|
1185
|
-
description=(
|
|
1186
|
-
"Execute a team collaboration with multiple agents working together on a task.\n\n"
|
|
1187
|
-
"**Features:**\n"
|
|
1188
|
-
"- Text message input with optional session management\n"
|
|
1189
|
-
"- Multi-media support: images (PNG, JPEG, WebP), audio (WAV, MP3), video (MP4, WebM, etc.)\n"
|
|
1190
|
-
"- Document processing: PDF, CSV, DOCX, TXT, JSON\n"
|
|
1191
|
-
"- Real-time streaming responses with Server-Sent Events (SSE)\n"
|
|
1192
|
-
"- User and session context preservation\n\n"
|
|
1193
|
-
"**Streaming Response:**\n"
|
|
1194
|
-
"When `stream=true`, returns SSE events with `event` and `data` fields."
|
|
1195
|
-
),
|
|
1196
|
-
responses={
|
|
1197
|
-
200: {
|
|
1198
|
-
"description": "Team run executed successfully",
|
|
1199
|
-
"content": {
|
|
1200
|
-
"text/event-stream": {
|
|
1201
|
-
"example": 'event: RunStarted\ndata: {"content": "Hello!", "run_id": "123..."}\n\n'
|
|
1202
|
-
},
|
|
1203
|
-
},
|
|
1204
|
-
},
|
|
1205
|
-
400: {"description": "Invalid request or unsupported file type", "model": BadRequestResponse},
|
|
1206
|
-
404: {"description": "Team not found", "model": NotFoundResponse},
|
|
1207
|
-
},
|
|
1208
|
-
)
|
|
1209
|
-
async def create_team_run(
|
|
1210
|
-
team_id: str,
|
|
1211
|
-
request: Request,
|
|
1212
|
-
message: str = Form(...),
|
|
1213
|
-
stream: bool = Form(True),
|
|
1214
|
-
monitor: bool = Form(True),
|
|
1215
|
-
session_id: Optional[str] = Form(None),
|
|
1216
|
-
user_id: Optional[str] = Form(None),
|
|
1217
|
-
files: Optional[List[UploadFile]] = File(None),
|
|
1218
|
-
):
|
|
1219
|
-
kwargs = await _get_request_kwargs(request, create_team_run)
|
|
1220
|
-
|
|
1221
|
-
if hasattr(request.state, "user_id"):
|
|
1222
|
-
if user_id:
|
|
1223
|
-
log_warning("User ID parameter passed in both request state and kwargs, using request state")
|
|
1224
|
-
user_id = request.state.user_id
|
|
1225
|
-
if hasattr(request.state, "session_id"):
|
|
1226
|
-
if session_id:
|
|
1227
|
-
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
1228
|
-
session_id = request.state.session_id
|
|
1229
|
-
if hasattr(request.state, "session_state"):
|
|
1230
|
-
session_state = request.state.session_state
|
|
1231
|
-
if "session_state" in kwargs:
|
|
1232
|
-
log_warning("Session state parameter passed in both request state and kwargs, using request state")
|
|
1233
|
-
kwargs["session_state"] = session_state
|
|
1234
|
-
if hasattr(request.state, "dependencies"):
|
|
1235
|
-
dependencies = request.state.dependencies
|
|
1236
|
-
if "dependencies" in kwargs:
|
|
1237
|
-
log_warning("Dependencies parameter passed in both request state and kwargs, using request state")
|
|
1238
|
-
kwargs["dependencies"] = dependencies
|
|
1239
|
-
if hasattr(request.state, "metadata"):
|
|
1240
|
-
metadata = request.state.metadata
|
|
1241
|
-
if "metadata" in kwargs:
|
|
1242
|
-
log_warning("Metadata parameter passed in both request state and kwargs, using request state")
|
|
1243
|
-
kwargs["metadata"] = metadata
|
|
1244
|
-
|
|
1245
|
-
logger.debug(f"Creating team run: {message=} {session_id=} {monitor=} {user_id=} {team_id=} {files=} {kwargs=}")
|
|
1246
|
-
|
|
1247
|
-
team = get_team_by_id(team_id, os.teams)
|
|
1248
|
-
if team is None:
|
|
1249
|
-
raise HTTPException(status_code=404, detail="Team not found")
|
|
1250
|
-
|
|
1251
|
-
if session_id is not None and session_id != "":
|
|
1252
|
-
logger.debug(f"Continuing session: {session_id}")
|
|
1253
|
-
else:
|
|
1254
|
-
logger.debug("Creating new session")
|
|
1255
|
-
session_id = str(uuid4())
|
|
1256
|
-
|
|
1257
|
-
base64_images: List[Image] = []
|
|
1258
|
-
base64_audios: List[Audio] = []
|
|
1259
|
-
base64_videos: List[Video] = []
|
|
1260
|
-
document_files: List[FileMedia] = []
|
|
258
|
+
while True:
|
|
259
|
+
data = await websocket.receive_text()
|
|
260
|
+
message = json.loads(data)
|
|
261
|
+
action = message.get("action")
|
|
1261
262
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
base64_images.append(base64_image)
|
|
1268
|
-
except Exception as e:
|
|
1269
|
-
logger.error(f"Error processing image {file.filename}: {e}")
|
|
1270
|
-
continue
|
|
1271
|
-
elif file.content_type in ["audio/wav", "audio/mp3", "audio/mpeg"]:
|
|
1272
|
-
try:
|
|
1273
|
-
base64_audio = process_audio(file)
|
|
1274
|
-
base64_audios.append(base64_audio)
|
|
1275
|
-
except Exception as e:
|
|
1276
|
-
logger.error(f"Error processing audio {file.filename}: {e}")
|
|
1277
|
-
continue
|
|
1278
|
-
elif file.content_type in [
|
|
1279
|
-
"video/x-flv",
|
|
1280
|
-
"video/quicktime",
|
|
1281
|
-
"video/mpeg",
|
|
1282
|
-
"video/mpegs",
|
|
1283
|
-
"video/mpgs",
|
|
1284
|
-
"video/mpg",
|
|
1285
|
-
"video/mpg",
|
|
1286
|
-
"video/mp4",
|
|
1287
|
-
"video/webm",
|
|
1288
|
-
"video/wmv",
|
|
1289
|
-
"video/3gpp",
|
|
1290
|
-
]:
|
|
1291
|
-
try:
|
|
1292
|
-
base64_video = process_video(file)
|
|
1293
|
-
base64_videos.append(base64_video)
|
|
1294
|
-
except Exception as e:
|
|
1295
|
-
logger.error(f"Error processing video {file.filename}: {e}")
|
|
263
|
+
# Handle authentication first
|
|
264
|
+
if action == "authenticate":
|
|
265
|
+
token = message.get("token")
|
|
266
|
+
if not token:
|
|
267
|
+
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Token is required"}))
|
|
1296
268
|
continue
|
|
1297
|
-
elif file.content_type in [
|
|
1298
|
-
"application/pdf",
|
|
1299
|
-
"text/csv",
|
|
1300
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1301
|
-
"text/plain",
|
|
1302
|
-
"application/json",
|
|
1303
|
-
]:
|
|
1304
|
-
document_file = process_document(file)
|
|
1305
|
-
if document_file is not None:
|
|
1306
|
-
document_files.append(document_file)
|
|
1307
|
-
else:
|
|
1308
|
-
raise HTTPException(status_code=400, detail="Unsupported file type")
|
|
1309
|
-
|
|
1310
|
-
if stream:
|
|
1311
|
-
return StreamingResponse(
|
|
1312
|
-
team_response_streamer(
|
|
1313
|
-
team,
|
|
1314
|
-
message,
|
|
1315
|
-
session_id=session_id,
|
|
1316
|
-
user_id=user_id,
|
|
1317
|
-
images=base64_images if base64_images else None,
|
|
1318
|
-
audio=base64_audios if base64_audios else None,
|
|
1319
|
-
videos=base64_videos if base64_videos else None,
|
|
1320
|
-
files=document_files if document_files else None,
|
|
1321
|
-
**kwargs,
|
|
1322
|
-
),
|
|
1323
|
-
media_type="text/event-stream",
|
|
1324
|
-
)
|
|
1325
|
-
else:
|
|
1326
|
-
try:
|
|
1327
|
-
run_response = await team.arun(
|
|
1328
|
-
input=message,
|
|
1329
|
-
session_id=session_id,
|
|
1330
|
-
user_id=user_id,
|
|
1331
|
-
images=base64_images if base64_images else None,
|
|
1332
|
-
audio=base64_audios if base64_audios else None,
|
|
1333
|
-
videos=base64_videos if base64_videos else None,
|
|
1334
|
-
files=document_files if document_files else None,
|
|
1335
|
-
stream=False,
|
|
1336
|
-
**kwargs,
|
|
1337
|
-
)
|
|
1338
|
-
return run_response.to_dict()
|
|
1339
|
-
|
|
1340
|
-
except InputCheckError as e:
|
|
1341
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
1342
|
-
|
|
1343
|
-
@router.post(
|
|
1344
|
-
"/teams/{team_id}/runs/{run_id}/cancel",
|
|
1345
|
-
tags=["Teams"],
|
|
1346
|
-
operation_id="cancel_team_run",
|
|
1347
|
-
response_model_exclude_none=True,
|
|
1348
|
-
summary="Cancel Team Run",
|
|
1349
|
-
description=(
|
|
1350
|
-
"Cancel a currently executing team run. This will attempt to stop the team's execution gracefully.\n\n"
|
|
1351
|
-
"**Note:** Cancellation may not be immediate for all operations."
|
|
1352
|
-
),
|
|
1353
|
-
responses={
|
|
1354
|
-
200: {},
|
|
1355
|
-
404: {"description": "Team not found", "model": NotFoundResponse},
|
|
1356
|
-
500: {"description": "Failed to cancel team run", "model": InternalServerErrorResponse},
|
|
1357
|
-
},
|
|
1358
|
-
)
|
|
1359
|
-
async def cancel_team_run(
|
|
1360
|
-
team_id: str,
|
|
1361
|
-
run_id: str,
|
|
1362
|
-
):
|
|
1363
|
-
team = get_team_by_id(team_id, os.teams)
|
|
1364
|
-
if team is None:
|
|
1365
|
-
raise HTTPException(status_code=404, detail="Team not found")
|
|
1366
|
-
|
|
1367
|
-
if not team.cancel_run(run_id=run_id):
|
|
1368
|
-
raise HTTPException(status_code=500, detail="Failed to cancel run")
|
|
1369
|
-
|
|
1370
|
-
return JSONResponse(content={}, status_code=200)
|
|
1371
|
-
|
|
1372
|
-
@router.get(
|
|
1373
|
-
"/teams",
|
|
1374
|
-
response_model=List[TeamResponse],
|
|
1375
|
-
response_model_exclude_none=True,
|
|
1376
|
-
tags=["Teams"],
|
|
1377
|
-
operation_id="get_teams",
|
|
1378
|
-
summary="List All Teams",
|
|
1379
|
-
description=(
|
|
1380
|
-
"Retrieve a comprehensive list of all teams configured in this OS instance.\n\n"
|
|
1381
|
-
"**Returns team information including:**\n"
|
|
1382
|
-
"- Team metadata (ID, name, description, execution mode)\n"
|
|
1383
|
-
"- Model configuration for team coordination\n"
|
|
1384
|
-
"- Team member roster with roles and capabilities\n"
|
|
1385
|
-
"- Knowledge sharing and memory configurations"
|
|
1386
|
-
),
|
|
1387
|
-
responses={
|
|
1388
|
-
200: {
|
|
1389
|
-
"description": "List of teams retrieved successfully",
|
|
1390
|
-
"content": {
|
|
1391
|
-
"application/json": {
|
|
1392
|
-
"example": [
|
|
1393
|
-
{
|
|
1394
|
-
"team_id": "basic-team",
|
|
1395
|
-
"name": "Basic Team",
|
|
1396
|
-
"mode": "coordinate",
|
|
1397
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1398
|
-
"tools": [
|
|
1399
|
-
{
|
|
1400
|
-
"name": "transfer_task_to_member",
|
|
1401
|
-
"description": "Use this function to transfer a task to the selected team member.\nYou must provide a clear and concise description of the task the member should achieve AND the expected output.",
|
|
1402
|
-
"parameters": {
|
|
1403
|
-
"type": "object",
|
|
1404
|
-
"properties": {
|
|
1405
|
-
"member_id": {
|
|
1406
|
-
"type": "string",
|
|
1407
|
-
"description": "(str) The ID of the member to transfer the task to. Use only the ID of the member, not the ID of the team followed by the ID of the member.",
|
|
1408
|
-
},
|
|
1409
|
-
"task_description": {
|
|
1410
|
-
"type": "string",
|
|
1411
|
-
"description": "(str) A clear and concise description of the task the member should achieve.",
|
|
1412
|
-
},
|
|
1413
|
-
"expected_output": {
|
|
1414
|
-
"type": "string",
|
|
1415
|
-
"description": "(str) The expected output from the member (optional).",
|
|
1416
|
-
},
|
|
1417
|
-
},
|
|
1418
|
-
"additionalProperties": False,
|
|
1419
|
-
"required": ["member_id", "task_description"],
|
|
1420
|
-
},
|
|
1421
|
-
}
|
|
1422
|
-
],
|
|
1423
|
-
"members": [
|
|
1424
|
-
{
|
|
1425
|
-
"agent_id": "basic-agent",
|
|
1426
|
-
"name": "Basic Agent",
|
|
1427
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI gpt-4o"},
|
|
1428
|
-
"memory": {
|
|
1429
|
-
"app_name": "Memory",
|
|
1430
|
-
"app_url": None,
|
|
1431
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1432
|
-
},
|
|
1433
|
-
"session_table": "agno_sessions",
|
|
1434
|
-
"memory_table": "agno_memories",
|
|
1435
|
-
}
|
|
1436
|
-
],
|
|
1437
|
-
"enable_agentic_context": False,
|
|
1438
|
-
"memory": {
|
|
1439
|
-
"app_name": "agno_memories",
|
|
1440
|
-
"app_url": "/memory/1",
|
|
1441
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1442
|
-
},
|
|
1443
|
-
"async_mode": False,
|
|
1444
|
-
"session_table": "agno_sessions",
|
|
1445
|
-
"memory_table": "agno_memories",
|
|
1446
|
-
}
|
|
1447
|
-
]
|
|
1448
|
-
}
|
|
1449
|
-
},
|
|
1450
|
-
}
|
|
1451
|
-
},
|
|
1452
|
-
)
|
|
1453
|
-
async def get_teams() -> List[TeamResponse]:
|
|
1454
|
-
"""Return the list of all Teams present in the contextual OS"""
|
|
1455
|
-
if os.teams is None:
|
|
1456
|
-
return []
|
|
1457
|
-
|
|
1458
|
-
teams = []
|
|
1459
|
-
for team in os.teams:
|
|
1460
|
-
team_response = await TeamResponse.from_team(team=team)
|
|
1461
|
-
teams.append(team_response)
|
|
1462
269
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
response_model_exclude_none=True,
|
|
1469
|
-
tags=["Teams"],
|
|
1470
|
-
operation_id="get_team",
|
|
1471
|
-
summary="Get Team Details",
|
|
1472
|
-
description=("Retrieve detailed configuration and member information for a specific team."),
|
|
1473
|
-
responses={
|
|
1474
|
-
200: {
|
|
1475
|
-
"description": "Team details retrieved successfully",
|
|
1476
|
-
"content": {
|
|
1477
|
-
"application/json": {
|
|
1478
|
-
"example": {
|
|
1479
|
-
"team_id": "basic-team",
|
|
1480
|
-
"name": "Basic Team",
|
|
1481
|
-
"description": None,
|
|
1482
|
-
"mode": "coordinate",
|
|
1483
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1484
|
-
"tools": [
|
|
1485
|
-
{
|
|
1486
|
-
"name": "transfer_task_to_member",
|
|
1487
|
-
"description": "Use this function to transfer a task to the selected team member.\nYou must provide a clear and concise description of the task the member should achieve AND the expected output.",
|
|
1488
|
-
"parameters": {
|
|
1489
|
-
"type": "object",
|
|
1490
|
-
"properties": {
|
|
1491
|
-
"member_id": {
|
|
1492
|
-
"type": "string",
|
|
1493
|
-
"description": "(str) The ID of the member to transfer the task to. Use only the ID of the member, not the ID of the team followed by the ID of the member.",
|
|
1494
|
-
},
|
|
1495
|
-
"task_description": {
|
|
1496
|
-
"type": "string",
|
|
1497
|
-
"description": "(str) A clear and concise description of the task the member should achieve.",
|
|
1498
|
-
},
|
|
1499
|
-
"expected_output": {
|
|
1500
|
-
"type": "string",
|
|
1501
|
-
"description": "(str) The expected output from the member (optional).",
|
|
1502
|
-
},
|
|
1503
|
-
},
|
|
1504
|
-
"additionalProperties": False,
|
|
1505
|
-
"required": ["member_id", "task_description"],
|
|
1506
|
-
},
|
|
1507
|
-
}
|
|
1508
|
-
],
|
|
1509
|
-
"instructions": None,
|
|
1510
|
-
"members": [
|
|
1511
|
-
{
|
|
1512
|
-
"agent_id": "basic-agent",
|
|
1513
|
-
"name": "Basic Agent",
|
|
1514
|
-
"description": None,
|
|
1515
|
-
"instructions": None,
|
|
1516
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI gpt-4o"},
|
|
1517
|
-
"tools": None,
|
|
1518
|
-
"memory": {
|
|
1519
|
-
"app_name": "Memory",
|
|
1520
|
-
"app_url": None,
|
|
1521
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1522
|
-
},
|
|
1523
|
-
"knowledge": None,
|
|
1524
|
-
"session_table": "agno_sessions",
|
|
1525
|
-
"memory_table": "agno_memories",
|
|
1526
|
-
"knowledge_table": None,
|
|
1527
|
-
}
|
|
1528
|
-
],
|
|
1529
|
-
"expected_output": None,
|
|
1530
|
-
"dependencies": None,
|
|
1531
|
-
"enable_agentic_context": False,
|
|
1532
|
-
"memory": {
|
|
1533
|
-
"app_name": "Memory",
|
|
1534
|
-
"app_url": None,
|
|
1535
|
-
"model": {"name": "OpenAIChat", "model": "gpt-4o", "provider": "OpenAI"},
|
|
1536
|
-
},
|
|
1537
|
-
"knowledge": None,
|
|
1538
|
-
"async_mode": False,
|
|
1539
|
-
"session_table": "agno_sessions",
|
|
1540
|
-
"memory_table": "agno_memories",
|
|
1541
|
-
"knowledge_table": None,
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
},
|
|
1545
|
-
},
|
|
1546
|
-
404: {"description": "Team not found", "model": NotFoundResponse},
|
|
1547
|
-
},
|
|
1548
|
-
)
|
|
1549
|
-
async def get_team(team_id: str) -> TeamResponse:
|
|
1550
|
-
team = get_team_by_id(team_id, os.teams)
|
|
1551
|
-
if team is None:
|
|
1552
|
-
raise HTTPException(status_code=404, detail="Team not found")
|
|
1553
|
-
|
|
1554
|
-
return await TeamResponse.from_team(team)
|
|
1555
|
-
|
|
1556
|
-
# -- Workflow routes ---
|
|
270
|
+
if validate_websocket_token(token, settings):
|
|
271
|
+
await websocket_manager.authenticate_websocket(websocket)
|
|
272
|
+
else:
|
|
273
|
+
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Invalid token"}))
|
|
274
|
+
continue
|
|
1557
275
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
tags=["Workflows"],
|
|
1563
|
-
operation_id="get_workflows",
|
|
1564
|
-
summary="List All Workflows",
|
|
1565
|
-
description=(
|
|
1566
|
-
"Retrieve a comprehensive list of all workflows configured in this OS instance.\n\n"
|
|
1567
|
-
"**Return Information:**\n"
|
|
1568
|
-
"- Workflow metadata (ID, name, description)\n"
|
|
1569
|
-
"- Input schema requirements\n"
|
|
1570
|
-
"- Step sequence and execution flow\n"
|
|
1571
|
-
"- Associated agents and teams"
|
|
1572
|
-
),
|
|
1573
|
-
responses={
|
|
1574
|
-
200: {
|
|
1575
|
-
"description": "List of workflows retrieved successfully",
|
|
1576
|
-
"content": {
|
|
1577
|
-
"application/json": {
|
|
1578
|
-
"example": [
|
|
276
|
+
# Check authentication for all other actions (only when required)
|
|
277
|
+
elif requires_auth and not websocket_manager.is_authenticated(websocket):
|
|
278
|
+
await websocket.send_text(
|
|
279
|
+
json.dumps(
|
|
1579
280
|
{
|
|
1580
|
-
"
|
|
1581
|
-
"
|
|
1582
|
-
"description": "Automated content creation from blog posts to social media",
|
|
1583
|
-
"db_id": "123",
|
|
281
|
+
"event": "auth_required",
|
|
282
|
+
"error": "Authentication required. Send authenticate action with valid token.",
|
|
1584
283
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
},
|
|
1590
|
-
)
|
|
1591
|
-
async def get_workflows() -> List[WorkflowSummaryResponse]:
|
|
1592
|
-
if os.workflows is None:
|
|
1593
|
-
return []
|
|
1594
|
-
|
|
1595
|
-
return [WorkflowSummaryResponse.from_workflow(workflow) for workflow in os.workflows]
|
|
1596
|
-
|
|
1597
|
-
@router.get(
|
|
1598
|
-
"/workflows/{workflow_id}",
|
|
1599
|
-
response_model=WorkflowResponse,
|
|
1600
|
-
response_model_exclude_none=True,
|
|
1601
|
-
tags=["Workflows"],
|
|
1602
|
-
operation_id="get_workflow",
|
|
1603
|
-
summary="Get Workflow Details",
|
|
1604
|
-
description=("Retrieve detailed configuration and step information for a specific workflow."),
|
|
1605
|
-
responses={
|
|
1606
|
-
200: {
|
|
1607
|
-
"description": "Workflow details retrieved successfully",
|
|
1608
|
-
"content": {
|
|
1609
|
-
"application/json": {
|
|
1610
|
-
"example": {
|
|
1611
|
-
"id": "content-creation-workflow",
|
|
1612
|
-
"name": "Content Creation Workflow",
|
|
1613
|
-
"description": "Automated content creation from blog posts to social media",
|
|
1614
|
-
"db_id": "123",
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
},
|
|
1618
|
-
},
|
|
1619
|
-
404: {"description": "Workflow not found", "model": NotFoundResponse},
|
|
1620
|
-
},
|
|
1621
|
-
)
|
|
1622
|
-
async def get_workflow(workflow_id: str) -> WorkflowResponse:
|
|
1623
|
-
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
1624
|
-
if workflow is None:
|
|
1625
|
-
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
1626
|
-
|
|
1627
|
-
return await WorkflowResponse.from_workflow(workflow)
|
|
1628
|
-
|
|
1629
|
-
@router.post(
|
|
1630
|
-
"/workflows/{workflow_id}/runs",
|
|
1631
|
-
tags=["Workflows"],
|
|
1632
|
-
operation_id="create_workflow_run",
|
|
1633
|
-
response_model_exclude_none=True,
|
|
1634
|
-
summary="Execute Workflow",
|
|
1635
|
-
description=(
|
|
1636
|
-
"Execute a workflow with the provided input data. Workflows can run in streaming or batch mode.\n\n"
|
|
1637
|
-
"**Execution Modes:**\n"
|
|
1638
|
-
"- **Streaming (`stream=true`)**: Real-time step-by-step execution updates via SSE\n"
|
|
1639
|
-
"- **Non-Streaming (`stream=false`)**: Complete workflow execution with final result\n\n"
|
|
1640
|
-
"**Workflow Execution Process:**\n"
|
|
1641
|
-
"1. Input validation against workflow schema\n"
|
|
1642
|
-
"2. Sequential or parallel step execution based on workflow design\n"
|
|
1643
|
-
"3. Data flow between steps with transformation\n"
|
|
1644
|
-
"4. Error handling and automatic retries where configured\n"
|
|
1645
|
-
"5. Final result compilation and response\n\n"
|
|
1646
|
-
"**Session Management:**\n"
|
|
1647
|
-
"Workflows support session continuity for stateful execution across multiple runs."
|
|
1648
|
-
),
|
|
1649
|
-
responses={
|
|
1650
|
-
200: {
|
|
1651
|
-
"description": "Workflow executed successfully",
|
|
1652
|
-
"content": {
|
|
1653
|
-
"text/event-stream": {
|
|
1654
|
-
"example": 'event: RunStarted\ndata: {"content": "Hello!", "run_id": "123..."}\n\n'
|
|
1655
|
-
},
|
|
1656
|
-
},
|
|
1657
|
-
},
|
|
1658
|
-
400: {"description": "Invalid input data or workflow configuration", "model": BadRequestResponse},
|
|
1659
|
-
404: {"description": "Workflow not found", "model": NotFoundResponse},
|
|
1660
|
-
500: {"description": "Workflow execution error", "model": InternalServerErrorResponse},
|
|
1661
|
-
},
|
|
1662
|
-
)
|
|
1663
|
-
async def create_workflow_run(
|
|
1664
|
-
workflow_id: str,
|
|
1665
|
-
request: Request,
|
|
1666
|
-
message: str = Form(...),
|
|
1667
|
-
stream: bool = Form(True),
|
|
1668
|
-
session_id: Optional[str] = Form(None),
|
|
1669
|
-
user_id: Optional[str] = Form(None),
|
|
1670
|
-
):
|
|
1671
|
-
kwargs = await _get_request_kwargs(request, create_workflow_run)
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
continue
|
|
1672
287
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
user_id = request.state.user_id
|
|
1677
|
-
if hasattr(request.state, "session_id"):
|
|
1678
|
-
if session_id:
|
|
1679
|
-
log_warning("Session ID parameter passed in both request state and kwargs, using request state")
|
|
1680
|
-
session_id = request.state.session_id
|
|
1681
|
-
if hasattr(request.state, "session_state"):
|
|
1682
|
-
session_state = request.state.session_state
|
|
1683
|
-
if "session_state" in kwargs:
|
|
1684
|
-
log_warning("Session state parameter passed in both request state and kwargs, using request state")
|
|
1685
|
-
kwargs["session_state"] = session_state
|
|
1686
|
-
if hasattr(request.state, "dependencies"):
|
|
1687
|
-
dependencies = request.state.dependencies
|
|
1688
|
-
if "dependencies" in kwargs:
|
|
1689
|
-
log_warning("Dependencies parameter passed in both request state and kwargs, using request state")
|
|
1690
|
-
kwargs["dependencies"] = dependencies
|
|
1691
|
-
if hasattr(request.state, "metadata"):
|
|
1692
|
-
metadata = request.state.metadata
|
|
1693
|
-
if "metadata" in kwargs:
|
|
1694
|
-
log_warning("Metadata parameter passed in both request state and kwargs, using request state")
|
|
1695
|
-
kwargs["metadata"] = metadata
|
|
288
|
+
# Handle authenticated actions
|
|
289
|
+
elif action == "ping":
|
|
290
|
+
await websocket.send_text(json.dumps({"event": "pong"}))
|
|
1696
291
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
292
|
+
elif action == "start-workflow":
|
|
293
|
+
# Handle workflow execution directly via WebSocket
|
|
294
|
+
await handle_workflow_via_websocket(websocket, message, os)
|
|
1701
295
|
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
logger.debug("Creating new session")
|
|
1706
|
-
session_id = str(uuid4())
|
|
296
|
+
elif action == "reconnect":
|
|
297
|
+
# Subscribe/reconnect to an existing workflow run
|
|
298
|
+
await handle_workflow_subscription(websocket, message, os)
|
|
1707
299
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
if stream:
|
|
1711
|
-
return StreamingResponse(
|
|
1712
|
-
workflow_response_streamer(
|
|
1713
|
-
workflow,
|
|
1714
|
-
input=message,
|
|
1715
|
-
session_id=session_id,
|
|
1716
|
-
user_id=user_id,
|
|
1717
|
-
**kwargs,
|
|
1718
|
-
),
|
|
1719
|
-
media_type="text/event-stream",
|
|
1720
|
-
)
|
|
1721
|
-
else:
|
|
1722
|
-
run_response = await workflow.arun(
|
|
1723
|
-
input=message,
|
|
1724
|
-
session_id=session_id,
|
|
1725
|
-
user_id=user_id,
|
|
1726
|
-
stream=False,
|
|
1727
|
-
**kwargs,
|
|
1728
|
-
)
|
|
1729
|
-
return run_response.to_dict()
|
|
300
|
+
else:
|
|
301
|
+
await websocket.send_text(json.dumps({"event": "error", "error": f"Unknown action: {action}"}))
|
|
1730
302
|
|
|
1731
|
-
except InputCheckError as e:
|
|
1732
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
1733
303
|
except Exception as e:
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
tags=["Workflows"],
|
|
1740
|
-
operation_id="cancel_workflow_run",
|
|
1741
|
-
summary="Cancel Workflow Run",
|
|
1742
|
-
description=(
|
|
1743
|
-
"Cancel a currently executing workflow run, stopping all active steps and cleanup.\n"
|
|
1744
|
-
"**Note:** Complex workflows with multiple parallel steps may take time to fully cancel."
|
|
1745
|
-
),
|
|
1746
|
-
responses={
|
|
1747
|
-
200: {},
|
|
1748
|
-
404: {"description": "Workflow or run not found", "model": NotFoundResponse},
|
|
1749
|
-
500: {"description": "Failed to cancel workflow run", "model": InternalServerErrorResponse},
|
|
1750
|
-
},
|
|
1751
|
-
)
|
|
1752
|
-
async def cancel_workflow_run(workflow_id: str, run_id: str):
|
|
1753
|
-
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
1754
|
-
|
|
1755
|
-
if workflow is None:
|
|
1756
|
-
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
1757
|
-
|
|
1758
|
-
if not workflow.cancel_run(run_id=run_id):
|
|
1759
|
-
raise HTTPException(status_code=500, detail="Failed to cancel run")
|
|
1760
|
-
|
|
1761
|
-
return JSONResponse(content={}, status_code=200)
|
|
304
|
+
if "1012" not in str(e) and "1001" not in str(e):
|
|
305
|
+
logger.error(f"WebSocket error: {e}")
|
|
306
|
+
finally:
|
|
307
|
+
# Clean up the websocket connection
|
|
308
|
+
await websocket_manager.disconnect_websocket(websocket)
|
|
1762
309
|
|
|
1763
|
-
return
|
|
310
|
+
return ws_router
|