agno 2.3.16__py3-none-any.whl → 2.3.18__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 (76) hide show
  1. agno/agent/__init__.py +2 -0
  2. agno/agent/agent.py +4 -53
  3. agno/agent/remote.py +351 -0
  4. agno/client/__init__.py +3 -0
  5. agno/client/os.py +2669 -0
  6. agno/db/base.py +20 -0
  7. agno/db/mongo/async_mongo.py +11 -0
  8. agno/db/mongo/mongo.py +10 -0
  9. agno/db/mysql/async_mysql.py +9 -0
  10. agno/db/mysql/mysql.py +9 -0
  11. agno/db/postgres/async_postgres.py +9 -0
  12. agno/db/postgres/postgres.py +9 -0
  13. agno/db/postgres/utils.py +3 -2
  14. agno/db/sqlite/async_sqlite.py +9 -0
  15. agno/db/sqlite/sqlite.py +11 -1
  16. agno/exceptions.py +23 -0
  17. agno/knowledge/chunking/semantic.py +123 -46
  18. agno/knowledge/reader/csv_reader.py +1 -1
  19. agno/knowledge/reader/field_labeled_csv_reader.py +1 -1
  20. agno/knowledge/reader/json_reader.py +1 -1
  21. agno/models/google/gemini.py +5 -0
  22. agno/os/app.py +108 -25
  23. agno/os/auth.py +25 -1
  24. agno/os/interfaces/a2a/a2a.py +7 -6
  25. agno/os/interfaces/a2a/router.py +13 -13
  26. agno/os/interfaces/agui/agui.py +5 -3
  27. agno/os/interfaces/agui/router.py +23 -16
  28. agno/os/interfaces/base.py +7 -7
  29. agno/os/interfaces/slack/router.py +6 -6
  30. agno/os/interfaces/slack/slack.py +7 -7
  31. agno/os/interfaces/whatsapp/router.py +29 -6
  32. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  33. agno/os/managers.py +326 -0
  34. agno/os/mcp.py +651 -79
  35. agno/os/router.py +125 -18
  36. agno/os/routers/agents/router.py +65 -22
  37. agno/os/routers/agents/schema.py +16 -4
  38. agno/os/routers/database.py +5 -0
  39. agno/os/routers/evals/evals.py +93 -11
  40. agno/os/routers/evals/utils.py +6 -6
  41. agno/os/routers/knowledge/knowledge.py +104 -16
  42. agno/os/routers/memory/memory.py +124 -7
  43. agno/os/routers/metrics/metrics.py +21 -4
  44. agno/os/routers/session/session.py +141 -12
  45. agno/os/routers/teams/router.py +40 -14
  46. agno/os/routers/teams/schema.py +12 -4
  47. agno/os/routers/traces/traces.py +54 -4
  48. agno/os/routers/workflows/router.py +223 -117
  49. agno/os/routers/workflows/schema.py +65 -1
  50. agno/os/schema.py +38 -12
  51. agno/os/utils.py +87 -166
  52. agno/remote/__init__.py +3 -0
  53. agno/remote/base.py +484 -0
  54. agno/run/workflow.py +1 -0
  55. agno/team/__init__.py +2 -0
  56. agno/team/remote.py +287 -0
  57. agno/team/team.py +25 -54
  58. agno/tracing/exporter.py +10 -6
  59. agno/tracing/setup.py +2 -1
  60. agno/utils/agent.py +58 -1
  61. agno/utils/http.py +68 -20
  62. agno/utils/os.py +0 -0
  63. agno/utils/remote.py +23 -0
  64. agno/vectordb/chroma/chromadb.py +452 -16
  65. agno/vectordb/pgvector/pgvector.py +7 -0
  66. agno/vectordb/redis/redisdb.py +1 -1
  67. agno/workflow/__init__.py +2 -0
  68. agno/workflow/agent.py +2 -2
  69. agno/workflow/remote.py +222 -0
  70. agno/workflow/types.py +0 -73
  71. agno/workflow/workflow.py +119 -68
  72. {agno-2.3.16.dist-info → agno-2.3.18.dist-info}/METADATA +1 -1
  73. {agno-2.3.16.dist-info → agno-2.3.18.dist-info}/RECORD +76 -66
  74. {agno-2.3.16.dist-info → agno-2.3.18.dist-info}/WHEEL +0 -0
  75. {agno-2.3.16.dist-info → agno-2.3.18.dist-info}/licenses/LICENSE +0 -0
  76. {agno-2.3.16.dist-info → agno-2.3.18.dist-info}/top_level.txt +0 -0
agno/team/remote.py ADDED
@@ -0,0 +1,287 @@
1
+ import json
2
+ import time
3
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Literal, Optional, Sequence, Tuple, Union, overload
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from agno.media import Audio, File, Image, Video
8
+ from agno.models.base import Model
9
+ from agno.models.message import Message
10
+ from agno.remote.base import BaseRemote, RemoteDb, RemoteKnowledge
11
+ from agno.run.agent import RunOutputEvent
12
+ from agno.run.team import TeamRunOutput, TeamRunOutputEvent
13
+ from agno.utils.agent import validate_input
14
+ from agno.utils.log import log_warning
15
+ from agno.utils.remote import serialize_input
16
+
17
+ if TYPE_CHECKING:
18
+ from agno.os.routers.teams.schema import TeamResponse
19
+
20
+
21
+ class RemoteTeam(BaseRemote):
22
+ # Private cache for team config with TTL: (config, timestamp)
23
+ _cached_team_config: Optional[Tuple["TeamResponse", float]] = None
24
+
25
+ def __init__(
26
+ self,
27
+ base_url: str,
28
+ team_id: str,
29
+ timeout: float = 300.0,
30
+ config_ttl: float = 300.0,
31
+ ):
32
+ """Initialize AgentOSRunner for local or remote execution.
33
+
34
+ For remote execution, provide base_url and team_id.
35
+
36
+ Args:
37
+ base_url: Base URL for remote AgentOS instance (e.g., "http://localhost:7777")
38
+ team_id: ID of remote team
39
+ timeout: Request timeout in seconds (default: 300)
40
+ config_ttl: Time-to-live for cached config in seconds (default: 300)
41
+ """
42
+ super().__init__(base_url, timeout, config_ttl)
43
+ self.team_id = team_id
44
+ self._cached_team_config = None
45
+
46
+ @property
47
+ def id(self) -> str:
48
+ return self.team_id
49
+
50
+ async def get_team_config(self) -> "TeamResponse":
51
+ """Get the team config from remote (always fetches fresh)."""
52
+ return await self.client.aget_team(self.team_id)
53
+
54
+ @property
55
+ def _team_config(self) -> "TeamResponse":
56
+ """Get the team config from remote, cached with TTL."""
57
+ from agno.os.routers.teams.schema import TeamResponse
58
+
59
+ current_time = time.time()
60
+
61
+ # Check if cache is valid
62
+ if self._cached_team_config is not None:
63
+ config, cached_at = self._cached_team_config
64
+ if current_time - cached_at < self.config_ttl:
65
+ return config
66
+
67
+ # Fetch fresh config
68
+ config: TeamResponse = self.client.get_team(self.team_id) # type: ignore
69
+ self._cached_team_config = (config, current_time)
70
+ return config
71
+
72
+ def refresh_config(self) -> "TeamResponse":
73
+ """Force refresh the cached team config."""
74
+ from agno.os.routers.teams.schema import TeamResponse
75
+
76
+ config: TeamResponse = self.client.get_team(self.team_id) # type: ignore
77
+ self._cached_team_config = (config, time.time())
78
+ return config
79
+
80
+ @property
81
+ def name(self) -> Optional[str]:
82
+ if self._team_config is not None:
83
+ return self._team_config.name
84
+ return None
85
+
86
+ @property
87
+ def description(self) -> Optional[str]:
88
+ if self._team_config is not None:
89
+ return self._team_config.description
90
+ return None
91
+
92
+ @property
93
+ def role(self) -> Optional[str]:
94
+ if self._team_config is not None:
95
+ return self._team_config.role
96
+ return None
97
+
98
+ @property
99
+ def tools(self) -> Optional[List[Dict[str, Any]]]:
100
+ if self._team_config is not None:
101
+ try:
102
+ return json.loads(self._team_config.tools["tools"]) if self._team_config.tools else None
103
+ except Exception as e:
104
+ log_warning(f"Failed to load tools for team {self.team_id}: {e}")
105
+ return None
106
+ return None
107
+
108
+ @property
109
+ def db(self) -> Optional[RemoteDb]:
110
+ if self._team_config is not None and self._team_config.db_id is not None:
111
+ return RemoteDb.from_config(
112
+ db_id=self._team_config.db_id,
113
+ client=self.client,
114
+ config=self._config,
115
+ )
116
+ return None
117
+
118
+ @property
119
+ def knowledge(self) -> Optional[RemoteKnowledge]:
120
+ """Whether the team has knowledge enabled."""
121
+ if self._team_config is not None and self._team_config.knowledge is not None:
122
+ return RemoteKnowledge(
123
+ client=self.client,
124
+ contents_db=RemoteDb(
125
+ id=self._team_config.knowledge.get("db_id"), # type: ignore
126
+ client=self.client,
127
+ knowledge_table_name=self._team_config.knowledge.get("knowledge_table"),
128
+ )
129
+ if self._team_config.knowledge.get("db_id") is not None
130
+ else None,
131
+ )
132
+ return None
133
+
134
+ @property
135
+ def model(self) -> Optional[Model]:
136
+ # We don't expose the remote team's models, since they can't be used by other services in AgentOS.
137
+ return None
138
+
139
+ @property
140
+ def user_id(self) -> Optional[str]:
141
+ return None
142
+
143
+ @overload
144
+ async def arun(
145
+ self,
146
+ input: Union[str, List, Dict, Message, BaseModel, List[Message]],
147
+ *,
148
+ stream: Literal[False] = False,
149
+ user_id: Optional[str] = None,
150
+ session_id: Optional[str] = None,
151
+ session_state: Optional[Dict[str, Any]] = None,
152
+ audio: Optional[Sequence[Audio]] = None,
153
+ images: Optional[Sequence[Image]] = None,
154
+ videos: Optional[Sequence[Video]] = None,
155
+ files: Optional[Sequence[File]] = None,
156
+ stream_events: Optional[bool] = None,
157
+ retries: Optional[int] = None,
158
+ knowledge_filters: Optional[Dict[str, Any]] = None,
159
+ add_history_to_context: Optional[bool] = None,
160
+ add_dependencies_to_context: Optional[bool] = None,
161
+ add_session_state_to_context: Optional[bool] = None,
162
+ dependencies: Optional[Dict[str, Any]] = None,
163
+ metadata: Optional[Dict[str, Any]] = None,
164
+ auth_token: Optional[str] = None,
165
+ **kwargs: Any,
166
+ ) -> TeamRunOutput: ...
167
+
168
+ @overload
169
+ def arun(
170
+ self,
171
+ input: Union[str, List, Dict, Message, BaseModel, List[Message]],
172
+ *,
173
+ stream: Literal[True] = True,
174
+ user_id: Optional[str] = None,
175
+ session_id: Optional[str] = None,
176
+ audio: Optional[Sequence[Audio]] = None,
177
+ images: Optional[Sequence[Image]] = None,
178
+ videos: Optional[Sequence[Video]] = None,
179
+ files: Optional[Sequence[File]] = None,
180
+ stream_events: Optional[bool] = None,
181
+ retries: Optional[int] = None,
182
+ knowledge_filters: Optional[Dict[str, Any]] = None,
183
+ add_history_to_context: Optional[bool] = None,
184
+ add_dependencies_to_context: Optional[bool] = None,
185
+ add_session_state_to_context: Optional[bool] = None,
186
+ dependencies: Optional[Dict[str, Any]] = None,
187
+ metadata: Optional[Dict[str, Any]] = None,
188
+ auth_token: Optional[str] = None,
189
+ **kwargs: Any,
190
+ ) -> AsyncIterator[TeamRunOutputEvent]: ...
191
+
192
+ def arun( # type: ignore
193
+ self,
194
+ input: Union[str, List, Dict, Message, BaseModel, List[Message]],
195
+ *,
196
+ stream: Optional[bool] = None,
197
+ user_id: Optional[str] = None,
198
+ session_id: Optional[str] = None,
199
+ session_state: Optional[Dict[str, Any]] = None,
200
+ audio: Optional[Sequence[Audio]] = None,
201
+ images: Optional[Sequence[Image]] = None,
202
+ videos: Optional[Sequence[Video]] = None,
203
+ files: Optional[Sequence[File]] = None,
204
+ stream_events: Optional[bool] = None,
205
+ retries: Optional[int] = None,
206
+ knowledge_filters: Optional[Dict[str, Any]] = None,
207
+ add_history_to_context: Optional[bool] = None,
208
+ add_dependencies_to_context: Optional[bool] = None,
209
+ add_session_state_to_context: Optional[bool] = None,
210
+ dependencies: Optional[Dict[str, Any]] = None,
211
+ metadata: Optional[Dict[str, Any]] = None,
212
+ auth_token: Optional[str] = None,
213
+ **kwargs: Any,
214
+ ) -> Union[
215
+ TeamRunOutput,
216
+ AsyncIterator[RunOutputEvent],
217
+ ]:
218
+ validated_input = validate_input(input)
219
+ serialized_input = serialize_input(validated_input)
220
+ headers = self._get_auth_headers(auth_token)
221
+
222
+ if stream:
223
+ # Handle streaming response
224
+ return self.get_client().run_team_stream( # type: ignore
225
+ team_id=self.team_id,
226
+ message=serialized_input,
227
+ session_id=session_id,
228
+ user_id=user_id,
229
+ audio=audio,
230
+ images=images,
231
+ videos=videos,
232
+ files=files,
233
+ session_state=session_state,
234
+ stream_events=stream_events,
235
+ retries=retries,
236
+ knowledge_filters=knowledge_filters,
237
+ add_history_to_context=add_history_to_context,
238
+ add_dependencies_to_context=add_dependencies_to_context,
239
+ add_session_state_to_context=add_session_state_to_context,
240
+ dependencies=dependencies,
241
+ metadata=metadata,
242
+ headers=headers,
243
+ **kwargs,
244
+ )
245
+ else:
246
+ return self.get_client().run_team( # type: ignore
247
+ team_id=self.team_id,
248
+ message=serialized_input,
249
+ session_id=session_id,
250
+ user_id=user_id,
251
+ audio=audio,
252
+ images=images,
253
+ videos=videos,
254
+ files=files,
255
+ session_state=session_state,
256
+ stream_events=stream_events,
257
+ retries=retries,
258
+ knowledge_filters=knowledge_filters,
259
+ add_history_to_context=add_history_to_context,
260
+ add_dependencies_to_context=add_dependencies_to_context,
261
+ add_session_state_to_context=add_session_state_to_context,
262
+ dependencies=dependencies,
263
+ metadata=metadata,
264
+ headers=headers,
265
+ **kwargs,
266
+ )
267
+
268
+ async def cancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
269
+ """Cancel a running team execution.
270
+
271
+ Args:
272
+ run_id (str): The run_id to cancel.
273
+ auth_token: Optional JWT token for authentication.
274
+
275
+ Returns:
276
+ bool: True if the run was found and marked for cancellation, False otherwise.
277
+ """
278
+ headers = self._get_auth_headers(auth_token)
279
+ try:
280
+ await self.get_client().cancel_team_run(
281
+ team_id=self.team_id,
282
+ run_id=run_id,
283
+ headers=headers,
284
+ )
285
+ return True
286
+ except Exception:
287
+ return False
agno/team/team.py CHANGED
@@ -103,11 +103,12 @@ from agno.utils.agent import (
103
103
  set_session_name_util,
104
104
  store_media_util,
105
105
  update_session_state_util,
106
+ validate_input,
106
107
  validate_media_object_id,
107
108
  wait_for_open_threads,
108
109
  wait_for_thread_tasks_stream,
109
110
  )
110
- from agno.utils.common import is_typed_dict, validate_typed_dict
111
+ from agno.utils.common import is_typed_dict
111
112
  from agno.utils.events import (
112
113
  add_team_error_event,
113
114
  create_team_parser_model_response_completed_event,
@@ -794,56 +795,6 @@ class Team:
794
795
  if telemetry_env is not None:
795
796
  self.telemetry = telemetry_env.lower() == "true"
796
797
 
797
- def _validate_input(
798
- self, input: Union[str, List, Dict, Message, BaseModel]
799
- ) -> Union[str, List, Dict, Message, BaseModel]:
800
- """Parse and validate input against input_schema if provided, otherwise return input as-is"""
801
- if self.input_schema is None:
802
- return input # Return input unchanged if no schema is set
803
-
804
- # Handle Message objects - extract content
805
- if isinstance(input, Message):
806
- input = input.content # type: ignore
807
-
808
- # If input is a string, convert it to a dict
809
- if isinstance(input, str):
810
- import json
811
-
812
- try:
813
- input = json.loads(input)
814
- except Exception as e:
815
- raise ValueError(f"Failed to parse input. Is it a valid JSON string?: {e}")
816
-
817
- # Case 1: Message is already a BaseModel instance
818
- if isinstance(input, BaseModel):
819
- if isinstance(input, self.input_schema):
820
- try:
821
- return input
822
- except Exception as e:
823
- raise ValueError(f"BaseModel validation failed: {str(e)}")
824
- else:
825
- # Different BaseModel types
826
- raise ValueError(f"Expected {self.input_schema.__name__} but got {type(input).__name__}")
827
-
828
- # Case 2: Message is a dict
829
- elif isinstance(input, dict):
830
- try:
831
- # Check if the schema is a TypedDict
832
- if is_typed_dict(self.input_schema):
833
- validated_dict = validate_typed_dict(input, self.input_schema)
834
- return validated_dict
835
- else:
836
- validated_model = self.input_schema(**input)
837
- return validated_model
838
- except Exception as e:
839
- raise ValueError(f"Failed to parse dict into {self.input_schema.__name__}: {str(e)}")
840
-
841
- # Case 3: Other types not supported for structured input
842
- else:
843
- raise ValueError(
844
- f"Cannot validate {type(input)} against input_schema. Expected dict or {self.input_schema.__name__} instance."
845
- )
846
-
847
798
  def _initialize_member(self, member: Union["Team", Agent], debug_mode: Optional[bool] = None) -> None:
848
799
  # Set debug mode for all members
849
800
  if debug_mode:
@@ -2077,7 +2028,7 @@ class Team:
2077
2028
  background_tasks: BackgroundTasks = background_tasks # type: ignore
2078
2029
 
2079
2030
  # Validate input against input_schema if provided
2080
- validated_input = self._validate_input(input)
2031
+ validated_input = validate_input(input, self.input_schema)
2081
2032
 
2082
2033
  # Normalise hook & guardails
2083
2034
  if not self._hooks_normalised:
@@ -2275,6 +2226,22 @@ class Team:
2275
2226
  return generator_wrapper(cancelled_run_error) # type: ignore
2276
2227
  else:
2277
2228
  return run_response
2229
+ except KeyboardInterrupt:
2230
+ # Handle KeyboardInterrupt - stop retries immediately
2231
+ run_response.status = RunStatus.cancelled
2232
+ run_response.content = "Operation cancelled by user"
2233
+ if stream:
2234
+ cancelled_run_error = handle_event(
2235
+ create_team_run_cancelled_event(
2236
+ from_run_response=run_response, reason="Operation cancelled by user"
2237
+ ),
2238
+ run_response,
2239
+ events_to_skip=self.events_to_skip,
2240
+ store_events=self.store_events,
2241
+ )
2242
+ return generator_wrapper(cancelled_run_error) # type: ignore
2243
+ else:
2244
+ return run_response
2278
2245
  except (InputCheckError, OutputCheckError) as e:
2279
2246
  run_response.status = RunStatus.error
2280
2247
 
@@ -3139,7 +3106,7 @@ class Team:
3139
3106
  background_tasks: BackgroundTasks = background_tasks # type: ignore
3140
3107
 
3141
3108
  # Validate input against input_schema if provided
3142
- validated_input = self._validate_input(input)
3109
+ validated_input = validate_input(input, self.input_schema)
3143
3110
 
3144
3111
  # Normalise hook & guardails
3145
3112
  if not self._hooks_normalised:
@@ -5416,6 +5383,10 @@ class Team:
5416
5383
  system_message_content += f"{indent * ' '} - {_tool.name}\n"
5417
5384
  elif callable(_tool):
5418
5385
  system_message_content += f"{indent * ' '} - {_tool.__name__}\n"
5386
+ elif isinstance(_tool, dict) and "name" in _tool and _tool.get("name") is not None:
5387
+ system_message_content += f"{indent * ' '} - {_tool['name']}\n"
5388
+ else:
5389
+ system_message_content += f"{indent * ' '} - {str(_tool)}\n"
5419
5390
 
5420
5391
  return system_message_content
5421
5392
 
@@ -7297,7 +7268,7 @@ class Team:
7297
7268
 
7298
7269
  # 7. Add member-level history for the member if enabled (because we won't load the session for the member, so history won't be loaded automatically)
7299
7270
  history = None
7300
- if member_agent.add_history_to_context:
7271
+ if hasattr(member_agent, "add_history_to_context") and member_agent.add_history_to_context:
7301
7272
  history = self._get_history_for_member_agent(session, member_agent)
7302
7273
  if history:
7303
7274
  if isinstance(member_agent_task, str):
agno/tracing/exporter.py CHANGED
@@ -10,6 +10,7 @@ from opentelemetry.sdk.trace import ReadableSpan # type: ignore
10
10
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult # type: ignore
11
11
 
12
12
  from agno.db.base import AsyncBaseDb, BaseDb
13
+ from agno.remote.base import RemoteDb
13
14
  from agno.tracing.schemas import Span, create_trace_from_spans
14
15
  from agno.utils.log import logger
15
16
 
@@ -17,7 +18,7 @@ from agno.utils.log import logger
17
18
  class DatabaseSpanExporter(SpanExporter):
18
19
  """Custom OpenTelemetry SpanExporter that writes to Agno database"""
19
20
 
20
- def __init__(self, db: Union[BaseDb, AsyncBaseDb]):
21
+ def __init__(self, db: Union[BaseDb, AsyncBaseDb, RemoteDb]):
21
22
  """
22
23
  Initialize the DatabaseSpanExporter.
23
24
 
@@ -71,7 +72,10 @@ class DatabaseSpanExporter(SpanExporter):
71
72
  spans_by_trace[converted_span.trace_id].append(converted_span)
72
73
 
73
74
  # Handle async DB
74
- if isinstance(self.db, AsyncBaseDb):
75
+ if isinstance(self.db, RemoteDb):
76
+ # Skipping remote database because it handles its own tracing
77
+ pass
78
+ elif isinstance(self.db, AsyncBaseDb):
75
79
  self._export_async(spans_by_trace)
76
80
  else:
77
81
  # Synchronous database
@@ -90,10 +94,10 @@ class DatabaseSpanExporter(SpanExporter):
90
94
  # Create trace record (aggregate of all spans)
91
95
  trace = create_trace_from_spans(spans)
92
96
  if trace:
93
- self.db.upsert_trace(trace)
97
+ self.db.upsert_trace(trace) # type: ignore
94
98
 
95
99
  # Create span records
96
- self.db.create_spans(spans)
100
+ self.db.create_spans(spans) # type: ignore
97
101
 
98
102
  except Exception as e:
99
103
  logger.error(f"Failed to export sync traces: {e}", exc_info=True)
@@ -124,12 +128,12 @@ class DatabaseSpanExporter(SpanExporter):
124
128
  # Create trace record (aggregate of all spans)
125
129
  trace = create_trace_from_spans(spans)
126
130
  if trace:
127
- create_trace_result = self.db.upsert_trace(trace)
131
+ create_trace_result = self.db.upsert_trace(trace) # type: ignore
128
132
  if create_trace_result is not None:
129
133
  await create_trace_result
130
134
 
131
135
  # Create span records
132
- create_spans_result = self.db.create_spans(spans)
136
+ create_spans_result = self.db.create_spans(spans) # type: ignore
133
137
  if create_spans_result is not None:
134
138
  await create_spans_result
135
139
 
agno/tracing/setup.py CHANGED
@@ -5,6 +5,7 @@ Setup helper functions for configuring Agno tracing.
5
5
  from typing import Union
6
6
 
7
7
  from agno.db.base import AsyncBaseDb, BaseDb
8
+ from agno.remote.base import RemoteDb
8
9
  from agno.tracing.exporter import DatabaseSpanExporter
9
10
  from agno.utils.log import logger
10
11
 
@@ -20,7 +21,7 @@ except ImportError:
20
21
 
21
22
 
22
23
  def setup_tracing(
23
- db: Union[BaseDb, AsyncBaseDb],
24
+ db: Union[BaseDb, AsyncBaseDb, RemoteDb],
24
25
  batch_processing: bool = False,
25
26
  max_queue_size: int = 2048,
26
27
  max_export_batch_size: int = 512,
agno/utils/agent.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from asyncio import Future, Task
2
- from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Optional, Sequence, Union
2
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Optional, Sequence, Type, Union
3
+
4
+ from pydantic import BaseModel
3
5
 
4
6
  from agno.media import Audio, File, Image, Video
5
7
  from agno.models.message import Message
@@ -10,6 +12,7 @@ from agno.run.agent import RunEvent, RunInput, RunOutput, RunOutputEvent
10
12
  from agno.run.team import RunOutputEvent as TeamRunOutputEvent
11
13
  from agno.run.team import TeamRunOutput
12
14
  from agno.session import AgentSession, TeamSession, WorkflowSession
15
+ from agno.utils.common import is_typed_dict, validate_typed_dict
13
16
  from agno.utils.events import (
14
17
  create_memory_update_completed_event,
15
18
  create_memory_update_started_event,
@@ -936,3 +939,57 @@ async def aexecute_system_message(
936
939
  return await system_message(**system_message_args)
937
940
  else:
938
941
  return system_message(**system_message_args)
942
+
943
+
944
+ def validate_input(
945
+ input: Union[str, List, Dict, Message, BaseModel], input_schema: Optional[Type[BaseModel]] = None
946
+ ) -> Union[str, List, Dict, Message, BaseModel]:
947
+ """Parse and validate input against input_schema if provided, otherwise return input as-is"""
948
+ if input_schema is None:
949
+ return input # Return input unchanged if no schema is set
950
+
951
+ if input is None:
952
+ raise ValueError("Input required when input_schema is set")
953
+
954
+ # Handle Message objects - extract content
955
+ if isinstance(input, Message):
956
+ input = input.content # type: ignore
957
+
958
+ # If input is a string, convert it to a dict
959
+ if isinstance(input, str):
960
+ import json
961
+
962
+ try:
963
+ input = json.loads(input)
964
+ except Exception as e:
965
+ raise ValueError(f"Failed to parse input. Is it a valid JSON string?: {e}")
966
+
967
+ # Case 1: Message is already a BaseModel instance
968
+ if isinstance(input, BaseModel):
969
+ if isinstance(input, input_schema):
970
+ try:
971
+ return input
972
+ except Exception as e:
973
+ raise ValueError(f"BaseModel validation failed: {str(e)}")
974
+ else:
975
+ # Different BaseModel types
976
+ raise ValueError(f"Expected {input_schema.__name__} but got {type(input).__name__}")
977
+
978
+ # Case 2: Message is a dict
979
+ elif isinstance(input, dict):
980
+ try:
981
+ # Check if the schema is a TypedDict
982
+ if is_typed_dict(input_schema):
983
+ validated_dict = validate_typed_dict(input, input_schema)
984
+ return validated_dict
985
+ else:
986
+ validated_model = input_schema(**input)
987
+ return validated_model
988
+ except Exception as e:
989
+ raise ValueError(f"Failed to parse dict into {input_schema.__name__}: {str(e)}")
990
+
991
+ # Case 3: Other types not supported for structured input
992
+ else:
993
+ raise ValueError(
994
+ f"Cannot validate {type(input)} against input_schema. Expected dict or {input_schema.__name__} instance."
995
+ )