rasa-pro 3.14.0rc2__py3-none-any.whl → 3.14.0rc4__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/protocol/a2a/a2a_agent.py +50 -42
- rasa/agents/protocol/mcp/mcp_base_agent.py +15 -1
- rasa/agents/utils.py +27 -5
- rasa/agents/validation.py +64 -13
- rasa/api.py +1 -2
- rasa/builder/copilot/constants.py +4 -0
- rasa/builder/copilot/copilot.py +37 -1
- rasa/builder/copilot/copilot_templated_message_provider.py +23 -0
- rasa/builder/copilot/models.py +43 -49
- rasa/builder/copilot/prompts/copilot_system_prompt.jinja2 +33 -12
- rasa/builder/copilot/prompts/latest_user_message_context_prompt.jinja2 +59 -29
- rasa/builder/copilot/telemetry.py +8 -0
- rasa/builder/copilot/templated_messages/copilot_templated_responses.yml +3 -0
- rasa/builder/copilot/templated_messages/copilot_welcome_messages.yml +56 -0
- rasa/builder/jobs.py +162 -5
- rasa/builder/models.py +3 -0
- rasa/builder/service.py +1 -0
- rasa/cli/dialogue_understanding_test.py +1 -0
- rasa/cli/e2e_test.py +1 -0
- rasa/cli/inspect.py +1 -0
- rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/feedback.yml +46 -0
- rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/goodbye.yml +9 -0
- rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/help.yml +8 -0
- rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/human_handoff.yml +41 -0
- rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/patterns.yml +32 -0
- rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/show_faqs.yml +8 -0
- rasa/cli/project_templates/telco/data/network/flow_solve_internet_issue.yml +2 -2
- rasa/cli/project_templates/telco/domain/network/solve_internet_issue.yml +1 -2
- rasa/cli/project_templates/telco/tests/e2e_test_cases/with_stub/network/solve_internet_not_slow.yml +33 -0
- rasa/cli/project_templates/telco/tests/e2e_test_cases/with_stub/network/solve_internet_slow.yml +47 -0
- rasa/cli/project_templates/telco/tests/e2e_test_cases/without_stub/general/hello.yml +8 -0
- rasa/cli/run.py +1 -5
- rasa/cli/shell.py +1 -0
- rasa/cli/train.py +1 -0
- rasa/cli/validation/bot_config.py +9 -2
- rasa/core/available_agents.py +65 -55
- rasa/core/config/available_endpoints.py +6 -3
- rasa/core/config/configuration.py +36 -1
- rasa/core/policies/flows/agent_executor.py +16 -8
- rasa/dialogue_understanding/commands/start_flow_command.py +10 -3
- rasa/dialogue_understanding/commands/utils.py +15 -4
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +4 -2
- rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +4 -4
- rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +4 -4
- rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +2 -2
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +22 -20
- rasa/dialogue_understanding_test/du_test_runner.py +2 -2
- rasa/e2e_test/e2e_test_runner.py +2 -2
- rasa/engine/validation.py +6 -2
- rasa/shared/agents/auth/auth_strategy/oauth2_auth_strategy.py +10 -4
- rasa/shared/agents/auth/constants.py +1 -0
- rasa/shared/agents/auth/utils.py +85 -0
- rasa/shared/constants.py +3 -0
- rasa/shared/core/flows/steps/call.py +2 -2
- rasa/shared/utils/llm.py +33 -0
- rasa/telemetry.py +3 -3
- rasa/utils/pypred.py +7 -0
- rasa/validator.py +127 -2
- rasa/version.py +1 -1
- {rasa_pro-3.14.0rc2.dist-info → rasa_pro-3.14.0rc4.dist-info}/METADATA +19 -7
- {rasa_pro-3.14.0rc2.dist-info → rasa_pro-3.14.0rc4.dist-info}/RECORD +81 -71
- rasa/cli/project_templates/telco/tests/e2e_test_cases/network/solve_internet_issue.yml +0 -57
- /rasa/cli/project_templates/{finance/tests/e2e_test_cases → basic/tests/e2e_test_cases/without_stub}/general/hello.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{accounts → without_stub/accounts}/check_balance.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{accounts → without_stub/accounts}/download_statements.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{cards → without_stub/cards}/block_card.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/bot_challenge.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/feedback.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/goodbye.yml +0 -0
- /rasa/cli/project_templates/{telco/tests/e2e_test_cases → finance/tests/e2e_test_cases/without_stub}/general/hello.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/human_handoff.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/patterns.yml +0 -0
- /rasa/cli/project_templates/finance/tests/e2e_test_cases/{transfers → without_stub/transfers}/transfer_money.yml +0 -0
- /rasa/cli/project_templates/telco/tests/e2e_test_cases/{billing → without_stub/billing}/understand_bill.yml +0 -0
- /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/bot_challenge.yml +0 -0
- /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/feedback.yml +0 -0
- /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/goodbye.yml +0 -0
- /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/human_handoff.yml +0 -0
- /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/patterns.yml +0 -0
- {rasa_pro-3.14.0rc2.dist-info → rasa_pro-3.14.0rc4.dist-info}/NOTICE +0 -0
- {rasa_pro-3.14.0rc2.dist-info → rasa_pro-3.14.0rc4.dist-info}/WHEEL +0 -0
- {rasa_pro-3.14.0rc2.dist-info → rasa_pro-3.14.0rc4.dist-info}/entry_points.txt +0 -0
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import time
|
|
5
5
|
import uuid
|
|
6
|
+
from contextlib import aclosing
|
|
6
7
|
from typing import Any, ClassVar, Dict, List, Optional
|
|
7
8
|
from urllib.parse import urlparse
|
|
8
9
|
|
|
@@ -168,8 +169,7 @@ class A2AAgent(AgentProtocol):
|
|
|
168
169
|
error=str(exception),
|
|
169
170
|
)
|
|
170
171
|
raise AgentInitializationException(
|
|
171
|
-
f"Failed to initialize A2A client for agent "
|
|
172
|
-
f"'{self._name}': {exception}"
|
|
172
|
+
f"Failed to initialize A2A client for agent '{self._name}': {exception}"
|
|
173
173
|
) from exception
|
|
174
174
|
|
|
175
175
|
await self._perform_health_check()
|
|
@@ -215,21 +215,26 @@ class A2AAgent(AgentProtocol):
|
|
|
215
215
|
task_id: Optional[str] = None
|
|
216
216
|
events_received = 0
|
|
217
217
|
try:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
218
|
+
# Use aclosing to ensure proper cleanup of the async generator
|
|
219
|
+
stream = self._client.send_message(message)
|
|
220
|
+
async with aclosing(stream) as stream: # type: ignore[type-var]
|
|
221
|
+
async for event in stream:
|
|
222
|
+
events_received += 1
|
|
223
|
+
agent_output = self._handle_send_message_response(
|
|
224
|
+
agent_input, event
|
|
225
|
+
)
|
|
226
|
+
if agent_output is not None:
|
|
227
|
+
return agent_output
|
|
228
|
+
else:
|
|
229
|
+
# Not a terminal response, save taskID (in case that's the only
|
|
230
|
+
# event, and we need to pool) and continue waiting for events
|
|
231
|
+
if (
|
|
232
|
+
isinstance(event, tuple)
|
|
233
|
+
and len(event) == 2
|
|
234
|
+
and isinstance(event[0], Task)
|
|
235
|
+
):
|
|
236
|
+
task_id = event[0].id
|
|
237
|
+
continue
|
|
233
238
|
except A2AClientJSONRPCError as e:
|
|
234
239
|
return self._handle_json_rpc_error_response(agent_input, e.error)
|
|
235
240
|
except A2AClientError as exception:
|
|
@@ -833,37 +838,40 @@ class A2AAgent(AgentProtocol):
|
|
|
833
838
|
parts=[Part(root=TextPart(text="hello"))],
|
|
834
839
|
message_id=str(uuid.uuid4()),
|
|
835
840
|
)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
841
|
+
# Use aclosing to ensure proper cleanup of the async generator
|
|
842
|
+
stream = self._client.send_message(test_message)
|
|
843
|
+
async with aclosing(stream) as stream: # type: ignore[type-var]
|
|
844
|
+
async for event in stream:
|
|
845
|
+
if (
|
|
846
|
+
isinstance(event, Message)
|
|
847
|
+
or isinstance(event, tuple)
|
|
848
|
+
and len(event) == 2
|
|
849
|
+
and isinstance(event[0], Task)
|
|
850
|
+
):
|
|
851
|
+
# We got a valid response, health check succeeded
|
|
852
|
+
return
|
|
845
853
|
|
|
846
|
-
|
|
854
|
+
event_info = "Unexpected response type during health check"
|
|
855
|
+
structlogger.error(
|
|
856
|
+
"a2a_agent.health_check.unexpected_response",
|
|
857
|
+
event_info=event_info,
|
|
858
|
+
agent_name=self._name,
|
|
859
|
+
response=event,
|
|
860
|
+
url=str(self.agent_card.url),
|
|
861
|
+
)
|
|
862
|
+
raise AgentInitializationException(f"{event_info}: {event}")
|
|
863
|
+
# If the loop completes with no return, no events were received
|
|
864
|
+
event_info = (
|
|
865
|
+
f"Health check failed for A2A agent '{self._name}' "
|
|
866
|
+
f"at {self.agent_card.url}: no events received"
|
|
867
|
+
)
|
|
847
868
|
structlogger.error(
|
|
848
|
-
"a2a_agent.health_check.
|
|
869
|
+
"a2a_agent.health_check.no_events",
|
|
849
870
|
event_info=event_info,
|
|
850
871
|
agent_name=self._name,
|
|
851
|
-
response=event,
|
|
852
872
|
url=str(self.agent_card.url),
|
|
853
873
|
)
|
|
854
|
-
raise AgentInitializationException(
|
|
855
|
-
# If the loop completes with no return, no events were received
|
|
856
|
-
event_info = (
|
|
857
|
-
f"Health check failed for A2A agent '{self._name}' "
|
|
858
|
-
f"at {self.agent_card.url}: no events received"
|
|
859
|
-
)
|
|
860
|
-
structlogger.error(
|
|
861
|
-
"a2a_agent.health_check.no_events",
|
|
862
|
-
event_info=event_info,
|
|
863
|
-
agent_name=self._name,
|
|
864
|
-
url=str(self.agent_card.url),
|
|
865
|
-
)
|
|
866
|
-
raise AgentInitializationException(event_info)
|
|
874
|
+
raise AgentInitializationException(event_info)
|
|
867
875
|
except Exception as exception:
|
|
868
876
|
event_info = (
|
|
869
877
|
f"Health check failed for A2A agent '{self._name}' at "
|
|
@@ -71,7 +71,7 @@ structlogger = structlog.get_logger()
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
class MCPBaseAgent(AgentProtocol):
|
|
74
|
-
"""MCP protocol implementation"""
|
|
74
|
+
"""MCP protocol implementation."""
|
|
75
75
|
|
|
76
76
|
MAX_ITERATIONS = 10
|
|
77
77
|
|
|
@@ -135,6 +135,20 @@ class MCPBaseAgent(AgentProtocol):
|
|
|
135
135
|
@classmethod
|
|
136
136
|
def from_config(cls, config: AgentConfig) -> "MCPBaseAgent":
|
|
137
137
|
"""Initialize the MCP Open Agent with the given configuration."""
|
|
138
|
+
# Warn if configuration.timeout is set for MCP agents
|
|
139
|
+
if config.configuration and config.configuration.timeout is not None:
|
|
140
|
+
structlogger.warning(
|
|
141
|
+
"mcp_agent.configuration.timeout.not_implemented",
|
|
142
|
+
event_info="configuration.timeout is not implemented for MCP agents. "
|
|
143
|
+
"MCP agents do not establish external connections, "
|
|
144
|
+
"so agent-level timeout is not used. "
|
|
145
|
+
"To set timeout for LLM requests, "
|
|
146
|
+
"configure 'timeout' in the model_group "
|
|
147
|
+
"in endpoints.yml and reference it via configuration.llm.model_group.",
|
|
148
|
+
agent_name=config.agent.name,
|
|
149
|
+
timeout_value=config.configuration.timeout,
|
|
150
|
+
)
|
|
151
|
+
|
|
138
152
|
return cls(
|
|
139
153
|
name=config.agent.name,
|
|
140
154
|
description=config.agent.description,
|
rasa/agents/utils.py
CHANGED
|
@@ -127,7 +127,9 @@ def is_agent_valid(agent_id: str) -> bool:
|
|
|
127
127
|
Returns:
|
|
128
128
|
True if the agent exists, False otherwise.
|
|
129
129
|
"""
|
|
130
|
-
agent_config =
|
|
130
|
+
agent_config = Configuration.get_instance().available_agents.get_agent_config(
|
|
131
|
+
agent_id
|
|
132
|
+
)
|
|
131
133
|
return agent_config is not None
|
|
132
134
|
|
|
133
135
|
|
|
@@ -159,7 +161,9 @@ def get_agent_info(agent_id: str) -> Optional[Dict[str, str]]:
|
|
|
159
161
|
Returns:
|
|
160
162
|
Dictionary with agent name and description if found, None otherwise.
|
|
161
163
|
"""
|
|
162
|
-
agent_config =
|
|
164
|
+
agent_config = Configuration.get_instance().available_agents.get_agent_config(
|
|
165
|
+
agent_id
|
|
166
|
+
)
|
|
163
167
|
if agent_config is None:
|
|
164
168
|
return None
|
|
165
169
|
|
|
@@ -170,7 +174,7 @@ def get_agent_info(agent_id: str) -> Optional[Dict[str, str]]:
|
|
|
170
174
|
|
|
171
175
|
|
|
172
176
|
def get_completed_agents_info(tracker: DialogueStateTracker) -> List[Dict[str, str]]:
|
|
173
|
-
"""Get information for all completed agents.
|
|
177
|
+
"""Get information for all completed agents in the currently active flow.
|
|
174
178
|
|
|
175
179
|
Args:
|
|
176
180
|
tracker: The dialogue state tracker.
|
|
@@ -178,12 +182,30 @@ def get_completed_agents_info(tracker: DialogueStateTracker) -> List[Dict[str, s
|
|
|
178
182
|
Returns:
|
|
179
183
|
List of dictionaries containing agent information for completed agents.
|
|
180
184
|
"""
|
|
185
|
+
from rasa.dialogue_understanding.stack.utils import top_user_flow_frame
|
|
186
|
+
|
|
187
|
+
# Get the currently active flow
|
|
188
|
+
top_flow_frame = top_user_flow_frame(tracker.stack)
|
|
189
|
+
if not top_flow_frame:
|
|
190
|
+
# No active flow, return empty list
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
current_flow_id = top_flow_frame.flow_id
|
|
181
194
|
completed_agents = []
|
|
195
|
+
|
|
196
|
+
# Get all agents that completed in the current flow
|
|
197
|
+
agents_completed_in_current_flow = set()
|
|
182
198
|
for event in reversed(tracker.events):
|
|
183
|
-
if isinstance(event, AgentCompleted):
|
|
184
|
-
|
|
199
|
+
if isinstance(event, AgentCompleted) and event.flow_id == current_flow_id:
|
|
200
|
+
agents_completed_in_current_flow.add(event.agent_id)
|
|
201
|
+
|
|
202
|
+
# Only include agents that are completed (not currently running)
|
|
203
|
+
for agent_id in agents_completed_in_current_flow:
|
|
204
|
+
if is_agent_completed(tracker, agent_id):
|
|
205
|
+
agent_info = get_agent_info(agent_id)
|
|
185
206
|
if agent_info:
|
|
186
207
|
completed_agents.append(agent_info)
|
|
208
|
+
|
|
187
209
|
return completed_agents
|
|
188
210
|
|
|
189
211
|
|
rasa/agents/validation.py
CHANGED
|
@@ -5,8 +5,9 @@ import urllib.parse
|
|
|
5
5
|
from collections import Counter
|
|
6
6
|
from typing import Any, Dict, List, NoReturn, Set
|
|
7
7
|
|
|
8
|
+
import jinja2.exceptions
|
|
9
|
+
import structlog
|
|
8
10
|
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
-
from ruamel import yaml
|
|
10
11
|
|
|
11
12
|
from rasa.agents.exceptions import (
|
|
12
13
|
AgentNameFlowConflictException,
|
|
@@ -18,11 +19,18 @@ from rasa.core.available_agents import (
|
|
|
18
19
|
AgentConfiguration,
|
|
19
20
|
AgentConnections,
|
|
20
21
|
AgentInfo,
|
|
22
|
+
AvailableAgents,
|
|
21
23
|
ProtocolConfig,
|
|
22
24
|
)
|
|
23
25
|
from rasa.core.config.available_endpoints import AvailableEndpoints
|
|
24
26
|
from rasa.core.config.configuration import Configuration
|
|
25
27
|
from rasa.exceptions import ValidationError
|
|
28
|
+
from rasa.shared.agents.auth.utils import validate_secrets_in_params
|
|
29
|
+
from rasa.shared.utils.llm import get_prompt_template, validate_jinja2_template
|
|
30
|
+
from rasa.shared.utils.yaml import read_config_file
|
|
31
|
+
|
|
32
|
+
# Initialize logger
|
|
33
|
+
structlogger = structlog.get_logger()
|
|
26
34
|
|
|
27
35
|
# Centralized allowed keys configuration to eliminate duplication
|
|
28
36
|
ALLOWED_KEYS = {
|
|
@@ -34,6 +42,7 @@ ALLOWED_KEYS = {
|
|
|
34
42
|
"timeout",
|
|
35
43
|
"max_retries",
|
|
36
44
|
"agent_card",
|
|
45
|
+
"auth",
|
|
37
46
|
},
|
|
38
47
|
"connections": {"mcp_servers"},
|
|
39
48
|
}
|
|
@@ -149,6 +158,41 @@ def _validate_a2a_config(agent_config: AgentConfig) -> None:
|
|
|
149
158
|
f"exist: {agent_card}",
|
|
150
159
|
)
|
|
151
160
|
|
|
161
|
+
# Validate right use of secrets in auth configuration
|
|
162
|
+
if agent_config.configuration.auth:
|
|
163
|
+
validate_secrets_in_params(
|
|
164
|
+
agent_config.configuration.auth, f"agent '{agent_name}'"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _validate_prompt_template_syntax(prompt_path: str, agent_name: str) -> None:
|
|
169
|
+
"""Validate Jinja2 syntax of a prompt template file."""
|
|
170
|
+
try:
|
|
171
|
+
# Use a simple default template, as we're assuming
|
|
172
|
+
# that the default templates are valid
|
|
173
|
+
default_template = "{{ content }}"
|
|
174
|
+
template_content = get_prompt_template(
|
|
175
|
+
prompt_path,
|
|
176
|
+
default_template,
|
|
177
|
+
log_source_component=f"agent.validation.{agent_name}",
|
|
178
|
+
log_source_method="init",
|
|
179
|
+
)
|
|
180
|
+
validate_jinja2_template(template_content)
|
|
181
|
+
|
|
182
|
+
except jinja2.exceptions.TemplateSyntaxError as e:
|
|
183
|
+
raise ValidationError(
|
|
184
|
+
code="agent.validation.prompt_template_syntax_error",
|
|
185
|
+
event_info=(
|
|
186
|
+
f"Agent '{agent_name}' has invalid Jinja2 template syntax at line "
|
|
187
|
+
f"{e.lineno}: {e.message}"
|
|
188
|
+
),
|
|
189
|
+
) from e
|
|
190
|
+
except Exception as e:
|
|
191
|
+
raise ValidationError(
|
|
192
|
+
code="agent.validation.optional.prompt_template_error",
|
|
193
|
+
event_info=(f"Agent '{agent_name}' has error reading prompt template: {e}"),
|
|
194
|
+
) from e
|
|
195
|
+
|
|
152
196
|
|
|
153
197
|
def _validate_optional_keys(agent_config: AgentConfig) -> None:
|
|
154
198
|
"""Validate optional keys in agent configuration."""
|
|
@@ -158,11 +202,22 @@ def _validate_optional_keys(agent_config: AgentConfig) -> None:
|
|
|
158
202
|
if agent_config.configuration and agent_config.configuration.prompt_template:
|
|
159
203
|
prompt_path = agent_config.configuration.prompt_template
|
|
160
204
|
if not os.path.exists(prompt_path):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
205
|
+
# If reading the custom prompt fails,
|
|
206
|
+
# allow fallback to default prompt template
|
|
207
|
+
structlogger.warning(
|
|
208
|
+
"agent.validation.optional.prompt_template_file_not_found",
|
|
209
|
+
agent_name=agent_name,
|
|
210
|
+
prompt_path=prompt_path,
|
|
211
|
+
event_info=(
|
|
212
|
+
f"Prompt template file not found: {prompt_path}. "
|
|
213
|
+
f"Agent will use default template."
|
|
214
|
+
),
|
|
165
215
|
)
|
|
216
|
+
# Don't raise ValidationError, allow fallback to default template
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
# Validate Jinja2 syntax
|
|
220
|
+
_validate_prompt_template_syntax(prompt_path, agent_name)
|
|
166
221
|
|
|
167
222
|
# Validate module if present
|
|
168
223
|
if agent_config.configuration and agent_config.configuration.module:
|
|
@@ -348,9 +403,9 @@ def _validate_mandatory_fields(data: Dict[str, Any], agent_name: str) -> None:
|
|
|
348
403
|
),
|
|
349
404
|
)
|
|
350
405
|
|
|
351
|
-
# Check for required fields
|
|
406
|
+
# Check for required fields (protocol is optional due to default in model)
|
|
352
407
|
missing_fields = []
|
|
353
|
-
for field in ["name", "
|
|
408
|
+
for field in ["name", "description"]:
|
|
354
409
|
if field not in agent_data or not agent_data[field]:
|
|
355
410
|
missing_fields.append(field)
|
|
356
411
|
|
|
@@ -433,8 +488,7 @@ def validate_agent_folder(agent_folder: str = DEFAULT_AGENTS_CONFIG_FOLDER) -> N
|
|
|
433
488
|
# Read and validate the config content
|
|
434
489
|
try:
|
|
435
490
|
# First read the raw YAML data to validate structure
|
|
436
|
-
|
|
437
|
-
data = yaml.safe_load(f)
|
|
491
|
+
data = read_config_file(config_path)
|
|
438
492
|
|
|
439
493
|
# Validate no additional keys
|
|
440
494
|
_validate_no_additional_keys_raw_data(data)
|
|
@@ -442,10 +496,7 @@ def validate_agent_folder(agent_folder: str = DEFAULT_AGENTS_CONFIG_FOLDER) -> N
|
|
|
442
496
|
# Validate mandatory fields before creating Pydantic models
|
|
443
497
|
_validate_mandatory_fields(data, agent_folder_name)
|
|
444
498
|
|
|
445
|
-
|
|
446
|
-
from rasa.core.available_agents import AvailableAgents
|
|
447
|
-
|
|
448
|
-
agent_config = AvailableAgents._read_agent_config(config_path)
|
|
499
|
+
agent_config = AvailableAgents.from_dict(data)
|
|
449
500
|
|
|
450
501
|
# Validate the agent config (protocol-specific and endpoint references)
|
|
451
502
|
validate_agent_config(agent_config)
|
rasa/api.py
CHANGED
|
@@ -37,11 +37,10 @@ def run(
|
|
|
37
37
|
"""
|
|
38
38
|
import rasa.core.run
|
|
39
39
|
import rasa.shared.utils.common
|
|
40
|
-
from rasa.core.available_agents import AvailableAgents
|
|
41
40
|
from rasa.shared.constants import DOCS_BASE_URL
|
|
42
41
|
from rasa.shared.utils.cli import print_warning
|
|
43
42
|
|
|
44
|
-
_sub_agents =
|
|
43
|
+
_sub_agents = Configuration.get_instance().available_agents
|
|
45
44
|
|
|
46
45
|
credentials = Configuration.get_instance().credentials
|
|
47
46
|
|
|
@@ -14,6 +14,7 @@ COPILOT_TRAINING_ERROR_HANDLER_PROMPT_FILE = (
|
|
|
14
14
|
COPILOT_MESSAGE_TEMPLATES_DIR = "builder.copilot.templated_messages"
|
|
15
15
|
RASA_INTERNAL_MESSAGES_TEMPLATES_FILE = "copilot_internal_messages_templates.yml"
|
|
16
16
|
COPILOT_HANDLER_RESPONSES_FILE = "copilot_templated_responses.yml"
|
|
17
|
+
COPILOT_WELCOME_MESSAGES_FILE = "copilot_welcome_messages.yml"
|
|
17
18
|
|
|
18
19
|
# OpenAI roles copilot utilizes - Use literal types to avoid type errors with OpenAI
|
|
19
20
|
ROLE_USER: Literal["user"] = "user"
|
|
@@ -32,3 +33,6 @@ COPILOT_SEGMENT_WRITE_KEY_ENV_VAR = "COPILOT_SEGMENT_WRITE_KEY"
|
|
|
32
33
|
|
|
33
34
|
# Copilot signing
|
|
34
35
|
SIGNATURE_VERSION_V1 = "v1"
|
|
36
|
+
|
|
37
|
+
# Welcome message key for prompt-to-bot (not a template enum)
|
|
38
|
+
PROMPT_TO_BOT_KEY = "prompt_to_bot"
|
rasa/builder/copilot/copilot.py
CHANGED
|
@@ -22,10 +22,12 @@ from rasa.builder.copilot.constants import (
|
|
|
22
22
|
)
|
|
23
23
|
from rasa.builder.copilot.exceptions import CopilotStreamError
|
|
24
24
|
from rasa.builder.copilot.models import (
|
|
25
|
+
ChatMessage,
|
|
25
26
|
CopilotChatMessage,
|
|
26
27
|
CopilotContext,
|
|
27
28
|
CopilotGenerationContext,
|
|
28
29
|
CopilotSystemMessage,
|
|
30
|
+
EventContent,
|
|
29
31
|
FileContent,
|
|
30
32
|
InternalCopilotRequestChatMessage,
|
|
31
33
|
ResponseCategory,
|
|
@@ -144,12 +146,16 @@ class Copilot:
|
|
|
144
146
|
"""
|
|
145
147
|
relevant_documents = await self.search_rasa_documentation(context)
|
|
146
148
|
messages = await self._build_messages(context, relevant_documents)
|
|
149
|
+
tracker_event_attachments = self._extract_tracker_event_attachments(
|
|
150
|
+
context.copilot_chat_history[-1]
|
|
151
|
+
)
|
|
147
152
|
|
|
148
153
|
support_evidence = CopilotGenerationContext(
|
|
149
154
|
relevant_documents=relevant_documents,
|
|
150
155
|
system_message=messages[0],
|
|
151
156
|
chat_history=messages[1:-1],
|
|
152
157
|
last_user_message=messages[-1],
|
|
158
|
+
tracker_event_attachments=tracker_event_attachments,
|
|
153
159
|
)
|
|
154
160
|
|
|
155
161
|
return (
|
|
@@ -272,8 +278,11 @@ class Copilot:
|
|
|
272
278
|
ValueError: If the message type is not supported.
|
|
273
279
|
"""
|
|
274
280
|
if isinstance(latest_message, UserChatMessage):
|
|
281
|
+
tracker_event_attachments = latest_message.get_content_blocks_by_type(
|
|
282
|
+
EventContent
|
|
283
|
+
)
|
|
275
284
|
rendered_prompt = self._render_last_user_message_context_prompt(
|
|
276
|
-
context, relevant_documents
|
|
285
|
+
context, relevant_documents, tracker_event_attachments
|
|
277
286
|
)
|
|
278
287
|
return latest_message.build_openai_message(prompt=rendered_prompt)
|
|
279
288
|
|
|
@@ -299,6 +308,7 @@ class Copilot:
|
|
|
299
308
|
self,
|
|
300
309
|
context: CopilotContext,
|
|
301
310
|
relevant_documents: List[Document],
|
|
311
|
+
tracker_event_attachments: List[EventContent],
|
|
302
312
|
) -> str:
|
|
303
313
|
# Format relevant documentation
|
|
304
314
|
documents = [doc.model_dump() for doc in relevant_documents]
|
|
@@ -306,6 +316,8 @@ class Copilot:
|
|
|
306
316
|
conversation = self._format_conversation_history(context.tracker_context)
|
|
307
317
|
# Format current state
|
|
308
318
|
current_state = self._format_current_state(context.tracker_context)
|
|
319
|
+
# Format tracker events
|
|
320
|
+
attachments = self._format_tracker_event_attachments(tracker_event_attachments)
|
|
309
321
|
|
|
310
322
|
rendered_prompt = self._last_user_message_context_prompt_template.render(
|
|
311
323
|
current_conversation=conversation,
|
|
@@ -313,6 +325,7 @@ class Copilot:
|
|
|
313
325
|
assistant_logs=context.assistant_logs,
|
|
314
326
|
assistant_files=context.assistant_files,
|
|
315
327
|
documentation_results=documents,
|
|
328
|
+
attachments=attachments,
|
|
316
329
|
)
|
|
317
330
|
return rendered_prompt
|
|
318
331
|
|
|
@@ -390,6 +403,8 @@ class Copilot:
|
|
|
390
403
|
@staticmethod
|
|
391
404
|
def _format_documents(results: List[Document]) -> Optional[str]:
|
|
392
405
|
"""Format documentation search results as JSON dump to be used in the prompt."""
|
|
406
|
+
# We want the special message that indicates no relevant documentation source
|
|
407
|
+
# found if there are no results.
|
|
393
408
|
if not results:
|
|
394
409
|
return None
|
|
395
410
|
|
|
@@ -524,3 +539,24 @@ class Copilot:
|
|
|
524
539
|
return f"Logs: {log_content}"
|
|
525
540
|
else:
|
|
526
541
|
return ""
|
|
542
|
+
|
|
543
|
+
@staticmethod
|
|
544
|
+
def _format_tracker_event_attachments(events: List[EventContent]) -> Optional[str]:
|
|
545
|
+
"""Format tracker events as JSON dump to be used in the prompt."""
|
|
546
|
+
# We don't want to display the attachment sectin in the last user message
|
|
547
|
+
# context prompt if there are no attachments.
|
|
548
|
+
if not events:
|
|
549
|
+
return None
|
|
550
|
+
# If there are attachments, return the formatted JSON dump.
|
|
551
|
+
return json.dumps(
|
|
552
|
+
[event_content.model_dump() for event_content in events],
|
|
553
|
+
ensure_ascii=False,
|
|
554
|
+
indent=2,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
@staticmethod
|
|
558
|
+
def _extract_tracker_event_attachments(message: ChatMessage) -> List[EventContent]:
|
|
559
|
+
"""Extract the tracker event attachments from the message."""
|
|
560
|
+
if not isinstance(message, UserChatMessage):
|
|
561
|
+
return []
|
|
562
|
+
return message.get_content_blocks_by_type(EventContent)
|
|
@@ -7,6 +7,7 @@ import yaml # type: ignore
|
|
|
7
7
|
from rasa.builder.copilot.constants import (
|
|
8
8
|
COPILOT_HANDLER_RESPONSES_FILE,
|
|
9
9
|
COPILOT_MESSAGE_TEMPLATES_DIR,
|
|
10
|
+
COPILOT_WELCOME_MESSAGES_FILE,
|
|
10
11
|
RASA_INTERNAL_MESSAGES_TEMPLATES_FILE,
|
|
11
12
|
)
|
|
12
13
|
from rasa.shared.constants import PACKAGE_NAME
|
|
@@ -56,3 +57,25 @@ def load_copilot_handler_default_responses() -> Dict[str, str]:
|
|
|
56
57
|
error=e,
|
|
57
58
|
)
|
|
58
59
|
return dict()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_copilot_welcome_messages() -> Dict[str, str]:
|
|
63
|
+
"""Load welcome message templates from the YAML configuration file.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dictionary mapping template names to welcome message text.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
config = yaml.safe_load(
|
|
70
|
+
importlib.resources.read_text(
|
|
71
|
+
f"{PACKAGE_NAME}.{COPILOT_MESSAGE_TEMPLATES_DIR}",
|
|
72
|
+
COPILOT_WELCOME_MESSAGES_FILE,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return config.get("welcome_messages", {})
|
|
76
|
+
except Exception as e:
|
|
77
|
+
structlogger.error(
|
|
78
|
+
"copilot_templated_message_provider.failed_to_load_welcome_messages",
|
|
79
|
+
error=e,
|
|
80
|
+
)
|
|
81
|
+
return dict()
|
rasa/builder/copilot/models.py
CHANGED
|
@@ -110,7 +110,7 @@ class LogContent(BaseContent):
|
|
|
110
110
|
)
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
class EventContent(
|
|
113
|
+
class EventContent(BaseContent):
|
|
114
114
|
type: Literal["event"]
|
|
115
115
|
event: str = Field(..., description="The event's type_name")
|
|
116
116
|
|
|
@@ -171,6 +171,38 @@ class BaseCopilotChatMessage(BaseModel, ABC):
|
|
|
171
171
|
return None if v is None else v.value
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
class BaseContentBlockCopilotChatMessage(BaseCopilotChatMessage, ABC):
|
|
175
|
+
"""Base class for messages that contain ContentBlock lists."""
|
|
176
|
+
|
|
177
|
+
content: List[ContentBlock]
|
|
178
|
+
|
|
179
|
+
def get_flattened_text_content(self) -> str:
|
|
180
|
+
"""Get the text content from the message."""
|
|
181
|
+
return "\n".join(
|
|
182
|
+
content_block.text
|
|
183
|
+
for content_block in self.content
|
|
184
|
+
if isinstance(content_block, TextContent)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def get_flattened_log_content(self) -> str:
|
|
188
|
+
"""Get the log content from the message."""
|
|
189
|
+
return "\n".join(
|
|
190
|
+
content_block.content
|
|
191
|
+
for content_block in self.content
|
|
192
|
+
if isinstance(content_block, LogContent)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def get_content_blocks_by_type(
|
|
196
|
+
self, content_type: Type[TContentBlock]
|
|
197
|
+
) -> List[TContentBlock]:
|
|
198
|
+
"""Get the content blocks from the message by type."""
|
|
199
|
+
return [
|
|
200
|
+
content_block
|
|
201
|
+
for content_block in self.content
|
|
202
|
+
if isinstance(content_block, content_type)
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
|
|
174
206
|
class CopilotSystemMessage(BaseCopilotChatMessage):
|
|
175
207
|
role: Literal["system"] = Field(
|
|
176
208
|
default=ROLE_SYSTEM,
|
|
@@ -183,13 +215,12 @@ class CopilotSystemMessage(BaseCopilotChatMessage):
|
|
|
183
215
|
return {"role": ROLE_SYSTEM, "content": prompt}
|
|
184
216
|
|
|
185
217
|
|
|
186
|
-
class UserChatMessage(
|
|
218
|
+
class UserChatMessage(BaseContentBlockCopilotChatMessage):
|
|
187
219
|
role: Literal["user"] = Field(
|
|
188
220
|
default=ROLE_USER,
|
|
189
221
|
pattern=f"^{ROLE_USER}",
|
|
190
222
|
description="The user who sent the message.",
|
|
191
223
|
)
|
|
192
|
-
content: List[ContentBlock]
|
|
193
224
|
|
|
194
225
|
@classmethod
|
|
195
226
|
@field_validator("content")
|
|
@@ -234,14 +265,6 @@ class UserChatMessage(BaseCopilotChatMessage):
|
|
|
234
265
|
|
|
235
266
|
return self
|
|
236
267
|
|
|
237
|
-
def get_flattened_text_content(self) -> str:
|
|
238
|
-
"""Get the text content from the message."""
|
|
239
|
-
return "\n".join(
|
|
240
|
-
content_block.text
|
|
241
|
-
for content_block in self.content
|
|
242
|
-
if isinstance(content_block, TextContent)
|
|
243
|
-
)
|
|
244
|
-
|
|
245
268
|
def build_openai_message( # type: ignore[no-untyped-def]
|
|
246
269
|
self, prompt: Optional[str] = None, *args, **kwargs
|
|
247
270
|
) -> Dict[str, Any]:
|
|
@@ -260,17 +283,8 @@ class UserChatMessage(BaseCopilotChatMessage):
|
|
|
260
283
|
return {"role": ROLE_USER, "content": self.get_flattened_text_content()}
|
|
261
284
|
|
|
262
285
|
|
|
263
|
-
class CopilotChatMessage(
|
|
286
|
+
class CopilotChatMessage(BaseContentBlockCopilotChatMessage):
|
|
264
287
|
role: Literal["copilot"]
|
|
265
|
-
content: List[ContentBlock]
|
|
266
|
-
|
|
267
|
-
def get_flattened_text_content(self) -> str:
|
|
268
|
-
"""Get the text content from the message."""
|
|
269
|
-
return "\n".join(
|
|
270
|
-
content_block.text
|
|
271
|
-
for content_block in self.content
|
|
272
|
-
if isinstance(content_block, TextContent)
|
|
273
|
-
)
|
|
274
288
|
|
|
275
289
|
def build_openai_message(self, *args, **kwargs) -> Dict[str, Any]: # type: ignore[no-untyped-def]
|
|
276
290
|
# For now the Copilot responds only with the text content and all the content
|
|
@@ -278,9 +292,8 @@ class CopilotChatMessage(BaseCopilotChatMessage):
|
|
|
278
292
|
return {"role": ROLE_ASSISTANT, "content": self.get_flattened_text_content()}
|
|
279
293
|
|
|
280
294
|
|
|
281
|
-
class InternalCopilotRequestChatMessage(
|
|
295
|
+
class InternalCopilotRequestChatMessage(BaseContentBlockCopilotChatMessage):
|
|
282
296
|
role: Literal["internal_copilot_request"]
|
|
283
|
-
content: List[ContentBlock]
|
|
284
297
|
|
|
285
298
|
@model_validator(mode="after")
|
|
286
299
|
def validate_response_category(self) -> "InternalCopilotRequestChatMessage":
|
|
@@ -311,32 +324,6 @@ class InternalCopilotRequestChatMessage(BaseCopilotChatMessage):
|
|
|
311
324
|
|
|
312
325
|
return self
|
|
313
326
|
|
|
314
|
-
def get_flattened_text_content(self) -> str:
|
|
315
|
-
"""Get the text content from the message."""
|
|
316
|
-
return "\n".join(
|
|
317
|
-
content_block.text
|
|
318
|
-
for content_block in self.content
|
|
319
|
-
if isinstance(content_block, TextContent)
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
def get_flattened_log_content(self) -> str:
|
|
323
|
-
"""Get the text content from the message."""
|
|
324
|
-
return "\n".join(
|
|
325
|
-
content_block.content
|
|
326
|
-
for content_block in self.content
|
|
327
|
-
if isinstance(content_block, LogContent)
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
def get_content_blocks_by_type(
|
|
331
|
-
self, content_type: Type[TContentBlock]
|
|
332
|
-
) -> List[TContentBlock]:
|
|
333
|
-
"""Get the content blocks from the message by type."""
|
|
334
|
-
return [
|
|
335
|
-
content_block
|
|
336
|
-
for content_block in self.content
|
|
337
|
-
if isinstance(content_block, content_type)
|
|
338
|
-
]
|
|
339
|
-
|
|
340
327
|
def build_openai_message(self, prompt: str, *args, **kwargs) -> Dict[str, Any]: # type: ignore[no-untyped-def]
|
|
341
328
|
"""Build OpenAI message with pre-rendered prompt.
|
|
342
329
|
|
|
@@ -689,6 +676,13 @@ class CopilotGenerationContext(BaseModel):
|
|
|
689
676
|
last_user_message: Optional[Dict[str, Any]] = Field(
|
|
690
677
|
None, description="The last user message with context that was processed."
|
|
691
678
|
)
|
|
679
|
+
tracker_event_attachments: List[EventContent] = Field(
|
|
680
|
+
...,
|
|
681
|
+
description=(
|
|
682
|
+
"The tracker event attachments passed with the user message used as "
|
|
683
|
+
"an additional context."
|
|
684
|
+
),
|
|
685
|
+
)
|
|
692
686
|
|
|
693
687
|
class Config:
|
|
694
688
|
"""Config for CopilotGenerationContext."""
|