agno 2.3.13__py3-none-any.whl → 2.3.14__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 (41) hide show
  1. agno/agent/agent.py +1131 -1402
  2. agno/eval/__init__.py +21 -8
  3. agno/knowledge/embedder/azure_openai.py +0 -1
  4. agno/knowledge/embedder/google.py +1 -1
  5. agno/models/anthropic/claude.py +4 -1
  6. agno/models/base.py +8 -4
  7. agno/models/openai/responses.py +2 -2
  8. agno/os/app.py +39 -0
  9. agno/os/interfaces/a2a/router.py +619 -9
  10. agno/os/interfaces/a2a/utils.py +31 -32
  11. agno/os/middleware/jwt.py +5 -5
  12. agno/os/routers/agents/schema.py +14 -1
  13. agno/os/routers/teams/schema.py +14 -1
  14. agno/os/utils.py +61 -53
  15. agno/reasoning/anthropic.py +85 -1
  16. agno/reasoning/azure_ai_foundry.py +93 -1
  17. agno/reasoning/deepseek.py +91 -1
  18. agno/reasoning/gemini.py +81 -1
  19. agno/reasoning/groq.py +103 -1
  20. agno/reasoning/manager.py +1244 -0
  21. agno/reasoning/ollama.py +93 -1
  22. agno/reasoning/openai.py +113 -1
  23. agno/reasoning/vertexai.py +85 -1
  24. agno/run/agent.py +11 -0
  25. agno/run/base.py +1 -1
  26. agno/run/team.py +11 -0
  27. agno/session/team.py +0 -3
  28. agno/team/team.py +1201 -1445
  29. agno/utils/events.py +69 -2
  30. agno/utils/hooks.py +4 -10
  31. agno/utils/print_response/agent.py +26 -0
  32. agno/utils/print_response/team.py +11 -0
  33. agno/utils/prompts.py +8 -6
  34. agno/utils/string.py +46 -0
  35. agno/utils/team.py +1 -1
  36. agno/vectordb/milvus/milvus.py +32 -3
  37. {agno-2.3.13.dist-info → agno-2.3.14.dist-info}/METADATA +3 -2
  38. {agno-2.3.13.dist-info → agno-2.3.14.dist-info}/RECORD +41 -40
  39. {agno-2.3.13.dist-info → agno-2.3.14.dist-info}/WHEEL +0 -0
  40. {agno-2.3.13.dist-info → agno-2.3.14.dist-info}/licenses/LICENSE +0 -0
  41. {agno-2.3.13.dist-info → agno-2.3.14.dist-info}/top_level.txt +0 -0
agno/utils/events.py CHANGED
@@ -16,6 +16,7 @@ from agno.run.agent import (
16
16
  PreHookCompletedEvent,
17
17
  PreHookStartedEvent,
18
18
  ReasoningCompletedEvent,
19
+ ReasoningContentDeltaEvent,
19
20
  ReasoningStartedEvent,
20
21
  ReasoningStepEvent,
21
22
  RunCancelledEvent,
@@ -47,6 +48,7 @@ from agno.run.team import PostHookStartedEvent as TeamPostHookStartedEvent
47
48
  from agno.run.team import PreHookCompletedEvent as TeamPreHookCompletedEvent
48
49
  from agno.run.team import PreHookStartedEvent as TeamPreHookStartedEvent
49
50
  from agno.run.team import ReasoningCompletedEvent as TeamReasoningCompletedEvent
51
+ from agno.run.team import ReasoningContentDeltaEvent as TeamReasoningContentDeltaEvent
50
52
  from agno.run.team import ReasoningStartedEvent as TeamReasoningStartedEvent
51
53
  from agno.run.team import ReasoningStepEvent as TeamReasoningStepEvent
52
54
  from agno.run.team import RunCancelledEvent as TeamRunCancelledEvent
@@ -161,23 +163,41 @@ def create_run_continued_event(from_run_response: RunOutput) -> RunContinuedEven
161
163
  )
162
164
 
163
165
 
164
- def create_team_run_error_event(from_run_response: TeamRunOutput, error: str) -> TeamRunErrorEvent:
166
+ def create_team_run_error_event(
167
+ from_run_response: TeamRunOutput,
168
+ error: str,
169
+ error_type: Optional[str] = None,
170
+ error_id: Optional[str] = None,
171
+ additional_data: Optional[Dict[str, Any]] = None,
172
+ ) -> TeamRunErrorEvent:
165
173
  return TeamRunErrorEvent(
166
174
  session_id=from_run_response.session_id,
167
175
  team_id=from_run_response.team_id, # type: ignore
168
176
  team_name=from_run_response.team_name, # type: ignore
169
177
  run_id=from_run_response.run_id,
170
178
  content=error,
179
+ error_type=error_type,
180
+ error_id=error_id,
181
+ additional_data=additional_data,
171
182
  )
172
183
 
173
184
 
174
- def create_run_error_event(from_run_response: RunOutput, error: str) -> RunErrorEvent:
185
+ def create_run_error_event(
186
+ from_run_response: RunOutput,
187
+ error: str,
188
+ error_type: Optional[str] = None,
189
+ error_id: Optional[str] = None,
190
+ additional_data: Optional[Dict[str, Any]] = None,
191
+ ) -> RunErrorEvent:
175
192
  return RunErrorEvent(
176
193
  session_id=from_run_response.session_id,
177
194
  agent_id=from_run_response.agent_id, # type: ignore
178
195
  agent_name=from_run_response.agent_name, # type: ignore
179
196
  run_id=from_run_response.run_id,
180
197
  content=error,
198
+ error_type=error_type,
199
+ error_id=error_id,
200
+ additional_data=additional_data,
181
201
  )
182
202
 
183
203
 
@@ -421,6 +441,19 @@ def create_reasoning_step_event(
421
441
  )
422
442
 
423
443
 
444
+ def create_reasoning_content_delta_event(
445
+ from_run_response: RunOutput, reasoning_content: str
446
+ ) -> ReasoningContentDeltaEvent:
447
+ """Create an event for streaming reasoning content chunks."""
448
+ return ReasoningContentDeltaEvent(
449
+ session_id=from_run_response.session_id,
450
+ agent_id=from_run_response.agent_id, # type: ignore
451
+ agent_name=from_run_response.agent_name, # type: ignore
452
+ run_id=from_run_response.run_id,
453
+ reasoning_content=reasoning_content,
454
+ )
455
+
456
+
424
457
  def create_team_reasoning_step_event(
425
458
  from_run_response: TeamRunOutput, reasoning_step: ReasoningStep, reasoning_content: str
426
459
  ) -> TeamReasoningStepEvent:
@@ -435,6 +468,19 @@ def create_team_reasoning_step_event(
435
468
  )
436
469
 
437
470
 
471
+ def create_team_reasoning_content_delta_event(
472
+ from_run_response: TeamRunOutput, reasoning_content: str
473
+ ) -> TeamReasoningContentDeltaEvent:
474
+ """Create an event for streaming reasoning content chunks for Team."""
475
+ return TeamReasoningContentDeltaEvent(
476
+ session_id=from_run_response.session_id,
477
+ team_id=from_run_response.team_id, # type: ignore
478
+ team_name=from_run_response.team_name, # type: ignore
479
+ run_id=from_run_response.run_id,
480
+ reasoning_content=reasoning_content,
481
+ )
482
+
483
+
438
484
  def create_reasoning_completed_event(
439
485
  from_run_response: RunOutput, content: Optional[Any] = None, content_type: Optional[str] = None
440
486
  ) -> ReasoningCompletedEvent:
@@ -698,3 +744,24 @@ def handle_event(
698
744
  run_response.events = []
699
745
  run_response.events.append(event) # type: ignore
700
746
  return event
747
+
748
+
749
+ def add_error_event(
750
+ error: RunErrorEvent,
751
+ events: Optional[List[RunOutputEvent]],
752
+ ):
753
+ if events is None:
754
+ events = []
755
+ events.append(error)
756
+
757
+ return events
758
+
759
+
760
+ def add_team_error_event(
761
+ error: TeamRunErrorEvent,
762
+ events: Optional[List[Union[RunOutputEvent, TeamRunOutputEvent]]],
763
+ ):
764
+ if events is None:
765
+ events = []
766
+ events.append(error)
767
+ return events
agno/utils/hooks.py CHANGED
@@ -1,9 +1,7 @@
1
1
  from copy import deepcopy
2
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
3
-
4
- if TYPE_CHECKING:
5
- from agno.eval.base import BaseEval
2
+ from typing import Any, Callable, Dict, List, Optional, Union
6
3
 
4
+ from agno.eval.base import BaseEval
7
5
  from agno.guardrails.base import BaseGuardrail
8
6
  from agno.hooks.decorator import HOOK_RUN_IN_BACKGROUND_ATTR
9
7
  from agno.utils.log import log_warning
@@ -57,7 +55,7 @@ def should_run_hook_in_background(hook: Callable[..., Any]) -> bool:
57
55
 
58
56
 
59
57
  def normalize_pre_hooks(
60
- hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]],
58
+ hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, BaseEval]]],
61
59
  async_mode: bool = False,
62
60
  ) -> Optional[List[Callable[..., Any]]]:
63
61
  """Normalize pre-hooks to a list format.
@@ -66,8 +64,6 @@ def normalize_pre_hooks(
66
64
  hooks: List of hook functions, guardrails, or eval instances
67
65
  async_mode: Whether to use async versions of methods
68
66
  """
69
- from agno.eval.base import BaseEval
70
-
71
67
  result_hooks: List[Callable[..., Any]] = []
72
68
 
73
69
  if hooks is not None:
@@ -102,7 +98,7 @@ def normalize_pre_hooks(
102
98
 
103
99
 
104
100
  def normalize_post_hooks(
105
- hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, "BaseEval"]]],
101
+ hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail, BaseEval]]],
106
102
  async_mode: bool = False,
107
103
  ) -> Optional[List[Callable[..., Any]]]:
108
104
  """Normalize post-hooks to a list format.
@@ -111,8 +107,6 @@ def normalize_post_hooks(
111
107
  hooks: List of hook functions, guardrails, or eval instances
112
108
  async_mode: Whether to use async versions of methods
113
109
  """
114
- from agno.eval.base import BaseEval
115
-
116
110
  result_hooks: List[Callable[..., Any]] = []
117
111
 
118
112
  if hooks is not None:
@@ -134,6 +134,11 @@ def print_response_stream(
134
134
  )
135
135
  except Exception as e:
136
136
  log_warning(f"Failed to convert response to JSON: {e}")
137
+ elif agent.output_schema is not None and isinstance(response_event.content, dict):
138
+ try:
139
+ response_content_batch = JSON(json.dumps(response_event.content), indent=2) # type: ignore
140
+ except Exception as e:
141
+ log_warning(f"Failed to convert response to JSON: {e}")
137
142
  else:
138
143
  try:
139
144
  response_content_batch = JSON(json.dumps(response_event.content), indent=4)
@@ -141,6 +146,12 @@ def print_response_stream(
141
146
  log_warning(f"Failed to convert response to JSON: {e}")
142
147
  if hasattr(response_event, "reasoning_content") and response_event.reasoning_content is not None: # type: ignore
143
148
  _response_reasoning_content += response_event.reasoning_content # type: ignore
149
+
150
+ # Handle streaming reasoning content delta events
151
+ if response_event.event == RunEvent.reasoning_content_delta: # type: ignore
152
+ if hasattr(response_event, "reasoning_content") and response_event.reasoning_content is not None: # type: ignore
153
+ _response_reasoning_content += response_event.reasoning_content # type: ignore
154
+
144
155
  if hasattr(response_event, "reasoning_steps") and response_event.reasoning_steps is not None: # type: ignore
145
156
  reasoning_steps = response_event.reasoning_steps # type: ignore
146
157
 
@@ -325,6 +336,11 @@ async def aprint_response_stream(
325
336
  response_content_batch = JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
326
337
  except Exception as e:
327
338
  log_warning(f"Failed to convert response to JSON: {e}")
339
+ elif agent.output_schema is not None and isinstance(resp.content, dict):
340
+ try:
341
+ response_content_batch = JSON(json.dumps(resp.content), indent=2) # type: ignore
342
+ except Exception as e:
343
+ log_warning(f"Failed to convert response to JSON: {e}")
328
344
  else:
329
345
  try:
330
346
  response_content_batch = JSON(json.dumps(resp.content), indent=4)
@@ -333,6 +349,11 @@ async def aprint_response_stream(
333
349
  if resp.reasoning_content is not None: # type: ignore
334
350
  _response_reasoning_content += resp.reasoning_content # type: ignore
335
351
 
352
+ # Handle streaming reasoning content delta events
353
+ if resp.event == RunEvent.reasoning_content_delta: # type: ignore
354
+ if hasattr(resp, "reasoning_content") and resp.reasoning_content is not None: # type: ignore
355
+ _response_reasoning_content += resp.reasoning_content # type: ignore
356
+
336
357
  if hasattr(resp, "reasoning_steps") and resp.reasoning_steps is not None: # type: ignore
337
358
  reasoning_steps = resp.reasoning_steps # type: ignore
338
359
 
@@ -883,6 +904,11 @@ def build_panels(
883
904
  response_content_batch = JSON(run_response.content.model_dump_json(exclude_none=True), indent=2)
884
905
  except Exception as e:
885
906
  log_warning(f"Failed to convert response to JSON: {e}")
907
+ elif output_schema is not None and isinstance(run_response.content, dict):
908
+ try:
909
+ response_content_batch = JSON(json.dumps(run_response.content), indent=2)
910
+ except Exception as e:
911
+ log_warning(f"Failed to convert response to JSON: {e}")
886
912
  else:
887
913
  try:
888
914
  response_content_batch = JSON(json.dumps(run_response.content), indent=4)
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Set, Union, get_args
2
3
 
3
4
  from pydantic import BaseModel
@@ -488,6 +489,11 @@ def print_response_stream(
488
489
  _response_content = JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
489
490
  except Exception as e:
490
491
  log_warning(f"Failed to convert response to JSON: {e}")
492
+ elif team.output_schema is not None and isinstance(resp.content, dict):
493
+ try:
494
+ _response_content = JSON(json.dumps(resp.content), indent=2) # type: ignore
495
+ except Exception as e:
496
+ log_warning(f"Failed to convert response to JSON: {e}")
491
497
  if hasattr(resp, "reasoning_content") and resp.reasoning_content is not None: # type: ignore
492
498
  _response_reasoning_content += resp.reasoning_content # type: ignore
493
499
  if hasattr(resp, "reasoning_steps") and resp.reasoning_steps is not None: # type: ignore
@@ -1412,6 +1418,11 @@ async def aprint_response_stream(
1412
1418
  _response_content = JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
1413
1419
  except Exception as e:
1414
1420
  log_warning(f"Failed to convert response to JSON: {e}")
1421
+ elif team.output_schema is not None and isinstance(resp.content, dict):
1422
+ try:
1423
+ _response_content = JSON(json.dumps(resp.content), indent=2) # type: ignore
1424
+ except Exception as e:
1425
+ log_warning(f"Failed to convert response to JSON: {e}")
1415
1426
  if hasattr(resp, "reasoning_content") and resp.reasoning_content is not None: # type: ignore
1416
1427
  _response_reasoning_content += resp.reasoning_content # type: ignore
1417
1428
  if hasattr(resp, "reasoning_steps") and resp.reasoning_steps is not None: # type: ignore
agno/utils/prompts.py CHANGED
@@ -6,7 +6,7 @@ from pydantic import BaseModel
6
6
  from agno.utils.log import log_warning
7
7
 
8
8
 
9
- def get_json_output_prompt(output_schema: Union[str, list, BaseModel]) -> str:
9
+ def get_json_output_prompt(output_schema: Union[str, list, dict, BaseModel]) -> str:
10
10
  """Return the JSON output prompt for the Agent.
11
11
 
12
12
  This is added to the system prompt when the output_schema is set and structured_outputs is False.
@@ -22,11 +22,13 @@ def get_json_output_prompt(output_schema: Union[str, list, BaseModel]) -> str:
22
22
  json_output_prompt += "\n<json_fields>"
23
23
  json_output_prompt += f"\n{json.dumps(output_schema)}"
24
24
  json_output_prompt += "\n</json_fields>"
25
- elif (
26
- issubclass(type(output_schema), BaseModel)
27
- or issubclass(output_schema, BaseModel) # type: ignore
28
- or isinstance(output_schema, BaseModel)
29
- ): # type: ignore
25
+ elif isinstance(output_schema, dict):
26
+ json_output_prompt += "\n<json_fields>"
27
+ json_output_prompt += f"\n{json.dumps(output_schema)}"
28
+ json_output_prompt += "\n</json_fields>"
29
+ elif (isinstance(output_schema, type) and issubclass(output_schema, BaseModel)) or isinstance(
30
+ output_schema, BaseModel
31
+ ):
30
32
  json_schema = output_schema.model_json_schema()
31
33
  if json_schema is not None:
32
34
  response_model_properties = {}
agno/utils/string.py CHANGED
@@ -201,6 +201,52 @@ def parse_response_model_str(content: str, output_schema: Type[BaseModel]) -> Op
201
201
  return structured_output
202
202
 
203
203
 
204
+ def parse_response_dict_str(content: str) -> Optional[dict]:
205
+ """Parse dict from string content, extracting JSON if needed"""
206
+ from agno.utils.reasoning import extract_thinking_content
207
+
208
+ # Handle thinking content b/w <think> tags
209
+ if "</think>" in content:
210
+ reasoning_content, output_content = extract_thinking_content(content)
211
+ if reasoning_content:
212
+ content = output_content
213
+
214
+ # Clean content first to simplify all parsing attempts
215
+ cleaned_content = _clean_json_content(content)
216
+
217
+ try:
218
+ # First attempt: direct JSON parsing on cleaned content
219
+ return json.loads(cleaned_content)
220
+ except json.JSONDecodeError as e:
221
+ logger.warning(f"Failed to parse cleaned JSON: {e}")
222
+
223
+ # Second attempt: Extract individual JSON objects
224
+ candidate_jsons = _extract_json_objects(cleaned_content)
225
+
226
+ if len(candidate_jsons) == 1:
227
+ # Single JSON object - try to parse it directly
228
+ try:
229
+ return json.loads(candidate_jsons[0])
230
+ except json.JSONDecodeError:
231
+ pass
232
+
233
+ if len(candidate_jsons) > 1:
234
+ # Final attempt: Merge multiple JSON objects
235
+ merged_data: dict = {}
236
+ for candidate in candidate_jsons:
237
+ try:
238
+ obj = json.loads(candidate)
239
+ if isinstance(obj, dict):
240
+ merged_data.update(obj)
241
+ except json.JSONDecodeError:
242
+ continue
243
+ if merged_data:
244
+ return merged_data
245
+
246
+ logger.warning("All parsing attempts failed.")
247
+ return None
248
+
249
+
204
250
  def generate_id(seed: Optional[str] = None) -> str:
205
251
  """
206
252
  Generate a deterministic UUID5 based on a seed string.
agno/utils/team.py CHANGED
@@ -59,7 +59,7 @@ def add_interaction_to_team_run_context(
59
59
  team_run_context: Dict[str, Any],
60
60
  member_name: str,
61
61
  task: str,
62
- run_response: Union[RunOutput, TeamRunOutput],
62
+ run_response: Optional[Union[RunOutput, TeamRunOutput]],
63
63
  ) -> None:
64
64
  if "member_responses" not in team_run_context:
65
65
  team_run_context["member_responses"] = []
@@ -651,7 +651,11 @@ class Milvus(VectorDb):
651
651
  return MILVUS_DISTANCE_MAP.get(self.distance, "COSINE")
652
652
 
653
653
  def search(
654
- self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
654
+ self,
655
+ query: str,
656
+ limit: int = 5,
657
+ filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
658
+ search_params: Optional[Dict[str, Any]] = None,
655
659
  ) -> List[Document]:
656
660
  """
657
661
  Search for documents matching the query.
@@ -660,6 +664,10 @@ class Milvus(VectorDb):
660
664
  query (str): Query string to search for
661
665
  limit (int): Maximum number of results to return
662
666
  filters (Optional[Dict[str, Any]]): Filters to apply to the search
667
+ search_params (Optional[Dict[str, Any]]): Milvus search parameters including:
668
+ - radius (float): Minimum similarity threshold for range search
669
+ - range_filter (float): Maximum similarity threshold for range search
670
+ - params (dict): Index-specific search params (e.g., nprobe, ef)
663
671
 
664
672
  Returns:
665
673
  List[Document]: List of matching documents
@@ -681,6 +689,7 @@ class Milvus(VectorDb):
681
689
  filter=self._build_expr(filters),
682
690
  output_fields=["*"],
683
691
  limit=limit,
692
+ search_params=search_params,
684
693
  )
685
694
 
686
695
  # Build search results
@@ -708,8 +717,27 @@ class Milvus(VectorDb):
708
717
  return search_results
709
718
 
710
719
  async def async_search(
711
- self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
720
+ self,
721
+ query: str,
722
+ limit: int = 5,
723
+ filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
724
+ search_params: Optional[Dict[str, Any]] = None,
712
725
  ) -> List[Document]:
726
+ """
727
+ Asynchronously search for documents matching the query.
728
+
729
+ Args:
730
+ query (str): Query string to search for
731
+ limit (int): Maximum number of results to return
732
+ filters (Optional[Dict[str, Any]]): Filters to apply to the search
733
+ search_params (Optional[Dict[str, Any]]): Milvus search parameters including:
734
+ - radius (float): Minimum similarity threshold for range search
735
+ - range_filter (float): Maximum similarity threshold for range search
736
+ - params (dict): Index-specific search params (e.g., nprobe, ef)
737
+
738
+ Returns:
739
+ List[Document]: List of matching documents
740
+ """
713
741
  if isinstance(filters, List):
714
742
  log_warning("Filters Expressions are not supported in Milvus. No filters will be applied.")
715
743
  filters = None
@@ -727,6 +755,7 @@ class Milvus(VectorDb):
727
755
  filter=self._build_expr(filters),
728
756
  output_fields=["*"],
729
757
  limit=limit,
758
+ search_params=search_params,
730
759
  )
731
760
 
732
761
  # Build search results
@@ -1073,7 +1102,7 @@ class Milvus(VectorDb):
1073
1102
  if isinstance(v, (list, tuple)):
1074
1103
  # For array values, use json_contains_any
1075
1104
  values_str = json.dumps(v)
1076
- expr = f'json_contains_any(meta_data, {values_str}, "{k}")'
1105
+ expr = f'json_contains_any(meta_data["{k}"], {values_str})'
1077
1106
  elif isinstance(v, str):
1078
1107
  # For string values
1079
1108
  expr = f'meta_data["{k}"] == "{v}"'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agno
3
- Version: 2.3.13
3
+ Version: 2.3.14
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
@@ -212,7 +212,7 @@ Provides-Extra: firestore
212
212
  Requires-Dist: google-cloud-firestore; extra == "firestore"
213
213
  Provides-Extra: redis
214
214
  Requires-Dist: redis; extra == "redis"
215
- Requires-Dist: redisvl; extra == "redis"
215
+ Requires-Dist: redisvl>=0.12.1; extra == "redis"
216
216
  Provides-Extra: mysql
217
217
  Requires-Dist: pymysql; extra == "mysql"
218
218
  Requires-Dist: asyncmy; extra == "mysql"
@@ -364,6 +364,7 @@ Requires-Dist: agno[pinecone]; extra == "vectordbs"
364
364
  Requires-Dist: agno[surrealdb]; extra == "vectordbs"
365
365
  Requires-Dist: agno[upstash]; extra == "vectordbs"
366
366
  Requires-Dist: agno[pylance]; extra == "vectordbs"
367
+ Requires-Dist: agno[redis]; extra == "vectordbs"
367
368
  Provides-Extra: knowledge
368
369
  Requires-Dist: agno[pdf]; extra == "knowledge"
369
370
  Requires-Dist: agno[docx]; extra == "knowledge"