agno 2.3.16__py3-none-any.whl → 2.3.17__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 (75) 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/os/app.py +104 -23
  22. agno/os/auth.py +25 -1
  23. agno/os/interfaces/a2a/a2a.py +7 -6
  24. agno/os/interfaces/a2a/router.py +13 -13
  25. agno/os/interfaces/agui/agui.py +5 -3
  26. agno/os/interfaces/agui/router.py +23 -16
  27. agno/os/interfaces/base.py +7 -7
  28. agno/os/interfaces/slack/router.py +6 -6
  29. agno/os/interfaces/slack/slack.py +7 -7
  30. agno/os/interfaces/whatsapp/router.py +29 -6
  31. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  32. agno/os/managers.py +326 -0
  33. agno/os/mcp.py +651 -79
  34. agno/os/router.py +125 -18
  35. agno/os/routers/agents/router.py +65 -22
  36. agno/os/routers/agents/schema.py +16 -4
  37. agno/os/routers/database.py +5 -0
  38. agno/os/routers/evals/evals.py +93 -11
  39. agno/os/routers/evals/utils.py +6 -6
  40. agno/os/routers/knowledge/knowledge.py +104 -16
  41. agno/os/routers/memory/memory.py +124 -7
  42. agno/os/routers/metrics/metrics.py +21 -4
  43. agno/os/routers/session/session.py +141 -12
  44. agno/os/routers/teams/router.py +40 -14
  45. agno/os/routers/teams/schema.py +12 -4
  46. agno/os/routers/traces/traces.py +54 -4
  47. agno/os/routers/workflows/router.py +223 -117
  48. agno/os/routers/workflows/schema.py +65 -1
  49. agno/os/schema.py +38 -12
  50. agno/os/utils.py +87 -166
  51. agno/remote/__init__.py +3 -0
  52. agno/remote/base.py +484 -0
  53. agno/run/workflow.py +1 -0
  54. agno/team/__init__.py +2 -0
  55. agno/team/remote.py +287 -0
  56. agno/team/team.py +25 -54
  57. agno/tracing/exporter.py +10 -6
  58. agno/tracing/setup.py +2 -1
  59. agno/utils/agent.py +58 -1
  60. agno/utils/http.py +68 -20
  61. agno/utils/os.py +0 -0
  62. agno/utils/remote.py +23 -0
  63. agno/vectordb/chroma/chromadb.py +452 -16
  64. agno/vectordb/pgvector/pgvector.py +7 -0
  65. agno/vectordb/redis/redisdb.py +1 -1
  66. agno/workflow/__init__.py +2 -0
  67. agno/workflow/agent.py +2 -2
  68. agno/workflow/remote.py +222 -0
  69. agno/workflow/types.py +0 -73
  70. agno/workflow/workflow.py +119 -68
  71. {agno-2.3.16.dist-info → agno-2.3.17.dist-info}/METADATA +1 -1
  72. {agno-2.3.16.dist-info → agno-2.3.17.dist-info}/RECORD +75 -65
  73. {agno-2.3.16.dist-info → agno-2.3.17.dist-info}/WHEEL +0 -0
  74. {agno-2.3.16.dist-info → agno-2.3.17.dist-info}/licenses/LICENSE +0 -0
  75. {agno-2.3.16.dist-info → agno-2.3.17.dist-info}/top_level.txt +0 -0
agno/agent/__init__.py CHANGED
@@ -5,6 +5,7 @@ from agno.agent.agent import (
5
5
  Message,
6
6
  Toolkit,
7
7
  )
8
+ from agno.agent.remote import RemoteAgent
8
9
  from agno.run.agent import (
9
10
  MemoryUpdateCompletedEvent,
10
11
  MemoryUpdateStartedEvent,
@@ -27,6 +28,7 @@ from agno.run.agent import (
27
28
 
28
29
  __all__ = [
29
30
  "Agent",
31
+ "RemoteAgent",
30
32
  "AgentSession",
31
33
  "Function",
32
34
  "Message",
agno/agent/agent.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_error_event,
113
114
  create_parser_model_response_completed_event,
@@ -762,56 +763,6 @@ class Agent:
762
763
  log_info("Setting default model to OpenAI Chat")
763
764
  self.model = OpenAIChat(id="gpt-4o")
764
765
 
765
- def _validate_input(
766
- self, input: Union[str, List, Dict, Message, BaseModel]
767
- ) -> Union[str, List, Dict, Message, BaseModel]:
768
- """Parse and validate input against input_schema if provided, otherwise return input as-is"""
769
- if self.input_schema is None:
770
- return input # Return input unchanged if no schema is set
771
-
772
- # Handle Message objects - extract content
773
- if isinstance(input, Message):
774
- input = input.content # type: ignore
775
-
776
- # If input is a string, convert it to a dict
777
- if isinstance(input, str):
778
- import json
779
-
780
- try:
781
- input = json.loads(input)
782
- except Exception as e:
783
- raise ValueError(f"Failed to parse input. Is it a valid JSON string?: {e}")
784
-
785
- # Case 1: Message is already a BaseModel instance
786
- if isinstance(input, BaseModel):
787
- if isinstance(input, self.input_schema):
788
- try:
789
- return input
790
- except Exception as e:
791
- raise ValueError(f"BaseModel validation failed: {str(e)}")
792
- else:
793
- # Different BaseModel types
794
- raise ValueError(f"Expected {self.input_schema.__name__} but got {type(input).__name__}")
795
-
796
- # Case 2: Message is a dict
797
- elif isinstance(input, dict):
798
- try:
799
- # Check if the schema is a TypedDict
800
- if is_typed_dict(self.input_schema):
801
- validated_dict = validate_typed_dict(input, self.input_schema)
802
- return validated_dict
803
- else:
804
- validated_model = self.input_schema(**input)
805
- return validated_model
806
- except Exception as e:
807
- raise ValueError(f"Failed to parse dict into {self.input_schema.__name__}: {str(e)}")
808
-
809
- # Case 3: Other types not supported for structured input
810
- else:
811
- raise ValueError(
812
- f"Cannot validate {type(input)} against input_schema. Expected dict or {self.input_schema.__name__} instance."
813
- )
814
-
815
766
  def _set_culture_manager(self) -> None:
816
767
  if self.db is None:
817
768
  log_warning("Database not provided. Cultural knowledge will not be stored.")
@@ -1654,7 +1605,7 @@ class Agent:
1654
1605
  background_tasks: BackgroundTasks = background_tasks # type: ignore
1655
1606
 
1656
1607
  # Validate input against input_schema if provided
1657
- validated_input = self._validate_input(input)
1608
+ validated_input = validate_input(input, self.input_schema)
1658
1609
 
1659
1610
  # Normalise hook & guardails
1660
1611
  if not self._hooks_normalised:
@@ -2828,7 +2779,7 @@ class Agent:
2828
2779
  background_tasks: BackgroundTasks = background_tasks # type: ignore
2829
2780
 
2830
2781
  # 2. Validate input against input_schema if provided
2831
- validated_input = self._validate_input(input)
2782
+ validated_input = validate_input(input, self.input_schema)
2832
2783
 
2833
2784
  # Normalise hooks & guardails
2834
2785
  if not self._hooks_normalised:
agno/agent/remote.py ADDED
@@ -0,0 +1,351 @@
1
+ import json
2
+ import time
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Literal, Optional, Sequence, Tuple, Union, overload
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from agno.media import Audio, File, Image, Video
9
+ from agno.models.base import Model
10
+ from agno.models.message import Message
11
+ from agno.models.response import ToolExecution
12
+ from agno.remote.base import BaseRemote, RemoteDb, RemoteKnowledge
13
+ from agno.run.agent import RunOutput, RunOutputEvent
14
+ from agno.utils.agent import validate_input
15
+ from agno.utils.log import log_warning
16
+ from agno.utils.remote import serialize_input
17
+
18
+ if TYPE_CHECKING:
19
+ from agno.os.routers.agents.schema import AgentResponse
20
+
21
+
22
+ @dataclass
23
+ class RemoteAgent(BaseRemote):
24
+ # Private cache for agent config with TTL: (config, timestamp)
25
+ _cached_agent_config: Optional[Tuple["AgentResponse", float]] = field(default=None, init=False, repr=False)
26
+
27
+ def __init__(
28
+ self,
29
+ base_url: str,
30
+ agent_id: str,
31
+ timeout: float = 60.0,
32
+ config_ttl: float = 300.0,
33
+ ):
34
+ """Initialize AgentOSRunner for local or remote execution.
35
+
36
+ For remote execution, provide base_url and agent_id.
37
+
38
+ Args:
39
+ base_url: Base URL for remote AgentOS instance (e.g., "http://localhost:7777")
40
+ agent_id: ID of remote agent
41
+ timeout: Request timeout in seconds (default: 60)
42
+ config_ttl: Time-to-live for cached config in seconds (default: 300)
43
+ """
44
+ super().__init__(base_url, timeout, config_ttl)
45
+ self.agent_id = agent_id
46
+ self._cached_agent_config = None
47
+
48
+ @property
49
+ def id(self) -> str:
50
+ return self.agent_id
51
+
52
+ async def get_agent_config(self) -> "AgentResponse":
53
+ """Get the agent config from remote (always fetches fresh)."""
54
+ return await self.client.aget_agent(self.agent_id)
55
+
56
+ @property
57
+ def _agent_config(self) -> "AgentResponse":
58
+ """Get the agent config from remote, cached with TTL."""
59
+ from agno.os.routers.agents.schema import AgentResponse
60
+
61
+ current_time = time.time()
62
+
63
+ # Check if cache is valid
64
+ if self._cached_agent_config is not None:
65
+ config, cached_at = self._cached_agent_config
66
+ if current_time - cached_at < self.config_ttl:
67
+ return config
68
+
69
+ # Fetch fresh config
70
+ config: AgentResponse = self.client.get_agent(self.agent_id) # type: ignore
71
+ self._cached_agent_config = (config, current_time)
72
+ return config
73
+
74
+ def refresh_config(self) -> "AgentResponse":
75
+ """Force refresh the cached agent config."""
76
+ from agno.os.routers.agents.schema import AgentResponse
77
+
78
+ config: AgentResponse = self.client.get_agent(self.agent_id)
79
+ self._cached_agent_config = (config, time.time())
80
+ return config
81
+
82
+ @property
83
+ def name(self) -> Optional[str]:
84
+ if self._agent_config is not None:
85
+ return self._agent_config.name
86
+ return self.agent_id
87
+
88
+ @property
89
+ def description(self) -> Optional[str]:
90
+ if self._agent_config is not None:
91
+ return self._agent_config.description
92
+ return ""
93
+
94
+ @property
95
+ def role(self) -> Optional[str]:
96
+ if self._agent_config is not None:
97
+ return self._agent_config.role
98
+ return None
99
+
100
+ @property
101
+ def tools(self) -> Optional[List[Dict[str, Any]]]:
102
+ if self._agent_config is not None:
103
+ try:
104
+ return json.loads(self._agent_config.tools["tools"]) if self._agent_config.tools else None
105
+ except Exception as e:
106
+ log_warning(f"Failed to load tools for agent {self.agent_id}: {e}")
107
+ return None
108
+ return None
109
+
110
+ @property
111
+ def db(self) -> Optional[RemoteDb]:
112
+ if self._agent_config is not None and self._agent_config.db_id is not None:
113
+ return RemoteDb.from_config(
114
+ db_id=self._agent_config.db_id,
115
+ client=self.client,
116
+ config=self._config,
117
+ )
118
+ return None
119
+
120
+ @property
121
+ def knowledge(self) -> Optional[RemoteKnowledge]:
122
+ """Whether the agent has knowledge enabled."""
123
+ if self._agent_config is not None and self._agent_config.knowledge is not None:
124
+ return RemoteKnowledge(
125
+ client=self.client,
126
+ contents_db=RemoteDb(
127
+ id=self._agent_config.knowledge.get("db_id"), # type: ignore
128
+ client=self.client,
129
+ knowledge_table_name=self._agent_config.knowledge.get("knowledge_table"),
130
+ )
131
+ if self._agent_config.knowledge.get("db_id") is not None
132
+ else None,
133
+ )
134
+ return None
135
+
136
+ @property
137
+ def model(self) -> Optional[Model]:
138
+ # We don't expose the remote agent's models, since they can't be used by other services in AgentOS.
139
+ return None
140
+
141
+ async def aget_tools(self, **kwargs: Any) -> List[Dict]:
142
+ if self._agent_config.tools is not None:
143
+ return json.loads(self._agent_config.tools["tools"])
144
+ return []
145
+
146
+ @overload
147
+ async def arun(
148
+ self,
149
+ input: Union[str, List, Dict, Message, BaseModel, List[Message]],
150
+ *,
151
+ stream: Literal[False] = False,
152
+ user_id: Optional[str] = None,
153
+ session_id: Optional[str] = None,
154
+ session_state: Optional[Dict[str, Any]] = None,
155
+ audio: Optional[Sequence[Audio]] = None,
156
+ images: Optional[Sequence[Image]] = None,
157
+ videos: Optional[Sequence[Video]] = None,
158
+ files: Optional[Sequence[File]] = None,
159
+ stream_events: Optional[bool] = None,
160
+ retries: Optional[int] = None,
161
+ knowledge_filters: Optional[Dict[str, Any]] = None,
162
+ add_history_to_context: Optional[bool] = None,
163
+ add_dependencies_to_context: Optional[bool] = None,
164
+ add_session_state_to_context: Optional[bool] = None,
165
+ dependencies: Optional[Dict[str, Any]] = None,
166
+ metadata: Optional[Dict[str, Any]] = None,
167
+ auth_token: Optional[str] = None,
168
+ **kwargs: Any,
169
+ ) -> RunOutput: ...
170
+
171
+ @overload
172
+ def arun(
173
+ self,
174
+ input: Union[str, List, Dict, Message, BaseModel, List[Message]],
175
+ *,
176
+ stream: Literal[True] = True,
177
+ user_id: Optional[str] = None,
178
+ session_id: Optional[str] = None,
179
+ audio: Optional[Sequence[Audio]] = None,
180
+ images: Optional[Sequence[Image]] = None,
181
+ videos: Optional[Sequence[Video]] = None,
182
+ files: Optional[Sequence[File]] = None,
183
+ stream_events: Optional[bool] = None,
184
+ retries: Optional[int] = None,
185
+ knowledge_filters: Optional[Dict[str, Any]] = None,
186
+ add_history_to_context: Optional[bool] = None,
187
+ add_dependencies_to_context: Optional[bool] = None,
188
+ add_session_state_to_context: Optional[bool] = None,
189
+ dependencies: Optional[Dict[str, Any]] = None,
190
+ metadata: Optional[Dict[str, Any]] = None,
191
+ auth_token: Optional[str] = None,
192
+ **kwargs: Any,
193
+ ) -> AsyncIterator[RunOutputEvent]: ...
194
+
195
+ def arun( # type: ignore
196
+ self,
197
+ input: Union[str, List, Dict, Message, BaseModel, List[Message]],
198
+ *,
199
+ stream: Optional[bool] = None,
200
+ user_id: Optional[str] = None,
201
+ session_id: Optional[str] = None,
202
+ session_state: Optional[Dict[str, Any]] = None,
203
+ audio: Optional[Sequence[Audio]] = None,
204
+ images: Optional[Sequence[Image]] = None,
205
+ videos: Optional[Sequence[Video]] = None,
206
+ files: Optional[Sequence[File]] = None,
207
+ stream_events: Optional[bool] = None,
208
+ retries: Optional[int] = None,
209
+ knowledge_filters: Optional[Dict[str, Any]] = None,
210
+ add_history_to_context: Optional[bool] = None,
211
+ add_dependencies_to_context: Optional[bool] = None,
212
+ add_session_state_to_context: Optional[bool] = None,
213
+ dependencies: Optional[Dict[str, Any]] = None,
214
+ metadata: Optional[Dict[str, Any]] = None,
215
+ auth_token: Optional[str] = None,
216
+ **kwargs: Any,
217
+ ) -> Union[
218
+ RunOutput,
219
+ AsyncIterator[RunOutputEvent],
220
+ ]:
221
+ validated_input = validate_input(input)
222
+ serialized_input = serialize_input(validated_input)
223
+ headers = self._get_auth_headers(auth_token)
224
+
225
+ if stream:
226
+ # Handle streaming response
227
+ return self.get_client().run_agent_stream(
228
+ agent_id=self.agent_id,
229
+ message=serialized_input,
230
+ session_id=session_id,
231
+ user_id=user_id,
232
+ audio=audio,
233
+ images=images,
234
+ videos=videos,
235
+ files=files,
236
+ session_state=session_state,
237
+ stream_events=stream_events,
238
+ retries=retries,
239
+ knowledge_filters=knowledge_filters,
240
+ add_history_to_context=add_history_to_context,
241
+ add_dependencies_to_context=add_dependencies_to_context,
242
+ add_session_state_to_context=add_session_state_to_context,
243
+ dependencies=dependencies,
244
+ metadata=metadata,
245
+ headers=headers,
246
+ **kwargs,
247
+ )
248
+ else:
249
+ return self.get_client().run_agent( # type: ignore
250
+ agent_id=self.agent_id,
251
+ message=serialized_input,
252
+ session_id=session_id,
253
+ user_id=user_id,
254
+ audio=audio,
255
+ images=images,
256
+ videos=videos,
257
+ files=files,
258
+ session_state=session_state,
259
+ stream_events=stream_events,
260
+ retries=retries,
261
+ knowledge_filters=knowledge_filters,
262
+ add_history_to_context=add_history_to_context,
263
+ add_dependencies_to_context=add_dependencies_to_context,
264
+ add_session_state_to_context=add_session_state_to_context,
265
+ dependencies=dependencies,
266
+ metadata=metadata,
267
+ headers=headers,
268
+ **kwargs,
269
+ )
270
+
271
+ @overload
272
+ async def acontinue_run(
273
+ self,
274
+ run_id: str,
275
+ updated_tools: List[ToolExecution],
276
+ stream: Literal[False] = False,
277
+ user_id: Optional[str] = None,
278
+ session_id: Optional[str] = None,
279
+ auth_token: Optional[str] = None,
280
+ **kwargs: Any,
281
+ ) -> RunOutput: ...
282
+
283
+ @overload
284
+ def acontinue_run(
285
+ self,
286
+ run_id: str,
287
+ updated_tools: List[ToolExecution],
288
+ stream: Literal[True] = True,
289
+ user_id: Optional[str] = None,
290
+ session_id: Optional[str] = None,
291
+ auth_token: Optional[str] = None,
292
+ **kwargs: Any,
293
+ ) -> AsyncIterator[RunOutputEvent]: ...
294
+
295
+ def acontinue_run( # type: ignore
296
+ self,
297
+ run_id: str, # type: ignore
298
+ updated_tools: List[ToolExecution],
299
+ stream: Optional[bool] = None,
300
+ user_id: Optional[str] = None,
301
+ session_id: Optional[str] = None,
302
+ auth_token: Optional[str] = None,
303
+ **kwargs: Any,
304
+ ) -> Union[
305
+ RunOutput,
306
+ AsyncIterator[RunOutputEvent],
307
+ ]:
308
+ headers = self._get_auth_headers(auth_token)
309
+
310
+ if stream:
311
+ # Handle streaming response
312
+ return self.get_client().continue_agent_run_stream( # type: ignore
313
+ agent_id=self.agent_id,
314
+ run_id=run_id,
315
+ user_id=user_id,
316
+ session_id=session_id,
317
+ tools=updated_tools,
318
+ headers=headers,
319
+ **kwargs,
320
+ )
321
+ else:
322
+ return self.get_client().continue_agent_run( # type: ignore
323
+ agent_id=self.agent_id,
324
+ run_id=run_id,
325
+ tools=updated_tools,
326
+ user_id=user_id,
327
+ session_id=session_id,
328
+ headers=headers,
329
+ **kwargs,
330
+ )
331
+
332
+ async def cancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
333
+ """Cancel a running agent execution.
334
+
335
+ Args:
336
+ run_id (str): The run_id to cancel.
337
+ auth_token: Optional JWT token for authentication.
338
+
339
+ Returns:
340
+ bool: True if the run was successfully cancelled, False otherwise.
341
+ """
342
+ headers = self._get_auth_headers(auth_token)
343
+ try:
344
+ await self.get_client().cancel_agent_run(
345
+ agent_id=self.agent_id,
346
+ run_id=run_id,
347
+ headers=headers,
348
+ )
349
+ return True
350
+ except Exception:
351
+ return False
@@ -0,0 +1,3 @@
1
+ from agno.client.os import AgentOSClient
2
+
3
+ __all__ = ["AgentOSClient"]