rasa-pro 3.14.0.dev3__py3-none-any.whl → 3.14.0.dev4__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 rasa-pro might be problematic. Click here for more details.
- rasa/agents/constants.py +8 -0
- rasa/agents/core/agent_protocol.py +1 -2
- rasa/agents/protocol/a2a/a2a_agent.py +628 -17
- rasa/agents/protocol/mcp/mcp_base_agent.py +4 -2
- rasa/core/actions/action.py +13 -8
- rasa/core/available_agents.py +3 -0
- rasa/core/channels/development_inspector.py +3 -3
- rasa/core/channels/hangouts.py +2 -2
- rasa/core/channels/inspector/dist/assets/{arc-2e78c586.js → arc-63212852.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-806b712e.js → blockDiagram-38ab4fdb-eecf6b13.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-0745efa9.js → c4Diagram-3d4e48cf-8f798a9a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/channel-0cd70adf.js +1 -0
- rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-7bd1082b.js → classDiagram-70f12bd4-df71a04c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-d937ba49.js → classDiagram-v2-f2320105-9b275968.js} +1 -1
- rasa/core/channels/inspector/dist/assets/clone-a0f9c4ed.js +1 -0
- rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-a2a564ca.js → createText-2e5e7dd3-1c669cad.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-b5256940.js → edges-e0da2a9e-b1553799.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-e6883ad2.js → erDiagram-9861fffd-112388d6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-e576fc02.js → flowDb-956e92f1-fdebec47.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-2e298d01.js → flowDiagram-66a62f08-6280ede1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-de9cc4aa.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-dd7b150a.js → flowchart-elk-definition-4a651766-e1dc03e5.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-5b79575c.js → ganttDiagram-c361ad54-83f68c51.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-3016f40a.js → gitGraphDiagram-72cf32ee-22f8666f.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{graph-3e19170f.js → graph-ca9e6217.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-3862675e-eb9c86de.js → index-3862675e-c5ceb692.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-1bd9135e.js → index-3e293924.js} +3 -3
- rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-b4280e4d.js → infoDiagram-f8f76790-faa9999b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-556091f8.js → journeyDiagram-49397b02-c4dda8d9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-08436411.js → layout-d4307784.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-683c4f3b.js → line-0567aaa7.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-cee6d791.js → linear-c11b95cf.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-a0bf0b1a.js → mindmap-definition-fc14e90a-0c7d3ca9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-3730d5c4.js → pieDiagram-8a3498a8-34b433fa.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-12a20fed.js → quadrantDiagram-120e2f19-4cab816e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-b9732102.js → requirementDiagram-deff3bca-8c22fa9e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-a2e72776.js → sankeyDiagram-04a897e0-70ce9e8e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-8b7a76bb.js → sequenceDiagram-704730f1-fbcd7fc9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-e65853ac.js → stateDiagram-587899a1-45f05ea6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-6f58a44b.js → stateDiagram-v2-d93cdb3a-beab1ea6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-df25b934.js → styles-6aaf32cf-2f29dbd5.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9a916d00-88357141.js → styles-9a916d00-951eac83.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-c10674c1-d600174d.js → styles-c10674c1-897fbfdd.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-4adc3e0b.js → svgDrawCommon-08f97a94-d667fac1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-42816fa1.js → timeline-definition-85554ec2-e3205144.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-621eb66a.js → xychartDiagram-e933f94c-4abeb0e2.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +1 -1
- rasa/core/channels/inspector/src/components/DialogueStack.tsx +1 -1
- rasa/core/channels/studio_chat.py +6 -6
- rasa/core/channels/voice_stream/genesys.py +1 -1
- rasa/core/policies/flows/flow_executor.py +42 -1
- rasa/core/policies/intentless_policy.py +1 -1
- rasa/core/policies/unexpected_intent_policy.py +1 -0
- rasa/core/processor.py +12 -14
- rasa/core/tracker_stores/tracker_store.py +3 -7
- rasa/core/train.py +1 -1
- rasa/core/training/interactive.py +16 -16
- rasa/core/training/story_conflict.py +5 -5
- rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +1 -1
- rasa/e2e_test/e2e_test_runner.py +7 -2
- rasa/engine/caching.py +2 -2
- rasa/engine/recipes/default_components.py +10 -18
- rasa/graph_components/validators/default_recipe_validator.py +134 -134
- rasa/hooks.py +5 -5
- rasa/llm_fine_tuning/utils.py +2 -2
- rasa/model_manager/warm_rasa_process.py +1 -1
- rasa/nlu/extractors/extractor.py +2 -1
- rasa/plugin.py +8 -8
- rasa/privacy/privacy_manager.py +11 -2
- rasa/server.py +4 -2
- rasa/shared/core/events.py +9 -1
- rasa/shared/core/flows/yaml_flows_io.py +1 -1
- rasa/shared/core/slots.py +2 -2
- rasa/shared/core/trackers.py +5 -2
- rasa/shared/exceptions.py +4 -0
- rasa/shared/utils/yaml.py +3 -1
- rasa/tracing/instrumentation/instrumentation.py +8 -8
- rasa/tracing/instrumentation/intentless_policy_instrumentation.py +4 -4
- rasa/utils/log_utils.py +1 -1
- rasa/utils/ml_utils.py +1 -1
- rasa/utils/tensorflow/rasa_layers.py +1 -1
- rasa/utils/train_utils.py +15 -15
- rasa/validator.py +16 -14
- rasa/version.py +1 -1
- {rasa_pro-3.14.0.dev3.dist-info → rasa_pro-3.14.0.dev4.dist-info}/METADATA +12 -15
- {rasa_pro-3.14.0.dev3.dist-info → rasa_pro-3.14.0.dev4.dist-info}/RECORD +90 -90
- rasa/core/channels/inspector/dist/assets/channel-c436ca7c.js +0 -1
- rasa/core/channels/inspector/dist/assets/clone-50dd656b.js +0 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-2b2aeaf8.js +0 -1
- {rasa_pro-3.14.0.dev3.dist-info → rasa_pro-3.14.0.dev4.dist-info}/NOTICE +0 -0
- {rasa_pro-3.14.0.dev3.dist-info → rasa_pro-3.14.0.dev4.dist-info}/WHEEL +0 -0
- {rasa_pro-3.14.0.dev3.dist-info → rasa_pro-3.14.0.dev4.dist-info}/entry_points.txt +0 -0
|
@@ -1,51 +1,662 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from urllib.parse import urlparse
|
|
2
7
|
|
|
3
8
|
import httpx
|
|
9
|
+
import structlog
|
|
10
|
+
from a2a.client import (
|
|
11
|
+
A2ACardResolver,
|
|
12
|
+
A2AClient,
|
|
13
|
+
A2AClientError,
|
|
14
|
+
A2AClientHTTPError,
|
|
15
|
+
A2AClientJSONError,
|
|
16
|
+
)
|
|
17
|
+
from a2a.types import (
|
|
18
|
+
AgentCapabilities,
|
|
19
|
+
AgentCard,
|
|
20
|
+
AgentSkill,
|
|
21
|
+
DataPart,
|
|
22
|
+
FilePart,
|
|
23
|
+
FileWithUri,
|
|
24
|
+
InternalError,
|
|
25
|
+
InvalidAgentResponseError,
|
|
26
|
+
JSONRPCErrorResponse,
|
|
27
|
+
Message,
|
|
28
|
+
MessageSendConfiguration,
|
|
29
|
+
MessageSendParams,
|
|
30
|
+
Part,
|
|
31
|
+
Role,
|
|
32
|
+
SendStreamingMessageRequest,
|
|
33
|
+
SendStreamingMessageResponse,
|
|
34
|
+
Task,
|
|
35
|
+
TaskArtifactUpdateEvent,
|
|
36
|
+
TaskState,
|
|
37
|
+
TaskStatus,
|
|
38
|
+
TaskStatusUpdateEvent,
|
|
39
|
+
TextPart,
|
|
40
|
+
)
|
|
4
41
|
|
|
42
|
+
from rasa.agents.constants import (
|
|
43
|
+
A2A_AGENT_CONTEXT_ID_KEY,
|
|
44
|
+
A2A_AGENT_TASK_ID_KEY,
|
|
45
|
+
AGENT_DEFAULT_MAX_RETRIES,
|
|
46
|
+
AGENT_DEFAULT_TIMEOUT_SECONDS,
|
|
47
|
+
AGENT_METADATA_TOOL_RESULTS_KEY,
|
|
48
|
+
MAX_AGENT_RETRY_DELAY_SECONDS,
|
|
49
|
+
)
|
|
5
50
|
from rasa.agents.core.agent_protocol import AgentProtocol
|
|
6
|
-
from rasa.agents.core.types import ProtocolType
|
|
51
|
+
from rasa.agents.core.types import AgentStatus, ProtocolType
|
|
7
52
|
from rasa.agents.schemas import AgentInput, AgentOutput
|
|
8
53
|
from rasa.core.available_agents import AgentConfig
|
|
54
|
+
from rasa.shared.exceptions import (
|
|
55
|
+
AgentInitializationException,
|
|
56
|
+
InvalidParameterException,
|
|
57
|
+
RasaException,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
structlogger = structlog.get_logger()
|
|
9
61
|
|
|
10
62
|
|
|
11
63
|
class A2AAgent(AgentProtocol):
|
|
12
64
|
"""A2A client implementation"""
|
|
13
65
|
|
|
14
|
-
def __init__(
|
|
15
|
-
self
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
name: str,
|
|
69
|
+
description: str,
|
|
70
|
+
agent_card_path: str,
|
|
71
|
+
timeout: int,
|
|
72
|
+
max_retries: int,
|
|
73
|
+
) -> None:
|
|
74
|
+
self._name = name
|
|
75
|
+
self._description = description
|
|
76
|
+
self._agent_card_path = agent_card_path
|
|
77
|
+
self._timeout = timeout
|
|
78
|
+
self._max_retries = max_retries
|
|
79
|
+
|
|
80
|
+
self.agent_card: Optional[AgentCard] = None
|
|
81
|
+
self._client: Optional[A2AClient] = None
|
|
19
82
|
|
|
20
83
|
@classmethod
|
|
21
84
|
def from_config(cls, config: AgentConfig) -> AgentProtocol:
|
|
22
85
|
"""Initialize the A2A Agent with the given configuration."""
|
|
23
|
-
|
|
86
|
+
agent_card_path = (
|
|
87
|
+
config.configuration.agent_card if config.configuration else None
|
|
88
|
+
)
|
|
89
|
+
if not agent_card_path:
|
|
90
|
+
raise InvalidParameterException(
|
|
91
|
+
"Agent card path or URL must be provided in the configuration "
|
|
92
|
+
"for A2A agents."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
timeout = (
|
|
96
|
+
config.configuration.timeout
|
|
97
|
+
if config.configuration and config.configuration.timeout
|
|
98
|
+
else AGENT_DEFAULT_TIMEOUT_SECONDS
|
|
99
|
+
)
|
|
100
|
+
max_retries = (
|
|
101
|
+
config.configuration.max_retries
|
|
102
|
+
if config.configuration and config.configuration.max_retries
|
|
103
|
+
else AGENT_DEFAULT_MAX_RETRIES
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return cls(
|
|
107
|
+
name=config.agent.name,
|
|
108
|
+
description=config.agent.description,
|
|
109
|
+
agent_card_path=agent_card_path,
|
|
110
|
+
timeout=timeout,
|
|
111
|
+
max_retries=max_retries,
|
|
112
|
+
)
|
|
24
113
|
|
|
25
114
|
@property
|
|
26
115
|
def protocol_type(self) -> ProtocolType:
|
|
27
116
|
return ProtocolType.A2A
|
|
28
117
|
|
|
29
118
|
async def connect(self) -> None:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
119
|
+
"""Fetch the AgentCard and initialize the A2A client."""
|
|
120
|
+
from rasa.nlu.utils import is_url
|
|
121
|
+
|
|
122
|
+
if is_url(self._agent_card_path):
|
|
123
|
+
self.agent_card = await A2AAgent._resolve_agent_card_with_retry(
|
|
124
|
+
self._agent_card_path, self._timeout, self._max_retries
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
self.agent_card = A2AAgent._load_agent_card_from_file(self._agent_card_path)
|
|
128
|
+
structlogger.debug(
|
|
129
|
+
"a2a_agent.from_config",
|
|
130
|
+
event_info=f"Loaded agent card from {self._agent_card_path}",
|
|
131
|
+
agent_card=self.agent_card,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
self._client = A2AClient(
|
|
136
|
+
httpx.AsyncClient(timeout=self._timeout), agent_card=self.agent_card
|
|
137
|
+
)
|
|
138
|
+
structlogger.debug(
|
|
139
|
+
"a2a_agent.connect.success",
|
|
140
|
+
event_info=f"Connected to A2A server '{self._name}' "
|
|
141
|
+
f"at {self.agent_card.url}",
|
|
142
|
+
)
|
|
143
|
+
# TODO: Make a test request to /tasks to verify the connection
|
|
144
|
+
except Exception as exception:
|
|
145
|
+
structlogger.error(
|
|
146
|
+
"a2a_agent.connect.error",
|
|
147
|
+
event_info="Failed to initialize A2A client",
|
|
148
|
+
agent_name=self._name,
|
|
149
|
+
error=str(exception),
|
|
150
|
+
)
|
|
151
|
+
raise AgentInitializationException(
|
|
152
|
+
f"Failed to initialize A2A client for agent "
|
|
153
|
+
f"'{self._name}': {exception}"
|
|
154
|
+
) from exception
|
|
33
155
|
|
|
34
156
|
async def disconnect(self) -> None:
|
|
35
|
-
|
|
157
|
+
"""We don't need to explicitly disconnect the A2A client"""
|
|
36
158
|
return
|
|
37
159
|
|
|
38
|
-
async def process_input(self,
|
|
160
|
+
async def process_input(self, agent_input: AgentInput) -> AgentInput:
|
|
39
161
|
"""Pre-process the input before sending it to the agent."""
|
|
40
162
|
# A2A-specific input processing logic
|
|
41
|
-
return
|
|
163
|
+
return agent_input
|
|
42
164
|
|
|
43
|
-
async def run(self,
|
|
165
|
+
async def run(self, agent_input: AgentInput) -> AgentOutput:
|
|
44
166
|
"""Send a message to Agent/server and return response."""
|
|
45
|
-
|
|
46
|
-
|
|
167
|
+
if not self._client or not self.agent_card:
|
|
168
|
+
structlogger.error(
|
|
169
|
+
"a2a_agent.run.error",
|
|
170
|
+
event_info="A2A client is not initialized. " "Call connect() first.",
|
|
171
|
+
)
|
|
172
|
+
return AgentOutput(
|
|
173
|
+
id=agent_input.id,
|
|
174
|
+
status=AgentStatus.FATAL_ERROR,
|
|
175
|
+
error_message="Client not initialized",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if self.agent_card.capabilities.streaming:
|
|
179
|
+
structlogger.info(
|
|
180
|
+
"a2a_agent.run.streaming",
|
|
181
|
+
event_info="Running A2A agent in streaming mode",
|
|
182
|
+
agent_name=self._name,
|
|
183
|
+
)
|
|
184
|
+
return await self._run_streaming_agent(agent_input)
|
|
185
|
+
else:
|
|
186
|
+
# we dont support non-streaming mode yet
|
|
187
|
+
structlogger.error(
|
|
188
|
+
"a2a_agent.run.error",
|
|
189
|
+
event_info="Non-streaming mode is currently not supported",
|
|
190
|
+
agent_name=self._name,
|
|
191
|
+
)
|
|
192
|
+
return AgentOutput(
|
|
193
|
+
id=agent_input.id,
|
|
194
|
+
status=AgentStatus.FATAL_ERROR,
|
|
195
|
+
error_message="Non-streaming mode is not supported",
|
|
196
|
+
)
|
|
47
197
|
|
|
48
198
|
async def process_output(self, output: AgentOutput) -> AgentOutput:
|
|
49
199
|
"""Post-process the output before returning it to Rasa."""
|
|
50
200
|
# A2A-specific output processing logic
|
|
51
201
|
return output
|
|
202
|
+
|
|
203
|
+
async def _run_streaming_agent(self, agent_input: AgentInput) -> AgentOutput:
|
|
204
|
+
if not self._client:
|
|
205
|
+
structlogger.error(
|
|
206
|
+
"a2a_agent.run_streaming_agent.error",
|
|
207
|
+
event_info="A2A client is not initialized. Call connect() first.",
|
|
208
|
+
)
|
|
209
|
+
return AgentOutput(
|
|
210
|
+
id=agent_input.id,
|
|
211
|
+
status=AgentStatus.FATAL_ERROR,
|
|
212
|
+
error_message="Client not initialized",
|
|
213
|
+
)
|
|
214
|
+
message_send_params = self._prepare_message_send_params(agent_input)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
async for response in self._client.send_message_streaming(
|
|
218
|
+
SendStreamingMessageRequest(
|
|
219
|
+
id=str(uuid.uuid4()), params=message_send_params
|
|
220
|
+
)
|
|
221
|
+
):
|
|
222
|
+
agent_output = self._handle_streaming_response(agent_input, response)
|
|
223
|
+
if agent_output is not None:
|
|
224
|
+
return agent_output
|
|
225
|
+
else:
|
|
226
|
+
# Not a terminal response, continue waiting for next responses
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
except A2AClientError as exception:
|
|
230
|
+
structlogger.error(
|
|
231
|
+
"a2a_agent.run_streaming_agent.error",
|
|
232
|
+
event_info="Error during streaming message to A2A server",
|
|
233
|
+
agent_name=self._name,
|
|
234
|
+
error=str(exception),
|
|
235
|
+
)
|
|
236
|
+
return AgentOutput(
|
|
237
|
+
id=agent_input.id,
|
|
238
|
+
status=AgentStatus.FATAL_ERROR,
|
|
239
|
+
error_message=f"Streaming error: {exception!s}",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# In case we didn't receive any response or the response ended unexpectedly
|
|
243
|
+
structlogger.error(
|
|
244
|
+
"a2a_agent.run_streaming_agent.unexpected_end",
|
|
245
|
+
event_info="Unexpected end of streaming response from A2A server",
|
|
246
|
+
agent_name=self._name,
|
|
247
|
+
)
|
|
248
|
+
return AgentOutput(
|
|
249
|
+
id=agent_input.id,
|
|
250
|
+
status=AgentStatus.FATAL_ERROR,
|
|
251
|
+
error_message="Unexpected end of streaming response",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _handle_streaming_response(
|
|
255
|
+
self, agent_input: AgentInput, response: SendStreamingMessageResponse
|
|
256
|
+
) -> Optional[AgentOutput]:
|
|
257
|
+
"""If the agent response is terminal (i.e., completed, failed, etc.),
|
|
258
|
+
this method will return an AgentOutput.
|
|
259
|
+
Otherwise, the task is still in progress (i.e., submitted, working), so this
|
|
260
|
+
method will return None, so that the streaming or pooling agent can continue
|
|
261
|
+
to wait for updates.
|
|
262
|
+
"""
|
|
263
|
+
if isinstance(response.root, JSONRPCErrorResponse):
|
|
264
|
+
return self._handle_json_rpc_error_response(agent_input, response.root)
|
|
265
|
+
|
|
266
|
+
response_result = response.root.result
|
|
267
|
+
if isinstance(response_result, Task):
|
|
268
|
+
return self._handle_task_response(agent_input, response_result)
|
|
269
|
+
elif isinstance(response_result, Message):
|
|
270
|
+
return self._handle_message_response(response_result)
|
|
271
|
+
elif isinstance(response_result, TaskStatusUpdateEvent):
|
|
272
|
+
return self._handle_task_status_update_event(agent_input, response_result)
|
|
273
|
+
elif isinstance(response_result, TaskArtifactUpdateEvent):
|
|
274
|
+
return self._handle_task_artifact_update_event(agent_input, response_result)
|
|
275
|
+
else:
|
|
276
|
+
# Currently, no other response types exist, so this branch is
|
|
277
|
+
# unreachable. It is kept as a safeguard against future changes
|
|
278
|
+
# to the A2A protocol: if new response types are introduced,
|
|
279
|
+
# the agent will log an error instead of crashing.
|
|
280
|
+
return self._handle_unexpected_response_type(agent_input, response_result)
|
|
281
|
+
|
|
282
|
+
def _handle_json_rpc_error_response(
|
|
283
|
+
self, agent_input: AgentInput, response: JSONRPCErrorResponse
|
|
284
|
+
) -> AgentOutput:
|
|
285
|
+
structlogger.error(
|
|
286
|
+
"a2a_agent.run_streaming_agent.error",
|
|
287
|
+
event_info="Received error response from A2A server during streaming",
|
|
288
|
+
agent_name=self._name,
|
|
289
|
+
error=str(response.error),
|
|
290
|
+
)
|
|
291
|
+
if isinstance(
|
|
292
|
+
response.error,
|
|
293
|
+
(
|
|
294
|
+
InternalError,
|
|
295
|
+
InvalidAgentResponseError,
|
|
296
|
+
),
|
|
297
|
+
):
|
|
298
|
+
return AgentOutput(
|
|
299
|
+
id=agent_input.id,
|
|
300
|
+
status=AgentStatus.RECOVERABLE_ERROR,
|
|
301
|
+
error_message=str(response.error),
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
return AgentOutput(
|
|
305
|
+
id=agent_input.id,
|
|
306
|
+
status=AgentStatus.FATAL_ERROR,
|
|
307
|
+
error_message=str(response.error),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def _handle_task_response(
|
|
311
|
+
self, agent_input: AgentInput, task: Task
|
|
312
|
+
) -> Optional[AgentOutput]:
|
|
313
|
+
structlogger.debug(
|
|
314
|
+
"a2a_agent.run_streaming_agent.task_received",
|
|
315
|
+
event_info="Received task from A2A",
|
|
316
|
+
agent_name=self._name,
|
|
317
|
+
task=task,
|
|
318
|
+
)
|
|
319
|
+
agent_output = self._handle_task_status(
|
|
320
|
+
agent_input=agent_input,
|
|
321
|
+
context_id=task.context_id,
|
|
322
|
+
task_id=task.id,
|
|
323
|
+
task_status=task.status,
|
|
324
|
+
)
|
|
325
|
+
return agent_output
|
|
326
|
+
|
|
327
|
+
def _handle_message_response(self, message: Message) -> Optional[AgentOutput]:
|
|
328
|
+
# Message represents an intermediate conversational update,
|
|
329
|
+
# we need to continue waiting for task updates
|
|
330
|
+
structlogger.debug(
|
|
331
|
+
"a2a_agent.run_streaming_agent.message_received",
|
|
332
|
+
event_info="Received message from A2A",
|
|
333
|
+
agent_name=self._name,
|
|
334
|
+
message=message,
|
|
335
|
+
)
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def _handle_task_status_update_event(
|
|
339
|
+
self, agent_input: AgentInput, event: TaskStatusUpdateEvent
|
|
340
|
+
) -> Optional[AgentOutput]:
|
|
341
|
+
structlogger.debug(
|
|
342
|
+
"a2a_agent.run_streaming_agent.task_status_update_received",
|
|
343
|
+
event_info="Received task status update from A2A",
|
|
344
|
+
agent_name=self._name,
|
|
345
|
+
task_status_update_event=event,
|
|
346
|
+
)
|
|
347
|
+
agent_output = self._handle_task_status(
|
|
348
|
+
agent_input=agent_input,
|
|
349
|
+
context_id=event.context_id,
|
|
350
|
+
task_id=event.task_id,
|
|
351
|
+
task_status=event.status,
|
|
352
|
+
)
|
|
353
|
+
return agent_output
|
|
354
|
+
|
|
355
|
+
def _handle_task_artifact_update_event(
|
|
356
|
+
self, agent_input: AgentInput, event: TaskArtifactUpdateEvent
|
|
357
|
+
) -> AgentOutput:
|
|
358
|
+
structlogger.debug(
|
|
359
|
+
"a2a_agent.run_streaming_agent.task_artifact_update_received",
|
|
360
|
+
event_info="Received task artifact update from A2A",
|
|
361
|
+
agent_name=self._name,
|
|
362
|
+
task_artifact_update_event=event,
|
|
363
|
+
)
|
|
364
|
+
return AgentOutput(
|
|
365
|
+
id=agent_input.id,
|
|
366
|
+
status=AgentStatus.COMPLETED,
|
|
367
|
+
response_message=self._generate_response_message_from_parts(
|
|
368
|
+
event.artifact.parts
|
|
369
|
+
),
|
|
370
|
+
tool_results=self._generate_tool_results_from_parts(
|
|
371
|
+
agent_input, event.artifact.parts
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def _handle_unexpected_response_type(
|
|
376
|
+
self, agent_input: AgentInput, response_result: Any
|
|
377
|
+
) -> AgentOutput:
|
|
378
|
+
structlogger.error(
|
|
379
|
+
"a2a_agent.run_streaming_agent.unexpected_response_type",
|
|
380
|
+
event_info="Received unexpected response type from A2A server "
|
|
381
|
+
"during streaming",
|
|
382
|
+
agent_name=self._name,
|
|
383
|
+
response_type=type(response_result),
|
|
384
|
+
)
|
|
385
|
+
return AgentOutput(
|
|
386
|
+
id=agent_input.id,
|
|
387
|
+
status=AgentStatus.FATAL_ERROR,
|
|
388
|
+
error_message=f"Unexpected response type: {type(response_result)}",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def _handle_task_status(
|
|
392
|
+
self,
|
|
393
|
+
agent_input: AgentInput,
|
|
394
|
+
context_id: str,
|
|
395
|
+
task_id: str,
|
|
396
|
+
task_status: TaskStatus,
|
|
397
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
398
|
+
) -> Optional[AgentOutput]:
|
|
399
|
+
"""If the task status is terminal (i.e., completed, failed, etc.),
|
|
400
|
+
return an AgentOutput.
|
|
401
|
+
If the task is still in progress (i.e., submitted, working), return None,
|
|
402
|
+
so that the streaming or pooling agent can continue to wait for updates.
|
|
403
|
+
"""
|
|
404
|
+
state = task_status.state
|
|
405
|
+
|
|
406
|
+
metadata = metadata or {}
|
|
407
|
+
metadata[A2A_AGENT_CONTEXT_ID_KEY] = context_id
|
|
408
|
+
metadata[A2A_AGENT_TASK_ID_KEY] = task_id
|
|
409
|
+
|
|
410
|
+
if state == TaskState.input_required:
|
|
411
|
+
response_message = (
|
|
412
|
+
self._generate_response_message_from_parts(task_status.message.parts)
|
|
413
|
+
if task_status.message
|
|
414
|
+
else ""
|
|
415
|
+
) # This should not happen, but as type of message property
|
|
416
|
+
# is optional, so we need to handle it
|
|
417
|
+
return AgentOutput(
|
|
418
|
+
id=agent_input.id,
|
|
419
|
+
status=AgentStatus.INPUT_REQUIRED,
|
|
420
|
+
response_message=response_message,
|
|
421
|
+
metadata=metadata,
|
|
422
|
+
)
|
|
423
|
+
elif state == TaskState.completed:
|
|
424
|
+
response_message = (
|
|
425
|
+
self._generate_response_message_from_parts(task_status.message.parts)
|
|
426
|
+
if task_status.message
|
|
427
|
+
else ""
|
|
428
|
+
) # This should not happen, but as type of message property
|
|
429
|
+
# is optional, so we need to handle it
|
|
430
|
+
return AgentOutput(
|
|
431
|
+
id=agent_input.id,
|
|
432
|
+
status=AgentStatus.COMPLETED,
|
|
433
|
+
response_message=response_message,
|
|
434
|
+
metadata=metadata,
|
|
435
|
+
)
|
|
436
|
+
elif (
|
|
437
|
+
state == TaskState.failed
|
|
438
|
+
or state == TaskState.canceled
|
|
439
|
+
or state == TaskState.rejected
|
|
440
|
+
or state == TaskState.auth_required
|
|
441
|
+
):
|
|
442
|
+
structlogger.error(
|
|
443
|
+
"a2a_agent.run_streaming_agent.unsuccessful_task_state",
|
|
444
|
+
event_info="Task execution finished with an unsuccessful state",
|
|
445
|
+
agent_name=self._name,
|
|
446
|
+
state=state,
|
|
447
|
+
)
|
|
448
|
+
return AgentOutput(
|
|
449
|
+
id=agent_input.id,
|
|
450
|
+
status=AgentStatus.RECOVERABLE_ERROR,
|
|
451
|
+
error_message=f"Task state: {state}",
|
|
452
|
+
metadata=metadata,
|
|
453
|
+
)
|
|
454
|
+
elif state == TaskState.submitted or state == TaskState.working:
|
|
455
|
+
# The task is still in progress, return None to continue waiting for updates
|
|
456
|
+
return None
|
|
457
|
+
else:
|
|
458
|
+
structlogger.error(
|
|
459
|
+
"a2a_agent.run_streaming_agent.unexpected_task_state",
|
|
460
|
+
event_info="Unexpected task state received from A2A",
|
|
461
|
+
agent_name=self._name,
|
|
462
|
+
state=state,
|
|
463
|
+
)
|
|
464
|
+
return AgentOutput(
|
|
465
|
+
id=agent_input.id,
|
|
466
|
+
status=AgentStatus.FATAL_ERROR,
|
|
467
|
+
error_message=f"Unexpected task state: {state}",
|
|
468
|
+
metadata=metadata,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def _prepare_message_send_params(agent_input: AgentInput) -> MessageSendParams:
|
|
473
|
+
parts: List[Part] = []
|
|
474
|
+
if agent_input.metadata and A2A_AGENT_CONTEXT_ID_KEY in agent_input.metadata:
|
|
475
|
+
# Agent knows the conversation history already, send the last
|
|
476
|
+
# user message only
|
|
477
|
+
parts.append(Part(root=TextPart(text=agent_input.user_message)))
|
|
478
|
+
else:
|
|
479
|
+
# Send the full conversation history
|
|
480
|
+
parts.append(Part(root=TextPart(text=agent_input.conversation_history)))
|
|
481
|
+
|
|
482
|
+
if len(agent_input.slots) > 0:
|
|
483
|
+
slots_dict: Dict[str, Any] = {
|
|
484
|
+
"slots": [
|
|
485
|
+
slot.model_dump(exclude={"type", "allowed_values"})
|
|
486
|
+
for slot in agent_input.slots
|
|
487
|
+
if slot.value is not None
|
|
488
|
+
]
|
|
489
|
+
}
|
|
490
|
+
parts.append(Part(root=DataPart(data=slots_dict)))
|
|
491
|
+
|
|
492
|
+
agent_message = Message(
|
|
493
|
+
role=Role.user,
|
|
494
|
+
parts=parts,
|
|
495
|
+
message_id=str(uuid.uuid4()),
|
|
496
|
+
context_id=agent_input.metadata.get(A2A_AGENT_CONTEXT_ID_KEY, None),
|
|
497
|
+
task_id=agent_input.metadata.get(A2A_AGENT_TASK_ID_KEY, None),
|
|
498
|
+
)
|
|
499
|
+
structlogger.debug(
|
|
500
|
+
"a2a_agent._prepare_message_send_params",
|
|
501
|
+
event_info="Prepared message to send to A2A server",
|
|
502
|
+
agent_name=agent_input.id,
|
|
503
|
+
message=agent_message,
|
|
504
|
+
)
|
|
505
|
+
return MessageSendParams(
|
|
506
|
+
message=agent_message,
|
|
507
|
+
configuration=MessageSendConfiguration(
|
|
508
|
+
accepted_output_modes=["text", "text/plain", "application/json"],
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
@staticmethod
|
|
513
|
+
def _generate_response_message_from_parts(parts: Optional[List[Part]]) -> str:
|
|
514
|
+
"""Convert a list of Part objects to a single string message."""
|
|
515
|
+
result = ""
|
|
516
|
+
if not parts:
|
|
517
|
+
return result
|
|
518
|
+
for part in parts:
|
|
519
|
+
if isinstance(part.root, TextPart):
|
|
520
|
+
result += part.root.text + "\n"
|
|
521
|
+
elif isinstance(part.root, DataPart):
|
|
522
|
+
# DataPart results will be returned as a pert of the tool results,
|
|
523
|
+
# we don't need to include it in the response message
|
|
524
|
+
continue
|
|
525
|
+
elif isinstance(part.root, FilePart) and isinstance(
|
|
526
|
+
part.root.file, FileWithUri
|
|
527
|
+
):
|
|
528
|
+
# If the file is a FileWithUri, we can include the URI
|
|
529
|
+
result += f"File: {part.root.file.uri}\n"
|
|
530
|
+
else:
|
|
531
|
+
structlogger.warning(
|
|
532
|
+
"a2a_agent._parts_to_single_message.warning",
|
|
533
|
+
event_info="Unsupported part type encountered",
|
|
534
|
+
part_type=type(part.root),
|
|
535
|
+
)
|
|
536
|
+
return result.strip()
|
|
537
|
+
|
|
538
|
+
@staticmethod
|
|
539
|
+
def _generate_tool_results_from_parts(
|
|
540
|
+
agent_input: AgentInput, parts: List[Part]
|
|
541
|
+
) -> Optional[List[List[Dict[str, Any]]]]:
|
|
542
|
+
tool_results_of_current_iteration: List[Dict[str, Any]] = []
|
|
543
|
+
for i, part in enumerate(parts):
|
|
544
|
+
if (
|
|
545
|
+
isinstance(part.root, DataPart)
|
|
546
|
+
and part.root.data
|
|
547
|
+
and len(part.root.data) > 0
|
|
548
|
+
):
|
|
549
|
+
# There might be multiple DataParts in the response,
|
|
550
|
+
# we will treat each of them as a separate tool result.
|
|
551
|
+
# The tool name will be the agent ID + index of the part.
|
|
552
|
+
tool_results_of_current_iteration.append(
|
|
553
|
+
{"tool_name": f"{agent_input.id}_{i}", "result": part.root.data}
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
previous_tool_results: List[List[Dict[str, Any]]] = (
|
|
557
|
+
agent_input.metadata.get(AGENT_METADATA_TOOL_RESULTS_KEY, []) or []
|
|
558
|
+
)
|
|
559
|
+
previous_tool_results.append(tool_results_of_current_iteration)
|
|
560
|
+
|
|
561
|
+
return previous_tool_results
|
|
562
|
+
|
|
563
|
+
@staticmethod
|
|
564
|
+
def _load_agent_card_from_file(agent_card_path: str) -> AgentCard:
|
|
565
|
+
"""Load agent card from JSON file."""
|
|
566
|
+
try:
|
|
567
|
+
with open(os.path.abspath(agent_card_path), "r") as f:
|
|
568
|
+
card_data = json.load(f)
|
|
569
|
+
|
|
570
|
+
skills = [
|
|
571
|
+
AgentSkill(
|
|
572
|
+
id=skill_data["id"],
|
|
573
|
+
name=skill_data["name"],
|
|
574
|
+
description=skill_data["description"],
|
|
575
|
+
tags=skill_data.get("tags", []),
|
|
576
|
+
examples=skill_data.get("examples", []),
|
|
577
|
+
)
|
|
578
|
+
for skill_data in card_data.get("skills", [])
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
return AgentCard(
|
|
582
|
+
name=card_data["name"],
|
|
583
|
+
description=card_data["description"],
|
|
584
|
+
url=card_data["url"],
|
|
585
|
+
version=card_data.get("version", "1.0.0"),
|
|
586
|
+
default_input_modes=card_data.get(
|
|
587
|
+
"defaultInputModes", ["text", "text/plain"]
|
|
588
|
+
),
|
|
589
|
+
default_output_modes=card_data.get(
|
|
590
|
+
"defaultOutputModes", ["text", "text/plain", "application/json"]
|
|
591
|
+
),
|
|
592
|
+
capabilities=AgentCapabilities(
|
|
593
|
+
streaming=card_data.get("capabilities", {}).get("streaming", True)
|
|
594
|
+
),
|
|
595
|
+
skills=skills,
|
|
596
|
+
)
|
|
597
|
+
except FileNotFoundError:
|
|
598
|
+
raise FileNotFoundError(f"Agent card file not found: {agent_card_path}")
|
|
599
|
+
except json.JSONDecodeError as e:
|
|
600
|
+
raise ValueError(f"Invalid JSON in agent card file {agent_card_path}: {e}")
|
|
601
|
+
except KeyError as e:
|
|
602
|
+
raise ValueError(
|
|
603
|
+
f"Missing required field in agent card {agent_card_path}: {e}"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
async def _resolve_agent_card_with_retry(
|
|
608
|
+
agent_card_path: str, timeout: int, max_retries: int
|
|
609
|
+
) -> AgentCard:
|
|
610
|
+
"""Resolve the agent card from a given path or URL."""
|
|
611
|
+
# split agent_card_path into base URL and path
|
|
612
|
+
try:
|
|
613
|
+
url_parts = urlparse(agent_card_path)
|
|
614
|
+
base_url = f"{url_parts.scheme}://{url_parts.netloc}"
|
|
615
|
+
path = url_parts.path
|
|
616
|
+
except ValueError:
|
|
617
|
+
raise RasaException(f"Could not parse the URL: '{agent_card_path}'.")
|
|
618
|
+
structlogger.debug(
|
|
619
|
+
"a2a_agent.resolve_agent_card",
|
|
620
|
+
event_info="Resolving agent card from remote URL",
|
|
621
|
+
agent_card_path=agent_card_path,
|
|
622
|
+
base_url=base_url,
|
|
623
|
+
path=path,
|
|
624
|
+
timeout=timeout,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
for attempt in range(max_retries):
|
|
628
|
+
if attempt > 0:
|
|
629
|
+
structlogger.debug(
|
|
630
|
+
"a2a_agent.resolve_agent_card.retrying",
|
|
631
|
+
agent_card_path=f"{base_url}/{path}",
|
|
632
|
+
attempt=attempt + 1,
|
|
633
|
+
num_retries=max_retries,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
agent_card = await A2ACardResolver(
|
|
638
|
+
httpx.AsyncClient(timeout=timeout),
|
|
639
|
+
base_url=base_url,
|
|
640
|
+
agent_card_path=path,
|
|
641
|
+
).get_agent_card()
|
|
642
|
+
|
|
643
|
+
if agent_card:
|
|
644
|
+
return agent_card
|
|
645
|
+
except (A2AClientHTTPError, A2AClientJSONError) as exception:
|
|
646
|
+
structlogger.warning(
|
|
647
|
+
"a2a_agent.resolve_agent_card.error",
|
|
648
|
+
event_info="Error while resolving agent card",
|
|
649
|
+
agent_card_path=agent_card_path,
|
|
650
|
+
attempt=attempt + 1,
|
|
651
|
+
num_retries=max_retries,
|
|
652
|
+
error=str(exception),
|
|
653
|
+
)
|
|
654
|
+
if attempt < max_retries - 1:
|
|
655
|
+
# exponential backoff - wait longer with each retry
|
|
656
|
+
# 1 second, 2 seconds, 4 seconds, etc.
|
|
657
|
+
await asyncio.sleep(min(2**attempt, MAX_AGENT_RETRY_DELAY_SECONDS))
|
|
658
|
+
|
|
659
|
+
raise AgentInitializationException(
|
|
660
|
+
f"Failed to resolve agent card from {agent_card_path} after "
|
|
661
|
+
f"{max_retries} attempts."
|
|
662
|
+
)
|
|
@@ -8,6 +8,8 @@ from jinja2 import Template
|
|
|
8
8
|
from mcp import ListToolsResult
|
|
9
9
|
|
|
10
10
|
from rasa.agents.constants import (
|
|
11
|
+
AGENT_DEFAULT_MAX_RETRIES,
|
|
12
|
+
AGENT_DEFAULT_TIMEOUT_SECONDS,
|
|
11
13
|
AGENT_METADATA_TOOL_RESULTS_KEY,
|
|
12
14
|
KEY_ARGUMENTS,
|
|
13
15
|
KEY_CONTENT,
|
|
@@ -105,9 +107,9 @@ class MCPBaseAgent(AgentProtocol):
|
|
|
105
107
|
log_source_method=LOG_COMPONENT_SOURCE_METHOD_INIT,
|
|
106
108
|
)
|
|
107
109
|
|
|
108
|
-
self._timeout = timeout or
|
|
110
|
+
self._timeout = timeout or AGENT_DEFAULT_TIMEOUT_SECONDS
|
|
109
111
|
|
|
110
|
-
self._max_retries = max_retries or
|
|
112
|
+
self._max_retries = max_retries or AGENT_DEFAULT_MAX_RETRIES
|
|
111
113
|
|
|
112
114
|
self._server_configs = server_configs or []
|
|
113
115
|
|