agno 2.1.4__py3-none-any.whl → 2.1.6__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/agent.py +1775 -538
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/async_postgres/async_postgres.py +1668 -0
- agno/db/async_postgres/schemas.py +124 -0
- agno/db/async_postgres/utils.py +289 -0
- agno/db/base.py +237 -2
- agno/db/dynamo/dynamo.py +2 -2
- agno/db/firestore/firestore.py +2 -2
- agno/db/firestore/utils.py +4 -2
- agno/db/gcs_json/gcs_json_db.py +2 -2
- agno/db/in_memory/in_memory_db.py +2 -2
- agno/db/json/json_db.py +2 -2
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/mongo/mongo.py +14 -6
- agno/db/mongo/utils.py +0 -4
- agno/db/mysql/mysql.py +23 -13
- agno/db/postgres/postgres.py +17 -6
- agno/db/redis/redis.py +2 -2
- agno/db/singlestore/singlestore.py +19 -10
- agno/db/sqlite/sqlite.py +22 -12
- agno/db/sqlite/utils.py +8 -3
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +259 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1193 -0
- agno/db/surrealdb/utils.py +87 -0
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +8 -2
- agno/knowledge/knowledge.py +260 -46
- agno/knowledge/reader/pdf_reader.py +4 -6
- agno/knowledge/reader/reader_factory.py +2 -3
- agno/memory/manager.py +254 -46
- agno/models/anthropic/claude.py +37 -0
- agno/os/app.py +8 -7
- agno/os/interfaces/a2a/router.py +3 -5
- agno/os/interfaces/agui/router.py +4 -1
- agno/os/interfaces/agui/utils.py +27 -6
- agno/os/interfaces/slack/router.py +2 -4
- agno/os/mcp.py +98 -41
- agno/os/router.py +23 -0
- agno/os/routers/evals/evals.py +52 -20
- agno/os/routers/evals/utils.py +14 -14
- agno/os/routers/knowledge/knowledge.py +130 -9
- agno/os/routers/knowledge/schemas.py +57 -0
- agno/os/routers/memory/memory.py +116 -44
- agno/os/routers/metrics/metrics.py +16 -6
- agno/os/routers/session/session.py +65 -22
- agno/os/schema.py +36 -0
- agno/os/utils.py +64 -11
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/session/workflow.py +3 -3
- agno/team/team.py +968 -179
- agno/tools/googlesheets.py +20 -5
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/scrapegraph.py +1 -1
- agno/utils/models/claude.py +3 -1
- agno/utils/streamlit.py +1 -1
- agno/vectordb/base.py +22 -1
- agno/vectordb/cassandra/cassandra.py +9 -0
- agno/vectordb/chroma/chromadb.py +26 -6
- agno/vectordb/clickhouse/clickhousedb.py +9 -1
- agno/vectordb/couchbase/couchbase.py +11 -0
- agno/vectordb/lancedb/lance_db.py +20 -0
- agno/vectordb/langchaindb/langchaindb.py +11 -0
- agno/vectordb/lightrag/lightrag.py +9 -0
- agno/vectordb/llamaindex/llamaindexdb.py +15 -1
- agno/vectordb/milvus/milvus.py +23 -0
- agno/vectordb/mongodb/mongodb.py +22 -0
- agno/vectordb/pgvector/pgvector.py +19 -0
- agno/vectordb/pineconedb/pineconedb.py +35 -4
- agno/vectordb/qdrant/qdrant.py +24 -0
- agno/vectordb/singlestore/singlestore.py +25 -17
- agno/vectordb/surrealdb/surrealdb.py +18 -2
- agno/vectordb/upstashdb/upstashdb.py +26 -1
- agno/vectordb/weaviate/weaviate.py +18 -0
- agno/workflow/condition.py +4 -0
- agno/workflow/loop.py +4 -0
- agno/workflow/parallel.py +4 -0
- agno/workflow/router.py +4 -0
- agno/workflow/step.py +30 -14
- agno/workflow/steps.py +4 -0
- agno/workflow/types.py +2 -2
- agno/workflow/workflow.py +328 -61
- {agno-2.1.4.dist-info → agno-2.1.6.dist-info}/METADATA +100 -41
- {agno-2.1.4.dist-info → agno-2.1.6.dist-info}/RECORD +95 -82
- {agno-2.1.4.dist-info → agno-2.1.6.dist-info}/WHEEL +0 -0
- {agno-2.1.4.dist-info → agno-2.1.6.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.4.dist-info → agno-2.1.6.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import List, Optional, Union
|
|
2
|
+
from typing import List, Optional, Union, cast
|
|
3
3
|
|
|
4
4
|
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
|
|
5
5
|
|
|
6
|
-
from agno.db.base import BaseDb, SessionType
|
|
6
|
+
from agno.db.base import AsyncBaseDb, BaseDb, SessionType
|
|
7
7
|
from agno.os.auth import get_authentication_dependency
|
|
8
8
|
from agno.os.schema import (
|
|
9
9
|
AgentSessionDetailSchema,
|
|
@@ -29,7 +29,9 @@ from agno.os.utils import get_db
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def get_session_router(
|
|
32
|
+
def get_session_router(
|
|
33
|
+
dbs: dict[str, Union[BaseDb, AsyncBaseDb]], settings: AgnoAPISettings = AgnoAPISettings()
|
|
34
|
+
) -> APIRouter:
|
|
33
35
|
"""Create session router with comprehensive OpenAPI documentation for session management endpoints."""
|
|
34
36
|
session_router = APIRouter(
|
|
35
37
|
dependencies=[Depends(get_authentication_dependency(settings))],
|
|
@@ -45,7 +47,7 @@ def get_session_router(dbs: dict[str, BaseDb], settings: AgnoAPISettings = AgnoA
|
|
|
45
47
|
return attach_routes(router=session_router, dbs=dbs)
|
|
46
48
|
|
|
47
49
|
|
|
48
|
-
def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
50
|
+
def attach_routes(router: APIRouter, dbs: dict[str, Union[BaseDb, AsyncBaseDb]]) -> APIRouter:
|
|
49
51
|
@router.get(
|
|
50
52
|
"/sessions",
|
|
51
53
|
response_model=PaginatedResponse[SessionSchema],
|
|
@@ -108,17 +110,31 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
108
110
|
if hasattr(request.state, "user_id"):
|
|
109
111
|
user_id = request.state.user_id
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
if isinstance(db, AsyncBaseDb):
|
|
114
|
+
db = cast(AsyncBaseDb, db)
|
|
115
|
+
sessions, total_count = await db.get_sessions(
|
|
116
|
+
session_type=session_type,
|
|
117
|
+
component_id=component_id,
|
|
118
|
+
user_id=user_id,
|
|
119
|
+
session_name=session_name,
|
|
120
|
+
limit=limit,
|
|
121
|
+
page=page,
|
|
122
|
+
sort_by=sort_by,
|
|
123
|
+
sort_order=sort_order,
|
|
124
|
+
deserialize=False,
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
sessions, total_count = db.get_sessions( # type: ignore
|
|
128
|
+
session_type=session_type,
|
|
129
|
+
component_id=component_id,
|
|
130
|
+
user_id=user_id,
|
|
131
|
+
session_name=session_name,
|
|
132
|
+
limit=limit,
|
|
133
|
+
page=page,
|
|
134
|
+
sort_by=sort_by,
|
|
135
|
+
sort_order=sort_order,
|
|
136
|
+
deserialize=False,
|
|
137
|
+
)
|
|
122
138
|
|
|
123
139
|
return PaginatedResponse(
|
|
124
140
|
data=[SessionSchema.from_dict(session) for session in sessions], # type: ignore
|
|
@@ -231,7 +247,11 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
231
247
|
if hasattr(request.state, "user_id"):
|
|
232
248
|
user_id = request.state.user_id
|
|
233
249
|
|
|
234
|
-
|
|
250
|
+
if isinstance(db, AsyncBaseDb):
|
|
251
|
+
db = cast(AsyncBaseDb, db)
|
|
252
|
+
session = await db.get_session(session_id=session_id, session_type=session_type, user_id=user_id)
|
|
253
|
+
else:
|
|
254
|
+
session = db.get_session(session_id=session_id, session_type=session_type, user_id=user_id)
|
|
235
255
|
if not session:
|
|
236
256
|
raise HTTPException(
|
|
237
257
|
status_code=404, detail=f"{session_type.value.title()} Session with id '{session_id}' not found"
|
|
@@ -373,7 +393,16 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
373
393
|
if hasattr(request.state, "user_id"):
|
|
374
394
|
user_id = request.state.user_id
|
|
375
395
|
|
|
376
|
-
|
|
396
|
+
if isinstance(db, AsyncBaseDb):
|
|
397
|
+
db = cast(AsyncBaseDb, db)
|
|
398
|
+
session = await db.get_session(
|
|
399
|
+
session_id=session_id, session_type=session_type, user_id=user_id, deserialize=False
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
session = db.get_session(
|
|
403
|
+
session_id=session_id, session_type=session_type, user_id=user_id, deserialize=False
|
|
404
|
+
)
|
|
405
|
+
|
|
377
406
|
if not session:
|
|
378
407
|
raise HTTPException(status_code=404, detail=f"Session with ID {session_id} not found")
|
|
379
408
|
|
|
@@ -381,11 +410,12 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
381
410
|
if not runs:
|
|
382
411
|
raise HTTPException(status_code=404, detail=f"Session with ID {session_id} has no runs")
|
|
383
412
|
|
|
413
|
+
run_responses: List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]] = []
|
|
414
|
+
|
|
384
415
|
if session_type == SessionType.AGENT:
|
|
385
416
|
return [RunSchema.from_dict(run) for run in runs]
|
|
386
417
|
|
|
387
418
|
elif session_type == SessionType.TEAM:
|
|
388
|
-
run_responses: List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]] = []
|
|
389
419
|
for run in runs:
|
|
390
420
|
if run.get("agent_id") is not None:
|
|
391
421
|
run_responses.append(RunSchema.from_dict(run))
|
|
@@ -394,7 +424,6 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
394
424
|
return run_responses
|
|
395
425
|
|
|
396
426
|
elif session_type == SessionType.WORKFLOW:
|
|
397
|
-
run_responses: List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]] = [] # type: ignore
|
|
398
427
|
for run in runs:
|
|
399
428
|
if run.get("workflow_id") is not None:
|
|
400
429
|
run_responses.append(WorkflowRunSchema.from_dict(run))
|
|
@@ -425,7 +454,11 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
425
454
|
db_id: Optional[str] = Query(default=None, description="Database ID to use for deletion"),
|
|
426
455
|
) -> None:
|
|
427
456
|
db = get_db(dbs, db_id)
|
|
428
|
-
db
|
|
457
|
+
if isinstance(db, AsyncBaseDb):
|
|
458
|
+
db = cast(AsyncBaseDb, db)
|
|
459
|
+
await db.delete_session(session_id=session_id)
|
|
460
|
+
else:
|
|
461
|
+
db.delete_session(session_id=session_id)
|
|
429
462
|
|
|
430
463
|
@router.delete(
|
|
431
464
|
"/sessions",
|
|
@@ -456,7 +489,11 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
456
489
|
raise HTTPException(status_code=400, detail="Session IDs and session types must have the same length")
|
|
457
490
|
|
|
458
491
|
db = get_db(dbs, db_id)
|
|
459
|
-
db
|
|
492
|
+
if isinstance(db, AsyncBaseDb):
|
|
493
|
+
db = cast(AsyncBaseDb, db)
|
|
494
|
+
await db.delete_sessions(session_ids=request.session_ids)
|
|
495
|
+
else:
|
|
496
|
+
db.delete_sessions(session_ids=request.session_ids)
|
|
460
497
|
|
|
461
498
|
@router.post(
|
|
462
499
|
"/sessions/{session_id}/rename",
|
|
@@ -555,7 +592,13 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
555
592
|
db_id: Optional[str] = Query(default=None, description="Database ID to use for rename operation"),
|
|
556
593
|
) -> Union[AgentSessionDetailSchema, TeamSessionDetailSchema, WorkflowSessionDetailSchema]:
|
|
557
594
|
db = get_db(dbs, db_id)
|
|
558
|
-
|
|
595
|
+
if isinstance(db, AsyncBaseDb):
|
|
596
|
+
db = cast(AsyncBaseDb, db)
|
|
597
|
+
session = await db.rename_session(
|
|
598
|
+
session_id=session_id, session_type=session_type, session_name=session_name
|
|
599
|
+
)
|
|
600
|
+
else:
|
|
601
|
+
session = db.rename_session(session_id=session_id, session_type=session_type, session_name=session_name)
|
|
559
602
|
if not session:
|
|
560
603
|
raise HTTPException(status_code=404, detail=f"Session with id '{session_id}' not found")
|
|
561
604
|
|
agno/os/schema.py
CHANGED
|
@@ -10,6 +10,7 @@ from agno.db.base import SessionType
|
|
|
10
10
|
from agno.models.message import Message
|
|
11
11
|
from agno.os.config import ChatConfig, EvalsConfig, KnowledgeConfig, MemoryConfig, MetricsConfig, SessionConfig
|
|
12
12
|
from agno.os.utils import (
|
|
13
|
+
extract_input_media,
|
|
13
14
|
format_team_tools,
|
|
14
15
|
format_tools,
|
|
15
16
|
get_run_input,
|
|
@@ -841,6 +842,12 @@ class RunSchema(BaseModel):
|
|
|
841
842
|
created_at: Optional[datetime]
|
|
842
843
|
references: Optional[List[dict]]
|
|
843
844
|
reasoning_messages: Optional[List[dict]]
|
|
845
|
+
images: Optional[List[dict]]
|
|
846
|
+
videos: Optional[List[dict]]
|
|
847
|
+
audio: Optional[List[dict]]
|
|
848
|
+
files: Optional[List[dict]]
|
|
849
|
+
response_audio: Optional[List[dict]]
|
|
850
|
+
input_media: Optional[Dict[str, Any]]
|
|
844
851
|
|
|
845
852
|
@classmethod
|
|
846
853
|
def from_dict(cls, run_dict: Dict[str, Any]) -> "RunSchema":
|
|
@@ -862,6 +869,12 @@ class RunSchema(BaseModel):
|
|
|
862
869
|
events=[event for event in run_dict["events"]] if run_dict.get("events") else None,
|
|
863
870
|
references=run_dict.get("references", []),
|
|
864
871
|
reasoning_messages=run_dict.get("reasoning_messages", []),
|
|
872
|
+
images=run_dict.get("images", []),
|
|
873
|
+
videos=run_dict.get("videos", []),
|
|
874
|
+
audio=run_dict.get("audio", []),
|
|
875
|
+
files=run_dict.get("files", []),
|
|
876
|
+
response_audio=run_dict.get("response_audio", []),
|
|
877
|
+
input_media=extract_input_media(run_dict),
|
|
865
878
|
created_at=datetime.fromtimestamp(run_dict.get("created_at", 0), tz=timezone.utc)
|
|
866
879
|
if run_dict.get("created_at") is not None
|
|
867
880
|
else None,
|
|
@@ -884,6 +897,12 @@ class TeamRunSchema(BaseModel):
|
|
|
884
897
|
created_at: Optional[datetime]
|
|
885
898
|
references: Optional[List[dict]]
|
|
886
899
|
reasoning_messages: Optional[List[dict]]
|
|
900
|
+
input_media: Optional[Dict[str, Any]]
|
|
901
|
+
images: Optional[List[dict]]
|
|
902
|
+
videos: Optional[List[dict]]
|
|
903
|
+
audio: Optional[List[dict]]
|
|
904
|
+
files: Optional[List[dict]]
|
|
905
|
+
response_audio: Optional[List[dict]]
|
|
887
906
|
|
|
888
907
|
@classmethod
|
|
889
908
|
def from_dict(cls, run_dict: Dict[str, Any]) -> "TeamRunSchema":
|
|
@@ -907,6 +926,12 @@ class TeamRunSchema(BaseModel):
|
|
|
907
926
|
else None,
|
|
908
927
|
references=run_dict.get("references", []),
|
|
909
928
|
reasoning_messages=run_dict.get("reasoning_messages", []),
|
|
929
|
+
images=run_dict.get("images", []),
|
|
930
|
+
videos=run_dict.get("videos", []),
|
|
931
|
+
audio=run_dict.get("audio", []),
|
|
932
|
+
files=run_dict.get("files", []),
|
|
933
|
+
response_audio=run_dict.get("response_audio", []),
|
|
934
|
+
input_media=extract_input_media(run_dict),
|
|
910
935
|
)
|
|
911
936
|
|
|
912
937
|
|
|
@@ -927,6 +952,11 @@ class WorkflowRunSchema(BaseModel):
|
|
|
927
952
|
reasoning_steps: Optional[List[dict]]
|
|
928
953
|
references: Optional[List[dict]]
|
|
929
954
|
reasoning_messages: Optional[List[dict]]
|
|
955
|
+
images: Optional[List[dict]]
|
|
956
|
+
videos: Optional[List[dict]]
|
|
957
|
+
audio: Optional[List[dict]]
|
|
958
|
+
files: Optional[List[dict]]
|
|
959
|
+
response_audio: Optional[List[dict]]
|
|
930
960
|
|
|
931
961
|
@classmethod
|
|
932
962
|
def from_dict(cls, run_response: Dict[str, Any]) -> "WorkflowRunSchema":
|
|
@@ -948,6 +978,11 @@ class WorkflowRunSchema(BaseModel):
|
|
|
948
978
|
reasoning_steps=run_response.get("reasoning_steps", []),
|
|
949
979
|
references=run_response.get("references", []),
|
|
950
980
|
reasoning_messages=run_response.get("reasoning_messages", []),
|
|
981
|
+
images=run_response.get("images", []),
|
|
982
|
+
videos=run_response.get("videos", []),
|
|
983
|
+
audio=run_response.get("audio", []),
|
|
984
|
+
files=run_response.get("files", []),
|
|
985
|
+
response_audio=run_response.get("response_audio", []),
|
|
951
986
|
)
|
|
952
987
|
|
|
953
988
|
|
|
@@ -964,6 +999,7 @@ class PaginationInfo(BaseModel):
|
|
|
964
999
|
limit: Optional[int] = 20
|
|
965
1000
|
total_pages: Optional[int] = 0
|
|
966
1001
|
total_count: Optional[int] = 0
|
|
1002
|
+
search_time_ms: Optional[float] = 0
|
|
967
1003
|
|
|
968
1004
|
|
|
969
1005
|
class PaginatedResponse(BaseModel, Generic[T]):
|
agno/os/utils.py
CHANGED
|
@@ -2,13 +2,15 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
|
|
|
2
2
|
|
|
3
3
|
from fastapi import FastAPI, HTTPException, UploadFile
|
|
4
4
|
from fastapi.routing import APIRoute, APIRouter
|
|
5
|
+
from pydantic import BaseModel
|
|
5
6
|
from starlette.middleware.cors import CORSMiddleware
|
|
6
7
|
|
|
7
8
|
from agno.agent.agent import Agent
|
|
8
|
-
from agno.db.base import BaseDb
|
|
9
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
9
10
|
from agno.knowledge.knowledge import Knowledge
|
|
10
11
|
from agno.media import Audio, Image, Video
|
|
11
12
|
from agno.media import File as FileMedia
|
|
13
|
+
from agno.models.message import Message
|
|
12
14
|
from agno.os.config import AgentOSConfig
|
|
13
15
|
from agno.team.team import Team
|
|
14
16
|
from agno.tools import Toolkit
|
|
@@ -17,7 +19,7 @@ from agno.utils.log import logger
|
|
|
17
19
|
from agno.workflow.workflow import Workflow
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
def get_db(dbs: dict[str, BaseDb], db_id: Optional[str] = None) -> BaseDb:
|
|
22
|
+
def get_db(dbs: dict[str, Union[BaseDb, AsyncBaseDb]], db_id: Optional[str] = None) -> Union[BaseDb, AsyncBaseDb]:
|
|
21
23
|
"""Return the database with the given ID, or the first database if no ID is provided."""
|
|
22
24
|
|
|
23
25
|
# Raise if multiple databases are provided but no db_id is provided
|
|
@@ -54,23 +56,31 @@ def get_knowledge_instance_by_db_id(knowledge_instances: List[Knowledge], db_id:
|
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
def get_run_input(run_dict: Dict[str, Any], is_workflow_run: bool = False) -> str:
|
|
57
|
-
"""Get the run input from the given run dictionary
|
|
59
|
+
"""Get the run input from the given run dictionary
|
|
60
|
+
|
|
61
|
+
Uses the RunInput/TeamRunInput object which stores the original user input.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# For agent or team runs, use the stored input_content
|
|
65
|
+
if not is_workflow_run and run_dict.get("input") is not None:
|
|
66
|
+
input_data = run_dict.get("input")
|
|
67
|
+
if isinstance(input_data, dict) and input_data.get("input_content") is not None:
|
|
68
|
+
return stringify_input_content(input_data["input_content"])
|
|
58
69
|
|
|
59
70
|
if is_workflow_run:
|
|
71
|
+
# Check the input field directly
|
|
72
|
+
if run_dict.get("input") is not None:
|
|
73
|
+
input_value = run_dict.get("input")
|
|
74
|
+
return str(input_value)
|
|
75
|
+
|
|
76
|
+
# Check the step executor runs for fallback
|
|
60
77
|
step_executor_runs = run_dict.get("step_executor_runs", [])
|
|
61
78
|
if step_executor_runs:
|
|
62
79
|
for message in reversed(step_executor_runs[0].get("messages", [])):
|
|
63
80
|
if message.get("role") == "user":
|
|
64
81
|
return message.get("content", "")
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
if run_dict.get("input") is not None:
|
|
68
|
-
input_value = run_dict.get("input")
|
|
69
|
-
if isinstance(input_value, str):
|
|
70
|
-
return input_value
|
|
71
|
-
else:
|
|
72
|
-
return str(input_value)
|
|
73
|
-
|
|
83
|
+
# Final fallback: scan messages
|
|
74
84
|
if run_dict.get("messages") is not None:
|
|
75
85
|
for message in reversed(run_dict["messages"]):
|
|
76
86
|
if message.get("role") == "user":
|
|
@@ -140,6 +150,23 @@ def get_session_name(session: Dict[str, Any]) -> str:
|
|
|
140
150
|
return ""
|
|
141
151
|
|
|
142
152
|
|
|
153
|
+
def extract_input_media(run_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
154
|
+
input_media: Dict[str, List[Any]] = {
|
|
155
|
+
"images": [],
|
|
156
|
+
"videos": [],
|
|
157
|
+
"audios": [],
|
|
158
|
+
"files": [],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
input = run_dict.get("input", [])
|
|
162
|
+
input_media["images"].extend(input.get("images", []))
|
|
163
|
+
input_media["videos"].extend(input.get("videos", []))
|
|
164
|
+
input_media["audios"].extend(input.get("audios", []))
|
|
165
|
+
input_media["files"].extend(input.get("files", []))
|
|
166
|
+
|
|
167
|
+
return input_media
|
|
168
|
+
|
|
169
|
+
|
|
143
170
|
def process_image(file: UploadFile) -> Image:
|
|
144
171
|
content = file.file.read()
|
|
145
172
|
if not content:
|
|
@@ -495,3 +522,29 @@ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> Non
|
|
|
495
522
|
elif isinstance(step, Workflow):
|
|
496
523
|
# Nested workflow
|
|
497
524
|
collect_mcp_tools_from_workflow(step, mcp_tools)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
|
|
528
|
+
"""Convert any given input_content into its string representation.
|
|
529
|
+
|
|
530
|
+
This handles both serialized (dict) and live (object) input_content formats.
|
|
531
|
+
"""
|
|
532
|
+
import json
|
|
533
|
+
|
|
534
|
+
if isinstance(input_content, str):
|
|
535
|
+
return input_content
|
|
536
|
+
elif isinstance(input_content, Message):
|
|
537
|
+
return json.dumps(input_content.to_dict())
|
|
538
|
+
elif isinstance(input_content, dict):
|
|
539
|
+
return json.dumps(input_content, indent=2, default=str)
|
|
540
|
+
elif isinstance(input_content, list):
|
|
541
|
+
if input_content:
|
|
542
|
+
# Handle live Message objects
|
|
543
|
+
if isinstance(input_content[0], Message):
|
|
544
|
+
return json.dumps([m.to_dict() for m in input_content])
|
|
545
|
+
# Handle serialized Message dicts
|
|
546
|
+
elif isinstance(input_content[0], dict) and input_content[0].get("role") == "user":
|
|
547
|
+
return input_content[0].get("content", str(input_content))
|
|
548
|
+
return str(input_content)
|
|
549
|
+
else:
|
|
550
|
+
return str(input_content)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from agno.models.base import Model
|
|
6
|
+
from agno.models.message import Message
|
|
7
|
+
from agno.utils.log import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_anthropic_reasoning_model(reasoning_model: Model) -> bool:
|
|
11
|
+
"""Check if the model is an Anthropic Claude model with thinking support."""
|
|
12
|
+
is_claude = reasoning_model.__class__.__name__ == "Claude"
|
|
13
|
+
if not is_claude:
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
# Check if provider is Anthropic (not VertexAI)
|
|
17
|
+
is_anthropic_provider = hasattr(reasoning_model, "provider") and reasoning_model.provider == "Anthropic"
|
|
18
|
+
|
|
19
|
+
# Check if thinking parameter is set
|
|
20
|
+
has_thinking = hasattr(reasoning_model, "thinking") and reasoning_model.thinking is not None
|
|
21
|
+
|
|
22
|
+
return is_claude and is_anthropic_provider and has_thinking
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_anthropic_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
26
|
+
"""Get reasoning from an Anthropic Claude model."""
|
|
27
|
+
from agno.run.agent import RunOutput
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.warning(f"Reasoning error: {e}")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
reasoning_content: str = ""
|
|
36
|
+
redacted_reasoning_content: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
if reasoning_agent_response.messages is not None:
|
|
39
|
+
for msg in reasoning_agent_response.messages:
|
|
40
|
+
if msg.reasoning_content is not None:
|
|
41
|
+
reasoning_content = msg.reasoning_content
|
|
42
|
+
if hasattr(msg, "redacted_reasoning_content") and msg.redacted_reasoning_content is not None:
|
|
43
|
+
redacted_reasoning_content = msg.redacted_reasoning_content
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
return Message(
|
|
47
|
+
role="assistant",
|
|
48
|
+
content=f"<thinking>\n{reasoning_content}\n</thinking>",
|
|
49
|
+
reasoning_content=reasoning_content,
|
|
50
|
+
redacted_reasoning_content=redacted_reasoning_content,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def aget_anthropic_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
55
|
+
"""Get reasoning from an Anthropic Claude model asynchronously."""
|
|
56
|
+
from agno.run.agent import RunOutput
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.warning(f"Reasoning error: {e}")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
reasoning_content: str = ""
|
|
65
|
+
redacted_reasoning_content: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
if reasoning_agent_response.messages is not None:
|
|
68
|
+
for msg in reasoning_agent_response.messages:
|
|
69
|
+
if msg.reasoning_content is not None:
|
|
70
|
+
reasoning_content = msg.reasoning_content
|
|
71
|
+
if hasattr(msg, "redacted_reasoning_content") and msg.redacted_reasoning_content is not None:
|
|
72
|
+
redacted_reasoning_content = msg.redacted_reasoning_content
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
return Message(
|
|
76
|
+
role="assistant",
|
|
77
|
+
content=f"<thinking>\n{reasoning_content}\n</thinking>",
|
|
78
|
+
reasoning_content=reasoning_content,
|
|
79
|
+
redacted_reasoning_content=redacted_reasoning_content,
|
|
80
|
+
)
|
agno/reasoning/gemini.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from agno.models.base import Model
|
|
6
|
+
from agno.models.message import Message
|
|
7
|
+
from agno.utils.log import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_gemini_reasoning_model(reasoning_model: Model) -> bool:
|
|
11
|
+
"""Check if the model is a Gemini model with thinking support."""
|
|
12
|
+
is_gemini_class = reasoning_model.__class__.__name__ == "Gemini"
|
|
13
|
+
if not is_gemini_class:
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
# Check if it's a Gemini 2.5+ model (supports thinking)
|
|
17
|
+
model_id = reasoning_model.id.lower()
|
|
18
|
+
has_thinking_support = "2.5" in model_id
|
|
19
|
+
|
|
20
|
+
# Also check if thinking parameters are set
|
|
21
|
+
# Note: thinking_budget=0 explicitly disables thinking mode per Google's API docs
|
|
22
|
+
has_thinking_budget = (
|
|
23
|
+
hasattr(reasoning_model, "thinking_budget")
|
|
24
|
+
and reasoning_model.thinking_budget is not None
|
|
25
|
+
and reasoning_model.thinking_budget > 0
|
|
26
|
+
)
|
|
27
|
+
has_include_thoughts = hasattr(reasoning_model, "include_thoughts") and reasoning_model.include_thoughts is not None
|
|
28
|
+
|
|
29
|
+
return is_gemini_class and (has_thinking_support or has_thinking_budget or has_include_thoughts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_gemini_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
33
|
+
"""Get reasoning from a Gemini model."""
|
|
34
|
+
from agno.run.agent import RunOutput
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logger.warning(f"Reasoning error: {e}")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
reasoning_content: str = ""
|
|
43
|
+
if reasoning_agent_response.messages is not None:
|
|
44
|
+
for msg in reasoning_agent_response.messages:
|
|
45
|
+
if msg.reasoning_content is not None:
|
|
46
|
+
reasoning_content = msg.reasoning_content
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
return Message(
|
|
50
|
+
role="assistant", content=f"<thinking>\n{reasoning_content}\n</thinking>", reasoning_content=reasoning_content
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def aget_gemini_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
55
|
+
"""Get reasoning from a Gemini model asynchronously."""
|
|
56
|
+
from agno.run.agent import RunOutput
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.warning(f"Reasoning error: {e}")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
reasoning_content: str = ""
|
|
65
|
+
if reasoning_agent_response.messages is not None:
|
|
66
|
+
for msg in reasoning_agent_response.messages:
|
|
67
|
+
if msg.reasoning_content is not None:
|
|
68
|
+
reasoning_content = msg.reasoning_content
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
return Message(
|
|
72
|
+
role="assistant", content=f"<thinking>\n{reasoning_content}\n</thinking>", reasoning_content=reasoning_content
|
|
73
|
+
)
|
agno/reasoning/openai.py
CHANGED
|
@@ -28,6 +28,11 @@ def is_openai_reasoning_model(reasoning_model: Model) -> bool:
|
|
|
28
28
|
def get_openai_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
29
29
|
from agno.run.agent import RunOutput
|
|
30
30
|
|
|
31
|
+
# Update system message role to "system"
|
|
32
|
+
for message in messages:
|
|
33
|
+
if message.role == "developer":
|
|
34
|
+
message.role = "system"
|
|
35
|
+
|
|
31
36
|
try:
|
|
32
37
|
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
33
38
|
except Exception as e:
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from agno.models.base import Model
|
|
6
|
+
from agno.models.message import Message
|
|
7
|
+
from agno.utils.log import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_vertexai_reasoning_model(reasoning_model: Model) -> bool:
|
|
11
|
+
"""Check if the model is a VertexAI model with thinking support."""
|
|
12
|
+
# Check if provider is VertexAI
|
|
13
|
+
is_vertexai_provider = hasattr(reasoning_model, "provider") and reasoning_model.provider == "VertexAI"
|
|
14
|
+
|
|
15
|
+
# Check if thinking parameter is set
|
|
16
|
+
has_thinking = hasattr(reasoning_model, "thinking") and reasoning_model.thinking is not None
|
|
17
|
+
|
|
18
|
+
return is_vertexai_provider and has_thinking
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_vertexai_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
22
|
+
"""Get reasoning from a VertexAI Claude model."""
|
|
23
|
+
from agno.run.agent import RunOutput
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.warning(f"Reasoning error: {e}")
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
reasoning_content: str = ""
|
|
32
|
+
redacted_reasoning_content: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
if reasoning_agent_response.messages is not None:
|
|
35
|
+
for msg in reasoning_agent_response.messages:
|
|
36
|
+
if msg.reasoning_content is not None:
|
|
37
|
+
reasoning_content = msg.reasoning_content
|
|
38
|
+
if hasattr(msg, "redacted_reasoning_content") and msg.redacted_reasoning_content is not None:
|
|
39
|
+
redacted_reasoning_content = msg.redacted_reasoning_content
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
return Message(
|
|
43
|
+
role="assistant",
|
|
44
|
+
content=f"<thinking>\n{reasoning_content}\n</thinking>",
|
|
45
|
+
reasoning_content=reasoning_content,
|
|
46
|
+
redacted_reasoning_content=redacted_reasoning_content,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def aget_vertexai_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
|
|
51
|
+
"""Get reasoning from a VertexAI Claude model asynchronously."""
|
|
52
|
+
from agno.run.agent import RunOutput
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.warning(f"Reasoning error: {e}")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
reasoning_content: str = ""
|
|
61
|
+
redacted_reasoning_content: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
if reasoning_agent_response.messages is not None:
|
|
64
|
+
for msg in reasoning_agent_response.messages:
|
|
65
|
+
if msg.reasoning_content is not None:
|
|
66
|
+
reasoning_content = msg.reasoning_content
|
|
67
|
+
if hasattr(msg, "redacted_reasoning_content") and msg.redacted_reasoning_content is not None:
|
|
68
|
+
redacted_reasoning_content = msg.redacted_reasoning_content
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
return Message(
|
|
72
|
+
role="assistant",
|
|
73
|
+
content=f"<thinking>\n{reasoning_content}\n</thinking>",
|
|
74
|
+
reasoning_content=reasoning_content,
|
|
75
|
+
redacted_reasoning_content=redacted_reasoning_content,
|
|
76
|
+
)
|
agno/session/workflow.py
CHANGED
|
@@ -77,7 +77,7 @@ class WorkflowSession:
|
|
|
77
77
|
|
|
78
78
|
def get_workflow_history(self, num_runs: Optional[int] = None) -> List[Tuple[str, str]]:
|
|
79
79
|
"""Get workflow history as structured data (input, response pairs)
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
Args:
|
|
82
82
|
num_runs: Number of recent runs to include. If None, returns all available history.
|
|
83
83
|
"""
|
|
@@ -88,7 +88,7 @@ class WorkflowSession:
|
|
|
88
88
|
|
|
89
89
|
# Get completed runs only (exclude current/pending run)
|
|
90
90
|
completed_runs = [run for run in self.runs if run.status == RunStatus.completed]
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
if num_runs is not None and len(completed_runs) > num_runs:
|
|
93
93
|
recent_runs = completed_runs[-num_runs:]
|
|
94
94
|
else:
|
|
@@ -116,7 +116,7 @@ class WorkflowSession:
|
|
|
116
116
|
|
|
117
117
|
def get_workflow_history_context(self, num_runs: Optional[int] = None) -> Optional[str]:
|
|
118
118
|
"""Get formatted workflow history context for steps
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
Args:
|
|
121
121
|
num_runs: Number of recent runs to include. If None, returns all available history.
|
|
122
122
|
"""
|