agno 2.2.4__py3-none-any.whl → 2.2.6__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 +82 -19
  2. agno/culture/manager.py +3 -4
  3. agno/knowledge/chunking/agentic.py +6 -2
  4. agno/memory/manager.py +9 -4
  5. agno/models/anthropic/claude.py +1 -2
  6. agno/models/azure/ai_foundry.py +31 -14
  7. agno/models/azure/openai_chat.py +12 -4
  8. agno/models/base.py +44 -11
  9. agno/models/cerebras/cerebras.py +11 -6
  10. agno/models/groq/groq.py +7 -4
  11. agno/models/meta/llama.py +12 -6
  12. agno/models/meta/llama_openai.py +5 -1
  13. agno/models/openai/chat.py +20 -12
  14. agno/models/openai/responses.py +10 -5
  15. agno/models/utils.py +254 -8
  16. agno/models/vertexai/claude.py +9 -13
  17. agno/os/app.py +48 -21
  18. agno/os/routers/evals/evals.py +8 -8
  19. agno/os/routers/evals/utils.py +1 -0
  20. agno/os/schema.py +48 -33
  21. agno/os/utils.py +27 -0
  22. agno/run/agent.py +5 -0
  23. agno/run/team.py +2 -0
  24. agno/run/workflow.py +39 -0
  25. agno/session/summary.py +8 -2
  26. agno/session/workflow.py +4 -3
  27. agno/team/team.py +50 -14
  28. agno/tools/file.py +153 -25
  29. agno/tools/function.py +5 -1
  30. agno/tools/notion.py +201 -0
  31. agno/utils/events.py +2 -0
  32. agno/utils/print_response/workflow.py +115 -16
  33. agno/vectordb/milvus/milvus.py +5 -0
  34. agno/workflow/__init__.py +2 -0
  35. agno/workflow/agent.py +298 -0
  36. agno/workflow/workflow.py +929 -64
  37. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/METADATA +4 -1
  38. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/RECORD +41 -39
  39. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/WHEEL +0 -0
  40. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/licenses/LICENSE +0 -0
  41. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/top_level.txt +0 -0
agno/agent/agent.py CHANGED
@@ -47,6 +47,7 @@ from agno.models.base import Model
47
47
  from agno.models.message import Message, MessageReferences
48
48
  from agno.models.metrics import Metrics
49
49
  from agno.models.response import ModelResponse, ModelResponseEvent, ToolExecution
50
+ from agno.models.utils import get_model
50
51
  from agno.reasoning.step import NextAction, ReasoningStep, ReasoningSteps
51
52
  from agno.run.agent import (
52
53
  RunEvent,
@@ -346,7 +347,7 @@ class Agent:
346
347
 
347
348
  # --- Agent Response Model Settings ---
348
349
  # Provide an input schema to validate the input
349
- input_schema: Optional[Union[Type[BaseModel], type]] = None
350
+ input_schema: Optional[Type[BaseModel]] = None
350
351
  # Provide a response model to get the response as a Pydantic model
351
352
  output_schema: Optional[Type[BaseModel]] = None
352
353
  # Provide a secondary model to parse the response from the primary model
@@ -416,7 +417,7 @@ class Agent:
416
417
  def __init__(
417
418
  self,
418
419
  *,
419
- model: Optional[Model] = None,
420
+ model: Optional[Union[Model, str]] = None,
420
421
  name: Optional[str] = None,
421
422
  id: Optional[str] = None,
422
423
  introduction: Optional[str] = None,
@@ -460,7 +461,7 @@ class Agent:
460
461
  pre_hooks: Optional[Union[List[Callable[..., Any]], List[BaseGuardrail]]] = None,
461
462
  post_hooks: Optional[Union[List[Callable[..., Any]], List[BaseGuardrail]]] = None,
462
463
  reasoning: bool = False,
463
- reasoning_model: Optional[Model] = None,
464
+ reasoning_model: Optional[Union[Model, str]] = None,
464
465
  reasoning_agent: Optional[Agent] = None,
465
466
  reasoning_min_steps: int = 1,
466
467
  reasoning_max_steps: int = 10,
@@ -488,12 +489,12 @@ class Agent:
488
489
  retries: int = 0,
489
490
  delay_between_retries: int = 1,
490
491
  exponential_backoff: bool = False,
491
- parser_model: Optional[Model] = None,
492
+ parser_model: Optional[Union[Model, str]] = None,
492
493
  parser_model_prompt: Optional[str] = None,
493
- input_schema: Optional[Union[Type[BaseModel], type]] = None,
494
+ input_schema: Optional[Type[BaseModel]] = None,
494
495
  output_schema: Optional[Type[BaseModel]] = None,
495
496
  parse_response: bool = True,
496
- output_model: Optional[Model] = None,
497
+ output_model: Optional[Union[Model, str]] = None,
497
498
  output_model_prompt: Optional[str] = None,
498
499
  structured_outputs: Optional[bool] = None,
499
500
  use_json_mode: bool = False,
@@ -512,7 +513,7 @@ class Agent:
512
513
  debug_level: Literal[1, 2] = 1,
513
514
  telemetry: bool = True,
514
515
  ):
515
- self.model = model
516
+ self.model = model # type: ignore[assignment]
516
517
  self.name = name
517
518
  self.id = id
518
519
  self.introduction = introduction
@@ -578,7 +579,7 @@ class Agent:
578
579
  self.post_hooks = post_hooks
579
580
 
580
581
  self.reasoning = reasoning
581
- self.reasoning_model = reasoning_model
582
+ self.reasoning_model = reasoning_model # type: ignore[assignment]
582
583
  self.reasoning_agent = reasoning_agent
583
584
  self.reasoning_min_steps = reasoning_min_steps
584
585
  self.reasoning_max_steps = reasoning_max_steps
@@ -609,12 +610,12 @@ class Agent:
609
610
  self.retries = retries
610
611
  self.delay_between_retries = delay_between_retries
611
612
  self.exponential_backoff = exponential_backoff
612
- self.parser_model = parser_model
613
+ self.parser_model = parser_model # type: ignore[assignment]
613
614
  self.parser_model_prompt = parser_model_prompt
614
615
  self.input_schema = input_schema
615
616
  self.output_schema = output_schema
616
617
  self.parse_response = parse_response
617
- self.output_model = output_model
618
+ self.output_model = output_model # type: ignore[assignment]
618
619
  self.output_model_prompt = output_model_prompt
619
620
 
620
621
  self.structured_outputs = structured_outputs
@@ -658,6 +659,8 @@ class Agent:
658
659
  # Lazy-initialized shared thread pool executor for background tasks (memory, cultural knowledge, etc.)
659
660
  self._background_executor: Optional[Any] = None
660
661
 
662
+ self._get_models()
663
+
661
664
  @property
662
665
  def background_executor(self) -> Any:
663
666
  """Lazy initialization of shared thread pool executor for background tasks.
@@ -814,6 +817,16 @@ class Agent:
814
817
  """Return True if the db the agent is equipped with is an Async implementation"""
815
818
  return self.db is not None and isinstance(self.db, AsyncBaseDb)
816
819
 
820
+ def _get_models(self) -> None:
821
+ if self.model is not None:
822
+ self.model = get_model(self.model)
823
+ if self.reasoning_model is not None:
824
+ self.reasoning_model = get_model(self.reasoning_model)
825
+ if self.parser_model is not None:
826
+ self.parser_model = get_model(self.parser_model)
827
+ if self.output_model is not None:
828
+ self.output_model = get_model(self.output_model)
829
+
817
830
  def initialize_agent(self, debug_mode: Optional[bool] = None) -> None:
818
831
  self._set_default_model()
819
832
  self._set_debug(debug_mode=debug_mode)
@@ -1268,6 +1281,7 @@ class Agent:
1268
1281
  tools=_tools,
1269
1282
  response_format=response_format,
1270
1283
  stream_events=stream_events,
1284
+ session_state=session_state,
1271
1285
  ):
1272
1286
  raise_if_cancelled(run_response.run_id) # type: ignore
1273
1287
  yield event
@@ -1284,6 +1298,7 @@ class Agent:
1284
1298
  tools=_tools,
1285
1299
  response_format=response_format,
1286
1300
  stream_events=stream_events,
1301
+ session_state=session_state,
1287
1302
  ):
1288
1303
  raise_if_cancelled(run_response.run_id) # type: ignore
1289
1304
  if isinstance(event, RunContentEvent):
@@ -1387,6 +1402,11 @@ class Agent:
1387
1402
  store_events=self.store_events,
1388
1403
  )
1389
1404
 
1405
+ # Update run_response.session_state before creating RunCompletedEvent
1406
+ # This ensures the event has the final state after all tool modifications
1407
+ if session.session_data is not None and "session_state" in session.session_data:
1408
+ run_response.session_state = session.session_data["session_state"]
1409
+
1390
1410
  # Create the run completed event
1391
1411
  completed_event = handle_event( # type: ignore
1392
1412
  create_run_completed_event(from_run_response=run_response),
@@ -1626,6 +1646,7 @@ class Agent:
1626
1646
  user_id=user_id,
1627
1647
  agent_name=self.name,
1628
1648
  metadata=metadata,
1649
+ session_state=session_state,
1629
1650
  input=run_input,
1630
1651
  )
1631
1652
 
@@ -2148,6 +2169,7 @@ class Agent:
2148
2169
  tools=_tools,
2149
2170
  response_format=response_format,
2150
2171
  stream_events=stream_events,
2172
+ session_state=session_state,
2151
2173
  ):
2152
2174
  raise_if_cancelled(run_response.run_id) # type: ignore
2153
2175
  yield event
@@ -2164,6 +2186,7 @@ class Agent:
2164
2186
  tools=_tools,
2165
2187
  response_format=response_format,
2166
2188
  stream_events=stream_events,
2189
+ session_state=session_state,
2167
2190
  ):
2168
2191
  raise_if_cancelled(run_response.run_id) # type: ignore
2169
2192
  if isinstance(event, RunContentEvent):
@@ -2270,6 +2293,11 @@ class Agent:
2270
2293
  store_events=self.store_events,
2271
2294
  )
2272
2295
 
2296
+ # Update run_response.session_state before creating RunCompletedEvent
2297
+ # This ensures the event has the final state after all tool modifications
2298
+ if agent_session.session_data is not None and "session_state" in agent_session.session_data:
2299
+ run_response.session_state = agent_session.session_data["session_state"]
2300
+
2273
2301
  # Create the run completed event
2274
2302
  completed_event = handle_event(
2275
2303
  create_run_completed_event(from_run_response=run_response),
@@ -2509,6 +2537,7 @@ class Agent:
2509
2537
  user_id=user_id,
2510
2538
  agent_name=self.name,
2511
2539
  metadata=metadata,
2540
+ session_state=session_state,
2512
2541
  input=run_input,
2513
2542
  )
2514
2543
 
@@ -3042,6 +3071,7 @@ class Agent:
3042
3071
  tools=tools,
3043
3072
  response_format=response_format,
3044
3073
  stream_events=stream_events,
3074
+ session_state=session_state,
3045
3075
  ):
3046
3076
  yield event
3047
3077
 
@@ -3110,6 +3140,11 @@ class Agent:
3110
3140
  store_events=self.store_events,
3111
3141
  )
3112
3142
 
3143
+ # Update run_response.session_state before creating RunCompletedEvent
3144
+ # This ensures the event has the final state after all tool modifications
3145
+ if session.session_data is not None and "session_state" in session.session_data:
3146
+ run_response.session_state = session.session_data["session_state"]
3147
+
3113
3148
  # Create the run completed event
3114
3149
  completed_event = handle_event(
3115
3150
  create_run_completed_event(run_response),
@@ -3780,6 +3815,11 @@ class Agent:
3780
3815
  store_events=self.store_events,
3781
3816
  )
3782
3817
 
3818
+ # Update run_response.session_state before creating RunCompletedEvent
3819
+ # This ensures the event has the final state after all tool modifications
3820
+ if agent_session.session_data is not None and "session_state" in agent_session.session_data:
3821
+ run_response.session_state = agent_session.session_data["session_state"]
3822
+
3783
3823
  # Create the run completed event
3784
3824
  completed_event = handle_event(
3785
3825
  create_run_completed_event(run_response),
@@ -4651,6 +4691,7 @@ class Agent:
4651
4691
  tools: Optional[List[Union[Function, dict]]] = None,
4652
4692
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
4653
4693
  stream_events: bool = False,
4694
+ session_state: Optional[Dict[str, Any]] = None,
4654
4695
  ) -> Iterator[RunOutputEvent]:
4655
4696
  self.model = cast(Model, self.model)
4656
4697
 
@@ -4683,6 +4724,7 @@ class Agent:
4683
4724
  reasoning_state=reasoning_state,
4684
4725
  parse_structured_output=self.should_parse_structured_output,
4685
4726
  stream_events=stream_events,
4727
+ session_state=session_state,
4686
4728
  )
4687
4729
 
4688
4730
  # Determine reasoning completed
@@ -4729,6 +4771,7 @@ class Agent:
4729
4771
  tools: Optional[List[Union[Function, dict]]] = None,
4730
4772
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
4731
4773
  stream_events: bool = False,
4774
+ session_state: Optional[Dict[str, Any]] = None,
4732
4775
  ) -> AsyncIterator[RunOutputEvent]:
4733
4776
  self.model = cast(Model, self.model)
4734
4777
 
@@ -4763,6 +4806,7 @@ class Agent:
4763
4806
  reasoning_state=reasoning_state,
4764
4807
  parse_structured_output=self.should_parse_structured_output,
4765
4808
  stream_events=stream_events,
4809
+ session_state=session_state,
4766
4810
  ):
4767
4811
  yield event
4768
4812
 
@@ -4810,9 +4854,14 @@ class Agent:
4810
4854
  reasoning_state: Optional[Dict[str, Any]] = None,
4811
4855
  parse_structured_output: bool = False,
4812
4856
  stream_events: bool = False,
4857
+ session_state: Optional[Dict[str, Any]] = None,
4813
4858
  ) -> Iterator[RunOutputEvent]:
4814
- if isinstance(model_response_event, tuple(get_args(RunOutputEvent))) or isinstance(
4815
- model_response_event, tuple(get_args(TeamRunOutputEvent))
4859
+ from agno.run.workflow import WorkflowRunOutputEvent
4860
+
4861
+ if (
4862
+ isinstance(model_response_event, tuple(get_args(RunOutputEvent)))
4863
+ or isinstance(model_response_event, tuple(get_args(TeamRunOutputEvent)))
4864
+ or isinstance(model_response_event, tuple(get_args(WorkflowRunOutputEvent)))
4816
4865
  ):
4817
4866
  if model_response_event.event == RunEvent.custom_event: # type: ignore
4818
4867
  model_response_event.agent_id = self.id # type: ignore
@@ -5018,11 +5067,15 @@ class Agent:
5018
5067
 
5019
5068
  # If the model response is a tool_call_completed, update the existing tool call in the run_response
5020
5069
  elif model_response_event.event == ModelResponseEvent.tool_call_completed.value:
5021
- if model_response_event.updated_session_state is not None and session.session_data is not None:
5022
- merge_dictionaries(
5023
- session.session_data["session_state"],
5024
- model_response_event.updated_session_state,
5025
- )
5070
+ if model_response_event.updated_session_state is not None:
5071
+ # update the session_state for RunOutput
5072
+ if session_state is not None:
5073
+ merge_dictionaries(session_state, model_response_event.updated_session_state)
5074
+ # update the DB session
5075
+ if session.session_data is not None and session.session_data.get("session_state") is not None:
5076
+ merge_dictionaries(
5077
+ session.session_data["session_state"], model_response_event.updated_session_state
5078
+ )
5026
5079
 
5027
5080
  if model_response_event.images is not None:
5028
5081
  for image in model_response_event.images:
@@ -8454,7 +8507,7 @@ class Agent:
8454
8507
  store_events=self.store_events,
8455
8508
  )
8456
8509
  else:
8457
- log_warning(
8510
+ log_info(
8458
8511
  f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
8459
8512
  )
8460
8513
  use_default_reasoning = True
@@ -8747,7 +8800,7 @@ class Agent:
8747
8800
  store_events=self.store_events,
8748
8801
  )
8749
8802
  else:
8750
- log_warning(
8803
+ log_info(
8751
8804
  f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
8752
8805
  )
8753
8806
  use_default_reasoning = True
@@ -10010,6 +10063,11 @@ class Agent:
10010
10063
  if run_response.metrics:
10011
10064
  run_response.metrics.stop_timer()
10012
10065
 
10066
+ # Update run_response.session_state from session before saving
10067
+ # This ensures RunOutput reflects all tool modifications
10068
+ if session.session_data is not None and "session_state" in session.session_data:
10069
+ run_response.session_state = session.session_data["session_state"]
10070
+
10013
10071
  # Optional: Save output to file if save_response_to_file is set
10014
10072
  self.save_run_response_to_file(
10015
10073
  run_response=run_response,
@@ -10037,6 +10095,11 @@ class Agent:
10037
10095
  if run_response.metrics:
10038
10096
  run_response.metrics.stop_timer()
10039
10097
 
10098
+ # Update run_response.session_state from session before saving
10099
+ # This ensures RunOutput reflects all tool modifications
10100
+ if session.session_data is not None and "session_state" in session.session_data:
10101
+ run_response.session_state = session.session_data["session_state"]
10102
+
10040
10103
  # Optional: Save output to file if save_response_to_file is set
10041
10104
  self.save_run_response_to_file(
10042
10105
  run_response=run_response,
agno/culture/manager.py CHANGED
@@ -8,6 +8,7 @@ from agno.db.base import AsyncBaseDb, BaseDb
8
8
  from agno.db.schemas.culture import CulturalKnowledge
9
9
  from agno.models.base import Model
10
10
  from agno.models.message import Message
11
+ from agno.models.utils import get_model
11
12
  from agno.tools.function import Function
12
13
  from agno.utils.log import (
13
14
  log_debug,
@@ -55,7 +56,7 @@ class CultureManager:
55
56
 
56
57
  def __init__(
57
58
  self,
58
- model: Optional[Model] = None,
59
+ model: Optional[Union[Model, str]] = None,
59
60
  db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
60
61
  system_message: Optional[str] = None,
61
62
  culture_capture_instructions: Optional[str] = None,
@@ -66,9 +67,7 @@ class CultureManager:
66
67
  clear_knowledge: bool = True,
67
68
  debug_mode: bool = False,
68
69
  ):
69
- self.model = model
70
- if self.model is not None and isinstance(self.model, str):
71
- raise ValueError("Model must be a Model object, not a string")
70
+ self.model = get_model(model)
72
71
  self.db = db
73
72
  self.system_message = system_message
74
73
  self.culture_capture_instructions = culture_capture_instructions
@@ -1,22 +1,26 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
 
3
3
  from agno.knowledge.chunking.strategy import ChunkingStrategy
4
4
  from agno.knowledge.document.base import Document
5
5
  from agno.models.base import Model
6
6
  from agno.models.defaults import DEFAULT_OPENAI_MODEL_ID
7
7
  from agno.models.message import Message
8
+ from agno.models.utils import get_model
8
9
 
9
10
 
10
11
  class AgenticChunking(ChunkingStrategy):
11
12
  """Chunking strategy that uses an LLM to determine natural breakpoints in the text"""
12
13
 
13
- def __init__(self, model: Optional[Model] = None, max_chunk_size: int = 5000):
14
+ def __init__(self, model: Optional[Union[Model, str]] = None, max_chunk_size: int = 5000):
15
+ # Convert model string to Model instance
16
+ model = get_model(model)
14
17
  if model is None:
15
18
  try:
16
19
  from agno.models.openai import OpenAIChat
17
20
  except Exception:
18
21
  raise ValueError("`openai` isn't installed. Please install it with `pip install openai`")
19
22
  model = OpenAIChat(DEFAULT_OPENAI_MODEL_ID)
23
+
20
24
  self.max_chunk_size = max_chunk_size
21
25
  self.model = model
22
26
 
agno/memory/manager.py CHANGED
@@ -11,6 +11,7 @@ from agno.db.base import AsyncBaseDb, BaseDb
11
11
  from agno.db.schemas import UserMemory
12
12
  from agno.models.base import Model
13
13
  from agno.models.message import Message
14
+ from agno.models.utils import get_model
14
15
  from agno.tools.function import Function
15
16
  from agno.utils.log import (
16
17
  log_debug,
@@ -66,7 +67,7 @@ class MemoryManager:
66
67
 
67
68
  def __init__(
68
69
  self,
69
- model: Optional[Model] = None,
70
+ model: Optional[Union[Model, str]] = None,
70
71
  system_message: Optional[str] = None,
71
72
  memory_capture_instructions: Optional[str] = None,
72
73
  additional_instructions: Optional[str] = None,
@@ -77,9 +78,7 @@ class MemoryManager:
77
78
  clear_memories: bool = False,
78
79
  debug_mode: bool = False,
79
80
  ):
80
- self.model = model
81
- if self.model is not None and isinstance(self.model, str):
82
- raise ValueError("Model must be a Model object, not a string")
81
+ self.model = model # type: ignore[assignment]
83
82
  self.system_message = system_message
84
83
  self.memory_capture_instructions = memory_capture_instructions
85
84
  self.additional_instructions = additional_instructions
@@ -90,6 +89,12 @@ class MemoryManager:
90
89
  self.clear_memories = clear_memories
91
90
  self.debug_mode = debug_mode
92
91
 
92
+ self._get_models()
93
+
94
+ def _get_models(self) -> None:
95
+ if self.model is not None:
96
+ self.model = get_model(self.model)
97
+
93
98
  def get_model(self) -> Model:
94
99
  if self.model is None:
95
100
  try:
@@ -98,7 +98,6 @@ class Claude(Model):
98
98
  timeout: Optional[float] = None
99
99
  client_params: Optional[Dict[str, Any]] = None
100
100
 
101
- # Anthropic clients
102
101
  client: Optional[AnthropicClient] = None
103
102
  async_client: Optional[AsyncAnthropicClient] = None
104
103
 
@@ -145,7 +144,7 @@ class Claude(Model):
145
144
  """
146
145
  Returns an instance of the async Anthropic client.
147
146
  """
148
- if self.async_client:
147
+ if self.async_client and not self.async_client.is_closed():
149
148
  return self.async_client
150
149
 
151
150
  _client_params = self._get_client_params()
@@ -160,7 +160,9 @@ class AzureAIFoundry(Model):
160
160
  Returns:
161
161
  ChatCompletionsClient: An instance of the Azure AI client.
162
162
  """
163
- if self.client:
163
+ # Check if client exists and is not closed
164
+ # Azure's client doesn't have is_closed(), so we check if _client exists
165
+ if self.client and hasattr(self.client, "_client"):
164
166
  return self.client
165
167
 
166
168
  client_params = self._get_client_params()
@@ -174,11 +176,28 @@ class AzureAIFoundry(Model):
174
176
  Returns:
175
177
  AsyncChatCompletionsClient: An instance of the asynchronous Azure AI client.
176
178
  """
179
+ # Check if client exists and is not closed
180
+ # Azure's async client doesn't have is_closed(), so we check if _client exists
181
+ if self.async_client and hasattr(self.async_client, "_client"):
182
+ return self.async_client
183
+
177
184
  client_params = self._get_client_params()
178
185
 
179
186
  self.async_client = AsyncChatCompletionsClient(**client_params)
180
187
  return self.async_client
181
188
 
189
+ def close(self) -> None:
190
+ """Close the synchronous client and clean up resources."""
191
+ if self.client:
192
+ self.client.close()
193
+ self.client = None
194
+
195
+ async def aclose(self) -> None:
196
+ """Close the asynchronous client and clean up resources."""
197
+ if self.async_client:
198
+ await self.async_client.close()
199
+ self.async_client = None
200
+
182
201
  def invoke(
183
202
  self,
184
203
  messages: List[Message],
@@ -236,11 +255,10 @@ class AzureAIFoundry(Model):
236
255
  run_response.metrics.set_time_to_first_token()
237
256
 
238
257
  assistant_message.metrics.start_timer()
239
- async with self.get_async_client() as client:
240
- provider_response = await client.complete(
241
- messages=[format_message(m) for m in messages],
242
- **self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
243
- )
258
+ provider_response = await self.get_async_client().complete(
259
+ messages=[format_message(m) for m in messages],
260
+ **self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
261
+ )
244
262
  assistant_message.metrics.stop_timer()
245
263
 
246
264
  model_response = self._parse_provider_response(provider_response, response_format=response_format) # type: ignore
@@ -316,14 +334,13 @@ class AzureAIFoundry(Model):
316
334
 
317
335
  assistant_message.metrics.start_timer()
318
336
 
319
- async with self.get_async_client() as client:
320
- async_stream = await client.complete(
321
- messages=[format_message(m) for m in messages],
322
- stream=True,
323
- **self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
324
- )
325
- async for chunk in async_stream: # type: ignore
326
- yield self._parse_provider_response_delta(chunk)
337
+ async_stream = await self.get_async_client().complete(
338
+ messages=[format_message(m) for m in messages],
339
+ stream=True,
340
+ **self.get_request_params(tools=tools, response_format=response_format, tool_choice=tool_choice),
341
+ )
342
+ async for chunk in async_stream: # type: ignore
343
+ yield self._parse_provider_response_delta(chunk)
327
344
 
328
345
  assistant_message.metrics.stop_timer()
329
346
 
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
5
5
  import httpx
6
6
 
7
7
  from agno.models.openai.like import OpenAILike
8
+ from agno.utils.log import log_debug
8
9
 
9
10
  try:
10
11
  from openai import AsyncAzureOpenAI as AsyncAzureOpenAIClient
@@ -70,7 +71,6 @@ class AzureOpenAI(OpenAILike):
70
71
  "base_url": self.base_url,
71
72
  "azure_ad_token": self.azure_ad_token,
72
73
  "azure_ad_token_provider": self.azure_ad_token_provider,
73
- "http_client": self.http_client,
74
74
  }
75
75
  if self.default_headers is not None:
76
76
  _client_params["default_headers"] = self.default_headers
@@ -95,7 +95,13 @@ class AzureOpenAI(OpenAILike):
95
95
 
96
96
  _client_params: Dict[str, Any] = self._get_client_params()
97
97
 
98
- # -*- Create client
98
+ if self.http_client:
99
+ if isinstance(self.http_client, httpx.Client):
100
+ _client_params["http_client"] = self.http_client
101
+ else:
102
+ log_debug("http_client is not an instance of httpx.Client.")
103
+
104
+ # Create client
99
105
  self.client = AzureOpenAIClient(**_client_params)
100
106
  return self.client
101
107
 
@@ -106,14 +112,16 @@ class AzureOpenAI(OpenAILike):
106
112
  Returns:
107
113
  AsyncAzureOpenAIClient: An instance of the asynchronous OpenAI client.
108
114
  """
109
- if self.async_client:
115
+ if self.async_client and not self.async_client.is_closed():
110
116
  return self.async_client
111
117
 
112
118
  _client_params: Dict[str, Any] = self._get_client_params()
113
119
 
114
- if self.http_client:
120
+ if self.http_client and isinstance(self.http_client, httpx.AsyncClient):
115
121
  _client_params["http_client"] = self.http_client
116
122
  else:
123
+ if self.http_client:
124
+ log_debug("The current http_client is not async. A default httpx.AsyncClient will be used instead.")
117
125
  # Create a new async HTTP client with custom limits
118
126
  _client_params["http_client"] = httpx.AsyncClient(
119
127
  limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100)
agno/models/base.py CHANGED
@@ -32,6 +32,7 @@ from agno.models.response import ModelResponse, ModelResponseEvent, ToolExecutio
32
32
  from agno.run.agent import CustomEvent, RunContentEvent, RunOutput, RunOutputEvent
33
33
  from agno.run.team import RunContentEvent as TeamRunContentEvent
34
34
  from agno.run.team import TeamRunOutputEvent
35
+ from agno.run.workflow import WorkflowRunOutputEvent
35
36
  from agno.tools.function import Function, FunctionCall, FunctionExecutionResult, UserInputField
36
37
  from agno.utils.log import log_debug, log_error, log_info, log_warning
37
38
  from agno.utils.timer import Timer
@@ -1440,11 +1441,13 @@ class Model(ABC):
1440
1441
 
1441
1442
  if isinstance(function_execution_result.result, (GeneratorType, collections.abc.Iterator)):
1442
1443
  for item in function_execution_result.result:
1443
- # This function yields agent/team run events
1444
- if isinstance(item, tuple(get_args(RunOutputEvent))) or isinstance(
1445
- item, tuple(get_args(TeamRunOutputEvent))
1444
+ # This function yields agent/team/workflow run events
1445
+ if (
1446
+ isinstance(item, tuple(get_args(RunOutputEvent)))
1447
+ or isinstance(item, tuple(get_args(TeamRunOutputEvent)))
1448
+ or isinstance(item, tuple(get_args(WorkflowRunOutputEvent)))
1446
1449
  ):
1447
- # We only capture content events
1450
+ # We only capture content events for output accumulation
1448
1451
  if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
1449
1452
  if item.content is not None and isinstance(item.content, BaseModel):
1450
1453
  function_call_output += item.content.model_dump_json()
@@ -1458,6 +1461,16 @@ class Model(ABC):
1458
1461
  if isinstance(item, CustomEvent):
1459
1462
  function_call_output += str(item)
1460
1463
 
1464
+ # For WorkflowCompletedEvent, extract content for final output
1465
+ from agno.run.workflow import WorkflowCompletedEvent
1466
+
1467
+ if isinstance(item, WorkflowCompletedEvent):
1468
+ if item.content is not None:
1469
+ if isinstance(item.content, BaseModel):
1470
+ function_call_output += item.content.model_dump_json()
1471
+ else:
1472
+ function_call_output += str(item.content)
1473
+
1461
1474
  # Yield the event itself to bubble it up
1462
1475
  yield item
1463
1476
 
@@ -1829,9 +1842,12 @@ class Model(ABC):
1829
1842
 
1830
1843
  try:
1831
1844
  async for item in function_call.result:
1832
- # This function yields agent/team run events
1833
- if isinstance(item, tuple(get_args(RunOutputEvent))) or isinstance(
1834
- item, tuple(get_args(TeamRunOutputEvent))
1845
+ # This function yields agent/team/workflow run events
1846
+ if isinstance(
1847
+ item,
1848
+ tuple(get_args(RunOutputEvent))
1849
+ + tuple(get_args(TeamRunOutputEvent))
1850
+ + tuple(get_args(WorkflowRunOutputEvent)),
1835
1851
  ):
1836
1852
  # We only capture content events
1837
1853
  if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
@@ -1848,6 +1864,16 @@ class Model(ABC):
1848
1864
  if isinstance(item, CustomEvent):
1849
1865
  function_call_output += str(item)
1850
1866
 
1867
+ # For WorkflowCompletedEvent, extract content for final output
1868
+ from agno.run.workflow import WorkflowCompletedEvent
1869
+
1870
+ if isinstance(item, WorkflowCompletedEvent):
1871
+ if item.content is not None:
1872
+ if isinstance(item.content, BaseModel):
1873
+ function_call_output += item.content.model_dump_json()
1874
+ else:
1875
+ function_call_output += str(item.content)
1876
+
1851
1877
  # Put the event into the queue to be yielded
1852
1878
  await event_queue.put(item)
1853
1879
 
@@ -1938,9 +1964,12 @@ class Model(ABC):
1938
1964
  # Events from async generators were already yielded in real-time above
1939
1965
  elif isinstance(function_call.result, (GeneratorType, collections.abc.Iterator)):
1940
1966
  for item in function_call.result:
1941
- # This function yields agent/team run events
1942
- if isinstance(item, tuple(get_args(RunOutputEvent))) or isinstance(
1943
- item, tuple(get_args(TeamRunOutputEvent))
1967
+ # This function yields agent/team/workflow run events
1968
+ if isinstance(
1969
+ item,
1970
+ tuple(get_args(RunOutputEvent))
1971
+ + tuple(get_args(TeamRunOutputEvent))
1972
+ + tuple(get_args(WorkflowRunOutputEvent)),
1944
1973
  ):
1945
1974
  # We only capture content events
1946
1975
  if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
@@ -2115,10 +2144,14 @@ class Model(ABC):
2115
2144
  new_model = cls.__new__(cls)
2116
2145
  memo[id(self)] = new_model
2117
2146
 
2118
- # Deep copy all attributes
2147
+ # Deep copy all attributes except client objects
2119
2148
  for k, v in self.__dict__.items():
2120
2149
  if k in {"response_format", "_tools", "_functions"}:
2121
2150
  continue
2151
+ # Skip client objects
2152
+ if k in {"client", "async_client", "http_client", "mistral_client", "model_client"}:
2153
+ setattr(new_model, k, None)
2154
+ continue
2122
2155
  try:
2123
2156
  setattr(new_model, k, deepcopy(v, memo))
2124
2157
  except Exception: