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
@@ -0,0 +1,85 @@
1
+ """
2
+ Pydantic configuration models for proxy applications.
3
+ """
4
+
5
+ from typing import List, Literal, Optional
6
+
7
+ from pydantic import Field, model_validator
8
+
9
+ from ....common.utils.pydantic_utils import SamConfigBase
10
+
11
+
12
+ class ArtifactServiceConfig(SamConfigBase):
13
+ """Configuration for the shared Artifact Service."""
14
+
15
+ type: str = Field(
16
+ ..., description="Service type (e.g., 'memory', 'gcs', 'filesystem')."
17
+ )
18
+ base_path: Optional[str] = Field(
19
+ default=None,
20
+ description="Base directory path (required for type 'filesystem').",
21
+ )
22
+ bucket_name: Optional[str] = Field(
23
+ default=None, description="GCS bucket name (required for type 'gcs')."
24
+ )
25
+ artifact_scope: Literal["namespace", "app", "custom"] = Field(
26
+ default="namespace", description="Process-wide scope for all artifact services."
27
+ )
28
+ artifact_scope_value: Optional[str] = Field(
29
+ default=None,
30
+ description="Custom identifier for artifact scope (required if artifact_scope is 'custom').",
31
+ )
32
+
33
+ @model_validator(mode="after")
34
+ def check_artifact_scope(self) -> "ArtifactServiceConfig":
35
+ if self.artifact_scope == "custom" and not self.artifact_scope_value:
36
+ raise ValueError(
37
+ "'artifact_scope_value' is required when 'artifact_scope' is 'custom'."
38
+ )
39
+ if self.artifact_scope != "custom" and self.artifact_scope_value:
40
+ from solace_ai_connector.common.log import log
41
+ log.warning(
42
+ "Configuration Warning: 'artifact_scope_value' is ignored when 'artifact_scope' is not 'custom'."
43
+ )
44
+ return self
45
+
46
+
47
+ class ProxiedAgentConfig(SamConfigBase):
48
+ """Base configuration for a proxied agent."""
49
+
50
+ name: str = Field(
51
+ ...,
52
+ description="The name the agent will have on the Solace mesh.",
53
+ )
54
+ request_timeout_seconds: Optional[int] = Field(
55
+ default=None,
56
+ description="Optional timeout override for this specific agent.",
57
+ )
58
+
59
+
60
+ class BaseProxyAppConfig(SamConfigBase):
61
+ """Base configuration for all proxy applications."""
62
+
63
+ namespace: str = Field(
64
+ ...,
65
+ description="Absolute topic prefix for A2A communication (e.g., 'myorg/dev').",
66
+ )
67
+ proxied_agents: List[ProxiedAgentConfig] = Field(
68
+ ...,
69
+ min_length=1,
70
+ description="A list of downstream agents to be proxied.",
71
+ )
72
+ artifact_service: ArtifactServiceConfig = Field(
73
+ default_factory=lambda: ArtifactServiceConfig(type="memory"),
74
+ description="Configuration for the shared Artifact Service.",
75
+ )
76
+ discovery_interval_seconds: int = Field(
77
+ default=60,
78
+ ge=0,
79
+ description="Interval (seconds) to re-fetch agent cards. <= 0 disables periodic discovery.",
80
+ )
81
+ default_request_timeout_seconds: int = Field(
82
+ default=300,
83
+ gt=0,
84
+ description="Default timeout in seconds for requests to downstream agents.",
85
+ )
@@ -0,0 +1,17 @@
1
+ """
2
+ Encapsulates the runtime state for a single, in-flight proxied agent task.
3
+ """
4
+
5
+ from typing import Any, Dict
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass
10
+ class ProxyTaskContext:
11
+ """
12
+ A class to hold all runtime state and control mechanisms for a single proxied agent task.
13
+ This object is created when a task is initiated and destroyed when it completes.
14
+ """
15
+
16
+ task_id: str
17
+ a2a_context: Dict[str, Any]
@@ -23,7 +23,7 @@ from ...common.a2a import (
23
23
  get_agent_status_subscription_topic,
24
24
  get_sam_events_subscription_topic,
25
25
  )
26
- from ...common.constants import DEFAULT_COMMUNICATION_TIMEOUT, TEXT_ARTIFACT_CONTEXT_MAX_LENGTH_CAPACITY, TEXT_ARTIFACT_CONTEXT_DEFAULT_LENGTH
26
+ from ...common.constants import DEFAULT_COMMUNICATION_TIMEOUT, TEXT_ARTIFACT_CONTEXT_MAX_LENGTH_CAPACITY, TEXT_ARTIFACT_CONTEXT_DEFAULT_LENGTH, HEALTH_CHECK_TTL_SECONDS, HEALTH_CHECK_INTERVAL_SECONDS
27
27
  from ...agent.sac.component import SamAgentComponent
28
28
  from ...agent.utils.artifact_helpers import DEFAULT_SCHEMA_MAX_KEYS
29
29
  from ...common.utils.pydantic_utils import SamConfigBase
@@ -79,6 +79,12 @@ class AgentDiscoveryConfig(SamConfigBase):
79
79
  enabled: bool = Field(
80
80
  default=True, description="Enable discovery and instruction injection."
81
81
  )
82
+ health_check_ttl_seconds: int = Field(
83
+ default=HEALTH_CHECK_TTL_SECONDS, description="Time-to-live in seconds after which an unresponsive agent is de-registered."
84
+ )
85
+ health_check_interval_seconds: int = Field(
86
+ default=HEALTH_CHECK_INTERVAL_SECONDS, description="Interval in seconds between health checks."
87
+ )
82
88
 
83
89
 
84
90
  class InterAgentCommunicationConfig(SamConfigBase):
@@ -452,8 +458,8 @@ class SamAgentApp(App):
452
458
  broker_config["queue_name"] = generated_queue_name
453
459
  log.debug("Injected generated broker.queue_name: %s", generated_queue_name)
454
460
 
455
- broker_config["temporary_queue"] = True
456
- log.debug("Set broker_config.temporary_queue = True")
461
+ broker_config["temporary_queue"] = app_info.get("broker", {}).get("temporary_queue", True)
462
+ log.debug("Set broker_config.temporary_queue = %s", broker_config["temporary_queue"])
457
463
 
458
464
  super().__init__(app_info, **kwargs)
459
465
  log.debug("%s Agent initialization complete.", agent_name)
@@ -8,10 +8,8 @@ import asyncio
8
8
  import functools
9
9
  import threading
10
10
  import concurrent.futures
11
- import uuid
12
11
  import fnmatch
13
- import base64
14
- from datetime import datetime, timezone
12
+ import time
15
13
  import json
16
14
  from solace_ai_connector.common.message import (
17
15
  Message as SolaceMessage,
@@ -73,9 +71,10 @@ from ...agent.tools.peer_agent_tool import (
73
71
  PEER_TOOL_PREFIX,
74
72
  )
75
73
  from ...common.middleware.registry import MiddlewareRegistry
76
- from ...common.constants import DEFAULT_COMMUNICATION_TIMEOUT
74
+ from ...common.constants import DEFAULT_COMMUNICATION_TIMEOUT, HEALTH_CHECK_TTL_SECONDS, HEALTH_CHECK_INTERVAL_SECONDS
77
75
  from ...agent.tools.registry import tool_registry
78
76
  from ...common.sac.sam_component_base import SamComponentBase
77
+ from ...common.agent_registry import AgentRegistry
79
78
 
80
79
  log = logging.getLogger(__name__)
81
80
 
@@ -113,6 +112,7 @@ class SamAgentComponent(SamComponentBase):
113
112
 
114
113
  CORRELATION_DATA_PREFIX = CORRELATION_DATA_PREFIX
115
114
  HOST_COMPONENT_VERSION = "1.0.0-alpha"
115
+ HEALTH_CHECK_TIMER_ID = "agent_health_check"
116
116
 
117
117
  def __init__(self, **kwargs):
118
118
  """
@@ -129,6 +129,9 @@ class SamAgentComponent(SamComponentBase):
129
129
  super().__init__(info, **kwargs)
130
130
  self.agent_name = self.get_config("agent_name")
131
131
  log.info("%s Initializing A2A ADK Host Component...", self.log_identifier)
132
+
133
+ # Initialize the agent registry for health tracking
134
+ self.agent_registry = AgentRegistry()
132
135
  try:
133
136
  self.namespace = self.get_config("namespace")
134
137
  if not self.namespace:
@@ -237,7 +240,7 @@ class SamAgentComponent(SamComponentBase):
237
240
  self.adk_agent: LlmAgent = None
238
241
  self.runner: Runner = None
239
242
  self.agent_card_tool_manifest: List[Dict[str, Any]] = []
240
- self.peer_agents: Dict[str, Any] = {}
243
+ self.peer_agents: Dict[str, Any] = {} # Keep for backward compatibility
241
244
  self._card_publish_timer_id: str = f"publish_card_{self.agent_name}"
242
245
  self._async_init_future = None
243
246
  self.peer_response_queues: Dict[str, asyncio.Queue] = {}
@@ -414,6 +417,26 @@ class SamAgentComponent(SamComponentBase):
414
417
  "%s Agent card publishing interval not configured or invalid, card will not be published periodically.",
415
418
  self.log_identifier,
416
419
  )
420
+
421
+ # Set up health check timer if enabled
422
+ health_check_interval_seconds = self.agent_discovery_config.get("health_check_interval_seconds", HEALTH_CHECK_INTERVAL_SECONDS)
423
+ if health_check_interval_seconds > 0:
424
+ log.info(
425
+ "%s Scheduling agent health check every %d seconds.",
426
+ self.log_identifier,
427
+ health_check_interval_seconds,
428
+ )
429
+ self.add_timer(
430
+ delay_ms=health_check_interval_seconds * 1000,
431
+ timer_id=self.HEALTH_CHECK_TIMER_ID,
432
+ interval_ms=health_check_interval_seconds * 1000,
433
+ )
434
+ else:
435
+ log.warning(
436
+ "%s Agent health check interval not configured or invalid, health checks will not run periodically.",
437
+ self.log_identifier,
438
+ )
439
+
417
440
  log.info(
418
441
  "%s Initialization complete for agent: %s",
419
442
  self.log_identifier,
@@ -488,10 +511,14 @@ class SamAgentComponent(SamComponentBase):
488
511
  )
489
512
 
490
513
  def handle_timer_event(self, timer_data: Dict[str, Any]):
491
- """Handles timer events, specifically for agent card publishing."""
514
+ """Handles timer events for agent card publishing and health checks."""
492
515
  log.debug("%s Received timer event: %s", self.log_identifier, timer_data)
493
- if timer_data.get("timer_id") == self._card_publish_timer_id:
516
+ timer_id = timer_data.get("timer_id")
517
+
518
+ if timer_id == self._card_publish_timer_id:
494
519
  publish_agent_card(self)
520
+ elif timer_id == self.HEALTH_CHECK_TIMER_ID:
521
+ self._check_agent_health()
495
522
 
496
523
  async def handle_cache_expiry_event(self, cache_data: Dict[str, Any]):
497
524
  """
@@ -866,7 +893,8 @@ class SamAgentComponent(SamComponentBase):
866
893
  peer_tools_to_add = []
867
894
  allowed_peer_descriptions = []
868
895
 
869
- for peer_name, agent_card in self.peer_agents.items():
896
+ # Sort peer agents alphabetically to ensure consistent tool ordering for prompt caching
897
+ for peer_name, agent_card in sorted(self.peer_agents.items()):
870
898
  if not isinstance(agent_card, AgentCard) or peer_name == self_name:
871
899
  continue
872
900
 
@@ -2991,6 +3019,7 @@ class SamAgentComponent(SamComponentBase):
2991
3019
  """Clean up resources on component shutdown."""
2992
3020
  log.info("%s Cleaning up A2A ADK Host Component.", self.log_identifier)
2993
3021
  self.cancel_timer(self._card_publish_timer_id)
3022
+ self.cancel_timer(self.HEALTH_CHECK_TIMER_ID)
2994
3023
 
2995
3024
  cleanup_func_details = self.get_config("agent_cleanup_function")
2996
3025
 
@@ -3151,6 +3180,129 @@ class SamAgentComponent(SamComponentBase):
3151
3180
  For now, using the agent name, but could be made more robust (e.g., hostname + agent name).
3152
3181
  """
3153
3182
  return self.agent_name
3183
+
3184
+ def _check_agent_health(self):
3185
+ """
3186
+ Checks the health of peer agents and de-registers unresponsive ones.
3187
+ This is called periodically by the health check timer.
3188
+ Uses TTL-based expiration to determine if an agent is unresponsive.
3189
+ """
3190
+
3191
+ log.debug("%s Performing agent health check...", self.log_identifier)
3192
+
3193
+ ttl_seconds = self.agent_discovery_config.get("health_check_ttl_seconds", HEALTH_CHECK_TTL_SECONDS)
3194
+ health_check_interval = self.agent_discovery_config.get("health_check_interval_seconds", HEALTH_CHECK_INTERVAL_SECONDS)
3195
+
3196
+ log.debug(
3197
+ "%s Health check configuration: interval=%d seconds, TTL=%d seconds",
3198
+ self.log_identifier,
3199
+ health_check_interval,
3200
+ ttl_seconds
3201
+ )
3202
+
3203
+ # Validate configuration values
3204
+ if ttl_seconds <= 0 or health_check_interval <= 0 or ttl_seconds < health_check_interval:
3205
+ log.error(
3206
+ "%s agent_health_check_ttl_seconds (%d) and agent_health_check_interval_seconds (%d) must be positive and TTL must be greater than interval.",
3207
+ self.log_identifier,
3208
+ ttl_seconds,
3209
+ health_check_interval
3210
+ )
3211
+ raise ValueError(f"Invalid health check configuration. agent_health_check_ttl_seconds ({ttl_seconds}) and agent_health_check_interval_seconds ({health_check_interval}) must be positive and TTL must be greater than interval.")
3212
+
3213
+ # Get all agent names from the registry
3214
+ agent_names = self.agent_registry.get_agent_names()
3215
+ total_agents = len(agent_names)
3216
+ agents_to_deregister = []
3217
+
3218
+ log.debug("%s Checking health of %d peer agents", self.log_identifier, total_agents)
3219
+
3220
+ for agent_name in agent_names:
3221
+ # Skip our own agent
3222
+ if agent_name == self.agent_name:
3223
+ continue
3224
+
3225
+ # Check if the agent's TTL has expired
3226
+ is_expired, time_since_last_seen = self.agent_registry.check_ttl_expired(agent_name, ttl_seconds)
3227
+
3228
+ if is_expired:
3229
+ log.warning(
3230
+ "%s Agent '%s' TTL has expired. De-registering. Time since last seen: %d seconds (TTL: %d seconds)",
3231
+ self.log_identifier,
3232
+ agent_name,
3233
+ time_since_last_seen,
3234
+ ttl_seconds
3235
+ )
3236
+ agents_to_deregister.append(agent_name)
3237
+
3238
+ # De-register unresponsive agents
3239
+ for agent_name in agents_to_deregister:
3240
+ self._deregister_agent(agent_name)
3241
+
3242
+ log.debug(
3243
+ "%s Agent health check completed. Total agents: %d, De-registered: %d",
3244
+ self.log_identifier,
3245
+ total_agents,
3246
+ len(agents_to_deregister)
3247
+ )
3248
+
3249
+ def _deregister_agent(self, agent_name: str):
3250
+ """
3251
+ De-registers an agent from the registry and publishes a de-registration event.
3252
+ """
3253
+ # Remove from registry
3254
+ registry_removed = self.agent_registry.remove_agent(agent_name)
3255
+
3256
+ # Always remove from peer_agents regardless of registry result
3257
+ peer_removed = False
3258
+ if agent_name in self.peer_agents:
3259
+ del self.peer_agents[agent_name]
3260
+ peer_removed = True
3261
+ log.info(
3262
+ "%s Removed agent '%s' from peer_agents dictionary",
3263
+ self.log_identifier,
3264
+ agent_name
3265
+ )
3266
+
3267
+ # Publish de-registration event if agent was in either data structure
3268
+ if registry_removed or peer_removed:
3269
+ try:
3270
+ # Create a de-registration event topic
3271
+ namespace = self.get_config("namespace")
3272
+ deregistration_topic = f"{namespace}/a2a/events/agent/deregistered"
3273
+
3274
+ current_time = time.time()
3275
+
3276
+ # Create the payload
3277
+ deregistration_payload = {
3278
+ "event_type": "agent.deregistered",
3279
+ "agent_name": agent_name,
3280
+ "reason": "health_check_failure",
3281
+ "metadata": {
3282
+ "timestamp": current_time,
3283
+ "deregistered_by": self.agent_name
3284
+ }
3285
+ }
3286
+
3287
+ # Publish the event
3288
+ self.publish_a2a_message(
3289
+ payload=deregistration_payload,
3290
+ topic=deregistration_topic
3291
+ )
3292
+
3293
+ log.info(
3294
+ "%s Published de-registration event for agent '%s' to topic '%s'",
3295
+ self.log_identifier,
3296
+ agent_name,
3297
+ deregistration_topic
3298
+ )
3299
+ except Exception as e:
3300
+ log.error(
3301
+ "%s Failed to publish de-registration event for agent '%s': %s",
3302
+ self.log_identifier,
3303
+ agent_name,
3304
+ e
3305
+ )
3154
3306
 
3155
3307
  async def _resolve_early_embeds_and_handle_signals(
3156
3308
  self, raw_text: str, a2a_context: Dict
@@ -25,6 +25,7 @@ from pydub import AudioSegment
25
25
  from ...agent.utils.artifact_helpers import (
26
26
  load_artifact_content_or_metadata,
27
27
  save_artifact_with_metadata,
28
+ ensure_correct_extension,
28
29
  DEFAULT_SCHEMA_MAX_KEYS,
29
30
  )
30
31
  from ...agent.utils.context_helpers import get_original_session_id
@@ -1210,14 +1211,18 @@ async def concatenate_audio(
1210
1211
 
1211
1212
  async def transcribe_audio(
1212
1213
  audio_filename: str,
1214
+ output_filename: Optional[str] = None,
1215
+ description: Optional[str] = None,
1213
1216
  tool_context: ToolContext = None,
1214
1217
  tool_config: Optional[Dict[str, Any]] = None,
1215
1218
  ) -> Dict[str, Any]:
1216
1219
  """
1217
- Transcribes an audio recording using an OpenAI-compatible audio transcription API.
1220
+ Transcribes an audio recording and saves the transcription as a text artifact.
1218
1221
 
1219
1222
  Args:
1220
1223
  audio_filename: The filename (and optional :version) of the input audio artifact.
1224
+ output_filename: Optional filename for the transcription text file (without extension).
1225
+ description: Optional description of the transcription for metadata.
1221
1226
  tool_context: The context provided by the ADK framework.
1222
1227
  tool_config: Configuration dictionary containing model, api_base, api_key.
1223
1228
 
@@ -1225,9 +1230,10 @@ async def transcribe_audio(
1225
1230
  A dictionary containing:
1226
1231
  - "status": "success" or "error".
1227
1232
  - "message": A descriptive message about the outcome.
1228
- - "transcription": The transcribed text from the API (if successful).
1229
- - "audio_filename": The name of the input audio artifact (if successful).
1230
- - "audio_version": The version of the input audio artifact (if successful).
1233
+ - "output_filename": The name of the saved transcription artifact.
1234
+ - "output_version": The version of the saved transcription artifact.
1235
+ - "audio_filename": The name of the input audio artifact.
1236
+ - "audio_version": The version of the input audio artifact.
1231
1237
  """
1232
1238
  log_identifier = f"[AudioTools:transcribe_audio:{audio_filename}]"
1233
1239
  if not tool_context:
@@ -1340,8 +1346,28 @@ async def transcribe_audio(
1340
1346
  )
1341
1347
 
1342
1348
  audio_bytes = audio_artifact_part.inline_data.data
1349
+ audio_mime_type = audio_artifact_part.inline_data.mime_type or "application/octet-stream"
1343
1350
  log.debug(f"{log_identifier} Loaded audio artifact: {len(audio_bytes)} bytes")
1344
1351
 
1352
+ # Load source audio metadata to copy description
1353
+ source_audio_metadata = {}
1354
+ try:
1355
+ metadata_result = await load_artifact_content_or_metadata(
1356
+ artifact_service=artifact_service,
1357
+ app_name=app_name,
1358
+ user_id=user_id,
1359
+ session_id=session_id,
1360
+ filename=filename_base_for_load,
1361
+ version=version_to_load,
1362
+ load_metadata_only=True,
1363
+ log_identifier_prefix=f"{log_identifier}[source_metadata]",
1364
+ )
1365
+ if metadata_result.get("status") == "success":
1366
+ source_audio_metadata = metadata_result.get("metadata", {})
1367
+ log.debug(f"{log_identifier} Loaded source audio metadata")
1368
+ except Exception as meta_err:
1369
+ log.warning(f"{log_identifier} Could not load source audio metadata: {meta_err}")
1370
+
1345
1371
  temp_file_path = None
1346
1372
  try:
1347
1373
  file_ext = os.path.splitext(filename_base_for_load)[1]
@@ -1383,12 +1409,93 @@ async def transcribe_audio(
1383
1409
  f"{log_identifier} Audio transcribed successfully. Transcription length: {len(transcription)} characters"
1384
1410
  )
1385
1411
 
1412
+ # Determine output filename
1413
+ if output_filename:
1414
+ final_filename = ensure_correct_extension(output_filename, "txt")
1415
+ else:
1416
+ # Auto-generate from source audio filename
1417
+ base_name = os.path.splitext(filename_base_for_load)[0]
1418
+ final_filename = f"{base_name}_transcription.txt"
1419
+
1420
+ # Build comprehensive metadata
1421
+ transcription_word_count = len(transcription.split())
1422
+ transcription_char_count = len(transcription)
1423
+
1424
+ # Build description from multiple sources
1425
+ description_parts = []
1426
+
1427
+ # Add user-provided description
1428
+ if description:
1429
+ description_parts.append(description)
1430
+
1431
+ # Add source audio description if available
1432
+ source_description = source_audio_metadata.get("description")
1433
+ if source_description:
1434
+ description_parts.append(f"Source: {source_description}")
1435
+
1436
+ # Add source audio info
1437
+ description_parts.append(f"Transcribed from audio file '{filename_base_for_load}' (version {version_to_load}, {audio_mime_type})")
1438
+
1439
+ # Combine all description parts
1440
+ final_description = ". ".join(description_parts)
1441
+
1442
+ metadata = {
1443
+ "description": final_description,
1444
+ "source_audio_filename": filename_base_for_load,
1445
+ "source_audio_version": version_to_load,
1446
+ "source_audio_mime_type": audio_mime_type,
1447
+ "transcription_model": model_name,
1448
+ "transcription_timestamp": datetime.now(timezone.utc).isoformat(),
1449
+ "transcription_word_count": transcription_word_count,
1450
+ "transcription_char_count": transcription_char_count,
1451
+ "generation_tool": "transcribe_audio",
1452
+ }
1453
+
1454
+ # Copy source audio description separately for reference
1455
+ if source_description:
1456
+ metadata["source_audio_description"] = source_description
1457
+
1458
+ # Add user-provided description separately if provided
1459
+ if description:
1460
+ metadata["user_provided_description"] = description
1461
+
1462
+ # Save transcription as text artifact
1463
+ transcription_bytes = transcription.encode("utf-8")
1464
+
1465
+ save_result = await save_artifact_with_metadata(
1466
+ artifact_service=artifact_service,
1467
+ app_name=app_name,
1468
+ user_id=user_id,
1469
+ session_id=session_id,
1470
+ filename=final_filename,
1471
+ content_bytes=transcription_bytes,
1472
+ mime_type="text/plain",
1473
+ metadata_dict=metadata,
1474
+ timestamp=datetime.now(timezone.utc),
1475
+ schema_max_keys=DEFAULT_SCHEMA_MAX_KEYS,
1476
+ tool_context=tool_context,
1477
+ )
1478
+
1479
+ if save_result.get("status") != "success":
1480
+ error_msg = save_result.get("message", "Failed to save transcription artifact")
1481
+ log.error(f"{log_identifier} {error_msg}")
1482
+ return {
1483
+ "status": "error",
1484
+ "message": f"Transcription succeeded but failed to save as artifact: {error_msg}",
1485
+ }
1486
+
1487
+ log.info(
1488
+ f"{log_identifier} Transcription saved to '{final_filename}' v{save_result['data_version']}"
1489
+ )
1490
+
1386
1491
  return {
1387
1492
  "status": "success",
1388
- "message": "Audio transcribed successfully",
1389
- "transcription": transcription,
1493
+ "message": "Audio transcribed and saved successfully",
1494
+ "output_filename": final_filename,
1495
+ "output_version": save_result["data_version"],
1390
1496
  "audio_filename": filename_base_for_load,
1391
1497
  "audio_version": version_to_load,
1498
+ "result_preview": f"Transcription saved to '{final_filename}' (v{save_result['data_version']}). Length: {transcription_char_count} characters, {transcription_word_count} words."
1392
1499
  }
1393
1500
 
1394
1501
  finally:
@@ -1600,9 +1707,9 @@ concatenate_audio_tool_def = BuiltinTool(
1600
1707
  transcribe_audio_tool_def = BuiltinTool(
1601
1708
  name="transcribe_audio",
1602
1709
  implementation=transcribe_audio,
1603
- description="Transcribes an audio recording using an OpenAI-compatible audio transcription API.",
1710
+ description="Transcribes an audio recording and saves the transcription as a text artifact.",
1604
1711
  category="audio",
1605
- required_scopes=["tool:audio:transcribe"],
1712
+ required_scopes=["tool:audio:transcribe", "tool:artifact:create"],
1606
1713
  parameters=adk_types.Schema(
1607
1714
  type=adk_types.Type.OBJECT,
1608
1715
  properties={
@@ -1610,6 +1717,16 @@ transcribe_audio_tool_def = BuiltinTool(
1610
1717
  type=adk_types.Type.STRING,
1611
1718
  description="The filename (and optional :version) of the input audio artifact.",
1612
1719
  ),
1720
+ "output_filename": adk_types.Schema(
1721
+ type=adk_types.Type.STRING,
1722
+ description="Optional filename for the transcription text file (without .txt extension). If not provided, will auto-generate from source audio filename.",
1723
+ nullable=True,
1724
+ ),
1725
+ "description": adk_types.Schema(
1726
+ type=adk_types.Type.STRING,
1727
+ description="Optional description of the transcription for metadata (e.g., 'Transcription of customer support call about billing inquiry'). Will be combined with source audio description if available.",
1728
+ nullable=True,
1729
+ ),
1613
1730
  },
1614
1731
  required=["audio_filename"],
1615
1732
  ),
@@ -92,7 +92,12 @@ async def web_request(
92
92
  log.error(f"{log_identifier} ToolContext is missing.")
93
93
  return {"status": "error", "message": "ToolContext is missing."}
94
94
 
95
- if not _is_safe_url(url):
95
+ # Check if loopback URLs are allowed (for testing)
96
+ allow_loopback = False
97
+ if tool_config:
98
+ allow_loopback = tool_config.get("allow_loopback", False)
99
+
100
+ if not allow_loopback and not _is_safe_url(url):
96
101
  log.error(f"{log_identifier} URL is not safe to request: {url}")
97
102
  return {"status": "error", "message": "URL is not safe to request."}
98
103
 
@@ -150,10 +155,9 @@ async def web_request(
150
155
  )
151
156
 
152
157
  response_content_bytes = response.content
153
- response_headers = dict(response.headers)
154
158
  response_status_code = response.status_code
155
159
  original_content_type = (
156
- response_headers.get("content-type", "application/octet-stream")
160
+ response.headers.get("content-type", "application/octet-stream")
157
161
  .split(";")[0]
158
162
  .strip()
159
163
  )
@@ -238,7 +242,7 @@ async def web_request(
238
242
  {k: v for k, v in headers.items() if k.lower() != "authorization"}
239
243
  ),
240
244
  "response_status_code": response_status_code,
241
- "response_headers": json.dumps(response_headers),
245
+ "response_headers": json.dumps(dict(response.headers)),
242
246
  "original_content_type": original_content_type,
243
247
  "processed_content_type": processed_content_type,
244
248
  "timestamp": datetime.now(timezone.utc).isoformat(),
@@ -285,7 +289,8 @@ async def web_request(
285
289
  return {
286
290
  "status": "success",
287
291
  "message": f"Successfully fetched content from {url} (status: {response_status_code}). "
288
- f"Saved as artifact '{final_artifact_filename}' v{save_result['data_version']}.",
292
+ f"Saved as artifact '{final_artifact_filename}' v{save_result['data_version']}. "
293
+ f"Analyze the content of '{final_artifact_filename}' before providing a final answer to the user.",
289
294
  "output_filename": final_artifact_filename,
290
295
  "output_version": save_result["data_version"],
291
296
  "response_status_code": response_status_code,