solace-agent-mesh 1.5.1__py3-none-any.whl → 1.6.0__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.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (180) hide show
  1. solace_agent_mesh/agent/adk/callbacks.py +0 -5
  2. solace_agent_mesh/agent/adk/models/lite_llm.py +123 -8
  3. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +245 -0
  4. solace_agent_mesh/agent/protocol/event_handlers.py +40 -1
  5. solace_agent_mesh/agent/proxies/__init__.py +0 -0
  6. solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
  7. solace_agent_mesh/agent/proxies/a2a/app.py +55 -0
  8. solace_agent_mesh/agent/proxies/a2a/component.py +1115 -0
  9. solace_agent_mesh/agent/proxies/a2a/config.py +140 -0
  10. solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
  11. solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
  12. solace_agent_mesh/agent/proxies/base/app.py +99 -0
  13. solace_agent_mesh/agent/proxies/base/component.py +619 -0
  14. solace_agent_mesh/agent/proxies/base/config.py +85 -0
  15. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +17 -0
  16. solace_agent_mesh/agent/sac/app.py +9 -3
  17. solace_agent_mesh/agent/sac/component.py +160 -8
  18. solace_agent_mesh/agent/tools/audio_tools.py +125 -8
  19. solace_agent_mesh/agent/tools/web_tools.py +10 -5
  20. solace_agent_mesh/agent/utils/artifact_helpers.py +141 -3
  21. solace_agent_mesh/assets/docs/404.html +3 -3
  22. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +1 -0
  23. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/{ad71b5ed.60668e9e.js → ad71b5ed.af3ecfd1.js} +1 -1
  29. solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
  30. solace_agent_mesh/assets/docs/assets/js/{da0b5bad.9d369087.js → da0b5bad.d08a9466.js} +1 -1
  31. solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
  32. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +1 -0
  35. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js → main.20feee82.js} +2 -2
  37. solace_agent_mesh/assets/docs/assets/js/runtime~main.0d198646.js +1 -0
  38. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +15 -4
  39. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
  40. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +262 -0
  50. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
  51. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +31 -3
  52. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
  53. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
  54. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
  55. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +5 -5
  56. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
  59. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +6 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
  61. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +5 -5
  64. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
  65. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
  66. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
  67. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
  68. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
  69. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
  70. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
  71. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
  72. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
  73. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
  74. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
  75. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
  76. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  77. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
  78. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +6 -5
  79. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
  80. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
  81. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +100 -3
  82. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
  83. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  84. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
  85. solace_agent_mesh/assets/docs/lunr-index-1761165361160.json +1 -0
  86. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  87. solace_agent_mesh/assets/docs/search-doc-1761165361160.json +1 -0
  88. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  89. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  90. solace_agent_mesh/cli/__init__.py +1 -1
  91. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +2 -69
  92. solace_agent_mesh/cli/commands/eval_cmd.py +11 -49
  93. solace_agent_mesh/cli/commands/init_cmd/__init__.py +0 -5
  94. solace_agent_mesh/cli/commands/init_cmd/env_step.py +10 -12
  95. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +9 -61
  96. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +9 -49
  97. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +1 -2
  98. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DwrxZE0E.js → authCallback-BTf6dqwp.js} +1 -1
  99. solace_agent_mesh/client/webui/frontend/static/assets/{client-DarGQzyw.js → client-CaY59VuC.js} +1 -1
  100. solace_agent_mesh/client/webui/frontend/static/assets/main-BGTaW0uv.js +342 -0
  101. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +1 -0
  102. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BKIeiHj_.js → vendor-BEmvJSYz.js} +1 -1
  103. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  104. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  105. solace_agent_mesh/common/a2a/__init__.py +24 -0
  106. solace_agent_mesh/common/a2a/artifact.py +39 -0
  107. solace_agent_mesh/common/a2a/events.py +29 -0
  108. solace_agent_mesh/common/a2a/message.py +68 -0
  109. solace_agent_mesh/common/a2a/protocol.py +73 -1
  110. solace_agent_mesh/common/agent_registry.py +83 -3
  111. solace_agent_mesh/common/constants.py +3 -1
  112. solace_agent_mesh/common/utils/pydantic_utils.py +12 -0
  113. solace_agent_mesh/config_portal/backend/common.py +1 -1
  114. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +98 -0
  115. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-44d62be6.js → manifest-61038fc6.js} +1 -1
  116. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  117. solace_agent_mesh/evaluation/evaluator.py +128 -104
  118. solace_agent_mesh/evaluation/message_organizer.py +116 -110
  119. solace_agent_mesh/evaluation/report_data_processor.py +84 -86
  120. solace_agent_mesh/evaluation/report_generator.py +73 -79
  121. solace_agent_mesh/evaluation/run.py +421 -235
  122. solace_agent_mesh/evaluation/shared/__init__.py +92 -0
  123. solace_agent_mesh/evaluation/shared/constants.py +47 -0
  124. solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
  125. solace_agent_mesh/evaluation/shared/helpers.py +35 -0
  126. solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
  127. solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
  128. solace_agent_mesh/evaluation/subscriber.py +111 -232
  129. solace_agent_mesh/evaluation/summary_builder.py +227 -117
  130. solace_agent_mesh/gateway/base/app.py +1 -1
  131. solace_agent_mesh/gateway/base/component.py +8 -1
  132. solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
  133. solace_agent_mesh/gateway/http_sse/component.py +98 -2
  134. solace_agent_mesh/gateway/http_sse/dependencies.py +4 -4
  135. solace_agent_mesh/gateway/http_sse/main.py +2 -1
  136. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +12 -13
  137. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +15 -18
  138. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +25 -18
  139. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +30 -26
  140. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +35 -44
  141. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +4 -3
  142. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +95 -203
  143. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +4 -3
  144. solace_agent_mesh/gateway/http_sse/routers/sessions.py +2 -2
  145. solace_agent_mesh/gateway/http_sse/routers/tasks.py +33 -41
  146. solace_agent_mesh/gateway/http_sse/routers/visualization.py +17 -11
  147. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +4 -4
  148. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +51 -43
  149. solace_agent_mesh/gateway/http_sse/services/session_service.py +20 -20
  150. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +8 -8
  151. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +45 -71
  152. solace_agent_mesh/gateway/http_sse/shared/types.py +0 -18
  153. solace_agent_mesh/templates/gateway_config_template.yaml +0 -5
  154. solace_agent_mesh/templates/logging_config_template.ini +10 -6
  155. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +0 -3
  156. solace_agent_mesh/templates/shared_config.yaml +40 -0
  157. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/METADATA +47 -21
  158. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/RECORD +162 -141
  159. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.e49689dd.js +0 -1
  160. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.39d5851d.js +0 -1
  161. solace_agent_mesh/assets/docs/assets/js/71da7b71.804d6567.js +0 -1
  162. solace_agent_mesh/assets/docs/assets/js/77cf947d.64c9bd6c.js +0 -1
  163. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.dd810042.js +0 -1
  164. solace_agent_mesh/assets/docs/assets/js/db924877.cbc66f02.js +0 -1
  165. solace_agent_mesh/assets/docs/assets/js/de915948.139b4b9c.js +0 -1
  166. solace_agent_mesh/assets/docs/assets/js/e6f9706b.582a78ca.js +0 -1
  167. solace_agent_mesh/assets/docs/assets/js/f284c35a.5766a13d.js +0 -1
  168. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.9c0297a6.js +0 -1
  169. solace_agent_mesh/assets/docs/assets/js/runtime~main.18dc45dd.js +0 -1
  170. solace_agent_mesh/assets/docs/lunr-index-1760121512891.json +0 -1
  171. solace_agent_mesh/assets/docs/search-doc-1760121512891.json +0 -1
  172. solace_agent_mesh/client/webui/frontend/static/assets/main-2nd1gbaH.js +0 -339
  173. solace_agent_mesh/client/webui/frontend/static/assets/main-DoKXctCM.css +0 -1
  174. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-BNuqpWDc.js +0 -98
  175. solace_agent_mesh/evaluation/config_loader.py +0 -657
  176. solace_agent_mesh/evaluation/test_case_loader.py +0 -714
  177. /solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js.LICENSE.txt → main.20feee82.js.LICENSE.txt} +0 -0
  178. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/WHEEL +0 -0
  179. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/entry_points.txt +0 -0
  180. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -1094,11 +1094,6 @@ If a plan is created:
1094
1094
  e_last_call,
1095
1095
  )
1096
1096
 
1097
- if host_component.get_config("inject_current_time", True):
1098
- current_time = datetime.now(timezone.utc).strftime("%A, %d %b %Y %H:%M:%S UTC")
1099
- instruction = f"Current time {current_time}."
1100
- injected_instructions.append(instruction)
1101
-
1102
1097
  if injected_instructions:
1103
1098
  combined_instructions = "\n\n---\n\n".join(injected_instructions)
1104
1099
  if llm_request.config is None:
@@ -53,6 +53,7 @@ from typing_extensions import override
53
53
  from google.adk.models.base_llm import BaseLlm
54
54
  from google.adk.models.llm_request import LlmRequest
55
55
  from google.adk.models.llm_response import LlmResponse
56
+ from .oauth2_token_manager import OAuth2ClientCredentialsTokenManager
56
57
 
57
58
  logger = logging.getLogger("google_adk." + __name__)
58
59
 
@@ -479,6 +480,7 @@ def _message_to_generate_content_response(
479
480
 
480
481
  def _get_completion_inputs(
481
482
  llm_request: LlmRequest,
483
+ cache_strategy: str = "5m",
482
484
  ) -> Tuple[
483
485
  List[Message],
484
486
  Optional[List[Dict]],
@@ -489,6 +491,7 @@ def _get_completion_inputs(
489
491
 
490
492
  Args:
491
493
  llm_request: The LlmRequest to convert.
494
+ cache_strategy: Cache strategy to apply ("none", "5m", "1h").
492
495
 
493
496
  Returns:
494
497
  The litellm inputs (message list, tool dictionary and response format).
@@ -501,16 +504,32 @@ def _get_completion_inputs(
501
504
  elif message_param_or_list: # Ensure it's not None before appending
502
505
  messages.append(message_param_or_list)
503
506
 
504
- if llm_request.config.system_instruction:
507
+ if llm_request.config and llm_request.config.system_instruction:
508
+ # Build system instruction content with optional cache control
509
+ system_content = {
510
+ "type": "text",
511
+ "text": llm_request.config.system_instruction,
512
+ }
513
+
514
+ # Add cache control based on strategy
515
+ # LiteLLM translates this to provider-specific format (Anthropic, OpenAI, Bedrock, Deepseek)
516
+ if cache_strategy == "5m":
517
+ # 5-minute ephemeral cache (Anthropic default)
518
+ system_content["cache_control"] = {"type": "ephemeral"}
519
+ elif cache_strategy == "1h":
520
+ # 1-hour extended cache (Anthropic extended)
521
+ system_content["cache_control"] = {"type": "ephemeral", "ttl": "1h"}
522
+ # For "none", no cache_control is added
523
+
505
524
  messages.insert(
506
525
  0,
507
526
  ChatCompletionDeveloperMessage(
508
527
  role="developer",
509
- content=llm_request.config.system_instruction,
528
+ content=[system_content],
510
529
  ),
511
530
  )
512
531
 
513
- # 2. Convert tool declarations
532
+ # 2. Convert tool declarations with caching support
514
533
  tools: Optional[List[Dict]] = None
515
534
  if (
516
535
  llm_request.config
@@ -522,6 +541,16 @@ def _get_completion_inputs(
522
541
  for tool in llm_request.config.tools[0].function_declarations
523
542
  ]
524
543
 
544
+ # Enable tool caching via LiteLLM's generic interface
545
+ # LiteLLM handles provider-specific translation (Anthropic, OpenAI, Bedrock, Deepseek)
546
+ # Tools are stable because peer agents are alphabetically sorted (component.py)
547
+ if tools and cache_strategy != "none":
548
+ # Add cache_control to the LAST tool (required by caching providers)
549
+ if cache_strategy == "5m":
550
+ tools[-1]["cache_control"] = {"type": "ephemeral"}
551
+ elif cache_strategy == "1h":
552
+ tools[-1]["cache_control"] = {"type": "ephemeral", "ttl": "1h"}
553
+
525
554
  # 3. Handle response format
526
555
  response_format: Optional[types.SchemaUnion] = None
527
556
  if llm_request.config and llm_request.config.response_schema:
@@ -595,7 +624,7 @@ def _build_request_log(req: LlmRequest) -> str:
595
624
 
596
625
  function_decls: list[types.FunctionDeclaration] = cast(
597
626
  list[types.FunctionDeclaration],
598
- req.config.tools[0].function_declarations if req.config.tools else [],
627
+ req.config.tools[0].function_declarations if req.config and req.config.tools else [],
599
628
  )
600
629
  function_logs = (
601
630
  [_build_function_declaration_log(func_decl) for func_decl in function_decls]
@@ -616,7 +645,7 @@ def _build_request_log(req: LlmRequest) -> str:
616
645
  LLM Request:
617
646
  -----------------------------------------------------------
618
647
  System Instruction:
619
- {req.config.system_instruction}
648
+ {req.config.system_instruction if req.config else None}
620
649
  -----------------------------------------------------------
621
650
  Contents:
622
651
  {_NEW_LINE.join(contents_logs)}
@@ -654,16 +683,42 @@ class LiteLlm(BaseLlm):
654
683
  """The LLM client to use for the model."""
655
684
 
656
685
  _additional_args: Dict[str, Any] = None
686
+ _oauth_token_manager: Optional[OAuth2ClientCredentialsTokenManager] = None
687
+ _cache_strategy: str = "5m" # Default to 5-minute ephemeral cache
657
688
 
658
- def __init__(self, model: str, **kwargs):
689
+ def __init__(self, model: str, cache_strategy: str = "5m", **kwargs):
659
690
  """Initializes the LiteLlm class.
660
691
 
661
692
  Args:
662
693
  model: The name of the LiteLlm model.
694
+ cache_strategy: Cache strategy to use. Options: "none", "5m" (ephemeral), "1h" (extended).
695
+ Defaults to "5m" for backward compatibility.
663
696
  **kwargs: Additional arguments to pass to the litellm completion api.
697
+ Can include OAuth configuration parameters.
664
698
  """
665
699
  super().__init__(model=model, **kwargs)
666
- self._additional_args = kwargs
700
+ self._additional_args = kwargs.copy()
701
+
702
+ # Validate and store cache strategy
703
+ valid_strategies = ["none", "5m", "1h"]
704
+ if cache_strategy not in valid_strategies:
705
+ logger.warning(
706
+ "Invalid cache_strategy '%s'. Valid options are: %s. Defaulting to '5m'.",
707
+ cache_strategy,
708
+ valid_strategies,
709
+ )
710
+ cache_strategy = "5m"
711
+ self._cache_strategy = cache_strategy
712
+ logger.info("LiteLlm initialized with cache strategy: %s", self._cache_strategy)
713
+
714
+ # Extract OAuth configuration if present
715
+ oauth_config = self._extract_oauth_config(self._additional_args)
716
+ if oauth_config:
717
+ self._oauth_token_manager = OAuth2ClientCredentialsTokenManager(**oauth_config)
718
+ logger.info("OAuth2 token manager initialized for model: %s", model)
719
+ else:
720
+ self._oauth_token_manager = None
721
+
667
722
  # preventing generation call with llm_client
668
723
  # and overriding messages, tools and stream which are managed internally
669
724
  self._additional_args.pop("llm_client", None)
@@ -672,6 +727,48 @@ class LiteLlm(BaseLlm):
672
727
  # public api called from runner determines to stream or not
673
728
  self._additional_args.pop("stream", None)
674
729
 
730
+ def _extract_oauth_config(self, kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
731
+ """Extract OAuth configuration from kwargs.
732
+
733
+ Args:
734
+ kwargs: Keyword arguments that may contain OAuth parameters
735
+
736
+ Returns:
737
+ OAuth configuration dictionary or None if no OAuth config found
738
+ """
739
+ oauth_params = [
740
+ "oauth_token_url",
741
+ "oauth_client_id",
742
+ "oauth_client_secret",
743
+ "oauth_scope",
744
+ "oauth_ca_cert",
745
+ "oauth_token_refresh_buffer_seconds",
746
+ "oauth_max_retries"
747
+ ]
748
+
749
+ oauth_config = {}
750
+ for param in oauth_params:
751
+ if param in kwargs:
752
+ # Map parameter names to OAuth2ClientCredentialsTokenManager constructor
753
+ if param == "oauth_ca_cert":
754
+ oauth_config["ca_cert_path"] = kwargs.pop(param)
755
+ elif param == "oauth_token_refresh_buffer_seconds":
756
+ oauth_config["refresh_buffer_seconds"] = kwargs.pop(param)
757
+ elif param == "oauth_max_retries":
758
+ oauth_config["max_retries"] = kwargs.pop(param)
759
+ else:
760
+ # Remove oauth_ prefix for the token manager
761
+ key = param.replace("oauth_", "")
762
+ oauth_config[key] = kwargs.pop(param)
763
+
764
+ # Return config only if we have the required parameters
765
+ if "token_url" in oauth_config and "client_id" in oauth_config and "client_secret" in oauth_config:
766
+ return oauth_config
767
+ elif oauth_config:
768
+ logger.warning("Incomplete OAuth configuration found, missing required parameters")
769
+
770
+ return None
771
+
675
772
  async def generate_content_async(
676
773
  self, llm_request: LlmRequest, stream: bool = False
677
774
  ) -> AsyncGenerator[LlmResponse, None]:
@@ -693,7 +790,7 @@ class LiteLlm(BaseLlm):
693
790
  logger.debug(_build_request_log(llm_request))
694
791
 
695
792
  messages, tools, response_format, generation_params = _get_completion_inputs(
696
- llm_request
793
+ llm_request, self._cache_strategy
697
794
  )
698
795
  completion_args = {
699
796
  "model": self.model,
@@ -704,6 +801,24 @@ class LiteLlm(BaseLlm):
704
801
  }
705
802
  completion_args.update(self._additional_args)
706
803
 
804
+ # Inject OAuth token if OAuth is configured
805
+ if self._oauth_token_manager:
806
+ try:
807
+ access_token = await self._oauth_token_manager.get_token()
808
+ # Inject Bearer token via extra_headers
809
+ extra_headers = completion_args.get("extra_headers", {})
810
+ extra_headers["Authorization"] = f"Bearer {access_token}"
811
+ completion_args["extra_headers"] = extra_headers
812
+ logger.debug("OAuth token injected into request headers")
813
+ except Exception as e:
814
+ logger.error("Failed to get OAuth token: %s", str(e))
815
+ # Check if we have a fallback API key
816
+ if "api_key" in completion_args:
817
+ logger.info("Falling back to API key authentication")
818
+ else:
819
+ logger.error("No fallback authentication available")
820
+ raise
821
+
707
822
  if generation_params:
708
823
  completion_args.update(generation_params)
709
824
 
@@ -0,0 +1,245 @@
1
+ """OAuth 2.0 Client Credentials Token Manager.
2
+
3
+ This module provides OAuth 2.0 Client Credentials flow implementation for LLM authentication.
4
+ It handles token acquisition, caching, and automatic refresh with proper error handling.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import random
10
+ import time
11
+ from typing import Any, Dict, Optional
12
+
13
+ import httpx
14
+
15
+ from solace_agent_mesh.common.utils.in_memory_cache import InMemoryCache
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class OAuth2ClientCredentialsTokenManager:
21
+ """Manages OAuth 2.0 Client Credentials tokens with caching and automatic refresh.
22
+
23
+ This class implements the OAuth 2.0 Client Credentials flow as defined in RFC 6749.
24
+ It provides thread-safe token management with automatic refresh before expiration
25
+ and integrates with the existing InMemoryCache for token storage.
26
+
27
+ Attributes:
28
+ token_url: OAuth 2.0 token endpoint URL
29
+ client_id: OAuth client identifier
30
+ client_secret: OAuth client secret
31
+ scope: OAuth scope (optional)
32
+ ca_cert_path: Path to custom CA certificate (optional)
33
+ refresh_buffer_seconds: Seconds before expiry to refresh token
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ token_url: str,
39
+ client_id: str,
40
+ client_secret: str,
41
+ scope: Optional[str] = None,
42
+ ca_cert_path: Optional[str] = None,
43
+ refresh_buffer_seconds: int = 300,
44
+ max_retries: int = 3,
45
+ ):
46
+ """Initialize the OAuth2 Client Credentials Token Manager.
47
+
48
+ Args:
49
+ token_url: OAuth 2.0 token endpoint URL
50
+ client_id: OAuth client identifier
51
+ client_secret: OAuth client secret
52
+ scope: OAuth scope (optional, space-separated string)
53
+ ca_cert_path: Path to custom CA certificate file (optional)
54
+ refresh_buffer_seconds: Seconds before actual expiry to refresh token
55
+ max_retries: Maximum number of retry attempts for token requests
56
+
57
+ Raises:
58
+ ValueError: If required parameters are missing or invalid
59
+ """
60
+ if not token_url:
61
+ raise ValueError("token_url is required")
62
+ if not client_id:
63
+ raise ValueError("client_id is required")
64
+ if not client_secret:
65
+ raise ValueError("client_secret is required")
66
+ if refresh_buffer_seconds < 0:
67
+ raise ValueError("refresh_buffer_seconds must be non-negative")
68
+
69
+ self.token_url = token_url
70
+ self.client_id = client_id
71
+ self.client_secret = client_secret
72
+ self.scope = scope
73
+ self.ca_cert_path = ca_cert_path
74
+ self.refresh_buffer_seconds = refresh_buffer_seconds
75
+ self.max_retries = max_retries
76
+
77
+ # Thread-safe token access
78
+ self._lock = asyncio.Lock()
79
+
80
+ # Token cache using existing InMemoryCache singleton
81
+ self._cache = InMemoryCache()
82
+
83
+ # Cache key for this token manager instance
84
+ self._cache_key = f"oauth_token_{hash((token_url, client_id))}"
85
+
86
+ logger.info(
87
+ "OAuth2ClientCredentialsTokenManager initialized for endpoint: %s",
88
+ token_url
89
+ )
90
+
91
+ async def get_token(self) -> str:
92
+ """Get a valid OAuth 2.0 access token.
93
+
94
+ This method checks the cache first and returns a cached token if it's still valid.
95
+ If no token exists or the token is expired/near expiry, it fetches a new token.
96
+
97
+ Returns:
98
+ Valid OAuth 2.0 access token
99
+
100
+ Raises:
101
+ httpx.HTTPError: If token request fails
102
+ ValueError: If token response is invalid
103
+ """
104
+ async with self._lock:
105
+ # Check if we have a cached token
106
+ cached_token_data = self._cache.get(self._cache_key)
107
+
108
+ if cached_token_data and not self._is_token_expired(cached_token_data):
109
+ logger.debug("Using cached OAuth token")
110
+ return cached_token_data["access_token"]
111
+
112
+ # Fetch new token
113
+ logger.info("Fetching new OAuth token from %s", self.token_url)
114
+ token_data = await self._fetch_token()
115
+
116
+ # Cache the token with TTL
117
+ expires_in = token_data.get("expires_in", 3600) # Default 1 hour
118
+ cache_ttl = max(expires_in - self.refresh_buffer_seconds, 60) # Min 1 minute
119
+
120
+ self._cache.set(self._cache_key, token_data, ttl=cache_ttl)
121
+
122
+ logger.info("OAuth token cached with TTL: %d seconds", cache_ttl)
123
+ return token_data["access_token"]
124
+
125
+ def _is_token_expired(self, token_data: Dict[str, Any]) -> bool:
126
+ """Check if a token is expired or near expiry.
127
+
128
+ Args:
129
+ token_data: Token data dictionary with 'expires_at' timestamp
130
+
131
+ Returns:
132
+ True if token is expired or near expiry, False otherwise
133
+ """
134
+ if "expires_at" not in token_data:
135
+ return True
136
+
137
+ current_time = time.time()
138
+ expires_at = token_data["expires_at"]
139
+
140
+ # Consider token expired if it expires within the buffer time
141
+ return current_time >= (expires_at - self.refresh_buffer_seconds)
142
+
143
+ async def _fetch_token(self) -> Dict[str, Any]:
144
+ """Fetch a new OAuth 2.0 access token from the token endpoint.
145
+
146
+ Implements retry logic with exponential backoff for transient failures.
147
+
148
+ Returns:
149
+ Token data dictionary containing access_token, expires_in, etc.
150
+
151
+ Raises:
152
+ httpx.HTTPError: If HTTP request fails after all retries
153
+ ValueError: If response is invalid or missing required fields
154
+ """
155
+ # Prepare request payload
156
+ payload = {
157
+ "grant_type": "client_credentials",
158
+ "client_id": self.client_id,
159
+ "client_secret": self.client_secret,
160
+ }
161
+
162
+ if self.scope:
163
+ payload["scope"] = self.scope
164
+
165
+ # Configure HTTP client with SSL settings
166
+ verify = True
167
+ if self.ca_cert_path:
168
+ verify = self.ca_cert_path
169
+
170
+ headers = {
171
+ "Content-Type": "application/x-www-form-urlencoded",
172
+ "Accept": "application/json",
173
+ }
174
+
175
+ last_exception = None
176
+
177
+ for attempt in range(self.max_retries + 1):
178
+ try:
179
+ async with httpx.AsyncClient(verify=verify) as client:
180
+ response = await client.post(
181
+ self.token_url,
182
+ data=payload,
183
+ headers=headers,
184
+ timeout=30.0,
185
+ )
186
+ response.raise_for_status()
187
+
188
+ token_data = response.json()
189
+
190
+ # Validate response
191
+ if "access_token" not in token_data:
192
+ raise ValueError("Token response missing 'access_token' field")
193
+
194
+ # Add expiration timestamp for cache management
195
+ expires_in = token_data.get("expires_in", 3600)
196
+ token_data["expires_at"] = time.time() + expires_in
197
+
198
+ logger.info("Successfully fetched OAuth token, expires in %d seconds", expires_in)
199
+ return token_data
200
+
201
+ except httpx.HTTPStatusError as e:
202
+ last_exception = e
203
+ # Don't retry on 4xx errors (client errors)
204
+ if 400 <= e.response.status_code < 500:
205
+ logger.error(
206
+ "OAuth token request failed with client error %d: %s",
207
+ e.response.status_code,
208
+ e.response.text
209
+ )
210
+ raise
211
+
212
+ logger.warning(
213
+ "OAuth token request failed with status %d (attempt %d/%d): %s",
214
+ e.response.status_code,
215
+ attempt + 1,
216
+ self.max_retries + 1,
217
+ e.response.text
218
+ )
219
+
220
+ except httpx.RequestError as e:
221
+ last_exception = e
222
+ logger.warning(
223
+ "OAuth token request failed (attempt %d/%d): %s",
224
+ attempt + 1,
225
+ self.max_retries + 1,
226
+ str(e)
227
+ )
228
+
229
+ except Exception as e:
230
+ last_exception = e
231
+ logger.error("Unexpected error during OAuth token fetch: %s", str(e))
232
+ raise
233
+
234
+ # Exponential backoff with jitter for retries
235
+ if attempt < self.max_retries:
236
+ delay = (2 ** attempt) + random.uniform(0, 1)
237
+ logger.info("Retrying OAuth token request in %.2f seconds", delay)
238
+ await asyncio.sleep(delay)
239
+
240
+ # All retries exhausted
241
+ logger.error("OAuth token request failed after %d attempts", self.max_retries + 1)
242
+ if last_exception:
243
+ raise last_exception
244
+ else:
245
+ raise RuntimeError("OAuth token request failed after all retries")
@@ -137,6 +137,9 @@ async def process_event(component, event: Event):
137
137
  )
138
138
  if timer_data.get("timer_id") == component._card_publish_timer_id:
139
139
  publish_agent_card(component)
140
+ else:
141
+ # Handle other timer events including health check timer
142
+ component.handle_timer_event(timer_data)
140
143
  elif event.event_type == EventType.CACHE_EXPIRY:
141
144
  # Delegate cache expiry handling to the component itself.
142
145
  await component.handle_cache_expiry_event(event.data)
@@ -794,9 +797,26 @@ def handle_agent_card_message(component, message: SolaceMessage):
794
797
  break
795
798
 
796
799
  if is_allowed:
797
- # The received card is stored as-is. We don't need to modify it.
800
+
801
+ # Also store in peer_agents for backward compatibility
798
802
  component.peer_agents[agent_name] = agent_card
799
803
 
804
+ # Store the agent card in the registry for health tracking
805
+ is_new = component.agent_registry.add_or_update_agent(agent_card)
806
+
807
+ if is_new:
808
+ log.info(
809
+ "%s Registered new agent '%s' in registry.",
810
+ component.log_identifier,
811
+ agent_name,
812
+ )
813
+ else:
814
+ log.debug(
815
+ "%s Updated existing agent '%s' in registry.",
816
+ component.log_identifier,
817
+ agent_name,
818
+ )
819
+
800
820
  message.call_acknowledgements()
801
821
 
802
822
  except Exception as e:
@@ -1438,6 +1458,7 @@ def publish_agent_card(component):
1438
1458
  dynamic_url = f"solace:{agent_request_topic}"
1439
1459
 
1440
1460
  # Define unique URIs for our custom extensions.
1461
+ DEPLOYMENT_EXTENSION_URI = "https://solace.com/a2a/extensions/sam/deployment"
1441
1462
  PEER_TOPOLOGY_EXTENSION_URI = (
1442
1463
  "https://solace.com/a2a/extensions/peer-agent-topology"
1443
1464
  )
@@ -1446,6 +1467,24 @@ def publish_agent_card(component):
1446
1467
 
1447
1468
  extensions_list = []
1448
1469
 
1470
+ # Create the extension object for deployment tracking.
1471
+ deployment_config = component.get_config("deployment", {})
1472
+ deployment_id = deployment_config.get("id")
1473
+
1474
+ if deployment_id:
1475
+ deployment_extension = AgentExtension(
1476
+ uri=DEPLOYMENT_EXTENSION_URI,
1477
+ description="SAM deployment tracking for rolling updates",
1478
+ required=False,
1479
+ params={"id": deployment_id}
1480
+ )
1481
+ extensions_list.append(deployment_extension)
1482
+ log.debug(
1483
+ "%s Added deployment extension with ID: %s",
1484
+ component.log_identifier,
1485
+ deployment_id
1486
+ )
1487
+
1449
1488
  # Create the extension object for peer agents.
1450
1489
  if peer_agents:
1451
1490
  peer_topology_extension = AgentExtension(
File without changes
@@ -0,0 +1,3 @@
1
+ """
2
+ This package contains the concrete implementation for proxying standard A2A-over-HTTPS agents.
3
+ """
@@ -0,0 +1,55 @@
1
+ """
2
+ Concrete App class for the A2A-over-HTTPS proxy.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, Type
8
+
9
+ from pydantic import ValidationError
10
+ from solace_ai_connector.common.log import log
11
+
12
+ from ..base.app import BaseProxyApp
13
+ from ..base.component import BaseProxyComponent
14
+ from .component import A2AProxyComponent
15
+ from .config import A2AProxyAppConfig
16
+
17
+ info = {
18
+ "class_name": "A2AProxyApp",
19
+ }
20
+
21
+
22
+ class A2AProxyApp(BaseProxyApp):
23
+ """
24
+ Concrete App class for the A2A-over-HTTPS proxy.
25
+
26
+ Extends the BaseProxyApp to add specific configuration validation for
27
+ A2A agents (e.g., URL, authentication).
28
+ """
29
+
30
+ # Keep app_schema for documentation purposes, but it's now redundant
31
+ # The Pydantic models handle all validation
32
+ app_schema = {}
33
+
34
+ def __init__(self, app_info: Dict[str, Any], **kwargs):
35
+ app_info["class_name"] = "A2AProxyApp"
36
+
37
+ # Validate A2A-specific configuration before calling super().__init__
38
+ app_config_dict = app_info.get("app_config", {})
39
+ try:
40
+ # Validate with A2A-specific config model
41
+ app_config = A2AProxyAppConfig.model_validate_and_clean(app_config_dict)
42
+ # Overwrite the raw dict with the validated object
43
+ app_info["app_config"] = app_config
44
+ log.debug("A2A proxy configuration validated successfully.")
45
+ except ValidationError as e:
46
+ log.error("A2A proxy configuration validation failed:\n%s", e)
47
+ raise ValueError(f"Invalid A2A proxy configuration: {e}") from e
48
+
49
+ super().__init__(app_info, **kwargs)
50
+
51
+ def _get_component_class(self) -> Type[BaseProxyComponent]:
52
+ """
53
+ Returns the concrete A2AProxyComponent class.
54
+ """
55
+ return A2AProxyComponent