solace-agent-mesh 1.4.11__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.
- solace_agent_mesh/agent/adk/adk_llm.txt +3 -4
- solace_agent_mesh/agent/adk/adk_llm_detail.txt +566 -0
- solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +1 -1
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +1 -0
- solace_agent_mesh/agent/adk/callbacks.py +51 -2
- solace_agent_mesh/agent/adk/models/lite_llm.py +1 -0
- solace_agent_mesh/agent/adk/models/models_llm.txt +1 -2
- solace_agent_mesh/agent/adk/services.py +30 -15
- solace_agent_mesh/agent/adk/setup.py +4 -0
- solace_agent_mesh/agent/agent_llm.txt +1 -1
- solace_agent_mesh/agent/agent_llm_detail.txt +1702 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +2 -13
- solace_agent_mesh/agent/protocol/protocol_llm.txt +15 -2
- solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +92 -0
- solace_agent_mesh/agent/sac/component.py +51 -21
- solace_agent_mesh/agent/sac/sac_llm.txt +15 -1
- solace_agent_mesh/agent/sac/sac_llm_detail.txt +200 -0
- solace_agent_mesh/agent/sac/task_execution_context.py +73 -0
- solace_agent_mesh/agent/testing/testing_llm_detail.txt +68 -0
- solace_agent_mesh/agent/tools/tools_llm.txt +148 -154
- solace_agent_mesh/agent/tools/tools_llm_detail.txt +274 -0
- solace_agent_mesh/agent/utils/utils_llm.txt +1 -1
- solace_agent_mesh/agent/utils/utils_llm_detail.txt +149 -0
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/0e682baa.d54b8668.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/483cef9a.bf9398af.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.dc155742.js → main.0c149855.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/{runtime~main.0d2ff2b6.js → runtime~main.c66557e4.js} +1 -1
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/rbac-setup-guilde/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +8 -4
- 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
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/litellm_models/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +7 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1760032255022.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1760032255022.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-j1LW-wlq.js → authCallback-DwrxZE0E.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{client-B9p_nFNA.js → client-DarGQzyw.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-CZbpmwfA.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-C__uuUkB.js +339 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{vendor-CS5YMf8a.js → vendor-BKIeiHj_.js} +80 -70
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
- solace_agent_mesh/common/a2a/a2a_llm.txt +1 -1
- solace_agent_mesh/common/a2a/a2a_llm_detail.txt +193 -0
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +1 -1
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +736 -0
- solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +23 -0
- solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +93 -15
- solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +23 -0
- solace_agent_mesh/common/common_llm.txt +24 -39
- solace_agent_mesh/common/common_llm_detail.txt +2562 -0
- solace_agent_mesh/common/data_parts.py +9 -1
- solace_agent_mesh/common/middleware/middleware_llm_detail.txt +185 -0
- solace_agent_mesh/common/sac/sac_llm.txt +1 -1
- solace_agent_mesh/common/sac/sac_llm_detail.txt +82 -0
- solace_agent_mesh/common/sam_events/sam_events_llm.txt +104 -0
- solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +115 -0
- solace_agent_mesh/common/services/identity_service.py +7 -4
- solace_agent_mesh/common/services/providers/local_file_identity_service.py +4 -2
- solace_agent_mesh/common/services/services_llm.txt +57 -6
- solace_agent_mesh/common/services/services_llm_detail.txt +459 -0
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +1 -1
- solace_agent_mesh/common/utils/utils_llm.txt +75 -87
- solace_agent_mesh/common/utils/utils_llm_detail.txt +572 -0
- solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +101 -0
- solace_agent_mesh/gateway/base/app.py +1 -1
- solace_agent_mesh/gateway/base/base_llm.txt +1 -1
- solace_agent_mesh/gateway/base/base_llm_detail.txt +235 -0
- solace_agent_mesh/gateway/base/component.py +1 -1
- solace_agent_mesh/gateway/gateway_llm.txt +242 -235
- solace_agent_mesh/gateway/gateway_llm_detail.txt +3885 -0
- solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +295 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +10 -1
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251006_98882922fa59_add_tasks_events_feedback_chat_tasks.py +190 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +155 -0
- solace_agent_mesh/gateway/http_sse/alembic.ini +1 -1
- solace_agent_mesh/gateway/http_sse/app.py +148 -2
- solace_agent_mesh/gateway/http_sse/component.py +368 -60
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +46 -6
- solace_agent_mesh/gateway/http_sse/components/task_logger_forwarder.py +108 -0
- solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +1 -1
- solace_agent_mesh/gateway/http_sse/dependencies.py +116 -26
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +172 -172
- solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +3278 -0
- solace_agent_mesh/gateway/http_sse/main.py +146 -41
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +3 -12
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +103 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/__init__.py +5 -3
- solace_agent_mesh/gateway/http_sse/repository/entities/chat_task.py +75 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +263 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/feedback.py +20 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session_history.py +0 -16
- solace_agent_mesh/gateway/http_sse/repository/entities/task.py +25 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/task_event.py +21 -0
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +81 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +73 -18
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +9 -5
- solace_agent_mesh/gateway/http_sse/repository/models/chat_task_model.py +31 -0
- solace_agent_mesh/gateway/http_sse/repository/models/feedback_model.py +21 -0
- solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +266 -0
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +3 -3
- solace_agent_mesh/gateway/http_sse/repository/models/task_event_model.py +25 -0
- solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +32 -0
- solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +340 -0
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +4 -53
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +173 -0
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/config.py +26 -4
- solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +346 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/__init__.py +3 -3
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +83 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +2 -10
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/task_requests.py +58 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/__init__.py +5 -3
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +107 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +1 -15
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/task_responses.py +30 -0
- solace_agent_mesh/gateway/http_sse/routers/feedback.py +37 -0
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +255 -204
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +220 -40
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +168 -42
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +272 -0
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +241 -0
- solace_agent_mesh/gateway/http_sse/services/people_service.py +0 -80
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +177 -13
- solace_agent_mesh/gateway/http_sse/services/session_service.py +151 -84
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +317 -0
- solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +25 -14
- solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +285 -0
- solace_agent_mesh/gateway/http_sse/shared/types.py +7 -0
- solace_agent_mesh/gateway/http_sse/utils/__init__.py +1 -0
- solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +32 -0
- solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +47 -0
- solace_agent_mesh/solace_agent_mesh_llm.txt +1 -1
- solace_agent_mesh/solace_agent_mesh_llm_detail.txt +8599 -0
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/METADATA +1 -1
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/RECORD +179 -131
- solace_agent_mesh/agent/adk/invocation_monitor.py +0 -295
- solace_agent_mesh/assets/docs/assets/js/0e682baa.d054e1d8.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/483cef9a.4736f2d8.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1759514789087.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1759514789087.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-ChRwcV89.css +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-DnnE01OM.js +0 -339
- solace_agent_mesh/gateway/http_sse/repository/entities/message.py +0 -41
- solace_agent_mesh/gateway/http_sse/repository/message_repository.py +0 -84
- solace_agent_mesh/gateway/http_sse/repository/models/message_model.py +0 -45
- /solace_agent_mesh/assets/docs/assets/js/{main.dc155742.js.LICENSE.txt → main.0c149855.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.4.11.dist-info → solace_agent_mesh-1.5.0.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.4.11.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 -
|
|
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.
|
|
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 -
|
|
73
|
+
"""Handle business rule violations - 422 Unprocessable Entity."""
|
|
76
74
|
error_dto = EventErrorDTO.create(exc.message)
|
|
77
|
-
return JSONResponse(status_code=status.
|
|
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 -
|
|
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.
|
|
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 -
|
|
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.
|
|
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)
|