agno 2.0.2__py3-none-any.whl → 2.0.4__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 (63) hide show
  1. agno/agent/agent.py +164 -87
  2. agno/db/dynamo/dynamo.py +8 -0
  3. agno/db/firestore/firestore.py +8 -1
  4. agno/db/gcs_json/gcs_json_db.py +9 -0
  5. agno/db/json/json_db.py +8 -0
  6. agno/db/mongo/mongo.py +10 -1
  7. agno/db/mysql/mysql.py +10 -0
  8. agno/db/postgres/postgres.py +16 -8
  9. agno/db/redis/redis.py +6 -0
  10. agno/db/singlestore/schemas.py +1 -1
  11. agno/db/singlestore/singlestore.py +8 -1
  12. agno/db/sqlite/sqlite.py +9 -1
  13. agno/db/utils.py +14 -0
  14. agno/knowledge/chunking/fixed.py +1 -1
  15. agno/knowledge/knowledge.py +91 -65
  16. agno/knowledge/reader/base.py +3 -0
  17. agno/knowledge/reader/csv_reader.py +1 -1
  18. agno/knowledge/reader/json_reader.py +1 -1
  19. agno/knowledge/reader/markdown_reader.py +5 -5
  20. agno/knowledge/reader/s3_reader.py +0 -12
  21. agno/knowledge/reader/text_reader.py +5 -5
  22. agno/models/base.py +2 -2
  23. agno/models/cerebras/cerebras.py +5 -3
  24. agno/models/cerebras/cerebras_openai.py +5 -3
  25. agno/models/google/gemini.py +33 -11
  26. agno/models/litellm/chat.py +1 -1
  27. agno/models/openai/chat.py +3 -0
  28. agno/models/openai/responses.py +81 -40
  29. agno/models/response.py +5 -0
  30. agno/models/siliconflow/__init__.py +5 -0
  31. agno/models/siliconflow/siliconflow.py +25 -0
  32. agno/os/app.py +4 -1
  33. agno/os/auth.py +24 -14
  34. agno/os/interfaces/slack/router.py +1 -1
  35. agno/os/interfaces/whatsapp/router.py +2 -0
  36. agno/os/router.py +187 -76
  37. agno/os/routers/evals/utils.py +9 -9
  38. agno/os/routers/health.py +26 -0
  39. agno/os/routers/knowledge/knowledge.py +11 -11
  40. agno/os/routers/session/session.py +24 -8
  41. agno/os/schema.py +8 -2
  42. agno/run/agent.py +5 -2
  43. agno/run/base.py +6 -3
  44. agno/run/team.py +11 -3
  45. agno/run/workflow.py +69 -12
  46. agno/session/team.py +1 -0
  47. agno/team/team.py +196 -93
  48. agno/tools/mcp.py +1 -0
  49. agno/tools/mem0.py +11 -17
  50. agno/tools/memory.py +419 -0
  51. agno/tools/workflow.py +279 -0
  52. agno/utils/audio.py +27 -0
  53. agno/utils/common.py +90 -1
  54. agno/utils/print_response/agent.py +6 -2
  55. agno/utils/streamlit.py +14 -8
  56. agno/vectordb/chroma/chromadb.py +8 -2
  57. agno/workflow/step.py +111 -13
  58. agno/workflow/workflow.py +16 -13
  59. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/METADATA +1 -1
  60. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/RECORD +63 -58
  61. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/WHEEL +0 -0
  62. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/licenses/LICENSE +0 -0
  63. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/top_level.txt +0 -0
agno/workflow/step.py CHANGED
@@ -164,6 +164,38 @@ class Step:
164
164
  return response.metrics
165
165
  return None
166
166
 
167
+ def _call_custom_function(
168
+ self,
169
+ func: Callable,
170
+ step_input: StepInput,
171
+ session_state: Optional[Dict[str, Any]] = None,
172
+ ) -> Any:
173
+ """Call custom function with session_state support if the function accepts it"""
174
+ if session_state is not None and self._function_has_session_state_param():
175
+ return func(step_input, session_state)
176
+ else:
177
+ return func(step_input)
178
+
179
+ async def _acall_custom_function(
180
+ self,
181
+ func: Callable,
182
+ step_input: StepInput,
183
+ session_state: Optional[Dict[str, Any]] = None,
184
+ ) -> Any:
185
+ """Call custom async function with session_state support if the function accepts it"""
186
+ import inspect
187
+
188
+ if inspect.isasyncgenfunction(func):
189
+ if session_state is not None and self._function_has_session_state_param():
190
+ return func(step_input, session_state)
191
+ else:
192
+ return func(step_input)
193
+ else:
194
+ if session_state is not None and self._function_has_session_state_param():
195
+ return await func(step_input, session_state)
196
+ else:
197
+ return await func(step_input)
198
+
167
199
  def execute(
168
200
  self,
169
201
  step_input: StepInput,
@@ -191,8 +223,11 @@ class Step:
191
223
  if inspect.isgeneratorfunction(self.active_executor):
192
224
  content = ""
193
225
  final_response = None
226
+ session_state_copy = copy(session_state) if session_state else None
194
227
  try:
195
- for chunk in self.active_executor(step_input): # type: ignore
228
+ for chunk in self._call_custom_function(
229
+ self.active_executor, step_input, session_state_copy
230
+ ): # type: ignore
196
231
  if (
197
232
  hasattr(chunk, "content")
198
233
  and chunk.content is not None
@@ -208,13 +243,22 @@ class Step:
208
243
  if hasattr(e, "value") and isinstance(e.value, StepOutput):
209
244
  final_response = e.value
210
245
 
246
+ # Merge session_state changes back
247
+ if session_state_copy and session_state:
248
+ merge_dictionaries(session_state, session_state_copy)
249
+
211
250
  if final_response is not None:
212
251
  response = final_response
213
252
  else:
214
253
  response = StepOutput(content=content)
215
254
  else:
216
- # Execute function directly with StepInput
217
- result = self.active_executor(step_input) # type: ignore
255
+ # Execute function with signature inspection for session_state support
256
+ session_state_copy = copy(session_state) if session_state else None
257
+ result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
258
+
259
+ # Merge session_state changes back
260
+ if session_state_copy and session_state:
261
+ merge_dictionaries(session_state, session_state_copy)
218
262
 
219
263
  # If function returns StepOutput, use it directly
220
264
  if isinstance(result, StepOutput):
@@ -291,6 +335,19 @@ class Step:
291
335
 
292
336
  return StepOutput(content=f"Step {self.name} failed but skipped", success=False)
293
337
 
338
+ def _function_has_session_state_param(self) -> bool:
339
+ """Check if the custom function has a session_state parameter"""
340
+ if self._executor_type != "function":
341
+ return False
342
+
343
+ try:
344
+ from inspect import signature
345
+
346
+ sig = signature(self.active_executor) # type: ignore
347
+ return "session_state" in sig.parameters
348
+ except Exception:
349
+ return False
350
+
294
351
  def execute_stream(
295
352
  self,
296
353
  step_input: StepInput,
@@ -338,8 +395,10 @@ class Step:
338
395
  if inspect.isgeneratorfunction(self.active_executor):
339
396
  log_debug("Function returned iterable, streaming events")
340
397
  content = ""
398
+ session_state_copy = copy(session_state) if session_state else None
341
399
  try:
342
- for event in self.active_executor(step_input): # type: ignore
400
+ iterator = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
401
+ for event in iterator: # type: ignore
343
402
  if (
344
403
  hasattr(event, "content")
345
404
  and event.content is not None
@@ -353,6 +412,11 @@ class Step:
353
412
  break
354
413
  else:
355
414
  yield event # type: ignore[misc]
415
+
416
+ # Merge session_state changes back
417
+ if session_state_copy and session_state:
418
+ merge_dictionaries(session_state, session_state_copy)
419
+
356
420
  if not final_response:
357
421
  final_response = StepOutput(content=content)
358
422
  except StopIteration as e:
@@ -360,7 +424,13 @@ class Step:
360
424
  final_response = e.value
361
425
 
362
426
  else:
363
- result = self.active_executor(step_input) # type: ignore
427
+ session_state_copy = copy(session_state) if session_state else None
428
+ result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
429
+
430
+ # Merge session_state changes back
431
+ if session_state_copy and session_state:
432
+ merge_dictionaries(session_state, session_state_copy)
433
+
364
434
  if isinstance(result, StepOutput):
365
435
  final_response = result
366
436
  else:
@@ -505,9 +575,13 @@ class Step:
505
575
  ):
506
576
  content = ""
507
577
  final_response = None
578
+ session_state_copy = copy(session_state) if session_state else None
508
579
  try:
509
580
  if inspect.isgeneratorfunction(self.active_executor):
510
- for chunk in self.active_executor(step_input): # type: ignore
581
+ iterator = self._call_custom_function(
582
+ self.active_executor, step_input, session_state_copy
583
+ ) # type: ignore
584
+ for chunk in iterator: # type: ignore
511
585
  if (
512
586
  hasattr(chunk, "content")
513
587
  and chunk.content is not None
@@ -520,7 +594,10 @@ class Step:
520
594
  final_response = chunk
521
595
  else:
522
596
  if inspect.isasyncgenfunction(self.active_executor):
523
- async for chunk in self.active_executor(step_input): # type: ignore
597
+ iterator = await self._acall_custom_function(
598
+ self.active_executor, step_input, session_state_copy
599
+ ) # type: ignore
600
+ async for chunk in iterator: # type: ignore
524
601
  if (
525
602
  hasattr(chunk, "content")
526
603
  and chunk.content is not None
@@ -536,15 +613,26 @@ class Step:
536
613
  if hasattr(e, "value") and isinstance(e.value, StepOutput):
537
614
  final_response = e.value
538
615
 
616
+ # Merge session_state changes back
617
+ if session_state_copy and session_state:
618
+ merge_dictionaries(session_state, session_state_copy)
619
+
539
620
  if final_response is not None:
540
621
  response = final_response
541
622
  else:
542
623
  response = StepOutput(content=content)
543
624
  else:
625
+ session_state_copy = copy(session_state) if session_state else None
544
626
  if inspect.iscoroutinefunction(self.active_executor):
545
- result = await self.active_executor(step_input) # type: ignore
627
+ result = await self._acall_custom_function(
628
+ self.active_executor, step_input, session_state_copy
629
+ ) # type: ignore
546
630
  else:
547
- result = self.active_executor(step_input) # type: ignore
631
+ result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
632
+
633
+ # Merge session_state changes back
634
+ if session_state_copy and session_state:
635
+ merge_dictionaries(session_state, session_state_copy)
548
636
 
549
637
  # If function returns StepOutput, use it directly
550
638
  if isinstance(result, StepOutput):
@@ -662,11 +750,16 @@ class Step:
662
750
  log_debug(f"Executing async function executor for step: {self.name}")
663
751
  import inspect
664
752
 
753
+ session_state_copy = copy(session_state) if session_state else None
754
+
665
755
  # Check if the function is an async generator
666
756
  if inspect.isasyncgenfunction(self.active_executor):
667
757
  content = ""
668
758
  # It's an async generator - iterate over it
669
- async for event in self.active_executor(step_input): # type: ignore
759
+ iterator = await self._acall_custom_function(
760
+ self.active_executor, step_input, session_state_copy
761
+ ) # type: ignore
762
+ async for event in iterator: # type: ignore
670
763
  if (
671
764
  hasattr(event, "content")
672
765
  and event.content is not None
@@ -684,7 +777,7 @@ class Step:
684
777
  final_response = StepOutput(content=content)
685
778
  elif inspect.iscoroutinefunction(self.active_executor):
686
779
  # It's a regular async function - await it
687
- result = await self.active_executor(step_input) # type: ignore
780
+ result = await self._acall_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
688
781
  if isinstance(result, StepOutput):
689
782
  final_response = result
690
783
  else:
@@ -692,7 +785,8 @@ class Step:
692
785
  elif inspect.isgeneratorfunction(self.active_executor):
693
786
  content = ""
694
787
  # It's a regular generator function - iterate over it
695
- for event in self.active_executor(step_input): # type: ignore
788
+ iterator = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
789
+ for event in iterator: # type: ignore
696
790
  if (
697
791
  hasattr(event, "content")
698
792
  and event.content is not None
@@ -710,11 +804,15 @@ class Step:
710
804
  final_response = StepOutput(content=content)
711
805
  else:
712
806
  # It's a regular function - call it directly
713
- result = self.active_executor(step_input) # type: ignore
807
+ result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
714
808
  if isinstance(result, StepOutput):
715
809
  final_response = result
716
810
  else:
717
811
  final_response = StepOutput(content=str(result))
812
+
813
+ # Merge session_state changes back
814
+ if session_state_copy and session_state:
815
+ merge_dictionaries(session_state, session_state_copy)
718
816
  else:
719
817
  # For agents and teams, prepare message with context
720
818
  message = self._prepare_message(
agno/workflow/workflow.py CHANGED
@@ -51,6 +51,7 @@ from agno.run.workflow import (
51
51
  )
52
52
  from agno.session.workflow import WorkflowSession
53
53
  from agno.team.team import Team
54
+ from agno.utils.common import is_typed_dict, validate_typed_dict
54
55
  from agno.utils.log import (
55
56
  log_debug,
56
57
  log_warning,
@@ -217,14 +218,18 @@ class Workflow:
217
218
 
218
219
  def _validate_input(
219
220
  self, input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]]
220
- ) -> Optional[BaseModel]:
221
+ ) -> Optional[Union[str, List, Dict, Message, BaseModel]]:
221
222
  """Parse and validate input against input_schema if provided"""
222
223
  if self.input_schema is None:
223
- return None
224
+ return input # Return input unchanged if no schema is set
224
225
 
225
226
  if input is None:
226
227
  raise ValueError("Input required when input_schema is set")
227
228
 
229
+ # Handle Message objects - extract content
230
+ if isinstance(input, Message):
231
+ input = input.content # type: ignore
232
+
228
233
  # If input is a string, convert it to a dict
229
234
  if isinstance(input, str):
230
235
  import json
@@ -238,8 +243,6 @@ class Workflow:
238
243
  if isinstance(input, BaseModel):
239
244
  if isinstance(input, self.input_schema):
240
245
  try:
241
- # Re-validate to catch any field validation errors
242
- input.model_validate(input.model_dump())
243
246
  return input
244
247
  except Exception as e:
245
248
  raise ValueError(f"BaseModel validation failed: {str(e)}")
@@ -250,8 +253,13 @@ class Workflow:
250
253
  # Case 2: Message is a dict
251
254
  elif isinstance(input, dict):
252
255
  try:
253
- validated_model = self.input_schema(**input)
254
- return validated_model
256
+ # Check if the schema is a TypedDict
257
+ if is_typed_dict(self.input_schema):
258
+ validated_dict = validate_typed_dict(input, self.input_schema)
259
+ return validated_dict
260
+ else:
261
+ validated_model = self.input_schema(**input)
262
+ return validated_model
255
263
  except Exception as e:
256
264
  raise ValueError(f"Failed to parse dict into {self.input_schema.__name__}: {str(e)}")
257
265
 
@@ -1924,10 +1932,7 @@ class Workflow:
1924
1932
  ) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
1925
1933
  """Execute the workflow synchronously with optional streaming"""
1926
1934
 
1927
- validated_input = self._validate_input(input)
1928
- if validated_input is not None:
1929
- input = validated_input
1930
-
1935
+ input = self._validate_input(input)
1931
1936
  if background:
1932
1937
  raise RuntimeError("Background execution is not supported for sync run()")
1933
1938
 
@@ -2059,9 +2064,7 @@ class Workflow:
2059
2064
  ) -> Union[WorkflowRunOutput, AsyncIterator[WorkflowRunOutputEvent]]:
2060
2065
  """Execute the workflow synchronously with optional streaming"""
2061
2066
 
2062
- validated_input = self._validate_input(input)
2063
- if validated_input is not None:
2064
- input = validated_input
2067
+ input = self._validate_input(input)
2065
2068
 
2066
2069
  websocket_handler = None
2067
2070
  if websocket:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agno
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Agno: a lightweight library for building Multi-Agent Systems
5
5
  Author-email: Ashpreet Bedi <ashpreet@agno.com>
6
6
  Project-URL: homepage, https://agno.com