agno 2.1.4__py3-none-any.whl → 2.1.5__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.
Files changed (88) hide show
  1. agno/agent/agent.py +1767 -535
  2. agno/db/async_postgres/__init__.py +3 -0
  3. agno/db/async_postgres/async_postgres.py +1668 -0
  4. agno/db/async_postgres/schemas.py +124 -0
  5. agno/db/async_postgres/utils.py +289 -0
  6. agno/db/base.py +237 -2
  7. agno/db/dynamo/dynamo.py +2 -2
  8. agno/db/firestore/firestore.py +2 -2
  9. agno/db/firestore/utils.py +4 -2
  10. agno/db/gcs_json/gcs_json_db.py +2 -2
  11. agno/db/in_memory/in_memory_db.py +2 -2
  12. agno/db/json/json_db.py +2 -2
  13. agno/db/migrations/v1_to_v2.py +30 -13
  14. agno/db/mongo/mongo.py +18 -6
  15. agno/db/mysql/mysql.py +35 -13
  16. agno/db/postgres/postgres.py +29 -6
  17. agno/db/redis/redis.py +2 -2
  18. agno/db/singlestore/singlestore.py +2 -2
  19. agno/db/sqlite/sqlite.py +34 -12
  20. agno/db/sqlite/utils.py +8 -3
  21. agno/eval/accuracy.py +50 -43
  22. agno/eval/performance.py +6 -3
  23. agno/eval/reliability.py +6 -3
  24. agno/eval/utils.py +33 -16
  25. agno/exceptions.py +8 -2
  26. agno/knowledge/knowledge.py +260 -46
  27. agno/knowledge/reader/pdf_reader.py +4 -6
  28. agno/knowledge/reader/reader_factory.py +2 -3
  29. agno/memory/manager.py +241 -33
  30. agno/models/anthropic/claude.py +37 -0
  31. agno/os/app.py +8 -7
  32. agno/os/interfaces/a2a/router.py +3 -5
  33. agno/os/interfaces/agui/router.py +4 -1
  34. agno/os/interfaces/agui/utils.py +27 -6
  35. agno/os/interfaces/slack/router.py +2 -4
  36. agno/os/mcp.py +98 -41
  37. agno/os/router.py +23 -0
  38. agno/os/routers/evals/evals.py +52 -20
  39. agno/os/routers/evals/utils.py +14 -14
  40. agno/os/routers/knowledge/knowledge.py +130 -9
  41. agno/os/routers/knowledge/schemas.py +57 -0
  42. agno/os/routers/memory/memory.py +116 -44
  43. agno/os/routers/metrics/metrics.py +16 -6
  44. agno/os/routers/session/session.py +65 -22
  45. agno/os/schema.py +36 -0
  46. agno/os/utils.py +67 -12
  47. agno/reasoning/anthropic.py +80 -0
  48. agno/reasoning/gemini.py +73 -0
  49. agno/reasoning/openai.py +5 -0
  50. agno/reasoning/vertexai.py +76 -0
  51. agno/session/workflow.py +3 -3
  52. agno/team/team.py +918 -175
  53. agno/tools/googlesheets.py +20 -5
  54. agno/tools/mcp_toolbox.py +3 -3
  55. agno/tools/scrapegraph.py +1 -1
  56. agno/utils/models/claude.py +3 -1
  57. agno/utils/streamlit.py +1 -1
  58. agno/vectordb/base.py +22 -1
  59. agno/vectordb/cassandra/cassandra.py +9 -0
  60. agno/vectordb/chroma/chromadb.py +26 -6
  61. agno/vectordb/clickhouse/clickhousedb.py +9 -1
  62. agno/vectordb/couchbase/couchbase.py +11 -0
  63. agno/vectordb/lancedb/lance_db.py +20 -0
  64. agno/vectordb/langchaindb/langchaindb.py +11 -0
  65. agno/vectordb/lightrag/lightrag.py +9 -0
  66. agno/vectordb/llamaindex/llamaindexdb.py +15 -1
  67. agno/vectordb/milvus/milvus.py +23 -0
  68. agno/vectordb/mongodb/mongodb.py +22 -0
  69. agno/vectordb/pgvector/pgvector.py +19 -0
  70. agno/vectordb/pineconedb/pineconedb.py +35 -4
  71. agno/vectordb/qdrant/qdrant.py +24 -0
  72. agno/vectordb/singlestore/singlestore.py +25 -17
  73. agno/vectordb/surrealdb/surrealdb.py +18 -1
  74. agno/vectordb/upstashdb/upstashdb.py +26 -1
  75. agno/vectordb/weaviate/weaviate.py +18 -0
  76. agno/workflow/condition.py +4 -0
  77. agno/workflow/loop.py +4 -0
  78. agno/workflow/parallel.py +4 -0
  79. agno/workflow/router.py +4 -0
  80. agno/workflow/step.py +22 -14
  81. agno/workflow/steps.py +4 -0
  82. agno/workflow/types.py +2 -2
  83. agno/workflow/workflow.py +328 -61
  84. {agno-2.1.4.dist-info → agno-2.1.5.dist-info}/METADATA +100 -41
  85. {agno-2.1.4.dist-info → agno-2.1.5.dist-info}/RECORD +88 -81
  86. {agno-2.1.4.dist-info → agno-2.1.5.dist-info}/WHEEL +0 -0
  87. {agno-2.1.4.dist-info → agno-2.1.5.dist-info}/licenses/LICENSE +0 -0
  88. {agno-2.1.4.dist-info → agno-2.1.5.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(dbs: dict[str, BaseDb], settings: AgnoAPISettings = AgnoAPISettings()) -> APIRouter:
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
- sessions, total_count = db.get_sessions(
112
- session_type=session_type,
113
- component_id=component_id,
114
- user_id=user_id,
115
- session_name=session_name,
116
- limit=limit,
117
- page=page,
118
- sort_by=sort_by,
119
- sort_order=sort_order,
120
- deserialize=False,
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
- session = db.get_session(session_id=session_id, session_type=session_type, user_id=user_id)
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
- session = db.get_session(session_id=session_id, session_type=session_type, user_id=user_id, deserialize=False)
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.delete_session(session_id=session_id)
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.delete_sessions(session_ids=request.session_ids)
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
- session = db.rename_session(session_id=session_id, session_type=session_type, session_name=session_name)
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
@@ -3,9 +3,11 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
3
3
  from fastapi import FastAPI, HTTPException, UploadFile
4
4
  from fastapi.routing import APIRoute, APIRouter
5
5
  from starlette.middleware.cors import CORSMiddleware
6
+ from pydantic import BaseModel
6
7
 
8
+ from agno.models.message import Message
7
9
  from agno.agent.agent import Agent
8
- from agno.db.base import BaseDb
10
+ from agno.db.base import AsyncBaseDb, BaseDb
9
11
  from agno.knowledge.knowledge import Knowledge
10
12
  from agno.media import Audio, Image, Video
11
13
  from agno.media import File as FileMedia
@@ -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
- if is_workflow_run:
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
- # Check the input field directly as final fallback
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,25 @@ def get_session_name(session: Dict[str, Any]) -> str:
140
150
  return ""
141
151
 
142
152
 
153
+
154
+
155
+ def extract_input_media(run_dict: Dict[str, Any]) -> Dict[str, Any]:
156
+ input_media: Dict[str, List[Any]] = {
157
+ "images": [],
158
+ "videos": [],
159
+ "audios": [],
160
+ "files": [],
161
+ }
162
+
163
+ input = run_dict.get("input", [])
164
+ input_media["images"].extend(input.get("images", []))
165
+ input_media["videos"].extend(input.get("videos", []))
166
+ input_media["audios"].extend(input.get("audios", []))
167
+ input_media["files"].extend(input.get("files", []))
168
+
169
+ return input_media
170
+
171
+
143
172
  def process_image(file: UploadFile) -> Image:
144
173
  content = file.file.read()
145
174
  if not content:
@@ -495,3 +524,29 @@ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> Non
495
524
  elif isinstance(step, Workflow):
496
525
  # Nested workflow
497
526
  collect_mcp_tools_from_workflow(step, mcp_tools)
527
+
528
+
529
+ def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
530
+ """Convert any given input_content into its string representation.
531
+
532
+ This handles both serialized (dict) and live (object) input_content formats.
533
+ """
534
+ import json
535
+
536
+ if isinstance(input_content, str):
537
+ return input_content
538
+ elif isinstance(input_content, Message):
539
+ return json.dumps(input_content.to_dict())
540
+ elif isinstance(input_content, dict):
541
+ return json.dumps(input_content, indent=2, default=str)
542
+ elif isinstance(input_content, list):
543
+ if input_content:
544
+ # Handle live Message objects
545
+ if isinstance(input_content[0], Message):
546
+ return json.dumps([m.to_dict() for m in input_content])
547
+ # Handle serialized Message dicts
548
+ elif isinstance(input_content[0], dict) and input_content[0].get("role") == "user":
549
+ return input_content[0].get("content", str(input_content))
550
+ return str(input_content)
551
+ else:
552
+ 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
+ )
@@ -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
  """