agno 2.2.13__py3-none-any.whl → 2.3.1__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 (92) hide show
  1. agno/agent/agent.py +197 -110
  2. agno/api/api.py +2 -0
  3. agno/db/base.py +26 -0
  4. agno/db/dynamo/dynamo.py +8 -0
  5. agno/db/dynamo/schemas.py +1 -0
  6. agno/db/firestore/firestore.py +8 -0
  7. agno/db/firestore/schemas.py +1 -0
  8. agno/db/gcs_json/gcs_json_db.py +8 -0
  9. agno/db/in_memory/in_memory_db.py +8 -1
  10. agno/db/json/json_db.py +8 -0
  11. agno/db/migrations/manager.py +199 -0
  12. agno/db/migrations/versions/__init__.py +0 -0
  13. agno/db/migrations/versions/v2_3_0.py +938 -0
  14. agno/db/mongo/async_mongo.py +16 -6
  15. agno/db/mongo/mongo.py +11 -0
  16. agno/db/mongo/schemas.py +3 -0
  17. agno/db/mongo/utils.py +17 -0
  18. agno/db/mysql/mysql.py +76 -3
  19. agno/db/mysql/schemas.py +20 -10
  20. agno/db/postgres/async_postgres.py +99 -25
  21. agno/db/postgres/postgres.py +75 -6
  22. agno/db/postgres/schemas.py +30 -20
  23. agno/db/redis/redis.py +15 -2
  24. agno/db/redis/schemas.py +4 -0
  25. agno/db/schemas/memory.py +13 -0
  26. agno/db/singlestore/schemas.py +11 -0
  27. agno/db/singlestore/singlestore.py +79 -5
  28. agno/db/sqlite/async_sqlite.py +97 -19
  29. agno/db/sqlite/schemas.py +10 -0
  30. agno/db/sqlite/sqlite.py +79 -2
  31. agno/db/surrealdb/surrealdb.py +8 -0
  32. agno/knowledge/chunking/semantic.py +7 -2
  33. agno/knowledge/embedder/nebius.py +1 -1
  34. agno/knowledge/knowledge.py +57 -86
  35. agno/knowledge/reader/csv_reader.py +7 -9
  36. agno/knowledge/reader/docx_reader.py +5 -5
  37. agno/knowledge/reader/field_labeled_csv_reader.py +16 -18
  38. agno/knowledge/reader/json_reader.py +5 -4
  39. agno/knowledge/reader/markdown_reader.py +8 -8
  40. agno/knowledge/reader/pdf_reader.py +11 -11
  41. agno/knowledge/reader/pptx_reader.py +5 -5
  42. agno/knowledge/reader/s3_reader.py +3 -3
  43. agno/knowledge/reader/text_reader.py +8 -8
  44. agno/knowledge/reader/web_search_reader.py +1 -48
  45. agno/knowledge/reader/website_reader.py +10 -10
  46. agno/models/anthropic/claude.py +319 -28
  47. agno/models/aws/claude.py +32 -0
  48. agno/models/azure/openai_chat.py +19 -10
  49. agno/models/base.py +612 -545
  50. agno/models/cerebras/cerebras.py +8 -11
  51. agno/models/cohere/chat.py +27 -1
  52. agno/models/google/gemini.py +39 -7
  53. agno/models/groq/groq.py +25 -11
  54. agno/models/meta/llama.py +20 -9
  55. agno/models/meta/llama_openai.py +3 -19
  56. agno/models/nebius/nebius.py +4 -4
  57. agno/models/openai/chat.py +30 -14
  58. agno/models/openai/responses.py +10 -13
  59. agno/models/response.py +1 -0
  60. agno/models/vertexai/claude.py +26 -0
  61. agno/os/app.py +8 -19
  62. agno/os/router.py +54 -0
  63. agno/os/routers/knowledge/knowledge.py +2 -2
  64. agno/os/schema.py +2 -2
  65. agno/session/agent.py +57 -92
  66. agno/session/summary.py +1 -1
  67. agno/session/team.py +62 -112
  68. agno/session/workflow.py +353 -57
  69. agno/team/team.py +227 -125
  70. agno/tools/models/nebius.py +5 -5
  71. agno/tools/models_labs.py +20 -10
  72. agno/tools/nano_banana.py +151 -0
  73. agno/tools/yfinance.py +12 -11
  74. agno/utils/http.py +111 -0
  75. agno/utils/media.py +11 -0
  76. agno/utils/models/claude.py +8 -0
  77. agno/utils/print_response/agent.py +33 -12
  78. agno/utils/print_response/team.py +22 -12
  79. agno/vectordb/couchbase/couchbase.py +6 -2
  80. agno/workflow/condition.py +13 -0
  81. agno/workflow/loop.py +13 -0
  82. agno/workflow/parallel.py +13 -0
  83. agno/workflow/router.py +13 -0
  84. agno/workflow/step.py +120 -20
  85. agno/workflow/steps.py +13 -0
  86. agno/workflow/workflow.py +76 -63
  87. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/METADATA +6 -2
  88. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/RECORD +91 -88
  89. agno/tools/googlesearch.py +0 -98
  90. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/WHEEL +0 -0
  91. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/licenses/LICENSE +0 -0
  92. {agno-2.2.13.dist-info → agno-2.3.1.dist-info}/top_level.txt +0 -0
agno/models/base.py CHANGED
@@ -325,155 +325,168 @@ class Model(ABC):
325
325
  run_response: Run response to use
326
326
  send_media_to_model: Whether to send media to the model
327
327
  """
328
+ try:
329
+ # Check cache if enabled
330
+ if self.cache_response:
331
+ cache_key = self._get_model_cache_key(
332
+ messages, stream=False, response_format=response_format, tools=tools
333
+ )
334
+ cached_data = self._get_cached_model_response(cache_key)
328
335
 
329
- # Check cache if enabled
330
- if self.cache_response:
331
- cache_key = self._get_model_cache_key(messages, stream=False, response_format=response_format, tools=tools)
332
- cached_data = self._get_cached_model_response(cache_key)
333
-
334
- if cached_data:
335
- log_info("Cache hit for model response")
336
- return self._model_response_from_cache(cached_data)
337
-
338
- log_debug(f"{self.get_provider()} Response Start", center=True, symbol="-")
339
- log_debug(f"Model: {self.id}", center=True, symbol="-")
340
-
341
- _log_messages(messages)
342
- model_response = ModelResponse()
343
-
344
- function_call_count = 0
345
-
346
- _tool_dicts = self._format_tools(tools) if tools is not None else []
347
- _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
348
-
349
- while True:
350
- # Get response from model
351
- assistant_message = Message(role=self.assistant_message_role)
352
- self._process_model_response(
353
- messages=messages,
354
- assistant_message=assistant_message,
355
- model_response=model_response,
356
- response_format=response_format,
357
- tools=_tool_dicts,
358
- tool_choice=tool_choice or self._tool_choice,
359
- run_response=run_response,
360
- )
336
+ if cached_data:
337
+ log_info("Cache hit for model response")
338
+ return self._model_response_from_cache(cached_data)
361
339
 
362
- # Add assistant message to messages
363
- messages.append(assistant_message)
340
+ log_debug(f"{self.get_provider()} Response Start", center=True, symbol="-")
341
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
364
342
 
365
- # Log response and metrics
366
- assistant_message.log(metrics=True)
343
+ _log_messages(messages)
344
+ model_response = ModelResponse()
367
345
 
368
- # Handle tool calls if present
369
- if assistant_message.tool_calls:
370
- # Prepare function calls
371
- function_calls_to_run = self._prepare_function_calls(
372
- assistant_message=assistant_message,
346
+ function_call_count = 0
347
+
348
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
349
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
350
+
351
+ while True:
352
+ # Get response from model
353
+ assistant_message = Message(role=self.assistant_message_role)
354
+ self._process_model_response(
373
355
  messages=messages,
356
+ assistant_message=assistant_message,
374
357
  model_response=model_response,
375
- functions=_functions,
376
- )
377
- function_call_results: List[Message] = []
378
-
379
- # Execute function calls
380
- for function_call_response in self.run_function_calls(
381
- function_calls=function_calls_to_run,
382
- function_call_results=function_call_results,
383
- current_function_call_count=function_call_count,
384
- function_call_limit=tool_call_limit,
385
- ):
386
- if isinstance(function_call_response, ModelResponse):
387
- # The session state is updated by the function call
388
- if function_call_response.updated_session_state is not None:
389
- model_response.updated_session_state = function_call_response.updated_session_state
390
-
391
- # Media artifacts are generated by the function call
392
- if function_call_response.images is not None:
393
- if model_response.images is None:
394
- model_response.images = []
395
- model_response.images.extend(function_call_response.images)
396
-
397
- if function_call_response.audios is not None:
398
- if model_response.audios is None:
399
- model_response.audios = []
400
- model_response.audios.extend(function_call_response.audios)
401
-
402
- if function_call_response.videos is not None:
403
- if model_response.videos is None:
404
- model_response.videos = []
405
- model_response.videos.extend(function_call_response.videos)
406
-
407
- if function_call_response.files is not None:
408
- if model_response.files is None:
409
- model_response.files = []
410
- model_response.files.extend(function_call_response.files)
411
-
412
- if (
413
- function_call_response.event
414
- in [
415
- ModelResponseEvent.tool_call_completed.value,
416
- ModelResponseEvent.tool_call_paused.value,
417
- ]
418
- and function_call_response.tool_executions is not None
419
- ):
420
- if model_response.tool_executions is None:
421
- model_response.tool_executions = []
422
- model_response.tool_executions.extend(function_call_response.tool_executions)
423
-
424
- elif function_call_response.event not in [
425
- ModelResponseEvent.tool_call_started.value,
426
- ModelResponseEvent.tool_call_completed.value,
427
- ]:
428
- if function_call_response.content:
429
- model_response.content += function_call_response.content # type: ignore
430
-
431
- # Add a function call for each successful execution
432
- function_call_count += len(function_call_results)
433
-
434
- # Format and add results to messages
435
- self.format_function_call_results(
436
- messages=messages, function_call_results=function_call_results, **model_response.extra or {}
358
+ response_format=response_format,
359
+ tools=_tool_dicts,
360
+ tool_choice=tool_choice or self._tool_choice,
361
+ run_response=run_response,
437
362
  )
438
363
 
439
- if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
440
- # Handle function call media
441
- self._handle_function_call_media(
364
+ # Add assistant message to messages
365
+ messages.append(assistant_message)
366
+
367
+ # Log response and metrics
368
+ assistant_message.log(metrics=True)
369
+
370
+ # Handle tool calls if present
371
+ if assistant_message.tool_calls:
372
+ # Prepare function calls
373
+ function_calls_to_run = self._prepare_function_calls(
374
+ assistant_message=assistant_message,
442
375
  messages=messages,
376
+ model_response=model_response,
377
+ functions=_functions,
378
+ )
379
+ function_call_results: List[Message] = []
380
+
381
+ # Execute function calls
382
+ for function_call_response in self.run_function_calls(
383
+ function_calls=function_calls_to_run,
443
384
  function_call_results=function_call_results,
444
- send_media_to_model=send_media_to_model,
385
+ current_function_call_count=function_call_count,
386
+ function_call_limit=tool_call_limit,
387
+ ):
388
+ if isinstance(function_call_response, ModelResponse):
389
+ # The session state is updated by the function call
390
+ if function_call_response.updated_session_state is not None:
391
+ model_response.updated_session_state = function_call_response.updated_session_state
392
+
393
+ # Media artifacts are generated by the function call
394
+ if function_call_response.images is not None:
395
+ if model_response.images is None:
396
+ model_response.images = []
397
+ model_response.images.extend(function_call_response.images)
398
+
399
+ if function_call_response.audios is not None:
400
+ if model_response.audios is None:
401
+ model_response.audios = []
402
+ model_response.audios.extend(function_call_response.audios)
403
+
404
+ if function_call_response.videos is not None:
405
+ if model_response.videos is None:
406
+ model_response.videos = []
407
+ model_response.videos.extend(function_call_response.videos)
408
+
409
+ if function_call_response.files is not None:
410
+ if model_response.files is None:
411
+ model_response.files = []
412
+ model_response.files.extend(function_call_response.files)
413
+
414
+ if (
415
+ function_call_response.event
416
+ in [
417
+ ModelResponseEvent.tool_call_completed.value,
418
+ ModelResponseEvent.tool_call_paused.value,
419
+ ]
420
+ and function_call_response.tool_executions is not None
421
+ ):
422
+ if model_response.tool_executions is None:
423
+ model_response.tool_executions = []
424
+ model_response.tool_executions.extend(function_call_response.tool_executions)
425
+
426
+ elif function_call_response.event not in [
427
+ ModelResponseEvent.tool_call_started.value,
428
+ ModelResponseEvent.tool_call_completed.value,
429
+ ]:
430
+ if function_call_response.content:
431
+ model_response.content += function_call_response.content # type: ignore
432
+
433
+ # Add a function call for each successful execution
434
+ function_call_count += len(function_call_results)
435
+
436
+ # Format and add results to messages
437
+ self.format_function_call_results(
438
+ messages=messages, function_call_results=function_call_results, **model_response.extra or {}
445
439
  )
446
440
 
447
- for function_call_result in function_call_results:
448
- function_call_result.log(metrics=True)
441
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
442
+ # Handle function call media
443
+ self._handle_function_call_media(
444
+ messages=messages,
445
+ function_call_results=function_call_results,
446
+ send_media_to_model=send_media_to_model,
447
+ )
449
448
 
450
- # Check if we should stop after tool calls
451
- if any(m.stop_after_tool_call for m in function_call_results):
452
- break
449
+ for function_call_result in function_call_results:
450
+ function_call_result.log(metrics=True)
453
451
 
454
- # If we have any tool calls that require confirmation, break the loop
455
- if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
456
- break
452
+ # Check if we should stop after tool calls
453
+ if any(m.stop_after_tool_call for m in function_call_results):
454
+ break
457
455
 
458
- # If we have any tool calls that require external execution, break the loop
459
- if any(tc.external_execution_required for tc in model_response.tool_executions or []):
460
- break
456
+ # If we have any tool calls that require confirmation, break the loop
457
+ if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
458
+ break
461
459
 
462
- # If we have any tool calls that require user input, break the loop
463
- if any(tc.requires_user_input for tc in model_response.tool_executions or []):
464
- break
460
+ # If we have any tool calls that require external execution, break the loop
461
+ if any(tc.external_execution_required for tc in model_response.tool_executions or []):
462
+ break
465
463
 
466
- # Continue loop to get next response
467
- continue
464
+ # If we have any tool calls that require user input, break the loop
465
+ if any(tc.requires_user_input for tc in model_response.tool_executions or []):
466
+ break
468
467
 
469
- # No tool calls or finished processing them
470
- break
468
+ # Continue loop to get next response
469
+ continue
471
470
 
472
- log_debug(f"{self.get_provider()} Response End", center=True, symbol="-")
471
+ # No tool calls or finished processing them
472
+ break
473
473
 
474
- # Save to cache if enabled
475
- if self.cache_response:
476
- self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
474
+ log_debug(f"{self.get_provider()} Response End", center=True, symbol="-")
475
+
476
+ # Save to cache if enabled
477
+ if self.cache_response:
478
+ self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
479
+ finally:
480
+ # Close the Gemini client
481
+ if self.__class__.__name__ == "Gemini" and self.client is not None: # type: ignore
482
+ try:
483
+ self.client.close() # type: ignore
484
+ self.client = None
485
+ except AttributeError:
486
+ log_warning(
487
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
488
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
489
+ )
477
490
 
478
491
  return model_response
479
492
 
@@ -490,153 +503,166 @@ class Model(ABC):
490
503
  """
491
504
  Generate an asynchronous response from the model.
492
505
  """
506
+ try:
507
+ # Check cache if enabled
508
+ if self.cache_response:
509
+ cache_key = self._get_model_cache_key(
510
+ messages, stream=False, response_format=response_format, tools=tools
511
+ )
512
+ cached_data = self._get_cached_model_response(cache_key)
493
513
 
494
- # Check cache if enabled
495
- if self.cache_response:
496
- cache_key = self._get_model_cache_key(messages, stream=False, response_format=response_format, tools=tools)
497
- cached_data = self._get_cached_model_response(cache_key)
498
-
499
- if cached_data:
500
- log_info("Cache hit for model response")
501
- return self._model_response_from_cache(cached_data)
502
-
503
- log_debug(f"{self.get_provider()} Async Response Start", center=True, symbol="-")
504
- log_debug(f"Model: {self.id}", center=True, symbol="-")
505
- _log_messages(messages)
506
- model_response = ModelResponse()
507
-
508
- _tool_dicts = self._format_tools(tools) if tools is not None else []
509
- _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
510
-
511
- function_call_count = 0
512
-
513
- while True:
514
- # Get response from model
515
- assistant_message = Message(role=self.assistant_message_role)
516
- await self._aprocess_model_response(
517
- messages=messages,
518
- assistant_message=assistant_message,
519
- model_response=model_response,
520
- response_format=response_format,
521
- tools=_tool_dicts,
522
- tool_choice=tool_choice or self._tool_choice,
523
- run_response=run_response,
524
- )
514
+ if cached_data:
515
+ log_info("Cache hit for model response")
516
+ return self._model_response_from_cache(cached_data)
525
517
 
526
- # Add assistant message to messages
527
- messages.append(assistant_message)
518
+ log_debug(f"{self.get_provider()} Async Response Start", center=True, symbol="-")
519
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
520
+ _log_messages(messages)
521
+ model_response = ModelResponse()
528
522
 
529
- # Log response and metrics
530
- assistant_message.log(metrics=True)
523
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
524
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
531
525
 
532
- # Handle tool calls if present
533
- if assistant_message.tool_calls:
534
- # Prepare function calls
535
- function_calls_to_run = self._prepare_function_calls(
536
- assistant_message=assistant_message,
526
+ function_call_count = 0
527
+
528
+ while True:
529
+ # Get response from model
530
+ assistant_message = Message(role=self.assistant_message_role)
531
+ await self._aprocess_model_response(
537
532
  messages=messages,
533
+ assistant_message=assistant_message,
538
534
  model_response=model_response,
539
- functions=_functions,
540
- )
541
- function_call_results: List[Message] = []
542
-
543
- # Execute function calls
544
- async for function_call_response in self.arun_function_calls(
545
- function_calls=function_calls_to_run,
546
- function_call_results=function_call_results,
547
- current_function_call_count=function_call_count,
548
- function_call_limit=tool_call_limit,
549
- ):
550
- if isinstance(function_call_response, ModelResponse):
551
- # The session state is updated by the function call
552
- if function_call_response.updated_session_state is not None:
553
- model_response.updated_session_state = function_call_response.updated_session_state
554
-
555
- # Media artifacts are generated by the function call
556
- if function_call_response.images is not None:
557
- if model_response.images is None:
558
- model_response.images = []
559
- model_response.images.extend(function_call_response.images)
560
-
561
- if function_call_response.audios is not None:
562
- if model_response.audios is None:
563
- model_response.audios = []
564
- model_response.audios.extend(function_call_response.audios)
565
-
566
- if function_call_response.videos is not None:
567
- if model_response.videos is None:
568
- model_response.videos = []
569
- model_response.videos.extend(function_call_response.videos)
570
-
571
- if function_call_response.files is not None:
572
- if model_response.files is None:
573
- model_response.files = []
574
- model_response.files.extend(function_call_response.files)
575
-
576
- if (
577
- function_call_response.event
578
- in [
579
- ModelResponseEvent.tool_call_completed.value,
580
- ModelResponseEvent.tool_call_paused.value,
581
- ]
582
- and function_call_response.tool_executions is not None
583
- ):
584
- if model_response.tool_executions is None:
585
- model_response.tool_executions = []
586
- model_response.tool_executions.extend(function_call_response.tool_executions)
587
- elif function_call_response.event not in [
588
- ModelResponseEvent.tool_call_started.value,
589
- ModelResponseEvent.tool_call_completed.value,
590
- ]:
591
- if function_call_response.content:
592
- model_response.content += function_call_response.content # type: ignore
593
-
594
- # Add a function call for each successful execution
595
- function_call_count += len(function_call_results)
596
-
597
- # Format and add results to messages
598
- self.format_function_call_results(
599
- messages=messages, function_call_results=function_call_results, **model_response.extra or {}
535
+ response_format=response_format,
536
+ tools=_tool_dicts,
537
+ tool_choice=tool_choice or self._tool_choice,
538
+ run_response=run_response,
600
539
  )
601
540
 
602
- if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
603
- # Handle function call media
604
- self._handle_function_call_media(
541
+ # Add assistant message to messages
542
+ messages.append(assistant_message)
543
+
544
+ # Log response and metrics
545
+ assistant_message.log(metrics=True)
546
+
547
+ # Handle tool calls if present
548
+ if assistant_message.tool_calls:
549
+ # Prepare function calls
550
+ function_calls_to_run = self._prepare_function_calls(
551
+ assistant_message=assistant_message,
605
552
  messages=messages,
553
+ model_response=model_response,
554
+ functions=_functions,
555
+ )
556
+ function_call_results: List[Message] = []
557
+
558
+ # Execute function calls
559
+ async for function_call_response in self.arun_function_calls(
560
+ function_calls=function_calls_to_run,
606
561
  function_call_results=function_call_results,
607
- send_media_to_model=send_media_to_model,
562
+ current_function_call_count=function_call_count,
563
+ function_call_limit=tool_call_limit,
564
+ ):
565
+ if isinstance(function_call_response, ModelResponse):
566
+ # The session state is updated by the function call
567
+ if function_call_response.updated_session_state is not None:
568
+ model_response.updated_session_state = function_call_response.updated_session_state
569
+
570
+ # Media artifacts are generated by the function call
571
+ if function_call_response.images is not None:
572
+ if model_response.images is None:
573
+ model_response.images = []
574
+ model_response.images.extend(function_call_response.images)
575
+
576
+ if function_call_response.audios is not None:
577
+ if model_response.audios is None:
578
+ model_response.audios = []
579
+ model_response.audios.extend(function_call_response.audios)
580
+
581
+ if function_call_response.videos is not None:
582
+ if model_response.videos is None:
583
+ model_response.videos = []
584
+ model_response.videos.extend(function_call_response.videos)
585
+
586
+ if function_call_response.files is not None:
587
+ if model_response.files is None:
588
+ model_response.files = []
589
+ model_response.files.extend(function_call_response.files)
590
+
591
+ if (
592
+ function_call_response.event
593
+ in [
594
+ ModelResponseEvent.tool_call_completed.value,
595
+ ModelResponseEvent.tool_call_paused.value,
596
+ ]
597
+ and function_call_response.tool_executions is not None
598
+ ):
599
+ if model_response.tool_executions is None:
600
+ model_response.tool_executions = []
601
+ model_response.tool_executions.extend(function_call_response.tool_executions)
602
+ elif function_call_response.event not in [
603
+ ModelResponseEvent.tool_call_started.value,
604
+ ModelResponseEvent.tool_call_completed.value,
605
+ ]:
606
+ if function_call_response.content:
607
+ model_response.content += function_call_response.content # type: ignore
608
+
609
+ # Add a function call for each successful execution
610
+ function_call_count += len(function_call_results)
611
+
612
+ # Format and add results to messages
613
+ self.format_function_call_results(
614
+ messages=messages, function_call_results=function_call_results, **model_response.extra or {}
608
615
  )
609
616
 
610
- for function_call_result in function_call_results:
611
- function_call_result.log(metrics=True)
617
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
618
+ # Handle function call media
619
+ self._handle_function_call_media(
620
+ messages=messages,
621
+ function_call_results=function_call_results,
622
+ send_media_to_model=send_media_to_model,
623
+ )
612
624
 
613
- # Check if we should stop after tool calls
614
- if any(m.stop_after_tool_call for m in function_call_results):
615
- break
625
+ for function_call_result in function_call_results:
626
+ function_call_result.log(metrics=True)
616
627
 
617
- # If we have any tool calls that require confirmation, break the loop
618
- if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
619
- break
628
+ # Check if we should stop after tool calls
629
+ if any(m.stop_after_tool_call for m in function_call_results):
630
+ break
620
631
 
621
- # If we have any tool calls that require external execution, break the loop
622
- if any(tc.external_execution_required for tc in model_response.tool_executions or []):
623
- break
632
+ # If we have any tool calls that require confirmation, break the loop
633
+ if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
634
+ break
624
635
 
625
- # If we have any tool calls that require user input, break the loop
626
- if any(tc.requires_user_input for tc in model_response.tool_executions or []):
627
- break
636
+ # If we have any tool calls that require external execution, break the loop
637
+ if any(tc.external_execution_required for tc in model_response.tool_executions or []):
638
+ break
628
639
 
629
- # Continue loop to get next response
630
- continue
640
+ # If we have any tool calls that require user input, break the loop
641
+ if any(tc.requires_user_input for tc in model_response.tool_executions or []):
642
+ break
631
643
 
632
- # No tool calls or finished processing them
633
- break
644
+ # Continue loop to get next response
645
+ continue
634
646
 
635
- log_debug(f"{self.get_provider()} Async Response End", center=True, symbol="-")
647
+ # No tool calls or finished processing them
648
+ break
636
649
 
637
- # Save to cache if enabled
638
- if self.cache_response:
639
- self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
650
+ log_debug(f"{self.get_provider()} Async Response End", center=True, symbol="-")
651
+
652
+ # Save to cache if enabled
653
+ if self.cache_response:
654
+ self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
655
+ finally:
656
+ # Close the Gemini client
657
+ if self.__class__.__name__ == "Gemini" and self.client is not None:
658
+ try:
659
+ await self.client.aio.aclose() # type: ignore
660
+ self.client = None
661
+ except AttributeError:
662
+ log_warning(
663
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
664
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
665
+ )
640
666
 
641
667
  return model_response
642
668
 
@@ -865,143 +891,158 @@ class Model(ABC):
865
891
  """
866
892
  Generate a streaming response from the model.
867
893
  """
894
+ try:
895
+ # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
896
+ cache_key = None
897
+ if self.cache_response:
898
+ cache_key = self._get_model_cache_key(
899
+ messages, stream=True, response_format=response_format, tools=tools
900
+ )
901
+ cached_data = self._get_cached_model_response(cache_key)
868
902
 
869
- # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
870
- cache_key = None
871
- if self.cache_response:
872
- cache_key = self._get_model_cache_key(messages, stream=True, response_format=response_format, tools=tools)
873
- cached_data = self._get_cached_model_response(cache_key)
874
-
875
- if cached_data:
876
- log_info("Cache hit for streaming model response")
877
- # Yield cached responses
878
- for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
879
- yield response
880
- return
903
+ if cached_data:
904
+ log_info("Cache hit for streaming model response")
905
+ # Yield cached responses
906
+ for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
907
+ yield response
908
+ return
881
909
 
882
- log_info("Cache miss for streaming model response")
910
+ log_info("Cache miss for streaming model response")
883
911
 
884
- # Track streaming responses for caching
885
- streaming_responses: List[ModelResponse] = []
912
+ # Track streaming responses for caching
913
+ streaming_responses: List[ModelResponse] = []
886
914
 
887
- log_debug(f"{self.get_provider()} Response Stream Start", center=True, symbol="-")
888
- log_debug(f"Model: {self.id}", center=True, symbol="-")
889
- _log_messages(messages)
915
+ log_debug(f"{self.get_provider()} Response Stream Start", center=True, symbol="-")
916
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
917
+ _log_messages(messages)
890
918
 
891
- _tool_dicts = self._format_tools(tools) if tools is not None else []
892
- _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
919
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
920
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
893
921
 
894
- function_call_count = 0
922
+ function_call_count = 0
895
923
 
896
- while True:
897
- assistant_message = Message(role=self.assistant_message_role)
898
- # Create assistant message and stream data
899
- stream_data = MessageData()
900
- model_response = ModelResponse()
901
- if stream_model_response:
902
- # Generate response
903
- for response in self.process_response_stream(
904
- messages=messages,
905
- assistant_message=assistant_message,
906
- stream_data=stream_data,
907
- response_format=response_format,
908
- tools=_tool_dicts,
909
- tool_choice=tool_choice or self._tool_choice,
910
- run_response=run_response,
911
- ):
912
- if self.cache_response and isinstance(response, ModelResponse):
913
- streaming_responses.append(response)
914
- yield response
924
+ while True:
925
+ assistant_message = Message(role=self.assistant_message_role)
926
+ # Create assistant message and stream data
927
+ stream_data = MessageData()
928
+ model_response = ModelResponse()
929
+ if stream_model_response:
930
+ # Generate response
931
+ for response in self.process_response_stream(
932
+ messages=messages,
933
+ assistant_message=assistant_message,
934
+ stream_data=stream_data,
935
+ response_format=response_format,
936
+ tools=_tool_dicts,
937
+ tool_choice=tool_choice or self._tool_choice,
938
+ run_response=run_response,
939
+ ):
940
+ if self.cache_response and isinstance(response, ModelResponse):
941
+ streaming_responses.append(response)
942
+ yield response
915
943
 
916
- else:
917
- self._process_model_response(
918
- messages=messages,
919
- assistant_message=assistant_message,
920
- model_response=model_response,
921
- response_format=response_format,
922
- tools=_tool_dicts,
923
- tool_choice=tool_choice or self._tool_choice,
924
- )
925
- if self.cache_response:
926
- streaming_responses.append(model_response)
927
- yield model_response
928
-
929
- # Add assistant message to messages
930
- messages.append(assistant_message)
931
- assistant_message.log(metrics=True)
932
-
933
- # Handle tool calls if present
934
- if assistant_message.tool_calls is not None:
935
- # Prepare function calls
936
- function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
937
- assistant_message=assistant_message, messages=messages, functions=_functions
938
- )
939
- function_call_results: List[Message] = []
940
-
941
- # Execute function calls
942
- for function_call_response in self.run_function_calls(
943
- function_calls=function_calls_to_run,
944
- function_call_results=function_call_results,
945
- current_function_call_count=function_call_count,
946
- function_call_limit=tool_call_limit,
947
- ):
948
- if self.cache_response and isinstance(function_call_response, ModelResponse):
949
- streaming_responses.append(function_call_response)
950
- yield function_call_response
951
-
952
- # Add a function call for each successful execution
953
- function_call_count += len(function_call_results)
954
-
955
- # Format and add results to messages
956
- if stream_data and stream_data.extra is not None:
957
- self.format_function_call_results(
958
- messages=messages, function_call_results=function_call_results, **stream_data.extra
944
+ else:
945
+ self._process_model_response(
946
+ messages=messages,
947
+ assistant_message=assistant_message,
948
+ model_response=model_response,
949
+ response_format=response_format,
950
+ tools=_tool_dicts,
951
+ tool_choice=tool_choice or self._tool_choice,
959
952
  )
960
- elif model_response and model_response.extra is not None:
961
- self.format_function_call_results(
962
- messages=messages, function_call_results=function_call_results, **model_response.extra
953
+ if self.cache_response:
954
+ streaming_responses.append(model_response)
955
+ yield model_response
956
+
957
+ # Add assistant message to messages
958
+ messages.append(assistant_message)
959
+ assistant_message.log(metrics=True)
960
+
961
+ # Handle tool calls if present
962
+ if assistant_message.tool_calls is not None:
963
+ # Prepare function calls
964
+ function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
965
+ assistant_message=assistant_message, messages=messages, functions=_functions
963
966
  )
964
- else:
965
- self.format_function_call_results(messages=messages, function_call_results=function_call_results)
967
+ function_call_results: List[Message] = []
966
968
 
967
- # Handle function call media
968
- if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
969
- self._handle_function_call_media(
970
- messages=messages,
969
+ # Execute function calls
970
+ for function_call_response in self.run_function_calls(
971
+ function_calls=function_calls_to_run,
971
972
  function_call_results=function_call_results,
972
- send_media_to_model=send_media_to_model,
973
- )
973
+ current_function_call_count=function_call_count,
974
+ function_call_limit=tool_call_limit,
975
+ ):
976
+ if self.cache_response and isinstance(function_call_response, ModelResponse):
977
+ streaming_responses.append(function_call_response)
978
+ yield function_call_response
974
979
 
975
- for function_call_result in function_call_results:
976
- function_call_result.log(metrics=True)
980
+ # Add a function call for each successful execution
981
+ function_call_count += len(function_call_results)
977
982
 
978
- # Check if we should stop after tool calls
979
- if any(m.stop_after_tool_call for m in function_call_results):
980
- break
983
+ # Format and add results to messages
984
+ if stream_data and stream_data.extra is not None:
985
+ self.format_function_call_results(
986
+ messages=messages, function_call_results=function_call_results, **stream_data.extra
987
+ )
988
+ elif model_response and model_response.extra is not None:
989
+ self.format_function_call_results(
990
+ messages=messages, function_call_results=function_call_results, **model_response.extra
991
+ )
992
+ else:
993
+ self.format_function_call_results(
994
+ messages=messages, function_call_results=function_call_results
995
+ )
996
+
997
+ # Handle function call media
998
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
999
+ self._handle_function_call_media(
1000
+ messages=messages,
1001
+ function_call_results=function_call_results,
1002
+ send_media_to_model=send_media_to_model,
1003
+ )
981
1004
 
982
- # If we have any tool calls that require confirmation, break the loop
983
- if any(fc.function.requires_confirmation for fc in function_calls_to_run):
984
- break
1005
+ for function_call_result in function_call_results:
1006
+ function_call_result.log(metrics=True)
985
1007
 
986
- # If we have any tool calls that require external execution, break the loop
987
- if any(fc.function.external_execution for fc in function_calls_to_run):
988
- break
1008
+ # Check if we should stop after tool calls
1009
+ if any(m.stop_after_tool_call for m in function_call_results):
1010
+ break
989
1011
 
990
- # If we have any tool calls that require user input, break the loop
991
- if any(fc.function.requires_user_input for fc in function_calls_to_run):
992
- break
1012
+ # If we have any tool calls that require confirmation, break the loop
1013
+ if any(fc.function.requires_confirmation for fc in function_calls_to_run):
1014
+ break
993
1015
 
994
- # Continue loop to get next response
995
- continue
1016
+ # If we have any tool calls that require external execution, break the loop
1017
+ if any(fc.function.external_execution for fc in function_calls_to_run):
1018
+ break
1019
+
1020
+ # If we have any tool calls that require user input, break the loop
1021
+ if any(fc.function.requires_user_input for fc in function_calls_to_run):
1022
+ break
996
1023
 
997
- # No tool calls or finished processing them
998
- break
1024
+ # Continue loop to get next response
1025
+ continue
999
1026
 
1000
- log_debug(f"{self.get_provider()} Response Stream End", center=True, symbol="-")
1027
+ # No tool calls or finished processing them
1028
+ break
1001
1029
 
1002
- # Save streaming responses to cache if enabled
1003
- if self.cache_response and cache_key and streaming_responses:
1004
- self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1030
+ log_debug(f"{self.get_provider()} Response Stream End", center=True, symbol="-")
1031
+
1032
+ # Save streaming responses to cache if enabled
1033
+ if self.cache_response and cache_key and streaming_responses:
1034
+ self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1035
+ finally:
1036
+ # Close the Gemini client
1037
+ if self.__class__.__name__ == "Gemini" and self.client is not None:
1038
+ try:
1039
+ self.client.close() # type: ignore
1040
+ self.client = None
1041
+ except AttributeError:
1042
+ log_warning(
1043
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
1044
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
1045
+ )
1005
1046
 
1006
1047
  async def aprocess_response_stream(
1007
1048
  self,
@@ -1047,144 +1088,160 @@ class Model(ABC):
1047
1088
  """
1048
1089
  Generate an asynchronous streaming response from the model.
1049
1090
  """
1091
+ try:
1092
+ # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
1093
+ cache_key = None
1094
+ if self.cache_response:
1095
+ cache_key = self._get_model_cache_key(
1096
+ messages, stream=True, response_format=response_format, tools=tools
1097
+ )
1098
+ cached_data = self._get_cached_model_response(cache_key)
1050
1099
 
1051
- # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
1052
- cache_key = None
1053
- if self.cache_response:
1054
- cache_key = self._get_model_cache_key(messages, stream=True, response_format=response_format, tools=tools)
1055
- cached_data = self._get_cached_model_response(cache_key)
1100
+ if cached_data:
1101
+ log_info("Cache hit for async streaming model response")
1102
+ # Yield cached responses
1103
+ for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
1104
+ yield response
1105
+ return
1056
1106
 
1057
- if cached_data:
1058
- log_info("Cache hit for async streaming model response")
1059
- # Yield cached responses
1060
- for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
1061
- yield response
1062
- return
1107
+ log_info("Cache miss for async streaming model response")
1063
1108
 
1064
- log_info("Cache miss for async streaming model response")
1109
+ # Track streaming responses for caching
1110
+ streaming_responses: List[ModelResponse] = []
1065
1111
 
1066
- # Track streaming responses for caching
1067
- streaming_responses: List[ModelResponse] = []
1112
+ log_debug(f"{self.get_provider()} Async Response Stream Start", center=True, symbol="-")
1113
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
1114
+ _log_messages(messages)
1068
1115
 
1069
- log_debug(f"{self.get_provider()} Async Response Stream Start", center=True, symbol="-")
1070
- log_debug(f"Model: {self.id}", center=True, symbol="-")
1071
- _log_messages(messages)
1116
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
1117
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
1072
1118
 
1073
- _tool_dicts = self._format_tools(tools) if tools is not None else []
1074
- _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
1119
+ function_call_count = 0
1075
1120
 
1076
- function_call_count = 0
1121
+ while True:
1122
+ # Create assistant message and stream data
1123
+ assistant_message = Message(role=self.assistant_message_role)
1124
+ stream_data = MessageData()
1125
+ model_response = ModelResponse()
1126
+ if stream_model_response:
1127
+ # Generate response
1128
+ async for model_response in self.aprocess_response_stream(
1129
+ messages=messages,
1130
+ assistant_message=assistant_message,
1131
+ stream_data=stream_data,
1132
+ response_format=response_format,
1133
+ tools=_tool_dicts,
1134
+ tool_choice=tool_choice or self._tool_choice,
1135
+ run_response=run_response,
1136
+ ):
1137
+ if self.cache_response and isinstance(model_response, ModelResponse):
1138
+ streaming_responses.append(model_response)
1139
+ yield model_response
1077
1140
 
1078
- while True:
1079
- # Create assistant message and stream data
1080
- assistant_message = Message(role=self.assistant_message_role)
1081
- stream_data = MessageData()
1082
- model_response = ModelResponse()
1083
- if stream_model_response:
1084
- # Generate response
1085
- async for model_response in self.aprocess_response_stream(
1086
- messages=messages,
1087
- assistant_message=assistant_message,
1088
- stream_data=stream_data,
1089
- response_format=response_format,
1090
- tools=_tool_dicts,
1091
- tool_choice=tool_choice or self._tool_choice,
1092
- run_response=run_response,
1093
- ):
1094
- if self.cache_response and isinstance(model_response, ModelResponse):
1141
+ else:
1142
+ await self._aprocess_model_response(
1143
+ messages=messages,
1144
+ assistant_message=assistant_message,
1145
+ model_response=model_response,
1146
+ response_format=response_format,
1147
+ tools=_tool_dicts,
1148
+ tool_choice=tool_choice or self._tool_choice,
1149
+ run_response=run_response,
1150
+ )
1151
+ if self.cache_response:
1095
1152
  streaming_responses.append(model_response)
1096
1153
  yield model_response
1097
1154
 
1098
- else:
1099
- await self._aprocess_model_response(
1100
- messages=messages,
1101
- assistant_message=assistant_message,
1102
- model_response=model_response,
1103
- response_format=response_format,
1104
- tools=_tool_dicts,
1105
- tool_choice=tool_choice or self._tool_choice,
1106
- run_response=run_response,
1107
- )
1108
- if self.cache_response:
1109
- streaming_responses.append(model_response)
1110
- yield model_response
1111
-
1112
- # Add assistant message to messages
1113
- messages.append(assistant_message)
1114
- assistant_message.log(metrics=True)
1115
-
1116
- # Handle tool calls if present
1117
- if assistant_message.tool_calls is not None:
1118
- # Prepare function calls
1119
- function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
1120
- assistant_message=assistant_message, messages=messages, functions=_functions
1121
- )
1122
- function_call_results: List[Message] = []
1123
-
1124
- # Execute function calls
1125
- async for function_call_response in self.arun_function_calls(
1126
- function_calls=function_calls_to_run,
1127
- function_call_results=function_call_results,
1128
- current_function_call_count=function_call_count,
1129
- function_call_limit=tool_call_limit,
1130
- ):
1131
- if self.cache_response and isinstance(function_call_response, ModelResponse):
1132
- streaming_responses.append(function_call_response)
1133
- yield function_call_response
1134
-
1135
- # Add a function call for each successful execution
1136
- function_call_count += len(function_call_results)
1137
-
1138
- # Format and add results to messages
1139
- if stream_data and stream_data.extra is not None:
1140
- self.format_function_call_results(
1141
- messages=messages, function_call_results=function_call_results, **stream_data.extra
1142
- )
1143
- elif model_response and model_response.extra is not None:
1144
- self.format_function_call_results(
1145
- messages=messages, function_call_results=function_call_results, **model_response.extra or {}
1155
+ # Add assistant message to messages
1156
+ messages.append(assistant_message)
1157
+ assistant_message.log(metrics=True)
1158
+
1159
+ # Handle tool calls if present
1160
+ if assistant_message.tool_calls is not None:
1161
+ # Prepare function calls
1162
+ function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
1163
+ assistant_message=assistant_message, messages=messages, functions=_functions
1146
1164
  )
1147
- else:
1148
- self.format_function_call_results(messages=messages, function_call_results=function_call_results)
1165
+ function_call_results: List[Message] = []
1149
1166
 
1150
- # Handle function call media
1151
- if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
1152
- self._handle_function_call_media(
1153
- messages=messages,
1167
+ # Execute function calls
1168
+ async for function_call_response in self.arun_function_calls(
1169
+ function_calls=function_calls_to_run,
1154
1170
  function_call_results=function_call_results,
1155
- send_media_to_model=send_media_to_model,
1156
- )
1171
+ current_function_call_count=function_call_count,
1172
+ function_call_limit=tool_call_limit,
1173
+ ):
1174
+ if self.cache_response and isinstance(function_call_response, ModelResponse):
1175
+ streaming_responses.append(function_call_response)
1176
+ yield function_call_response
1157
1177
 
1158
- for function_call_result in function_call_results:
1159
- function_call_result.log(metrics=True)
1178
+ # Add a function call for each successful execution
1179
+ function_call_count += len(function_call_results)
1160
1180
 
1161
- # Check if we should stop after tool calls
1162
- if any(m.stop_after_tool_call for m in function_call_results):
1163
- break
1181
+ # Format and add results to messages
1182
+ if stream_data and stream_data.extra is not None:
1183
+ self.format_function_call_results(
1184
+ messages=messages, function_call_results=function_call_results, **stream_data.extra
1185
+ )
1186
+ elif model_response and model_response.extra is not None:
1187
+ self.format_function_call_results(
1188
+ messages=messages, function_call_results=function_call_results, **model_response.extra or {}
1189
+ )
1190
+ else:
1191
+ self.format_function_call_results(
1192
+ messages=messages, function_call_results=function_call_results
1193
+ )
1164
1194
 
1165
- # If we have any tool calls that require confirmation, break the loop
1166
- if any(fc.function.requires_confirmation for fc in function_calls_to_run):
1167
- break
1195
+ # Handle function call media
1196
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
1197
+ self._handle_function_call_media(
1198
+ messages=messages,
1199
+ function_call_results=function_call_results,
1200
+ send_media_to_model=send_media_to_model,
1201
+ )
1168
1202
 
1169
- # If we have any tool calls that require external execution, break the loop
1170
- if any(fc.function.external_execution for fc in function_calls_to_run):
1171
- break
1203
+ for function_call_result in function_call_results:
1204
+ function_call_result.log(metrics=True)
1172
1205
 
1173
- # If we have any tool calls that require user input, break the loop
1174
- if any(fc.function.requires_user_input for fc in function_calls_to_run):
1175
- break
1206
+ # Check if we should stop after tool calls
1207
+ if any(m.stop_after_tool_call for m in function_call_results):
1208
+ break
1176
1209
 
1177
- # Continue loop to get next response
1178
- continue
1210
+ # If we have any tool calls that require confirmation, break the loop
1211
+ if any(fc.function.requires_confirmation for fc in function_calls_to_run):
1212
+ break
1179
1213
 
1180
- # No tool calls or finished processing them
1181
- break
1214
+ # If we have any tool calls that require external execution, break the loop
1215
+ if any(fc.function.external_execution for fc in function_calls_to_run):
1216
+ break
1182
1217
 
1183
- log_debug(f"{self.get_provider()} Async Response Stream End", center=True, symbol="-")
1218
+ # If we have any tool calls that require user input, break the loop
1219
+ if any(fc.function.requires_user_input for fc in function_calls_to_run):
1220
+ break
1184
1221
 
1185
- # Save streaming responses to cache if enabled
1186
- if self.cache_response and cache_key and streaming_responses:
1187
- self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1222
+ # Continue loop to get next response
1223
+ continue
1224
+
1225
+ # No tool calls or finished processing them
1226
+ break
1227
+
1228
+ log_debug(f"{self.get_provider()} Async Response Stream End", center=True, symbol="-")
1229
+
1230
+ # Save streaming responses to cache if enabled
1231
+ if self.cache_response and cache_key and streaming_responses:
1232
+ self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1233
+
1234
+ finally:
1235
+ # Close the Gemini client
1236
+ if self.__class__.__name__ == "Gemini" and self.client is not None:
1237
+ try:
1238
+ await self.client.aio.aclose() # type: ignore
1239
+ self.client = None
1240
+ except AttributeError:
1241
+ log_warning(
1242
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
1243
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
1244
+ )
1188
1245
 
1189
1246
  def _populate_assistant_message_from_stream_data(
1190
1247
  self, assistant_message: Message, stream_data: MessageData
@@ -1452,44 +1509,49 @@ class Model(ABC):
1452
1509
  function_call_output: str = ""
1453
1510
 
1454
1511
  if isinstance(function_execution_result.result, (GeneratorType, collections.abc.Iterator)):
1455
- for item in function_execution_result.result:
1456
- # This function yields agent/team/workflow run events
1457
- if (
1458
- isinstance(item, tuple(get_args(RunOutputEvent)))
1459
- or isinstance(item, tuple(get_args(TeamRunOutputEvent)))
1460
- or isinstance(item, tuple(get_args(WorkflowRunOutputEvent)))
1461
- ):
1462
- # We only capture content events for output accumulation
1463
- if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
1464
- if item.content is not None and isinstance(item.content, BaseModel):
1465
- function_call_output += item.content.model_dump_json()
1466
- else:
1467
- # Capture output
1468
- function_call_output += item.content or ""
1512
+ try:
1513
+ for item in function_execution_result.result:
1514
+ # This function yields agent/team/workflow run events
1515
+ if (
1516
+ isinstance(item, tuple(get_args(RunOutputEvent)))
1517
+ or isinstance(item, tuple(get_args(TeamRunOutputEvent)))
1518
+ or isinstance(item, tuple(get_args(WorkflowRunOutputEvent)))
1519
+ ):
1520
+ # We only capture content events for output accumulation
1521
+ if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
1522
+ if item.content is not None and isinstance(item.content, BaseModel):
1523
+ function_call_output += item.content.model_dump_json()
1524
+ else:
1525
+ # Capture output
1526
+ function_call_output += item.content or ""
1469
1527
 
1470
- if function_call.function.show_result and item.content is not None:
1471
- yield ModelResponse(content=item.content)
1528
+ if function_call.function.show_result and item.content is not None:
1529
+ yield ModelResponse(content=item.content)
1472
1530
 
1473
- if isinstance(item, CustomEvent):
1474
- function_call_output += str(item)
1531
+ if isinstance(item, CustomEvent):
1532
+ function_call_output += str(item)
1475
1533
 
1476
- # For WorkflowCompletedEvent, extract content for final output
1477
- from agno.run.workflow import WorkflowCompletedEvent
1534
+ # For WorkflowCompletedEvent, extract content for final output
1535
+ from agno.run.workflow import WorkflowCompletedEvent
1478
1536
 
1479
- if isinstance(item, WorkflowCompletedEvent):
1480
- if item.content is not None:
1481
- if isinstance(item.content, BaseModel):
1482
- function_call_output += item.content.model_dump_json()
1483
- else:
1484
- function_call_output += str(item.content)
1537
+ if isinstance(item, WorkflowCompletedEvent):
1538
+ if item.content is not None:
1539
+ if isinstance(item.content, BaseModel):
1540
+ function_call_output += item.content.model_dump_json()
1541
+ else:
1542
+ function_call_output += str(item.content)
1485
1543
 
1486
- # Yield the event itself to bubble it up
1487
- yield item
1544
+ # Yield the event itself to bubble it up
1545
+ yield item
1488
1546
 
1489
- else:
1490
- function_call_output += str(item)
1491
- if function_call.function.show_result and item is not None:
1492
- yield ModelResponse(content=str(item))
1547
+ else:
1548
+ function_call_output += str(item)
1549
+ if function_call.function.show_result and item is not None:
1550
+ yield ModelResponse(content=str(item))
1551
+ except Exception as e:
1552
+ log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
1553
+ function_call.error = str(e)
1554
+ function_call_success = False
1493
1555
  else:
1494
1556
  from agno.tools.function import ToolResult
1495
1557
 
@@ -1975,32 +2037,37 @@ class Model(ABC):
1975
2037
  function_call_output = async_function_call_output
1976
2038
  # Events from async generators were already yielded in real-time above
1977
2039
  elif isinstance(function_call.result, (GeneratorType, collections.abc.Iterator)):
1978
- for item in function_call.result:
1979
- # This function yields agent/team/workflow run events
1980
- if isinstance(
1981
- item,
1982
- tuple(get_args(RunOutputEvent))
1983
- + tuple(get_args(TeamRunOutputEvent))
1984
- + tuple(get_args(WorkflowRunOutputEvent)),
1985
- ):
1986
- # We only capture content events
1987
- if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
1988
- if item.content is not None and isinstance(item.content, BaseModel):
1989
- function_call_output += item.content.model_dump_json()
1990
- else:
1991
- # Capture output
1992
- function_call_output += item.content or ""
1993
-
1994
- if function_call.function.show_result and item.content is not None:
1995
- yield ModelResponse(content=item.content)
1996
- continue
1997
-
1998
- # Yield the event itself to bubble it up
1999
- yield item
2000
- else:
2001
- function_call_output += str(item)
2002
- if function_call.function.show_result and item is not None:
2003
- yield ModelResponse(content=str(item))
2040
+ try:
2041
+ for item in function_call.result:
2042
+ # This function yields agent/team/workflow run events
2043
+ if isinstance(
2044
+ item,
2045
+ tuple(get_args(RunOutputEvent))
2046
+ + tuple(get_args(TeamRunOutputEvent))
2047
+ + tuple(get_args(WorkflowRunOutputEvent)),
2048
+ ):
2049
+ # We only capture content events
2050
+ if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
2051
+ if item.content is not None and isinstance(item.content, BaseModel):
2052
+ function_call_output += item.content.model_dump_json()
2053
+ else:
2054
+ # Capture output
2055
+ function_call_output += item.content or ""
2056
+
2057
+ if function_call.function.show_result and item.content is not None:
2058
+ yield ModelResponse(content=item.content)
2059
+ continue
2060
+
2061
+ # Yield the event itself to bubble it up
2062
+ yield item
2063
+ else:
2064
+ function_call_output += str(item)
2065
+ if function_call.function.show_result and item is not None:
2066
+ yield ModelResponse(content=str(item))
2067
+ except Exception as e:
2068
+ log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
2069
+ function_call.error = str(e)
2070
+ function_call_success = False
2004
2071
  else:
2005
2072
  from agno.tools.function import ToolResult
2006
2073