agno 2.0.6__py3-none-any.whl → 2.0.8__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 (52) hide show
  1. agno/agent/agent.py +94 -48
  2. agno/db/migrations/v1_to_v2.py +140 -11
  3. agno/knowledge/chunking/semantic.py +33 -6
  4. agno/knowledge/embedder/sentence_transformer.py +3 -3
  5. agno/knowledge/knowledge.py +152 -31
  6. agno/knowledge/types.py +8 -0
  7. agno/media.py +2 -0
  8. agno/models/base.py +38 -9
  9. agno/models/cometapi/__init__.py +5 -0
  10. agno/models/cometapi/cometapi.py +57 -0
  11. agno/models/google/gemini.py +4 -8
  12. agno/models/llama_cpp/__init__.py +5 -0
  13. agno/models/llama_cpp/llama_cpp.py +22 -0
  14. agno/models/nexus/__init__.py +1 -1
  15. agno/models/nexus/nexus.py +2 -5
  16. agno/models/ollama/chat.py +24 -1
  17. agno/models/openai/chat.py +2 -7
  18. agno/models/openai/responses.py +21 -17
  19. agno/os/app.py +4 -10
  20. agno/os/interfaces/agui/agui.py +2 -2
  21. agno/os/interfaces/agui/utils.py +81 -18
  22. agno/os/interfaces/slack/slack.py +2 -2
  23. agno/os/interfaces/whatsapp/whatsapp.py +2 -2
  24. agno/os/router.py +3 -4
  25. agno/os/routers/evals/evals.py +1 -1
  26. agno/os/routers/memory/memory.py +1 -1
  27. agno/os/schema.py +3 -4
  28. agno/os/utils.py +55 -12
  29. agno/reasoning/default.py +3 -1
  30. agno/run/agent.py +4 -0
  31. agno/run/team.py +3 -1
  32. agno/session/agent.py +8 -5
  33. agno/session/team.py +14 -10
  34. agno/team/team.py +239 -115
  35. agno/tools/decorator.py +4 -2
  36. agno/tools/function.py +43 -4
  37. agno/tools/mcp.py +61 -38
  38. agno/tools/memori.py +1 -53
  39. agno/utils/events.py +7 -1
  40. agno/utils/gemini.py +147 -19
  41. agno/utils/models/claude.py +9 -0
  42. agno/utils/print_response/agent.py +16 -0
  43. agno/utils/print_response/team.py +16 -0
  44. agno/vectordb/base.py +2 -2
  45. agno/vectordb/langchaindb/langchaindb.py +5 -7
  46. agno/vectordb/llamaindex/llamaindexdb.py +25 -6
  47. agno/workflow/workflow.py +59 -15
  48. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/METADATA +1 -1
  49. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/RECORD +52 -48
  50. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/WHEEL +0 -0
  51. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/licenses/LICENSE +0 -0
  52. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/top_level.txt +0 -0
agno/agent/agent.py CHANGED
@@ -30,6 +30,7 @@ from pydantic import BaseModel
30
30
  from agno.db.base import BaseDb, SessionType, UserMemory
31
31
  from agno.exceptions import ModelProviderError, RunCancelledException, StopAgentRun
32
32
  from agno.knowledge.knowledge import Knowledge
33
+ from agno.knowledge.types import KnowledgeFilter
33
34
  from agno.media import Audio, File, Image, Video
34
35
  from agno.memory import MemoryManager
35
36
  from agno.models.base import Model
@@ -455,6 +456,11 @@ class Agent:
455
456
  self.add_history_to_context = add_history_to_context
456
457
  self.num_history_runs = num_history_runs
457
458
 
459
+ if add_history_to_context and not db:
460
+ log_warning(
461
+ "add_history_to_context is True, but no database has been assigned to the agent. History will not be added to the context."
462
+ )
463
+
458
464
  self.store_media = store_media
459
465
 
460
466
  self.knowledge = knowledge
@@ -711,6 +717,14 @@ class Agent:
711
717
  # Determine the session_state
712
718
  if session_state is None:
713
719
  session_state = self.session_state or {}
720
+ else:
721
+ # If run session_state is provided, merge agent defaults under it
722
+ # This ensures run state takes precedence over agent defaults
723
+ if self.session_state:
724
+ base_state = self.session_state.copy()
725
+ merge_dictionaries(base_state, session_state)
726
+ session_state.clear()
727
+ session_state.update(base_state)
714
728
 
715
729
  if user_id is not None:
716
730
  session_state["current_user_id"] = user_id
@@ -762,6 +776,7 @@ class Agent:
762
776
  tool_call_limit=self.tool_call_limit,
763
777
  response_format=response_format,
764
778
  run_response=run_response,
779
+ send_media_to_model=self.send_media_to_model,
765
780
  )
766
781
 
767
782
  # Check for cancellation after model call
@@ -1176,6 +1191,7 @@ class Agent:
1176
1191
  run_response=run_response,
1177
1192
  session=agent_session,
1178
1193
  session_state=session_state,
1194
+ dependencies=run_dependencies,
1179
1195
  user_id=user_id,
1180
1196
  async_mode=False,
1181
1197
  knowledge_filters=effective_filters,
@@ -1359,6 +1375,7 @@ class Agent:
1359
1375
  tool_choice=self.tool_choice,
1360
1376
  tool_call_limit=self.tool_call_limit,
1361
1377
  response_format=response_format,
1378
+ send_media_to_model=self.send_media_to_model,
1362
1379
  )
1363
1380
 
1364
1381
  # Check for cancellation after model call
@@ -1807,6 +1824,7 @@ class Agent:
1807
1824
  run_response=run_response,
1808
1825
  session=agent_session,
1809
1826
  session_state=session_state,
1827
+ dependencies=run_dependencies,
1810
1828
  user_id=user_id,
1811
1829
  async_mode=True,
1812
1830
  knowledge_filters=effective_filters,
@@ -3100,6 +3118,8 @@ class Agent:
3100
3118
  # Update the run_response citations with the model response citations
3101
3119
  if model_response.citations is not None:
3102
3120
  run_response.citations = model_response.citations
3121
+ if model_response.provider_data is not None:
3122
+ run_response.model_provider_data = model_response.provider_data
3103
3123
 
3104
3124
  # Update the run_response tools with the model response tool_executions
3105
3125
  if model_response.tool_executions is not None:
@@ -3174,6 +3194,7 @@ class Agent:
3174
3194
  tool_call_limit=self.tool_call_limit,
3175
3195
  stream_model_response=stream_model_response,
3176
3196
  run_response=run_response,
3197
+ send_media_to_model=self.send_media_to_model,
3177
3198
  ):
3178
3199
  yield from self._handle_model_response_chunk(
3179
3200
  session=session,
@@ -3250,6 +3271,7 @@ class Agent:
3250
3271
  tool_call_limit=self.tool_call_limit,
3251
3272
  stream_model_response=stream_model_response,
3252
3273
  run_response=run_response,
3274
+ send_media_to_model=self.send_media_to_model,
3253
3275
  ) # type: ignore
3254
3276
 
3255
3277
  async for model_response_event in model_response_stream: # type: ignore
@@ -3339,6 +3361,7 @@ class Agent:
3339
3361
  run_response.content = model_response.content
3340
3362
  run_response.content_type = "str"
3341
3363
 
3364
+ # Process reasoning content
3342
3365
  if model_response_event.reasoning_content is not None:
3343
3366
  model_response.reasoning_content = (
3344
3367
  model_response.reasoning_content or ""
@@ -3352,8 +3375,12 @@ class Agent:
3352
3375
  model_response.reasoning_content += model_response_event.redacted_reasoning_content
3353
3376
  run_response.reasoning_content = model_response.reasoning_content
3354
3377
 
3378
+ # Handle provider data (one chunk)
3379
+ if model_response_event.provider_data is not None:
3380
+ run_response.model_provider_data = model_response_event.provider_data
3381
+
3382
+ # Handle citations (one chunk)
3355
3383
  if model_response_event.citations is not None:
3356
- # We get citations in one chunk
3357
3384
  run_response.citations = model_response_event.citations
3358
3385
 
3359
3386
  # Only yield if we have content to show
@@ -3372,6 +3399,7 @@ class Agent:
3372
3399
  or model_response_event.reasoning_content is not None
3373
3400
  or model_response_event.redacted_reasoning_content is not None
3374
3401
  or model_response_event.citations is not None
3402
+ or model_response_event.provider_data is not None
3375
3403
  ):
3376
3404
  yield self._handle_event(
3377
3405
  create_run_output_content_event(
@@ -3380,6 +3408,7 @@ class Agent:
3380
3408
  reasoning_content=model_response_event.reasoning_content,
3381
3409
  redacted_reasoning_content=model_response_event.redacted_reasoning_content,
3382
3410
  citations=model_response_event.citations,
3411
+ model_provider_data=model_response_event.provider_data,
3383
3412
  ),
3384
3413
  run_response,
3385
3414
  workflow_context=workflow_context,
@@ -3778,29 +3807,21 @@ class Agent:
3778
3807
 
3779
3808
  # If any of the tools has "agent" as parameter, set _rebuild_tools to True
3780
3809
  for tool in agent_tools:
3810
+ param_names = {"agent", "session_state", "team", "images", "videos", "audios", "files"}
3811
+
3781
3812
  if isinstance(tool, Function):
3782
- if "agent" in tool.parameters:
3783
- self._rebuild_tools = True
3784
- break
3785
- if "team" in tool.parameters:
3813
+ if param_names & set(tool.parameters):
3786
3814
  self._rebuild_tools = True
3787
3815
  break
3788
- if isinstance(tool, Toolkit):
3816
+ elif isinstance(tool, Toolkit):
3789
3817
  for func in tool.functions.values():
3790
- if "agent" in func.parameters:
3791
- self._rebuild_tools = True
3792
- break
3793
- if "team" in func.parameters:
3818
+ if param_names & set(func.parameters):
3794
3819
  self._rebuild_tools = True
3795
3820
  break
3796
- if callable(tool):
3821
+ elif callable(tool):
3797
3822
  from inspect import signature
3798
3823
 
3799
- sig = signature(tool)
3800
- if "agent" in sig.parameters:
3801
- self._rebuild_tools = True
3802
- break
3803
- if "team" in sig.parameters:
3824
+ if param_names & set(signature(tool).parameters):
3804
3825
  self._rebuild_tools = True
3805
3826
  break
3806
3827
 
@@ -3813,7 +3834,9 @@ class Agent:
3813
3834
  self._rebuild_tools = True
3814
3835
  if self.search_session_history:
3815
3836
  agent_tools.append(
3816
- self._get_previous_sessions_messages_function(num_history_sessions=self.num_history_sessions, user_id=user_id)
3837
+ self._get_previous_sessions_messages_function(
3838
+ num_history_sessions=self.num_history_sessions, user_id=user_id
3839
+ )
3817
3840
  )
3818
3841
  self._rebuild_tools = True
3819
3842
 
@@ -3993,6 +4016,7 @@ class Agent:
3993
4016
  run_response: RunOutput,
3994
4017
  session: AgentSession,
3995
4018
  session_state: Optional[Dict[str, Any]] = None,
4019
+ dependencies: Optional[Dict[str, Any]] = None,
3996
4020
  user_id: Optional[str] = None,
3997
4021
  async_mode: bool = False,
3998
4022
  knowledge_filters: Optional[Dict[str, Any]] = None,
@@ -4102,6 +4126,7 @@ class Agent:
4102
4126
 
4103
4127
  for func in self._functions_for_model.values():
4104
4128
  func._session_state = session_state
4129
+ func._dependencies = dependencies
4105
4130
  func._images = joint_images
4106
4131
  func._files = joint_files
4107
4132
  func._audios = joint_audios
@@ -4230,7 +4255,8 @@ class Agent:
4230
4255
  def _update_session_state(self, session: AgentSession, session_state: Dict[str, Any]):
4231
4256
  """Load the existing Agent from an AgentSession (from the database)"""
4232
4257
 
4233
- # Get the session_state from the database and update the current session_state
4258
+ # Get the session_state from the database and merge with proper precedence
4259
+ # At this point session_state contains: agent_defaults + run_params
4234
4260
  if session.session_data is not None and "session_state" in session.session_data:
4235
4261
  session_state_from_db = session.session_data.get("session_state")
4236
4262
 
@@ -4239,10 +4265,11 @@ class Agent:
4239
4265
  and isinstance(session_state_from_db, dict)
4240
4266
  and len(session_state_from_db) > 0
4241
4267
  ):
4242
- # This updates session_state_from_db
4243
- # If there are conflicting keys, values from provided session_state will take precedence
4244
- merge_dictionaries(session_state_from_db, session_state)
4245
- session_state = session_state_from_db
4268
+ # This preserves precedence: run_params > db_state > agent_defaults
4269
+ merged_state = session_state_from_db.copy()
4270
+ merge_dictionaries(merged_state, session_state)
4271
+ session_state.clear()
4272
+ session_state.update(merged_state)
4246
4273
 
4247
4274
  # Update the session_state in the session
4248
4275
  if session.session_data is not None:
@@ -4919,7 +4946,7 @@ class Agent:
4919
4946
  system_message_content += f"{get_response_model_format_prompt(self.output_schema)}"
4920
4947
 
4921
4948
  # 3.3.15 Add the session state to the system message
4922
- if self.add_session_state_to_context and session_state is not None:
4949
+ if add_session_state_to_context and session_state is not None:
4923
4950
  system_message_content += self._get_formatted_session_state_for_system_message(session_state)
4924
4951
 
4925
4952
  # Return the system message
@@ -5190,9 +5217,16 @@ class Agent:
5190
5217
  if add_history_to_context:
5191
5218
  from copy import deepcopy
5192
5219
 
5220
+ # Only skip messages from history when system_message_role is NOT a standard conversation role.
5221
+ # Standard conversation roles ("user", "assistant", "tool") should never be filtered
5222
+ # to preserve conversation continuity.
5223
+ skip_role = (
5224
+ self.system_message_role if self.system_message_role not in ["user", "assistant", "tool"] else None
5225
+ )
5226
+
5193
5227
  history: List[Message] = session.get_messages_from_last_n_runs(
5194
5228
  last_n=self.num_history_runs,
5195
- skip_role=self.system_message_role,
5229
+ skip_role=skip_role,
5196
5230
  agent_id=self.id if self.team_id is not None else None,
5197
5231
  )
5198
5232
 
@@ -5910,6 +5944,7 @@ class Agent:
5910
5944
  min_steps=self.reasoning_min_steps,
5911
5945
  max_steps=self.reasoning_max_steps,
5912
5946
  tools=self.tools,
5947
+ tool_call_limit=self.tool_call_limit,
5913
5948
  use_json_mode=self.use_json_mode,
5914
5949
  telemetry=self.telemetry,
5915
5950
  debug_mode=self.debug_mode,
@@ -6135,6 +6170,7 @@ class Agent:
6135
6170
  min_steps=self.reasoning_min_steps,
6136
6171
  max_steps=self.reasoning_max_steps,
6137
6172
  tools=self.tools,
6173
+ tool_call_limit=self.tool_call_limit,
6138
6174
  use_json_mode=self.use_json_mode,
6139
6175
  telemetry=self.telemetry,
6140
6176
  debug_mode=self.debug_mode,
@@ -6706,17 +6742,18 @@ class Agent:
6706
6742
  ) -> Function:
6707
6743
  """Factory function to create a search_knowledge_base function with filters."""
6708
6744
 
6709
- def search_knowledge_base(query: str, filters: Optional[Dict[str, Any]] = None) -> str:
6745
+ def search_knowledge_base(query: str, filters: Optional[List[KnowledgeFilter]] = None) -> str:
6710
6746
  """Use this function to search the knowledge base for information about a query.
6711
6747
 
6712
6748
  Args:
6713
6749
  query: The query to search for.
6714
- filters: The filters to apply to the search. This is a dictionary of key-value pairs.
6750
+ filters (optional): The filters to apply to the search. This is a list of KnowledgeFilter objects.
6715
6751
 
6716
6752
  Returns:
6717
6753
  str: A string containing the response from the knowledge base.
6718
6754
  """
6719
- search_filters = get_agentic_or_user_search_filters(filters, knowledge_filters)
6755
+ filters_dict = {filt.key: filt.value for filt in filters} if filters else None
6756
+ search_filters = get_agentic_or_user_search_filters(filters_dict, knowledge_filters)
6720
6757
 
6721
6758
  # Get the relevant documents from the knowledge base, passing filters
6722
6759
  retrieval_timer = Timer()
@@ -6739,17 +6776,18 @@ class Agent:
6739
6776
  return "No documents found"
6740
6777
  return self._convert_documents_to_string(docs_from_knowledge)
6741
6778
 
6742
- async def asearch_knowledge_base(query: str, filters: Optional[Dict[str, Any]] = None) -> str:
6779
+ async def asearch_knowledge_base(query: str, filters: Optional[List[KnowledgeFilter]] = None) -> str:
6743
6780
  """Use this function to search the knowledge base for information about a query asynchronously.
6744
6781
 
6745
6782
  Args:
6746
6783
  query: The query to search for.
6747
- filters: The filters to apply to the search. This is a dictionary of key-value pairs.
6784
+ filters (optional): The filters to apply to the search. This is a list of KnowledgeFilter objects.
6748
6785
 
6749
6786
  Returns:
6750
6787
  str: A string containing the response from the knowledge base.
6751
6788
  """
6752
- search_filters = get_agentic_or_user_search_filters(filters, knowledge_filters)
6789
+ filters_dict = {filt.key: filt.value for filt in filters} if filters else None
6790
+ search_filters = get_agentic_or_user_search_filters(filters_dict, knowledge_filters)
6753
6791
 
6754
6792
  retrieval_timer = Timer()
6755
6793
  retrieval_timer.start()
@@ -6880,21 +6918,21 @@ class Agent:
6880
6918
  stream: Optional[bool] = None,
6881
6919
  stream_intermediate_steps: Optional[bool] = None,
6882
6920
  markdown: Optional[bool] = None,
6921
+ knowledge_filters: Optional[Dict[str, Any]] = None,
6922
+ add_history_to_context: Optional[bool] = None,
6923
+ add_dependencies_to_context: Optional[bool] = None,
6924
+ dependencies: Optional[Dict[str, Any]] = None,
6925
+ add_session_state_to_context: Optional[bool] = None,
6926
+ metadata: Optional[Dict[str, Any]] = None,
6927
+ debug_mode: Optional[bool] = None,
6883
6928
  show_message: bool = True,
6884
6929
  show_reasoning: bool = True,
6885
6930
  show_full_reasoning: bool = False,
6886
6931
  console: Optional[Any] = None,
6887
6932
  # Add tags to include in markdown content
6888
6933
  tags_to_include_in_markdown: Optional[Set[str]] = None,
6889
- knowledge_filters: Optional[Dict[str, Any]] = None,
6890
- add_history_to_context: Optional[bool] = None,
6891
- dependencies: Optional[Dict[str, Any]] = None,
6892
- metadata: Optional[Dict[str, Any]] = None,
6893
- debug_mode: Optional[bool] = None,
6894
6934
  **kwargs: Any,
6895
6935
  ) -> None:
6896
- add_history = add_history_to_context if add_history_to_context is not None else self.add_history_to_context
6897
-
6898
6936
  if not tags_to_include_in_markdown:
6899
6937
  tags_to_include_in_markdown = {"think", "thinking"}
6900
6938
 
@@ -6931,8 +6969,10 @@ class Agent:
6931
6969
  show_full_reasoning=show_full_reasoning,
6932
6970
  tags_to_include_in_markdown=tags_to_include_in_markdown,
6933
6971
  console=console,
6934
- add_history_to_context=add_history,
6972
+ add_history_to_context=add_history_to_context,
6935
6973
  dependencies=dependencies,
6974
+ add_dependencies_to_context=add_dependencies_to_context,
6975
+ add_session_state_to_context=add_session_state_to_context,
6936
6976
  metadata=metadata,
6937
6977
  **kwargs,
6938
6978
  )
@@ -6957,8 +6997,10 @@ class Agent:
6957
6997
  show_full_reasoning=show_full_reasoning,
6958
6998
  tags_to_include_in_markdown=tags_to_include_in_markdown,
6959
6999
  console=console,
6960
- add_history_to_context=add_history,
7000
+ add_history_to_context=add_history_to_context,
6961
7001
  dependencies=dependencies,
7002
+ add_dependencies_to_context=add_dependencies_to_context,
7003
+ add_session_state_to_context=add_session_state_to_context,
6962
7004
  metadata=metadata,
6963
7005
  **kwargs,
6964
7006
  )
@@ -6977,21 +7019,21 @@ class Agent:
6977
7019
  stream: Optional[bool] = None,
6978
7020
  stream_intermediate_steps: Optional[bool] = None,
6979
7021
  markdown: Optional[bool] = None,
7022
+ knowledge_filters: Optional[Dict[str, Any]] = None,
7023
+ add_history_to_context: Optional[bool] = None,
7024
+ dependencies: Optional[Dict[str, Any]] = None,
7025
+ add_dependencies_to_context: Optional[bool] = None,
7026
+ add_session_state_to_context: Optional[bool] = None,
7027
+ metadata: Optional[Dict[str, Any]] = None,
7028
+ debug_mode: Optional[bool] = None,
6980
7029
  show_message: bool = True,
6981
7030
  show_reasoning: bool = True,
6982
7031
  show_full_reasoning: bool = False,
6983
7032
  console: Optional[Any] = None,
6984
7033
  # Add tags to include in markdown content
6985
7034
  tags_to_include_in_markdown: Optional[Set[str]] = None,
6986
- knowledge_filters: Optional[Dict[str, Any]] = None,
6987
- add_history_to_context: Optional[bool] = None,
6988
- dependencies: Optional[Dict[str, Any]] = None,
6989
- metadata: Optional[Dict[str, Any]] = None,
6990
- debug_mode: Optional[bool] = None,
6991
7035
  **kwargs: Any,
6992
7036
  ) -> None:
6993
- add_history = add_history_to_context if add_history_to_context is not None else self.add_history_to_context
6994
-
6995
7037
  if not tags_to_include_in_markdown:
6996
7038
  tags_to_include_in_markdown = {"think", "thinking"}
6997
7039
 
@@ -7027,8 +7069,10 @@ class Agent:
7027
7069
  show_full_reasoning=show_full_reasoning,
7028
7070
  tags_to_include_in_markdown=tags_to_include_in_markdown,
7029
7071
  console=console,
7030
- add_history_to_context=add_history,
7072
+ add_history_to_context=add_history_to_context,
7031
7073
  dependencies=dependencies,
7074
+ add_dependencies_to_context=add_dependencies_to_context,
7075
+ add_session_state_to_context=add_session_state_to_context,
7032
7076
  metadata=metadata,
7033
7077
  **kwargs,
7034
7078
  )
@@ -7052,8 +7096,10 @@ class Agent:
7052
7096
  show_full_reasoning=show_full_reasoning,
7053
7097
  tags_to_include_in_markdown=tags_to_include_in_markdown,
7054
7098
  console=console,
7055
- add_history_to_context=add_history,
7099
+ add_history_to_context=add_history_to_context,
7056
7100
  dependencies=dependencies,
7101
+ add_dependencies_to_context=add_dependencies_to_context,
7102
+ add_session_state_to_context=add_session_state_to_context,
7057
7103
  metadata=metadata,
7058
7104
  **kwargs,
7059
7105
  )
@@ -47,10 +47,10 @@ def convert_v1_metrics_to_v2(metrics_dict: Dict[str, Any]) -> Dict[str, Any]:
47
47
 
48
48
 
49
49
  def convert_any_metrics_in_data(data: Any) -> Any:
50
- """Recursively find and convert any metrics dictionaries in the data structure."""
50
+ """Recursively find and convert any metrics dictionaries and handle v1 to v2 field conversion."""
51
51
  if isinstance(data, dict):
52
- # First filter out deprecated v1 fields
53
- data = filter_deprecated_v1_fields(data)
52
+ # First apply v1 to v2 field conversion (handles extra_data extraction, thinking/reasoning_content consolidation, etc.)
53
+ data = convert_v1_fields_to_v2(data)
54
54
 
55
55
  # Check if this looks like a metrics dictionary
56
56
  if _is_metrics_dict(data):
@@ -114,11 +114,11 @@ def _is_metrics_dict(data: Dict[str, Any]) -> bool:
114
114
 
115
115
 
116
116
  def convert_session_data_comprehensively(session_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
117
- """Comprehensively convert any metrics found anywhere in session_data from v1 to v2 format."""
117
+ """Comprehensively convert session data from v1 to v2 format, including metrics conversion and field mapping."""
118
118
  if not session_data:
119
119
  return session_data
120
120
 
121
- # Use the recursive converter to find and fix all metrics
121
+ # Use the recursive converter to handle all v1 to v2 conversions (metrics, field mapping, extra_data extraction, etc.)
122
122
  return convert_any_metrics_in_data(session_data)
123
123
 
124
124
 
@@ -145,21 +145,150 @@ def safe_get_runs_from_memory(memory_data: Any) -> Any:
145
145
  return None
146
146
 
147
147
 
148
- def filter_deprecated_v1_fields(data: Dict[str, Any]) -> Dict[str, Any]:
149
- """Remove v1-only fields that don't exist in v2 models."""
148
+ def convert_v1_media_to_v2(media_data: Dict[str, Any]) -> Dict[str, Any]:
149
+ """Convert v1 media objects to v2 format."""
150
+ if not isinstance(media_data, dict):
151
+ return media_data
152
+
153
+ # Create a copy to avoid modifying the original
154
+ v2_media = media_data.copy()
155
+
156
+ # Add id if missing (required in v2)
157
+ if "id" not in v2_media or v2_media["id"] is None:
158
+ from uuid import uuid4
159
+
160
+ v2_media["id"] = str(uuid4())
161
+
162
+ # Handle VideoArtifact → Video conversion
163
+ if "eta" in v2_media or "length" in v2_media:
164
+ # Convert length to duration if it's numeric
165
+ length = v2_media.pop("length", None)
166
+ if length and isinstance(length, (int, float)):
167
+ v2_media["duration"] = length
168
+ elif length and isinstance(length, str):
169
+ try:
170
+ v2_media["duration"] = float(length)
171
+ except ValueError:
172
+ pass # Keep as is if not convertible
173
+
174
+ # Handle AudioArtifact → Audio conversion
175
+ if "base64_audio" in v2_media:
176
+ # Map base64_audio to content
177
+ base64_audio = v2_media.pop("base64_audio", None)
178
+ if base64_audio:
179
+ v2_media["content"] = base64_audio
180
+
181
+ # Handle AudioResponse content conversion (base64 string to bytes if needed)
182
+ if "transcript" in v2_media and "content" in v2_media:
183
+ content = v2_media.get("content")
184
+ if content and isinstance(content, str):
185
+ # Try to decode base64 content to bytes for v2
186
+ try:
187
+ import base64
188
+
189
+ v2_media["content"] = base64.b64decode(content)
190
+ except Exception:
191
+ # If not valid base64, keep as string
192
+ pass
193
+
194
+ # Ensure format and mime_type are set appropriately
195
+ if "format" in v2_media and "mime_type" not in v2_media:
196
+ format_val = v2_media["format"]
197
+ if format_val:
198
+ # Set mime_type based on format for common types
199
+ mime_type_map = {
200
+ "mp4": "video/mp4",
201
+ "mov": "video/quicktime",
202
+ "avi": "video/x-msvideo",
203
+ "webm": "video/webm",
204
+ "mp3": "audio/mpeg",
205
+ "wav": "audio/wav",
206
+ "ogg": "audio/ogg",
207
+ "png": "image/png",
208
+ "jpg": "image/jpeg",
209
+ "jpeg": "image/jpeg",
210
+ "gif": "image/gif",
211
+ "webp": "image/webp",
212
+ }
213
+ if format_val.lower() in mime_type_map:
214
+ v2_media["mime_type"] = mime_type_map[format_val.lower()]
215
+
216
+ return v2_media
217
+
218
+
219
+ def convert_v1_fields_to_v2(data: Dict[str, Any]) -> Dict[str, Any]:
220
+ """Convert v1 fields to v2 format with proper field mapping and extraction."""
150
221
  if not isinstance(data, dict):
151
222
  return data
152
223
 
153
- # Fields that existed in v1 but were removed in v2
224
+ # Create a copy to avoid modifying the original
225
+ v2_data = data.copy()
226
+
227
+ # Fields that should be completely ignored/removed in v2
154
228
  deprecated_fields = {
155
229
  "team_session_id", # RunOutput v1 field, removed in v2
156
230
  "formatted_tool_calls", # RunOutput v1 field, removed in v2
231
+ "event", # Remove event field
232
+ "events", # Remove events field
157
233
  # Add other deprecated fields here as needed
158
234
  }
159
235
 
160
- # Create a copy and remove deprecated fields
161
- filtered_data = {k: v for k, v in data.items() if k not in deprecated_fields}
162
- return filtered_data
236
+ # Extract and map fields from extra_data before removing it
237
+ extra_data = v2_data.get("extra_data")
238
+ if extra_data and isinstance(extra_data, dict):
239
+ # Map extra_data fields to their v2 locations
240
+ if "add_messages" in extra_data:
241
+ v2_data["additional_input"] = extra_data["add_messages"]
242
+ if "references" in extra_data:
243
+ v2_data["references"] = extra_data["references"]
244
+ if "reasoning_steps" in extra_data:
245
+ v2_data["reasoning_steps"] = extra_data["reasoning_steps"]
246
+ if "reasoning_content" in extra_data:
247
+ # reasoning_content from extra_data also goes to reasoning_content
248
+ v2_data["reasoning_content"] = extra_data["reasoning_content"]
249
+ if "reasoning_messages" in extra_data:
250
+ v2_data["reasoning_messages"] = extra_data["reasoning_messages"]
251
+
252
+ # Handle thinking and reasoning_content consolidation
253
+ # Both thinking and reasoning_content from v1 should become reasoning_content in v2
254
+ thinking = v2_data.get("thinking")
255
+ reasoning_content = v2_data.get("reasoning_content")
256
+
257
+ # Consolidate thinking and reasoning_content into reasoning_content
258
+ if thinking and reasoning_content:
259
+ # Both exist, combine them (thinking first, then reasoning_content)
260
+ v2_data["reasoning_content"] = f"{thinking}\n{reasoning_content}"
261
+ elif thinking and not reasoning_content:
262
+ # Only thinking exists, move it to reasoning_content
263
+ v2_data["reasoning_content"] = thinking
264
+ # If only reasoning_content exists, keep it as is
265
+
266
+ # Remove thinking field since it's now consolidated into reasoning_content
267
+ if "thinking" in v2_data:
268
+ del v2_data["thinking"]
269
+
270
+ # Handle media object conversions
271
+ media_fields = ["images", "videos", "audio", "response_audio"]
272
+ for field in media_fields:
273
+ if field in v2_data and v2_data[field]:
274
+ if isinstance(v2_data[field], list):
275
+ # Handle list of media objects
276
+ v2_data[field] = [
277
+ convert_v1_media_to_v2(item) if isinstance(item, dict) else item for item in v2_data[field]
278
+ ]
279
+ elif isinstance(v2_data[field], dict):
280
+ # Handle single media object
281
+ v2_data[field] = convert_v1_media_to_v2(v2_data[field])
282
+
283
+ # Remove extra_data after extraction
284
+ if "extra_data" in v2_data:
285
+ del v2_data["extra_data"]
286
+
287
+ # Remove other deprecated fields
288
+ for field in deprecated_fields:
289
+ v2_data.pop(field, None)
290
+
291
+ return v2_data
163
292
 
164
293
 
165
294
  def migrate(
@@ -1,4 +1,5 @@
1
- from typing import List, Optional
1
+ import inspect
2
+ from typing import Any, Dict, List, Optional
2
3
 
3
4
  from agno.knowledge.chunking.strategy import ChunkingStrategy
4
5
  from agno.knowledge.document.base import Document
@@ -26,11 +27,37 @@ class SemanticChunking(ChunkingStrategy):
26
27
  "Please install it using `pip install chonkie` to use SemanticChunking."
27
28
  )
28
29
 
29
- self.chunker = SemanticChunker(
30
- embedding_model=self.embedder.id, # type: ignore
31
- chunk_size=self.chunk_size,
32
- threshold=self.similarity_threshold,
33
- )
30
+ # Build arguments dynamically based on chonkie's supported signature
31
+ params: Dict[str, Any] = {
32
+ "chunk_size": self.chunk_size,
33
+ "threshold": self.similarity_threshold,
34
+ }
35
+
36
+ try:
37
+ sig = inspect.signature(SemanticChunker)
38
+ param_names = set(sig.parameters.keys())
39
+
40
+ # Prefer passing a callable to avoid Chonkie initializing its own client
41
+ if "embedding_fn" in param_names:
42
+ params["embedding_fn"] = self.embedder.get_embedding # type: ignore[attr-defined]
43
+ # If chonkie allows specifying dimensions, provide them
44
+ if "embedding_dimensions" in param_names and getattr(self.embedder, "dimensions", None):
45
+ params["embedding_dimensions"] = self.embedder.dimensions # type: ignore[attr-defined]
46
+ elif "embedder" in param_names:
47
+ # Some versions may accept an embedder object directly
48
+ params["embedder"] = self.embedder
49
+ else:
50
+ # Fallback to model id
51
+ params["embedding_model"] = getattr(self.embedder, "id", None) or "text-embedding-3-small"
52
+
53
+ self.chunker = SemanticChunker(**params)
54
+ except Exception:
55
+ # As a final fallback, use the original behavior
56
+ self.chunker = SemanticChunker(
57
+ embedding_model=getattr(self.embedder, "id", None) or "text-embedding-3-small",
58
+ chunk_size=self.chunk_size,
59
+ threshold=self.similarity_threshold,
60
+ )
34
61
 
35
62
  def chunk(self, document: Document) -> List[Document]:
36
63
  """Split document into semantic chunks using chonkie"""
@@ -27,9 +27,9 @@ class SentenceTransformerEmbedder(Embedder):
27
27
 
28
28
  def get_embedding(self, text: Union[str, List[str]]) -> List[float]:
29
29
  if not self.sentence_transformer_client:
30
- model = SentenceTransformer(model_name_or_path=self.id)
31
- else:
32
- model = self.sentence_transformer_client
30
+ self.sentence_transformer_client = SentenceTransformer(model_name_or_path=self.id)
31
+
32
+ model = self.sentence_transformer_client
33
33
  embedding = model.encode(text, prompt=self.prompt, normalize_embeddings=self.normalize_embeddings)
34
34
  try:
35
35
  if isinstance(embedding, np.ndarray):