solace-agent-mesh 1.4.12__py3-none-any.whl → 1.5.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 (181) hide show
  1. solace_agent_mesh/agent/adk/adk_llm.txt +3 -4
  2. solace_agent_mesh/agent/adk/adk_llm_detail.txt +566 -0
  3. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +1 -1
  4. solace_agent_mesh/agent/adk/callbacks.py +51 -2
  5. solace_agent_mesh/agent/adk/models/lite_llm.py +1 -0
  6. solace_agent_mesh/agent/adk/models/models_llm.txt +1 -2
  7. solace_agent_mesh/agent/agent_llm.txt +1 -1
  8. solace_agent_mesh/agent/agent_llm_detail.txt +1702 -0
  9. solace_agent_mesh/agent/protocol/event_handlers.py +2 -13
  10. solace_agent_mesh/agent/protocol/protocol_llm.txt +15 -2
  11. solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +92 -0
  12. solace_agent_mesh/agent/sac/component.py +51 -21
  13. solace_agent_mesh/agent/sac/sac_llm.txt +15 -1
  14. solace_agent_mesh/agent/sac/sac_llm_detail.txt +200 -0
  15. solace_agent_mesh/agent/sac/task_execution_context.py +73 -0
  16. solace_agent_mesh/agent/testing/testing_llm_detail.txt +68 -0
  17. solace_agent_mesh/agent/tools/tools_llm.txt +148 -154
  18. solace_agent_mesh/agent/tools/tools_llm_detail.txt +274 -0
  19. solace_agent_mesh/agent/utils/utils_llm.txt +1 -1
  20. solace_agent_mesh/agent/utils/utils_llm_detail.txt +149 -0
  21. solace_agent_mesh/assets/docs/404.html +3 -3
  22. solace_agent_mesh/assets/docs/assets/js/483cef9a.bf9398af.js +1 -0
  23. solace_agent_mesh/assets/docs/assets/js/{main.f67fc9f4.js → main.0c149855.js} +2 -2
  24. solace_agent_mesh/assets/docs/assets/js/{runtime~main.40527046.js → runtime~main.c66557e4.js} +1 -1
  25. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +3 -3
  26. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/rbac-setup-guilde/index.html +3 -3
  27. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +8 -4
  28. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  29. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
  30. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
  31. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
  32. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
  33. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
  34. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
  35. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
  36. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
  37. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
  38. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
  39. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
  40. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
  41. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/litellm_models/index.html +3 -3
  42. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
  43. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  44. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
  45. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
  46. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
  47. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
  48. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
  49. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
  50. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
  51. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
  52. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
  53. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
  54. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
  55. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
  56. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
  57. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
  58. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
  59. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
  60. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
  61. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
  62. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
  63. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
  64. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
  65. solace_agent_mesh/assets/docs/lunr-index-1760032255022.json +1 -0
  66. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  67. solace_agent_mesh/assets/docs/search-doc-1760032255022.json +1 -0
  68. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  69. solace_agent_mesh/cli/__init__.py +1 -1
  70. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-j1LW-wlq.js → authCallback-DwrxZE0E.js} +1 -1
  71. solace_agent_mesh/client/webui/frontend/static/assets/{client-B9p_nFNA.js → client-DarGQzyw.js} +1 -1
  72. solace_agent_mesh/client/webui/frontend/static/assets/main-CZbpmwfA.css +1 -0
  73. solace_agent_mesh/client/webui/frontend/static/assets/main-C__uuUkB.js +339 -0
  74. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-CS5YMf8a.js → vendor-BKIeiHj_.js} +80 -70
  75. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  76. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  77. solace_agent_mesh/common/a2a/a2a_llm.txt +1 -1
  78. solace_agent_mesh/common/a2a/a2a_llm_detail.txt +193 -0
  79. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +1 -1
  80. solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +736 -0
  81. solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +23 -0
  82. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +93 -15
  83. solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +23 -0
  84. solace_agent_mesh/common/common_llm.txt +24 -39
  85. solace_agent_mesh/common/common_llm_detail.txt +2562 -0
  86. solace_agent_mesh/common/data_parts.py +9 -1
  87. solace_agent_mesh/common/middleware/middleware_llm_detail.txt +185 -0
  88. solace_agent_mesh/common/sac/sac_llm.txt +1 -1
  89. solace_agent_mesh/common/sac/sac_llm_detail.txt +82 -0
  90. solace_agent_mesh/common/sam_events/sam_events_llm.txt +104 -0
  91. solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +115 -0
  92. solace_agent_mesh/common/services/services_llm.txt +57 -6
  93. solace_agent_mesh/common/services/services_llm_detail.txt +459 -0
  94. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +1 -1
  95. solace_agent_mesh/common/utils/utils_llm.txt +75 -87
  96. solace_agent_mesh/common/utils/utils_llm_detail.txt +572 -0
  97. solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +101 -0
  98. solace_agent_mesh/gateway/base/app.py +1 -1
  99. solace_agent_mesh/gateway/base/base_llm.txt +1 -1
  100. solace_agent_mesh/gateway/base/base_llm_detail.txt +235 -0
  101. solace_agent_mesh/gateway/gateway_llm.txt +242 -235
  102. solace_agent_mesh/gateway/gateway_llm_detail.txt +3885 -0
  103. solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +295 -0
  104. solace_agent_mesh/gateway/http_sse/alembic/env.py +10 -1
  105. solace_agent_mesh/gateway/http_sse/alembic/versions/20251006_98882922fa59_add_tasks_events_feedback_chat_tasks.py +190 -0
  106. solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +155 -0
  107. solace_agent_mesh/gateway/http_sse/alembic.ini +1 -1
  108. solace_agent_mesh/gateway/http_sse/app.py +148 -2
  109. solace_agent_mesh/gateway/http_sse/component.py +368 -60
  110. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +46 -6
  111. solace_agent_mesh/gateway/http_sse/components/task_logger_forwarder.py +108 -0
  112. solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +1 -1
  113. solace_agent_mesh/gateway/http_sse/dependencies.py +116 -26
  114. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +172 -172
  115. solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +3278 -0
  116. solace_agent_mesh/gateway/http_sse/main.py +146 -41
  117. solace_agent_mesh/gateway/http_sse/repository/__init__.py +3 -12
  118. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +103 -0
  119. solace_agent_mesh/gateway/http_sse/repository/entities/__init__.py +5 -3
  120. solace_agent_mesh/gateway/http_sse/repository/entities/chat_task.py +75 -0
  121. solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +263 -0
  122. solace_agent_mesh/gateway/http_sse/repository/entities/feedback.py +20 -0
  123. solace_agent_mesh/gateway/http_sse/repository/entities/session_history.py +0 -16
  124. solace_agent_mesh/gateway/http_sse/repository/entities/task.py +25 -0
  125. solace_agent_mesh/gateway/http_sse/repository/entities/task_event.py +21 -0
  126. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +81 -0
  127. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +73 -18
  128. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +9 -5
  129. solace_agent_mesh/gateway/http_sse/repository/models/chat_task_model.py +31 -0
  130. solace_agent_mesh/gateway/http_sse/repository/models/feedback_model.py +21 -0
  131. solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +266 -0
  132. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +3 -3
  133. solace_agent_mesh/gateway/http_sse/repository/models/task_event_model.py +25 -0
  134. solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +32 -0
  135. solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +340 -0
  136. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +4 -53
  137. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +173 -0
  138. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +1 -1
  139. solace_agent_mesh/gateway/http_sse/routers/config.py +26 -4
  140. solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +346 -0
  141. solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +3 -3
  142. solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +83 -0
  143. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +2 -10
  144. solace_agent_mesh/gateway/http_sse/routers/dto/requests/task_requests.py +58 -0
  145. solace_agent_mesh/gateway/http_sse/routers/dto/responses/__init__.py +5 -3
  146. solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +107 -0
  147. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +1 -15
  148. solace_agent_mesh/gateway/http_sse/routers/dto/responses/task_responses.py +30 -0
  149. solace_agent_mesh/gateway/http_sse/routers/feedback.py +37 -0
  150. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +255 -204
  151. solace_agent_mesh/gateway/http_sse/routers/sessions.py +220 -40
  152. solace_agent_mesh/gateway/http_sse/routers/tasks.py +168 -42
  153. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +272 -0
  154. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +241 -0
  155. solace_agent_mesh/gateway/http_sse/services/people_service.py +0 -80
  156. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +177 -13
  157. solace_agent_mesh/gateway/http_sse/services/session_service.py +151 -84
  158. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +317 -0
  159. solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +25 -14
  160. solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +285 -0
  161. solace_agent_mesh/gateway/http_sse/shared/types.py +7 -0
  162. solace_agent_mesh/gateway/http_sse/utils/__init__.py +1 -0
  163. solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +32 -0
  164. solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +47 -0
  165. solace_agent_mesh/solace_agent_mesh_llm.txt +1 -1
  166. solace_agent_mesh/solace_agent_mesh_llm_detail.txt +8599 -0
  167. {solace_agent_mesh-1.4.12.dist-info → solace_agent_mesh-1.5.0.dist-info}/METADATA +1 -1
  168. {solace_agent_mesh-1.4.12.dist-info → solace_agent_mesh-1.5.0.dist-info}/RECORD +172 -124
  169. solace_agent_mesh/agent/adk/invocation_monitor.py +0 -295
  170. solace_agent_mesh/assets/docs/assets/js/483cef9a.4736f2d8.js +0 -1
  171. solace_agent_mesh/assets/docs/lunr-index-1759936913198.json +0 -1
  172. solace_agent_mesh/assets/docs/search-doc-1759936913198.json +0 -1
  173. solace_agent_mesh/client/webui/frontend/static/assets/main-ChRwcV89.css +0 -1
  174. solace_agent_mesh/client/webui/frontend/static/assets/main-DnnE01OM.js +0 -339
  175. solace_agent_mesh/gateway/http_sse/repository/entities/message.py +0 -41
  176. solace_agent_mesh/gateway/http_sse/repository/message_repository.py +0 -84
  177. solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +0 -45
  178. /solace_agent_mesh/assets/docs/assets/js/{main.f67fc9f4.js.LICENSE.txt → main.0c149855.js.LICENSE.txt} +0 -0
  179. {solace_agent_mesh-1.4.12.dist-info → solace_agent_mesh-1.5.0.dist-info}/WHEEL +0 -0
  180. {solace_agent_mesh-1.4.12.dist-info → solace_agent_mesh-1.5.0.dist-info}/entry_points.txt +0 -0
  181. {solace_agent_mesh-1.4.12.dist-info → solace_agent_mesh-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,317 @@
1
+ """
2
+ Service for logging A2A tasks and events to the database.
3
+ """
4
+
5
+ import copy
6
+ import uuid
7
+ from typing import Any, Callable, Dict, Union
8
+
9
+ from a2a.types import (
10
+ A2ARequest,
11
+ JSONRPCError,
12
+ JSONRPCResponse,
13
+ Task as A2ATask,
14
+ TaskArtifactUpdateEvent,
15
+ TaskStatusUpdateEvent,
16
+ )
17
+ from solace_ai_connector.common.log import log
18
+ from sqlalchemy.orm import Session as DBSession
19
+
20
+ from ....common import a2a
21
+ from ..repository.entities import Task, TaskEvent
22
+ from ..repository.task_repository import TaskRepository
23
+ from ..shared import now_epoch_ms
24
+
25
+
26
+ class TaskLoggerService:
27
+ """Service for logging A2A tasks and events to the database."""
28
+
29
+ def __init__(
30
+ self, session_factory: Callable[[], DBSession] | None, config: Dict[str, Any]
31
+ ):
32
+ self.session_factory = session_factory
33
+ self.config = config
34
+ self.log_identifier = "[TaskLoggerService]"
35
+ log.info(f"{self.log_identifier} Initialized.")
36
+
37
+ def log_event(self, event_data: Dict[str, Any]):
38
+ """
39
+ Parses a raw A2A message and logs it as a task event.
40
+ Creates or updates the master task record as needed.
41
+ """
42
+ if not self.config.get("enabled", False):
43
+ return
44
+
45
+ if not self.session_factory:
46
+ log.warning(
47
+ f"{self.log_identifier} Task logging is enabled but no database is configured. Skipping event."
48
+ )
49
+ return
50
+
51
+ topic = event_data.get("topic")
52
+ payload = event_data.get("payload")
53
+ user_properties = event_data.get("user_properties", {})
54
+
55
+ if not topic or not payload:
56
+ log.warning(
57
+ f"{self.log_identifier} Received event with missing topic or payload."
58
+ )
59
+ return
60
+
61
+ if "discovery" in topic:
62
+ # Ignore discovery messages
63
+ return
64
+
65
+ # Parse the event into a Pydantic model first.
66
+ parsed_event = self._parse_a2a_event(topic, payload)
67
+ if parsed_event is None:
68
+ # Parsing failed or event should be ignored.
69
+ return
70
+
71
+ db = self.session_factory()
72
+ try:
73
+ repo = TaskRepository(db)
74
+
75
+ # Infer details from the parsed event
76
+ direction, task_id, user_id = self._infer_event_details(
77
+ topic, parsed_event, user_properties
78
+ )
79
+
80
+ if not task_id:
81
+ log.debug(
82
+ f"{self.log_identifier} Could not determine task_id for event on topic {topic}. Skipping."
83
+ )
84
+ return
85
+
86
+ # Check if we should log this event type
87
+ if not self._should_log_event(topic, parsed_event):
88
+ log.debug(
89
+ f"{self.log_identifier} Event on topic {topic} is configured to be skipped."
90
+ )
91
+ return
92
+
93
+ # Sanitize the original raw payload before storing
94
+ sanitized_payload = self._sanitize_payload(payload)
95
+
96
+ # Check for existing task or create a new one
97
+ task = repo.find_by_id(task_id)
98
+ if not task:
99
+ if direction == "request":
100
+ initial_text = self._extract_initial_text(parsed_event)
101
+ new_task = Task(
102
+ id=task_id,
103
+ user_id=user_id or "unknown",
104
+ start_time=now_epoch_ms(),
105
+ initial_request_text=(
106
+ initial_text[:1024] if initial_text else None
107
+ ), # Truncate
108
+ )
109
+ repo.save_task(new_task)
110
+ log.info(
111
+ f"{self.log_identifier} Created new task record for ID: {task_id}"
112
+ )
113
+ else:
114
+ # We received an event for a task we haven't seen the start of.
115
+ # This can happen if the logger starts mid-conversation. Create a placeholder.
116
+ placeholder_task = Task(
117
+ id=task_id,
118
+ user_id=user_id or "unknown",
119
+ start_time=now_epoch_ms(),
120
+ initial_request_text="[Task started before logger was active]",
121
+ )
122
+ repo.save_task(placeholder_task)
123
+ log.info(
124
+ f"{self.log_identifier} Created placeholder task record for ID: {task_id}"
125
+ )
126
+
127
+ # Create and save the event using the sanitized raw payload
128
+ task_event = TaskEvent(
129
+ id=str(uuid.uuid4()),
130
+ task_id=task_id,
131
+ user_id=user_id,
132
+ created_time=now_epoch_ms(),
133
+ topic=topic,
134
+ direction=direction,
135
+ payload=sanitized_payload,
136
+ )
137
+ repo.save_event(task_event)
138
+
139
+ # If it's a final event, update the master task record
140
+ final_status = self._get_final_status(parsed_event)
141
+ if final_status:
142
+ task_to_update = repo.find_by_id(task_id)
143
+ if task_to_update:
144
+ task_to_update.end_time = now_epoch_ms()
145
+ task_to_update.status = final_status
146
+
147
+ # Extract and store token usage if present
148
+ if isinstance(parsed_event, A2ATask) and parsed_event.metadata:
149
+ token_usage = parsed_event.metadata.get("token_usage")
150
+ if token_usage and isinstance(token_usage, dict):
151
+ task_to_update.total_input_tokens = token_usage.get("total_input_tokens")
152
+ task_to_update.total_output_tokens = token_usage.get("total_output_tokens")
153
+ task_to_update.total_cached_input_tokens = token_usage.get("total_cached_input_tokens")
154
+ task_to_update.token_usage_details = token_usage
155
+ log.info(
156
+ f"{self.log_identifier} Stored token usage for task {task_id}: "
157
+ f"input={token_usage.get('total_input_tokens')}, "
158
+ f"output={token_usage.get('total_output_tokens')}, "
159
+ f"cached={token_usage.get('total_cached_input_tokens')}"
160
+ )
161
+
162
+ repo.save_task(task_to_update)
163
+ log.info(
164
+ f"{self.log_identifier} Finalized task record for ID: {task_id} with status: {final_status}"
165
+ )
166
+ db.commit()
167
+ except Exception as e:
168
+ log.exception(
169
+ f"{self.log_identifier} Error logging event on topic {topic}: {e}"
170
+ )
171
+ db.rollback()
172
+ finally:
173
+ db.close()
174
+
175
+ def _parse_a2a_event(self, topic: str, payload: dict) -> Union[
176
+ A2ARequest,
177
+ A2ATask,
178
+ TaskStatusUpdateEvent,
179
+ TaskArtifactUpdateEvent,
180
+ JSONRPCError,
181
+ None,
182
+ ]:
183
+ """
184
+ Safely parses a raw A2A message payload into a Pydantic model.
185
+ Returns the parsed model or None if parsing fails or is not applicable.
186
+ """
187
+ # Ignore discovery messages
188
+ if "/discovery/agentcards" in topic:
189
+ return None
190
+
191
+ try:
192
+ # Check if it's a response (has 'result' or 'error')
193
+ if "result" in payload or "error" in payload:
194
+ rpc_response = JSONRPCResponse.model_validate(payload)
195
+ error = a2a.get_response_error(rpc_response)
196
+ if error:
197
+ return error
198
+ result = a2a.get_response_result(rpc_response)
199
+ if result:
200
+ # The result is already a parsed Pydantic model
201
+ return result
202
+ # Check if it's a request
203
+ elif "method" in payload:
204
+ return A2ARequest.model_validate(payload)
205
+
206
+ log.warning(
207
+ f"{self.log_identifier} Payload for topic '{topic}' is not a recognizable JSON-RPC request or response. Payload: {payload}"
208
+ )
209
+ return None
210
+
211
+ except Exception as e:
212
+ log.error(
213
+ f"{self.log_identifier} Failed to parse A2A event for topic '{topic}': {e}. Payload: {payload}"
214
+ )
215
+ return None
216
+
217
+ def _infer_event_details(
218
+ self, topic: str, parsed_event: Any, user_props: Dict | None
219
+ ) -> tuple[str, str | None, str | None]:
220
+ """Infers direction, task_id, and user_id from a parsed A2A event."""
221
+ direction = "unknown"
222
+ task_id = None
223
+ # Ensure user_props is a dict, not None
224
+ user_props = user_props or {}
225
+ user_id = user_props.get("userId")
226
+
227
+ if isinstance(parsed_event, A2ARequest):
228
+ direction = "request"
229
+ task_id = a2a.get_request_id(parsed_event)
230
+ elif isinstance(
231
+ parsed_event, (A2ATask, TaskStatusUpdateEvent, TaskArtifactUpdateEvent)
232
+ ):
233
+ direction = "response" if isinstance(parsed_event, A2ATask) else "status"
234
+ task_id = getattr(parsed_event, "task_id", None) or getattr(
235
+ parsed_event, "id", None
236
+ )
237
+ elif isinstance(parsed_event, JSONRPCError):
238
+ direction = "error"
239
+ if isinstance(parsed_event.data, dict):
240
+ task_id = parsed_event.data.get("taskId")
241
+
242
+ if not user_id:
243
+ user_config = user_props.get("a2aUserConfig") or user_props.get("a2a_user_config")
244
+ if isinstance(user_config, dict):
245
+ user_profile = user_config.get("user_profile", {})
246
+ if isinstance(user_profile, dict):
247
+ user_id = user_profile.get("id")
248
+
249
+ return direction, str(task_id) if task_id else None, user_id
250
+
251
+ def _extract_initial_text(self, parsed_event: Any) -> str | None:
252
+ """Extracts the initial text from a send message request."""
253
+ try:
254
+ if isinstance(parsed_event, A2ARequest):
255
+ message = a2a.get_message_from_send_request(parsed_event)
256
+ if message:
257
+ return a2a.get_text_from_message(message)
258
+ except Exception:
259
+ return None
260
+ return None
261
+
262
+ def _get_final_status(self, parsed_event: Any) -> str | None:
263
+ """Checks if a parsed event represents a final task status and returns the state."""
264
+ if isinstance(parsed_event, A2ATask):
265
+ return parsed_event.status.state.value
266
+ elif isinstance(parsed_event, JSONRPCError):
267
+ return "failed"
268
+ return None
269
+
270
+ def _should_log_event(self, topic: str, parsed_event: Any) -> bool:
271
+ """Determines if an event should be logged based on configuration."""
272
+ if not self.config.get("log_status_updates", True):
273
+ if "status" in topic:
274
+ return False
275
+ if not self.config.get("log_artifact_events", True):
276
+ if isinstance(parsed_event, TaskArtifactUpdateEvent):
277
+ return False
278
+ return True
279
+
280
+ def _sanitize_payload(self, payload: Dict) -> Dict:
281
+ """Strips or truncates file content from payload based on configuration."""
282
+ new_payload = copy.deepcopy(payload)
283
+
284
+ def walk_and_sanitize(node):
285
+ if isinstance(node, dict):
286
+ for key, value in list(node.items()):
287
+ if key == "parts" and isinstance(value, list):
288
+ new_parts = []
289
+ for part in value:
290
+ if isinstance(part, dict) and "file" in part:
291
+ if not self.config.get("log_file_parts", True):
292
+ continue # Skip this part entirely
293
+
294
+ file_dict = part.get("file")
295
+ if isinstance(file_dict, dict) and "bytes" in file_dict:
296
+ max_bytes = self.config.get(
297
+ "max_file_part_size_bytes", 102400
298
+ )
299
+ file_bytes_b64 = file_dict.get("bytes")
300
+ if isinstance(file_bytes_b64, str):
301
+ if (len(file_bytes_b64) * 3 / 4) > max_bytes:
302
+ file_dict["bytes"] = (
303
+ f"[Content stripped, size > {max_bytes} bytes]"
304
+ )
305
+ new_parts.append(part)
306
+ else:
307
+ walk_and_sanitize(part)
308
+ new_parts.append(part)
309
+ node["parts"] = new_parts
310
+ else:
311
+ walk_and_sanitize(value)
312
+ elif isinstance(node, list):
313
+ for item in node:
314
+ walk_and_sanitize(item)
315
+
316
+ walk_and_sanitize(new_payload)
317
+ return new_payload
@@ -40,15 +40,13 @@ def create_error_response(
40
40
  async def validation_error_handler(
41
41
  request: Request, exc: ValidationError
42
42
  ) -> JSONResponse:
43
- """Handle domain validation errors - 400 Bad Request."""
43
+ """Handle domain validation errors - 422 Unprocessable Entity."""
44
44
  if exc.validation_details:
45
- # Validation errors with field details
46
45
  error_dto = EventErrorDTO.validation_error(exc.message, exc.validation_details)
47
46
  else:
48
- # General bad request
49
47
  error_dto = EventErrorDTO.create("bad request" if not exc.message else exc.message)
50
48
 
51
- return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=error_dto.model_dump())
49
+ return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump())
52
50
 
53
51
 
54
52
  async def entity_not_found_handler(
@@ -72,9 +70,9 @@ async def entity_already_exists_handler(
72
70
  async def business_rule_violation_handler(
73
71
  request: Request, exc: BusinessRuleViolationError
74
72
  ) -> JSONResponse:
75
- """Handle business rule violations - 400 Bad Request."""
73
+ """Handle business rule violations - 422 Unprocessable Entity."""
76
74
  error_dto = EventErrorDTO.create(exc.message)
77
- return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=error_dto.model_dump())
75
+ return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump())
78
76
 
79
77
 
80
78
  async def configuration_error_handler(
@@ -88,11 +86,11 @@ async def configuration_error_handler(
88
86
  async def data_integrity_error_handler(
89
87
  request: Request, exc: DataIntegrityError
90
88
  ) -> JSONResponse:
91
- """Handle data integrity errors - 400 Bad Request."""
89
+ """Handle data integrity errors - 422 Unprocessable Entity."""
92
90
  # Format: "An entity of type applicationDomain was passed in an invalid format"
93
91
  message = f"An entity of type {exc.entity_type} was passed in an invalid format" if hasattr(exc, 'entity_type') else "bad request"
94
92
  error_dto = EventErrorDTO.create(message)
95
- return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=error_dto.model_dump())
93
+ return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump())
96
94
 
97
95
 
98
96
  async def external_service_error_handler(
@@ -137,8 +135,7 @@ async def http_exception_handler(
137
135
  async def request_validation_exception_handler(
138
136
  request: Request, exc: RequestValidationError
139
137
  ) -> JSONResponse:
140
- """Handle FastAPI request validation errors - 400 Bad Request."""
141
- # Convert Pydantic validation errors to our format
138
+ """Handle FastAPI request validation errors - 422 Unprocessable Entity."""
142
139
  validation_details = {}
143
140
  for error in exc.errors():
144
141
  field_path = ".".join(str(x) for x in error["loc"] if x != "body")
@@ -147,14 +144,27 @@ async def request_validation_exception_handler(
147
144
  validation_details[field_path].append(error["msg"])
148
145
 
149
146
  if validation_details:
150
- # Field-specific validation errors
151
147
  message = "body must not be empty" if not validation_details else "Validation error"
152
148
  error_dto = EventErrorDTO.validation_error(message, validation_details)
153
149
  else:
154
- # General bad request
155
150
  error_dto = EventErrorDTO.create("bad request")
156
151
 
157
- return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=error_dto.model_dump())
152
+ return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump())
153
+
154
+
155
+ async def pydantic_validation_exception_handler(
156
+ request: Request, exc: PydanticValidationError
157
+ ) -> JSONResponse:
158
+ """Handle Pydantic validation errors raised in service layer - 422 Unprocessable Entity."""
159
+ validation_details = {}
160
+ for error in exc.errors():
161
+ field_path = ".".join(str(loc) for loc in error["loc"])
162
+ if field_path not in validation_details:
163
+ validation_details[field_path] = []
164
+ validation_details[field_path].append(error["msg"])
165
+
166
+ error_dto = EventErrorDTO.validation_error("Validation failed", validation_details)
167
+ return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=error_dto.model_dump())
158
168
 
159
169
 
160
170
  def register_exception_handlers(app):
@@ -189,4 +199,5 @@ def register_exception_handlers(app):
189
199
  # FastAPI built-in exception handlers
190
200
  app.add_exception_handler(HTTPException, http_exception_handler)
191
201
  app.add_exception_handler(StarletteHTTPException, http_exception_handler)
192
- app.add_exception_handler(RequestValidationError, request_validation_exception_handler)
202
+ app.add_exception_handler(RequestValidationError, request_validation_exception_handler)
203
+ app.add_exception_handler(PydanticValidationError, pydantic_validation_exception_handler)