agno 2.3.9__py3-none-any.whl → 2.3.11__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 (43) hide show
  1. agno/agent/agent.py +0 -12
  2. agno/db/base.py +5 -5
  3. agno/db/dynamo/dynamo.py +2 -2
  4. agno/db/firestore/firestore.py +2 -2
  5. agno/db/gcs_json/gcs_json_db.py +2 -2
  6. agno/db/in_memory/in_memory_db.py +2 -2
  7. agno/db/json/json_db.py +2 -2
  8. agno/db/mongo/async_mongo.py +171 -69
  9. agno/db/mongo/mongo.py +171 -77
  10. agno/db/mysql/async_mysql.py +93 -69
  11. agno/db/mysql/mysql.py +93 -68
  12. agno/db/postgres/async_postgres.py +104 -78
  13. agno/db/postgres/postgres.py +97 -69
  14. agno/db/redis/redis.py +2 -2
  15. agno/db/singlestore/singlestore.py +91 -66
  16. agno/db/sqlite/async_sqlite.py +102 -79
  17. agno/db/sqlite/sqlite.py +97 -69
  18. agno/db/surrealdb/surrealdb.py +2 -2
  19. agno/eval/accuracy.py +11 -8
  20. agno/eval/agent_as_judge.py +9 -8
  21. agno/knowledge/chunking/fixed.py +4 -1
  22. agno/knowledge/embedder/openai.py +1 -1
  23. agno/knowledge/knowledge.py +22 -4
  24. agno/knowledge/utils.py +52 -7
  25. agno/models/base.py +34 -1
  26. agno/models/google/gemini.py +69 -40
  27. agno/models/message.py +3 -0
  28. agno/models/openai/chat.py +21 -0
  29. agno/os/routers/evals/utils.py +15 -37
  30. agno/os/routers/knowledge/knowledge.py +21 -9
  31. agno/team/team.py +14 -8
  32. agno/tools/function.py +37 -23
  33. agno/tools/shopify.py +1519 -0
  34. agno/tools/spotify.py +2 -5
  35. agno/tracing/exporter.py +2 -2
  36. agno/vectordb/base.py +15 -2
  37. agno/vectordb/pgvector/pgvector.py +8 -8
  38. agno/workflow/parallel.py +2 -0
  39. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/METADATA +1 -1
  40. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/RECORD +43 -42
  41. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/WHEEL +0 -0
  42. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/licenses/LICENSE +0 -0
  43. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/top_level.txt +0 -0
agno/knowledge/utils.py CHANGED
@@ -1,5 +1,6 @@
1
- from typing import Dict, List
1
+ from typing import Any, Dict, List, Optional
2
2
 
3
+ from agno.knowledge.reader.base import Reader
3
4
  from agno.knowledge.reader.reader_factory import ReaderFactory
4
5
  from agno.knowledge.types import ContentType
5
6
  from agno.utils.log import log_debug
@@ -75,8 +76,33 @@ def get_reader_info(reader_key: str) -> Dict:
75
76
  raise ValueError(f"Unknown reader: {reader_key}. Error: {str(e)}")
76
77
 
77
78
 
78
- def get_all_readers_info() -> List[Dict]:
79
- """Get information about all available readers."""
79
+ def get_reader_info_from_instance(reader: Reader, reader_id: str) -> Dict:
80
+ """Get information about a reader instance."""
81
+ try:
82
+ reader_class = reader.__class__
83
+ supported_strategies = reader_class.get_supported_chunking_strategies()
84
+ supported_content_types = reader_class.get_supported_content_types()
85
+
86
+ return {
87
+ "id": reader_id,
88
+ "name": getattr(reader, "name", reader_class.__name__),
89
+ "description": getattr(reader, "description", f"Custom {reader_class.__name__}"),
90
+ "chunking_strategies": [strategy.value for strategy in supported_strategies],
91
+ "content_types": [ct.value for ct in supported_content_types],
92
+ }
93
+ except Exception as e:
94
+ raise ValueError(f"Failed to get info for reader '{reader_id}': {str(e)}")
95
+
96
+
97
+ def get_all_readers_info(knowledge_instance: Optional[Any] = None) -> List[Dict]:
98
+ """Get information about all available readers, including custom readers from a Knowledge instance.
99
+
100
+ Args:
101
+ knowledge_instance: Optional Knowledge instance to include custom readers from.
102
+
103
+ Returns:
104
+ List of reader info dictionaries.
105
+ """
80
106
  readers_info = []
81
107
  keys = ReaderFactory.get_all_reader_keys()
82
108
  for key in keys:
@@ -88,18 +114,35 @@ def get_all_readers_info() -> List[Dict]:
88
114
  # Log the error but don't fail the entire request
89
115
  log_debug(f"Skipping reader '{key}': {e}")
90
116
  continue
117
+
118
+ # Add custom readers from knowledge instance if provided
119
+ if knowledge_instance is not None:
120
+ custom_readers = knowledge_instance.get_readers()
121
+ if isinstance(custom_readers, dict):
122
+ for reader_id, reader in custom_readers.items():
123
+ try:
124
+ reader_info = get_reader_info_from_instance(reader, reader_id)
125
+ # Only add if not already present (custom readers take precedence)
126
+ if not any(r["id"] == reader_id for r in readers_info):
127
+ readers_info.append(reader_info)
128
+ except ValueError as e:
129
+ log_debug(f"Skipping custom reader '{reader_id}': {e}")
130
+ continue
131
+
91
132
  return readers_info
92
133
 
93
134
 
94
- def get_content_types_to_readers_mapping() -> Dict[str, List[str]]:
135
+ def get_content_types_to_readers_mapping(knowledge_instance: Optional[Any] = None) -> Dict[str, List[str]]:
95
136
  """Get mapping of content types to list of reader IDs that support them.
96
137
 
138
+ Args:
139
+ knowledge_instance: Optional Knowledge instance to include custom readers from.
140
+
97
141
  Returns:
98
142
  Dictionary mapping content type strings (ContentType enum values) to list of reader IDs.
99
143
  """
100
144
  content_type_mapping: Dict[str, List[str]] = {}
101
- readers_info = get_all_readers_info()
102
-
145
+ readers_info = get_all_readers_info(knowledge_instance)
103
146
  for reader_info in readers_info:
104
147
  reader_id = reader_info["id"]
105
148
  content_types = reader_info.get("content_types", [])
@@ -107,7 +150,9 @@ def get_content_types_to_readers_mapping() -> Dict[str, List[str]]:
107
150
  for content_type in content_types:
108
151
  if content_type not in content_type_mapping:
109
152
  content_type_mapping[content_type] = []
110
- content_type_mapping[content_type].append(reader_id)
153
+ # Avoid duplicates
154
+ if reader_id not in content_type_mapping[content_type]:
155
+ content_type_mapping[content_type].append(reader_id)
111
156
 
112
157
  return content_type_mapping
113
158
 
agno/models/base.py CHANGED
@@ -1801,6 +1801,17 @@ class Model(ABC):
1801
1801
  log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
1802
1802
  function_call.error = str(e)
1803
1803
  function_call_success = False
1804
+
1805
+ # For generators, re-capture updated_session_state after consumption
1806
+ # since session_state modifications were made during iteration
1807
+ if function_execution_result.updated_session_state is None:
1808
+ if (
1809
+ function_call.function._run_context is not None
1810
+ and function_call.function._run_context.session_state is not None
1811
+ ):
1812
+ function_execution_result.updated_session_state = function_call.function._run_context.session_state
1813
+ elif function_call.function._session_state is not None:
1814
+ function_execution_result.updated_session_state = function_call.function._session_state
1804
1815
  else:
1805
1816
  from agno.tools.function import ToolResult
1806
1817
 
@@ -2327,7 +2338,29 @@ class Model(ABC):
2327
2338
  log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
2328
2339
  function_call.error = str(e)
2329
2340
  function_call_success = False
2330
- else:
2341
+
2342
+ # For generators (sync or async), re-capture updated_session_state after consumption
2343
+ # since session_state modifications were made during iteration
2344
+ if async_function_call_output is not None or isinstance(
2345
+ function_call.result,
2346
+ (GeneratorType, collections.abc.Iterator, AsyncGeneratorType, collections.abc.AsyncIterator),
2347
+ ):
2348
+ if updated_session_state is None:
2349
+ if (
2350
+ function_call.function._run_context is not None
2351
+ and function_call.function._run_context.session_state is not None
2352
+ ):
2353
+ updated_session_state = function_call.function._run_context.session_state
2354
+ elif function_call.function._session_state is not None:
2355
+ updated_session_state = function_call.function._session_state
2356
+
2357
+ if not (
2358
+ async_function_call_output is not None
2359
+ or isinstance(
2360
+ function_call.result,
2361
+ (GeneratorType, collections.abc.Iterator, AsyncGeneratorType, collections.abc.AsyncIterator),
2362
+ )
2363
+ ):
2331
2364
  from agno.tools.function import ToolResult
2332
2365
 
2333
2366
  if isinstance(function_execution_result.result, ToolResult):
@@ -36,6 +36,7 @@ try:
36
36
  GenerateContentResponseUsageMetadata,
37
37
  GoogleSearch,
38
38
  GoogleSearchRetrieval,
39
+ GroundingMetadata,
39
40
  Operation,
40
41
  Part,
41
42
  Retrieval,
@@ -244,8 +245,8 @@ class Gemini(Model):
244
245
  builtin_tools = []
245
246
 
246
247
  if self.grounding:
247
- log_info(
248
- "Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead."
248
+ log_debug(
249
+ "Gemini Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead."
249
250
  )
250
251
  builtin_tools.append(
251
252
  Tool(
@@ -258,15 +259,15 @@ class Gemini(Model):
258
259
  )
259
260
 
260
261
  if self.search:
261
- log_info("Google Search enabled.")
262
+ log_debug("Gemini Google Search enabled.")
262
263
  builtin_tools.append(Tool(google_search=GoogleSearch()))
263
264
 
264
265
  if self.url_context:
265
- log_info("URL context enabled.")
266
+ log_debug("Gemini URL context enabled.")
266
267
  builtin_tools.append(Tool(url_context=UrlContext()))
267
268
 
268
269
  if self.vertexai_search:
269
- log_info("Vertex AI Search enabled.")
270
+ log_debug("Gemini Vertex AI Search enabled.")
270
271
  if not self.vertexai_search_datastore:
271
272
  log_error("vertexai_search_datastore must be provided when vertexai_search is enabled.")
272
273
  raise ValueError("vertexai_search_datastore must be provided when vertexai_search is enabled.")
@@ -1008,27 +1009,24 @@ class Gemini(Model):
1008
1009
  citations = Citations()
1009
1010
  citations_raw = {}
1010
1011
  citations_urls = []
1012
+ web_search_queries: List[str] = []
1011
1013
 
1012
1014
  if response.candidates and response.candidates[0].grounding_metadata is not None:
1013
- grounding_metadata = response.candidates[0].grounding_metadata.model_dump()
1014
- citations_raw["grounding_metadata"] = grounding_metadata
1015
+ grounding_metadata: GroundingMetadata = response.candidates[0].grounding_metadata
1016
+ citations_raw["grounding_metadata"] = grounding_metadata.model_dump()
1015
1017
 
1016
- chunks = grounding_metadata.get("grounding_chunks", []) or []
1017
- citation_pairs = []
1018
+ chunks = grounding_metadata.grounding_chunks or []
1019
+ web_search_queries = grounding_metadata.web_search_queries or []
1018
1020
  for chunk in chunks:
1019
- if not isinstance(chunk, dict):
1021
+ if not chunk:
1020
1022
  continue
1021
- web = chunk.get("web")
1022
- if not isinstance(web, dict):
1023
+ web = chunk.web
1024
+ if not web:
1023
1025
  continue
1024
- uri = web.get("uri")
1025
- title = web.get("title")
1026
+ uri = web.uri
1027
+ title = web.title
1026
1028
  if uri:
1027
- citation_pairs.append((uri, title))
1028
-
1029
- # Create citation objects from filtered pairs
1030
- grounding_urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
1031
- citations_urls.extend(grounding_urls)
1029
+ citations_urls.append(UrlCitation(url=uri, title=title))
1032
1030
 
1033
1031
  # Handle URLs from URL context tool
1034
1032
  if (
@@ -1036,22 +1034,29 @@ class Gemini(Model):
1036
1034
  and hasattr(response.candidates[0], "url_context_metadata")
1037
1035
  and response.candidates[0].url_context_metadata is not None
1038
1036
  ):
1039
- url_context_metadata = response.candidates[0].url_context_metadata.model_dump()
1040
- citations_raw["url_context_metadata"] = url_context_metadata
1037
+ url_context_metadata = response.candidates[0].url_context_metadata
1038
+ citations_raw["url_context_metadata"] = url_context_metadata.model_dump()
1041
1039
 
1042
- url_metadata_list = url_context_metadata.get("url_metadata", [])
1040
+ url_metadata_list = url_context_metadata.url_metadata or []
1043
1041
  for url_meta in url_metadata_list:
1044
- retrieved_url = url_meta.get("retrieved_url")
1045
- status = url_meta.get("url_retrieval_status", "UNKNOWN")
1042
+ retrieved_url = url_meta.retrieved_url
1043
+ status = "UNKNOWN"
1044
+ if url_meta.url_retrieval_status:
1045
+ status = url_meta.url_retrieval_status.value
1046
1046
  if retrieved_url and status == "URL_RETRIEVAL_STATUS_SUCCESS":
1047
1047
  # Avoid duplicate URLs
1048
1048
  existing_urls = [citation.url for citation in citations_urls]
1049
1049
  if retrieved_url not in existing_urls:
1050
1050
  citations_urls.append(UrlCitation(url=retrieved_url, title=retrieved_url))
1051
1051
 
1052
+ if citations_raw:
1053
+ citations.raw = citations_raw
1054
+ if citations_urls:
1055
+ citations.urls = citations_urls
1056
+ if web_search_queries:
1057
+ citations.search_queries = web_search_queries
1058
+
1052
1059
  if citations_raw or citations_urls:
1053
- citations.raw = citations_raw if citations_raw else None
1054
- citations.urls = citations_urls if citations_urls else None
1055
1060
  model_response.citations = citations
1056
1061
 
1057
1062
  # Extract usage metadata if present
@@ -1150,28 +1155,52 @@ class Gemini(Model):
1150
1155
 
1151
1156
  model_response.tool_calls.append(tool_call)
1152
1157
 
1153
- if response_delta.candidates[0].grounding_metadata is not None:
1154
- citations = Citations()
1155
- grounding_metadata = response_delta.candidates[0].grounding_metadata.model_dump()
1156
- citations.raw = grounding_metadata
1158
+ citations = Citations()
1159
+ citations.raw = {}
1160
+ citations.urls = []
1157
1161
 
1162
+ if (
1163
+ hasattr(response_delta.candidates[0], "grounding_metadata")
1164
+ and response_delta.candidates[0].grounding_metadata is not None
1165
+ ):
1166
+ grounding_metadata = response_delta.candidates[0].grounding_metadata
1167
+ citations.raw["grounding_metadata"] = grounding_metadata.model_dump()
1168
+ citations.search_queries = grounding_metadata.web_search_queries or []
1158
1169
  # Extract url and title
1159
- chunks = grounding_metadata.pop("grounding_chunks", None) or []
1160
- citation_pairs = []
1170
+ chunks = grounding_metadata.grounding_chunks or []
1161
1171
  for chunk in chunks:
1162
- if not isinstance(chunk, dict):
1172
+ if not chunk:
1163
1173
  continue
1164
- web = chunk.get("web")
1165
- if not isinstance(web, dict):
1174
+ web = chunk.web
1175
+ if not web:
1166
1176
  continue
1167
- uri = web.get("uri")
1168
- title = web.get("title")
1177
+ uri = web.uri
1178
+ title = web.title
1169
1179
  if uri:
1170
- citation_pairs.append((uri, title))
1180
+ citations.urls.append(UrlCitation(url=uri, title=title))
1181
+
1182
+ # Handle URLs from URL context tool
1183
+ if (
1184
+ hasattr(response_delta.candidates[0], "url_context_metadata")
1185
+ and response_delta.candidates[0].url_context_metadata is not None
1186
+ ):
1187
+ url_context_metadata = response_delta.candidates[0].url_context_metadata
1171
1188
 
1172
- # Create citation objects from filtered pairs
1173
- citations.urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
1189
+ citations.raw["url_context_metadata"] = url_context_metadata.model_dump()
1190
+
1191
+ url_metadata_list = url_context_metadata.url_metadata or []
1192
+ for url_meta in url_metadata_list:
1193
+ retrieved_url = url_meta.retrieved_url
1194
+ status = "UNKNOWN"
1195
+ if url_meta.url_retrieval_status:
1196
+ status = url_meta.url_retrieval_status.value
1197
+ if retrieved_url and status == "URL_RETRIEVAL_STATUS_SUCCESS":
1198
+ # Avoid duplicate URLs
1199
+ existing_urls = [citation.url for citation in citations.urls]
1200
+ if retrieved_url not in existing_urls:
1201
+ citations.urls.append(UrlCitation(url=retrieved_url, title=retrieved_url))
1174
1202
 
1203
+ if citations.raw or citations.urls:
1175
1204
  model_response.citations = citations
1176
1205
 
1177
1206
  # Extract usage metadata if present
agno/models/message.py CHANGED
@@ -42,6 +42,9 @@ class Citations(BaseModel):
42
42
  # Raw citations from the model
43
43
  raw: Optional[Any] = None
44
44
 
45
+ # Search queries used to retrieve the citations
46
+ search_queries: Optional[List[str]] = None
47
+
45
48
  # URLs of the citations.
46
49
  urls: Optional[List[UrlCitation]] = None
47
50
 
@@ -820,6 +820,16 @@ class OpenAIChat(Model):
820
820
  if response.usage is not None:
821
821
  model_response.response_usage = self._get_metrics(response.usage)
822
822
 
823
+ if model_response.provider_data is None:
824
+ model_response.provider_data = {}
825
+
826
+ if response.id:
827
+ model_response.provider_data["id"] = response.id
828
+ if response.system_fingerprint:
829
+ model_response.provider_data["system_fingerprint"] = response.system_fingerprint
830
+ if response.model_extra:
831
+ model_response.provider_data["model_extra"] = response.model_extra
832
+
823
833
  return model_response
824
834
 
825
835
  def _parse_provider_response_delta(self, response_delta: ChatCompletionChunk) -> ModelResponse:
@@ -842,6 +852,17 @@ class OpenAIChat(Model):
842
852
  if choice_delta.content is not None:
843
853
  model_response.content = choice_delta.content
844
854
 
855
+ # We only want to handle these if content is present
856
+ if model_response.provider_data is None:
857
+ model_response.provider_data = {}
858
+
859
+ if response_delta.id:
860
+ model_response.provider_data["id"] = response_delta.id
861
+ if response_delta.system_fingerprint:
862
+ model_response.provider_data["system_fingerprint"] = response_delta.system_fingerprint
863
+ if response_delta.model_extra:
864
+ model_response.provider_data["model_extra"] = response_delta.model_extra
865
+
845
866
  # Add tool calls
846
867
  if choice_delta.tool_calls is not None:
847
868
  model_response.tool_calls = choice_delta.tool_calls # type: ignore
@@ -66,14 +66,14 @@ async def run_agent_as_judge_eval(
66
66
 
67
67
  # Run agent/team to get output
68
68
  if agent:
69
- agent_response = await agent.arun(eval_run_input.input)
69
+ agent_response = await agent.arun(eval_run_input.input, stream=False)
70
70
  output = str(agent_response.content) if agent_response.content else ""
71
71
  model_id = agent.model.id if agent and agent.model else None
72
72
  model_provider = agent.model.provider if agent and agent.model else None
73
73
  agent_id = agent.id
74
74
  team_id = None
75
75
  elif team:
76
- team_response = await team.arun(eval_run_input.input)
76
+ team_response = await team.arun(eval_run_input.input, stream=False)
77
77
  output = str(team_response.content) if team_response.content else ""
78
78
  model_id = team.model.id if team and team.model else None
79
79
  model_provider = team.model.provider if team and team.model else None
@@ -125,39 +125,21 @@ async def run_performance_eval(
125
125
  default_model: Optional[Model] = None,
126
126
  ) -> EvalSchema:
127
127
  """Run a performance evaluation for the given agent or team"""
128
- # Create sync or async function based on DB type
129
- if isinstance(db, AsyncBaseDb):
130
- if agent:
131
-
132
- async def run_component(): # type: ignore
133
- return await agent.arun(eval_run_input.input)
134
-
135
- model_id = agent.model.id if agent and agent.model else None
136
- model_provider = agent.model.provider if agent and agent.model else None
137
-
138
- elif team:
139
-
140
- async def run_component(): # type: ignore
141
- return await team.arun(eval_run_input.input)
142
-
143
- model_id = team.model.id if team and team.model else None
144
- model_provider = team.model.provider if team and team.model else None
145
- else:
146
- if agent:
128
+ if agent:
147
129
 
148
- def run_component(): # type: ignore
149
- return agent.run(eval_run_input.input)
130
+ async def run_component(): # type: ignore
131
+ return await agent.arun(eval_run_input.input, stream=False)
150
132
 
151
- model_id = agent.model.id if agent and agent.model else None
152
- model_provider = agent.model.provider if agent and agent.model else None
133
+ model_id = agent.model.id if agent and agent.model else None
134
+ model_provider = agent.model.provider if agent and agent.model else None
153
135
 
154
- elif team:
136
+ elif team:
155
137
 
156
- def run_component():
157
- return team.run(eval_run_input.input)
138
+ async def run_component(): # type: ignore
139
+ return await team.arun(eval_run_input.input, stream=False)
158
140
 
159
- model_id = team.model.id if team and team.model else None
160
- model_provider = team.model.provider if team and team.model else None
141
+ model_id = team.model.id if team and team.model else None
142
+ model_provider = team.model.provider if team and team.model else None
161
143
 
162
144
  performance_eval = PerformanceEval(
163
145
  db=db,
@@ -171,11 +153,7 @@ async def run_performance_eval(
171
153
  model_provider=model_provider,
172
154
  )
173
155
 
174
- # PerformanceEval needs sync/async check because it wraps a function
175
- if isinstance(db, AsyncBaseDb):
176
- result = await performance_eval.arun(print_results=False, print_summary=False)
177
- else:
178
- result = performance_eval.run(print_results=False, print_summary=False)
156
+ result = await performance_eval.arun(print_results=False, print_summary=False)
179
157
  if not result:
180
158
  raise HTTPException(status_code=500, detail="Failed to run performance evaluation")
181
159
 
@@ -210,7 +188,7 @@ async def run_reliability_eval(
210
188
  raise HTTPException(status_code=400, detail="expected_tool_calls is required for reliability evaluations")
211
189
 
212
190
  if agent:
213
- agent_response = await agent.arun(eval_run_input.input)
191
+ agent_response = await agent.arun(eval_run_input.input, stream=False)
214
192
  reliability_eval = ReliabilityEval(
215
193
  db=db,
216
194
  name=eval_run_input.name,
@@ -221,7 +199,7 @@ async def run_reliability_eval(
221
199
  model_provider = agent.model.provider if agent and agent.model else None
222
200
 
223
201
  elif team:
224
- team_response = await team.arun(eval_run_input.input)
202
+ team_response = await team.arun(eval_run_input.input, stream=False)
225
203
  reliability_eval = ReliabilityEval(
226
204
  db=db,
227
205
  name=eval_run_input.name,
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  import math
4
- from typing import Dict, List, Optional
4
+ from typing import Any, Dict, List, Optional
5
5
 
6
6
  from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Path, Query, UploadFile
7
7
 
@@ -874,8 +874,8 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
874
874
  ) -> ConfigResponseSchema:
875
875
  knowledge = get_knowledge_instance_by_db_id(knowledge_instances, db_id)
876
876
 
877
- # Get factory readers info
878
- readers_info = get_all_readers_info()
877
+ # Get factory readers info (including custom readers from this knowledge instance)
878
+ readers_info = get_all_readers_info(knowledge)
879
879
  reader_schemas = {}
880
880
  # Add factory readers
881
881
  for reader_info in readers_info:
@@ -887,7 +887,13 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
887
887
  )
888
888
 
889
889
  # Add custom readers from knowledge.readers
890
- readers_dict: Dict[str, Reader] = knowledge.get_readers() or {}
890
+ readers_result: Any = knowledge.get_readers() or {}
891
+ print(f"readers_result: {readers_result}")
892
+ # Ensure readers_dict is a dictionary (defensive check)
893
+ if not isinstance(readers_result, dict):
894
+ readers_dict: Dict[str, Reader] = {}
895
+ else:
896
+ readers_dict = readers_result
891
897
  if readers_dict:
892
898
  for reader_id, reader in readers_dict.items():
893
899
  # Get chunking strategies from the reader
@@ -907,8 +913,8 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
907
913
  chunkers=chunking_strategies,
908
914
  )
909
915
 
910
- # Get content types to readers mapping
911
- types_of_readers = get_content_types_to_readers_mapping()
916
+ # Get content types to readers mapping (including custom readers from this knowledge instance)
917
+ types_of_readers = get_content_types_to_readers_mapping(knowledge)
912
918
  chunkers_list = get_all_chunkers_info()
913
919
 
914
920
  # Convert chunkers list to dictionary format expected by schema
@@ -961,20 +967,26 @@ async def process_content(
961
967
  try:
962
968
  if reader_id:
963
969
  reader = None
964
- if knowledge.readers and reader_id in knowledge.readers:
965
- reader = knowledge.readers[reader_id]
970
+ # Use get_readers() to ensure we get a dict (handles list conversion)
971
+ custom_readers = knowledge.get_readers()
972
+ if custom_readers and reader_id in custom_readers:
973
+ reader = custom_readers[reader_id]
974
+ log_debug(f"Found custom reader: {reader.__class__.__name__}")
966
975
  else:
976
+ # Try to resolve from factory readers
967
977
  key = reader_id.lower().strip().replace("-", "_").replace(" ", "_")
968
978
  candidates = [key] + ([key[:-6]] if key.endswith("reader") else [])
969
979
  for cand in candidates:
970
980
  try:
971
981
  reader = ReaderFactory.create_reader(cand)
972
- log_debug(f"Resolved reader: {reader.__class__.__name__}")
982
+ log_debug(f"Resolved reader from factory: {reader.__class__.__name__}")
973
983
  break
974
984
  except Exception:
975
985
  continue
976
986
  if reader:
977
987
  content.reader = reader
988
+ else:
989
+ log_debug(f"Could not resolve reader with id: {reader_id}")
978
990
  if chunker and content.reader:
979
991
  # Set the chunker name on the reader - let the reader handle it internally
980
992
  content.reader.set_chunking_strategy_from_string(chunker, chunk_size=chunk_size, overlap=chunk_overlap)
agno/team/team.py CHANGED
@@ -1541,6 +1541,8 @@ class Team:
1541
1541
  add_history_to_context=add_history_to_context,
1542
1542
  add_session_state_to_context=add_session_state_to_context,
1543
1543
  add_dependencies_to_context=add_dependencies_to_context,
1544
+ stream=False,
1545
+ stream_events=False,
1544
1546
  )
1545
1547
 
1546
1548
  # 3. Prepare run messages
@@ -1753,6 +1755,8 @@ class Team:
1753
1755
  add_history_to_context=add_history_to_context,
1754
1756
  add_session_state_to_context=add_session_state_to_context,
1755
1757
  add_dependencies_to_context=add_dependencies_to_context,
1758
+ stream=True,
1759
+ stream_events=stream_events,
1756
1760
  )
1757
1761
 
1758
1762
  # 3. Prepare run messages
@@ -2182,9 +2186,6 @@ class Team:
2182
2186
  if stream_events is None:
2183
2187
  stream_events = False if self.stream_events is None else self.stream_events
2184
2188
 
2185
- self.stream = self.stream or stream
2186
- self.stream_events = self.stream_events or stream_events
2187
-
2188
2189
  self.model = cast(Model, self.model)
2189
2190
 
2190
2191
  if self.metadata is not None:
@@ -2394,6 +2395,8 @@ class Team:
2394
2395
  add_history_to_context=add_history_to_context,
2395
2396
  add_dependencies_to_context=add_dependencies_to_context,
2396
2397
  add_session_state_to_context=add_session_state_to_context,
2398
+ stream=False,
2399
+ stream_events=False,
2397
2400
  )
2398
2401
 
2399
2402
  # 5. Prepare run messages
@@ -2638,6 +2641,10 @@ class Team:
2638
2641
  files=run_input.files,
2639
2642
  debug_mode=debug_mode,
2640
2643
  add_history_to_context=add_history_to_context,
2644
+ add_dependencies_to_context=add_dependencies_to_context,
2645
+ add_session_state_to_context=add_session_state_to_context,
2646
+ stream=True,
2647
+ stream_events=stream_events,
2641
2648
  )
2642
2649
 
2643
2650
  # 6. Prepare run messages
@@ -3046,9 +3053,6 @@ class Team:
3046
3053
  if stream_events is None:
3047
3054
  stream_events = False if self.stream_events is None else self.stream_events
3048
3055
 
3049
- self.stream = self.stream or stream
3050
- self.stream_events = self.stream_events or stream_events
3051
-
3052
3056
  self.model = cast(Model, self.model)
3053
3057
 
3054
3058
  if self.metadata is not None:
@@ -5397,6 +5401,8 @@ class Team:
5397
5401
  add_history_to_context: Optional[bool] = None,
5398
5402
  add_dependencies_to_context: Optional[bool] = None,
5399
5403
  add_session_state_to_context: Optional[bool] = None,
5404
+ stream: Optional[bool] = None,
5405
+ stream_events: Optional[bool] = None,
5400
5406
  check_mcp_tools: bool = True,
5401
5407
  ) -> List[Union[Function, dict]]:
5402
5408
  # Connect tools that require connection management
@@ -5491,8 +5497,8 @@ class Team:
5491
5497
  team_run_context=team_run_context,
5492
5498
  input=user_message_content,
5493
5499
  user_id=user_id,
5494
- stream=self.stream or False,
5495
- stream_events=self.stream_events or False,
5500
+ stream=stream or False,
5501
+ stream_events=stream_events or False,
5496
5502
  async_mode=async_mode,
5497
5503
  images=images, # type: ignore
5498
5504
  videos=videos, # type: ignore