rasa-pro 3.14.0rc1__py3-none-any.whl → 3.14.0rc3__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/utils.py +27 -5
- rasa/agents/validation.py +7 -9
- rasa/api.py +1 -2
- rasa/builder/copilot/constants.py +4 -1
- rasa/builder/copilot/copilot.py +191 -79
- rasa/builder/copilot/models.py +306 -116
- rasa/builder/copilot/prompts/copilot_system_prompt.jinja2 +33 -12
- rasa/builder/copilot/prompts/copilot_training_error_handler_prompt.jinja2 +53 -0
- rasa/builder/copilot/prompts/latest_user_message_context_prompt.jinja2 +59 -29
- rasa/builder/copilot/telemetry.py +8 -0
- rasa/builder/guardrails/policy_checker.py +1 -1
- rasa/builder/jobs.py +182 -12
- rasa/builder/models.py +12 -3
- rasa/builder/service.py +16 -2
- 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/finance/domain/general/help.yml +0 -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 +7 -2
- rasa/core/available_agents.py +65 -55
- rasa/core/brokers/kafka.py +5 -1
- rasa/core/concurrent_lock_store.py +38 -21
- rasa/core/config/available_endpoints.py +0 -3
- rasa/core/config/configuration.py +36 -1
- rasa/core/constants.py +6 -0
- rasa/core/iam_credentials_providers/aws_iam_credentials_providers.py +69 -4
- rasa/core/iam_credentials_providers/credentials_provider_protocol.py +2 -1
- rasa/core/lock_store.py +4 -0
- rasa/core/policies/flows/agent_executor.py +16 -8
- rasa/core/redis_connection_factory.py +7 -2
- rasa/core/tracker_stores/redis_tracker_store.py +4 -0
- rasa/core/tracker_stores/sql_tracker_store.py +3 -1
- 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_test/du_test_runner.py +2 -2
- rasa/e2e_test/e2e_test_runner.py +2 -2
- rasa/shared/agents/auth/auth_strategy/oauth2_auth_strategy.py +10 -4
- rasa/shared/agents/auth/constants.py +1 -0
- rasa/shared/core/flows/steps/call.py +2 -2
- rasa/telemetry.py +3 -3
- rasa/validator.py +37 -0
- rasa/version.py +1 -1
- {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/METADATA +14 -2
- {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/RECORD +83 -73
- 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.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/NOTICE +0 -0
- {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/WHEEL +0 -0
- {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.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 "
|
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
|
@@ -6,7 +6,6 @@ from collections import Counter
|
|
|
6
6
|
from typing import Any, Dict, List, NoReturn, Set
|
|
7
7
|
|
|
8
8
|
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
-
from ruamel import yaml
|
|
10
9
|
|
|
11
10
|
from rasa.agents.exceptions import (
|
|
12
11
|
AgentNameFlowConflictException,
|
|
@@ -18,11 +17,13 @@ from rasa.core.available_agents import (
|
|
|
18
17
|
AgentConfiguration,
|
|
19
18
|
AgentConnections,
|
|
20
19
|
AgentInfo,
|
|
20
|
+
AvailableAgents,
|
|
21
21
|
ProtocolConfig,
|
|
22
22
|
)
|
|
23
23
|
from rasa.core.config.available_endpoints import AvailableEndpoints
|
|
24
24
|
from rasa.core.config.configuration import Configuration
|
|
25
25
|
from rasa.exceptions import ValidationError
|
|
26
|
+
from rasa.shared.utils.yaml import read_config_file
|
|
26
27
|
|
|
27
28
|
# Centralized allowed keys configuration to eliminate duplication
|
|
28
29
|
ALLOWED_KEYS = {
|
|
@@ -34,6 +35,7 @@ ALLOWED_KEYS = {
|
|
|
34
35
|
"timeout",
|
|
35
36
|
"max_retries",
|
|
36
37
|
"agent_card",
|
|
38
|
+
"auth",
|
|
37
39
|
},
|
|
38
40
|
"connections": {"mcp_servers"},
|
|
39
41
|
}
|
|
@@ -348,9 +350,9 @@ def _validate_mandatory_fields(data: Dict[str, Any], agent_name: str) -> None:
|
|
|
348
350
|
),
|
|
349
351
|
)
|
|
350
352
|
|
|
351
|
-
# Check for required fields
|
|
353
|
+
# Check for required fields (protocol is optional due to default in model)
|
|
352
354
|
missing_fields = []
|
|
353
|
-
for field in ["name", "
|
|
355
|
+
for field in ["name", "description"]:
|
|
354
356
|
if field not in agent_data or not agent_data[field]:
|
|
355
357
|
missing_fields.append(field)
|
|
356
358
|
|
|
@@ -433,8 +435,7 @@ def validate_agent_folder(agent_folder: str = DEFAULT_AGENTS_CONFIG_FOLDER) -> N
|
|
|
433
435
|
# Read and validate the config content
|
|
434
436
|
try:
|
|
435
437
|
# First read the raw YAML data to validate structure
|
|
436
|
-
|
|
437
|
-
data = yaml.safe_load(f)
|
|
438
|
+
data = read_config_file(config_path)
|
|
438
439
|
|
|
439
440
|
# Validate no additional keys
|
|
440
441
|
_validate_no_additional_keys_raw_data(data)
|
|
@@ -442,10 +443,7 @@ def validate_agent_folder(agent_folder: str = DEFAULT_AGENTS_CONFIG_FOLDER) -> N
|
|
|
442
443
|
# Validate mandatory fields before creating Pydantic models
|
|
443
444
|
_validate_mandatory_fields(data, agent_folder_name)
|
|
444
445
|
|
|
445
|
-
|
|
446
|
-
from rasa.core.available_agents import AvailableAgents
|
|
447
|
-
|
|
448
|
-
agent_config = AvailableAgents._read_agent_config(config_path)
|
|
446
|
+
agent_config = AvailableAgents.from_dict(data)
|
|
449
447
|
|
|
450
448
|
# Validate the agent config (protocol-specific and endpoint references)
|
|
451
449
|
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
|
|
|
@@ -6,6 +6,9 @@ COPILOT_PROMPTS_FILE = "copilot_system_prompt.jinja2"
|
|
|
6
6
|
COPILOT_LAST_USER_MESSAGE_CONTEXT_PROMPT_FILE = (
|
|
7
7
|
"latest_user_message_context_prompt.jinja2"
|
|
8
8
|
)
|
|
9
|
+
COPILOT_TRAINING_ERROR_HANDLER_PROMPT_FILE = (
|
|
10
|
+
"copilot_training_error_handler_prompt.jinja2"
|
|
11
|
+
)
|
|
9
12
|
|
|
10
13
|
# A dot-path for importlib to the rasa internal messages templates
|
|
11
14
|
COPILOT_MESSAGE_TEMPLATES_DIR = "builder.copilot.templated_messages"
|
|
@@ -22,7 +25,7 @@ ROLE_COPILOT: Literal["copilot"] = "copilot"
|
|
|
22
25
|
|
|
23
26
|
# Rasa internal role - Used to indicate that the message is from the Rasa internal
|
|
24
27
|
# system components.
|
|
25
|
-
ROLE_COPILOT_INTERNAL: Literal["
|
|
28
|
+
ROLE_COPILOT_INTERNAL: Literal["internal_copilot_request"] = "internal_copilot_request"
|
|
26
29
|
|
|
27
30
|
# Copilot Telemetry
|
|
28
31
|
COPILOT_SEGMENT_WRITE_KEY_ENV_VAR = "COPILOT_SEGMENT_WRITE_KEY"
|
rasa/builder/copilot/copilot.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import copy
|
|
3
2
|
import importlib
|
|
4
3
|
import json
|
|
5
4
|
from contextlib import asynccontextmanager
|
|
6
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
6
|
|
|
8
7
|
import openai
|
|
9
8
|
import structlog
|
|
@@ -16,19 +15,24 @@ from rasa.builder.copilot.constants import (
|
|
|
16
15
|
COPILOT_LAST_USER_MESSAGE_CONTEXT_PROMPT_FILE,
|
|
17
16
|
COPILOT_PROMPTS_DIR,
|
|
18
17
|
COPILOT_PROMPTS_FILE,
|
|
18
|
+
COPILOT_TRAINING_ERROR_HANDLER_PROMPT_FILE,
|
|
19
19
|
ROLE_COPILOT,
|
|
20
20
|
ROLE_COPILOT_INTERNAL,
|
|
21
|
-
ROLE_SYSTEM,
|
|
22
21
|
ROLE_USER,
|
|
23
22
|
)
|
|
24
23
|
from rasa.builder.copilot.exceptions import CopilotStreamError
|
|
25
24
|
from rasa.builder.copilot.models import (
|
|
25
|
+
ChatMessage,
|
|
26
26
|
CopilotChatMessage,
|
|
27
27
|
CopilotContext,
|
|
28
28
|
CopilotGenerationContext,
|
|
29
|
+
CopilotSystemMessage,
|
|
30
|
+
EventContent,
|
|
31
|
+
FileContent,
|
|
32
|
+
InternalCopilotRequestChatMessage,
|
|
29
33
|
ResponseCategory,
|
|
30
|
-
TextContent,
|
|
31
34
|
UsageStatistics,
|
|
35
|
+
UserChatMessage,
|
|
32
36
|
)
|
|
33
37
|
from rasa.builder.document_retrieval.inkeep_document_retrieval import (
|
|
34
38
|
InKeepDocumentRetrieval,
|
|
@@ -60,6 +64,12 @@ class Copilot:
|
|
|
60
64
|
COPILOT_LAST_USER_MESSAGE_CONTEXT_PROMPT_FILE,
|
|
61
65
|
)
|
|
62
66
|
)
|
|
67
|
+
self._training_error_handler_prompt_template = Template(
|
|
68
|
+
importlib.resources.read_text(
|
|
69
|
+
f"{PACKAGE_NAME}.{COPILOT_PROMPTS_DIR}",
|
|
70
|
+
COPILOT_TRAINING_ERROR_HANDLER_PROMPT_FILE,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
63
73
|
|
|
64
74
|
# The final stream chunk includes usage statistics.
|
|
65
75
|
self.usage_statistics = UsageStatistics()
|
|
@@ -136,12 +146,16 @@ class Copilot:
|
|
|
136
146
|
"""
|
|
137
147
|
relevant_documents = await self.search_rasa_documentation(context)
|
|
138
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
|
+
)
|
|
139
152
|
|
|
140
153
|
support_evidence = CopilotGenerationContext(
|
|
141
154
|
relevant_documents=relevant_documents,
|
|
142
155
|
system_message=messages[0],
|
|
143
156
|
chat_history=messages[1:-1],
|
|
144
157
|
last_user_message=messages[-1],
|
|
158
|
+
tracker_event_attachments=tracker_event_attachments,
|
|
145
159
|
)
|
|
146
160
|
|
|
147
161
|
return (
|
|
@@ -205,91 +219,96 @@ class Copilot:
|
|
|
205
219
|
Returns:
|
|
206
220
|
A list of messages in OpenAI format.
|
|
207
221
|
"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
]
|
|
214
|
-
latest_message = context.copilot_chat_history[-1]
|
|
215
|
-
|
|
216
|
-
# Create the system message
|
|
217
|
-
system_message = await self._create_system_message()
|
|
218
|
-
# Create the chat history messages (excludes the last message)
|
|
219
|
-
chat_history = self._create_chat_history_messages(past_messages)
|
|
220
|
-
# Create the last message and add the context to it
|
|
221
|
-
latest_message_with_context = self._create_last_user_message_with_context(
|
|
222
|
-
latest_message, context, relevant_documents
|
|
222
|
+
if not context.copilot_chat_history:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
past_messages = self._create_chat_history_messages(
|
|
226
|
+
context.copilot_chat_history[:-1]
|
|
223
227
|
)
|
|
224
|
-
return [system_message, *chat_history, latest_message_with_context]
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
latest_message = self._process_latest_message(
|
|
230
|
+
context.copilot_chat_history[-1], context, relevant_documents
|
|
231
|
+
)
|
|
232
|
+
system_message = self._create_system_message()
|
|
233
|
+
|
|
234
|
+
return [system_message, *past_messages, latest_message]
|
|
230
235
|
|
|
231
236
|
def _create_chat_history_messages(
|
|
232
|
-
self,
|
|
233
|
-
past_messages: List["CopilotChatMessage"],
|
|
237
|
+
self, chat_history: List[Union[UserChatMessage, CopilotChatMessage]]
|
|
234
238
|
) -> List[Dict[str, Any]]:
|
|
235
|
-
"""
|
|
239
|
+
"""Filter and convert past messages to OpenAI format.
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
This will filter out all the user messages that were flagged by guardrails, but
|
|
239
|
-
also the copilot messages that were produced by guardrails.
|
|
241
|
+
Excludes guardrails policy violations and non-user/copilot messages.
|
|
240
242
|
|
|
241
243
|
Args:
|
|
242
|
-
|
|
244
|
+
chat_history: List of chat messages to filter and convert.
|
|
243
245
|
|
|
244
246
|
Returns:
|
|
245
|
-
List of messages in OpenAI format
|
|
247
|
+
List of messages in OpenAI format
|
|
246
248
|
"""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if
|
|
251
|
-
|
|
249
|
+
filtered_messages = []
|
|
250
|
+
|
|
251
|
+
for message in chat_history:
|
|
252
|
+
if (
|
|
253
|
+
message.response_category
|
|
254
|
+
!= ResponseCategory.GUARDRAILS_POLICY_VIOLATION
|
|
255
|
+
and message.role in [ROLE_USER, ROLE_COPILOT]
|
|
256
|
+
):
|
|
257
|
+
filtered_messages.append(message)
|
|
252
258
|
|
|
253
|
-
|
|
259
|
+
return [message.build_openai_message() for message in filtered_messages]
|
|
260
|
+
|
|
261
|
+
def _process_latest_message(
|
|
254
262
|
self,
|
|
255
|
-
latest_message:
|
|
263
|
+
latest_message: Any,
|
|
256
264
|
context: CopilotContext,
|
|
257
265
|
relevant_documents: List[Document],
|
|
258
266
|
) -> Dict[str, Any]:
|
|
259
|
-
"""
|
|
260
|
-
|
|
261
|
-
The last user message is the last message in the copilot chat history.
|
|
262
|
-
We add the context prompt with the current conversation, state, assistant logs,
|
|
263
|
-
assistant files, and relevant documents as a text content block to the beginning
|
|
264
|
-
of the message.
|
|
267
|
+
"""Process the latest message and convert it to OpenAI format.
|
|
265
268
|
|
|
266
269
|
Args:
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
latest_message: The most recent message from the chat history.
|
|
271
|
+
context: The copilot context containing conversation state.
|
|
272
|
+
relevant_documents: List of relevant documents for context.
|
|
269
273
|
|
|
270
274
|
Returns:
|
|
271
|
-
|
|
275
|
+
Message in OpenAI format.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ValueError: If the message type is not supported.
|
|
272
279
|
"""
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
280
|
+
if isinstance(latest_message, UserChatMessage):
|
|
281
|
+
tracker_event_attachments = latest_message.get_content_blocks_by_type(
|
|
282
|
+
EventContent
|
|
283
|
+
)
|
|
284
|
+
rendered_prompt = self._render_last_user_message_context_prompt(
|
|
285
|
+
context, relevant_documents, tracker_event_attachments
|
|
286
|
+
)
|
|
287
|
+
return latest_message.build_openai_message(prompt=rendered_prompt)
|
|
288
|
+
|
|
289
|
+
elif isinstance(latest_message, InternalCopilotRequestChatMessage):
|
|
290
|
+
rendered_prompt = self._render_training_error_handler_prompt(
|
|
291
|
+
latest_message, relevant_documents
|
|
292
|
+
)
|
|
293
|
+
return latest_message.build_openai_message(prompt=rendered_prompt)
|
|
294
|
+
|
|
295
|
+
else:
|
|
296
|
+
raise ValueError(f"Unexpected message type: {type(latest_message)}")
|
|
297
|
+
|
|
298
|
+
def _create_system_message(self) -> Dict[str, Any]:
|
|
299
|
+
"""Create the system message for the conversation.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
System message in OpenAI format with rendered prompt template.
|
|
303
|
+
"""
|
|
304
|
+
rendered_prompt = self._system_message_prompt_template.render()
|
|
305
|
+
return CopilotSystemMessage().build_openai_message(prompt=rendered_prompt)
|
|
288
306
|
|
|
289
307
|
def _render_last_user_message_context_prompt(
|
|
290
308
|
self,
|
|
291
309
|
context: CopilotContext,
|
|
292
310
|
relevant_documents: List[Document],
|
|
311
|
+
tracker_event_attachments: List[EventContent],
|
|
293
312
|
) -> str:
|
|
294
313
|
# Format relevant documentation
|
|
295
314
|
documents = [doc.model_dump() for doc in relevant_documents]
|
|
@@ -297,6 +316,8 @@ class Copilot:
|
|
|
297
316
|
conversation = self._format_conversation_history(context.tracker_context)
|
|
298
317
|
# Format current state
|
|
299
318
|
current_state = self._format_current_state(context.tracker_context)
|
|
319
|
+
# Format tracker events
|
|
320
|
+
attachments = self._format_tracker_event_attachments(tracker_event_attachments)
|
|
300
321
|
|
|
301
322
|
rendered_prompt = self._last_user_message_context_prompt_template.render(
|
|
302
323
|
current_conversation=conversation,
|
|
@@ -304,43 +325,90 @@ class Copilot:
|
|
|
304
325
|
assistant_logs=context.assistant_logs,
|
|
305
326
|
assistant_files=context.assistant_files,
|
|
306
327
|
documentation_results=documents,
|
|
328
|
+
attachments=attachments,
|
|
307
329
|
)
|
|
308
330
|
return rendered_prompt
|
|
309
331
|
|
|
332
|
+
def _render_training_error_handler_prompt(
|
|
333
|
+
self,
|
|
334
|
+
internal_request_message: InternalCopilotRequestChatMessage,
|
|
335
|
+
relevant_documents: List[Document],
|
|
336
|
+
) -> str:
|
|
337
|
+
"""Render the training error handler prompt with documentation and context.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
internal_request_message: Internal request message.
|
|
341
|
+
context: The copilot context.
|
|
342
|
+
relevant_documents: List of relevant documents for context.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Rendered prompt string for training error analysis.
|
|
346
|
+
"""
|
|
347
|
+
modified_files_dicts: Dict[str, str] = {
|
|
348
|
+
file.file_path: file.file_content
|
|
349
|
+
for file in internal_request_message.get_content_blocks_by_type(FileContent)
|
|
350
|
+
}
|
|
351
|
+
rendered_prompt = self._training_error_handler_prompt_template.render(
|
|
352
|
+
logs=internal_request_message.get_flattened_log_content(),
|
|
353
|
+
modified_files=modified_files_dicts,
|
|
354
|
+
documentation_results=self._format_documents(relevant_documents),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return rendered_prompt
|
|
358
|
+
|
|
310
359
|
@staticmethod
|
|
311
360
|
def _create_documentation_search_query(context: CopilotContext) -> str:
|
|
312
|
-
"""Format chat messages between user and copilot for documentation search.
|
|
361
|
+
"""Format chat messages between user and copilot for documentation search.
|
|
313
362
|
|
|
314
|
-
|
|
363
|
+
Filters out guardrails policy violations and only includes messages with
|
|
364
|
+
USER or COPILOT roles, then takes the last N relevant messages.
|
|
365
|
+
"""
|
|
315
366
|
role_to_prefix = {
|
|
316
367
|
ROLE_USER: "User",
|
|
317
368
|
ROLE_COPILOT: "Assistant",
|
|
318
|
-
ROLE_COPILOT_INTERNAL: "
|
|
369
|
+
ROLE_COPILOT_INTERNAL: "User",
|
|
319
370
|
}
|
|
371
|
+
allowed_message_types = (
|
|
372
|
+
UserChatMessage,
|
|
373
|
+
InternalCopilotRequestChatMessage,
|
|
374
|
+
CopilotChatMessage,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
query_chat_history: List[str] = []
|
|
320
378
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
379
|
+
for message in reversed(context.copilot_chat_history):
|
|
380
|
+
if (
|
|
381
|
+
message.response_category
|
|
382
|
+
== ResponseCategory.GUARDRAILS_POLICY_VIOLATION
|
|
383
|
+
or not isinstance(message, allowed_message_types)
|
|
384
|
+
):
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
if (
|
|
388
|
+
len(query_chat_history)
|
|
389
|
+
>= COPILOT_DOCUMENTATION_SEARCH_QUERY_HISTORY_MESSAGES
|
|
390
|
+
):
|
|
391
|
+
break
|
|
325
392
|
|
|
326
|
-
for message in messages_to_include:
|
|
327
393
|
prefix = role_to_prefix[message.role]
|
|
328
|
-
text =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
394
|
+
text = (
|
|
395
|
+
Copilot._format_internal_message_for_query_chat_history(message)
|
|
396
|
+
if isinstance(message, InternalCopilotRequestChatMessage)
|
|
397
|
+
else Copilot._format_normal_message_for_query_chat_history(message)
|
|
398
|
+
)
|
|
399
|
+
query_chat_history.insert(0, f"{prefix}: {text}")
|
|
334
400
|
|
|
335
|
-
return
|
|
401
|
+
return "\n".join(query_chat_history)
|
|
336
402
|
|
|
337
403
|
@staticmethod
|
|
338
404
|
def _format_documents(results: List[Document]) -> Optional[str]:
|
|
339
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.
|
|
340
408
|
if not results:
|
|
341
409
|
return None
|
|
342
410
|
|
|
343
|
-
formatted_results = {
|
|
411
|
+
formatted_results: Dict[str, Any] = {
|
|
344
412
|
"sources": [
|
|
345
413
|
{
|
|
346
414
|
# Start the reference from 1, not 0.
|
|
@@ -448,3 +516,47 @@ class Copilot:
|
|
|
448
516
|
return json.dumps({}, ensure_ascii=False, indent=2)
|
|
449
517
|
current_state = tracker_context.current_state.model_dump()
|
|
450
518
|
return json.dumps(current_state, ensure_ascii=False, indent=2)
|
|
519
|
+
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _format_normal_message_for_query_chat_history(
|
|
522
|
+
message: Union[UserChatMessage, CopilotChatMessage],
|
|
523
|
+
) -> str:
|
|
524
|
+
"""Format normal message for query chat history."""
|
|
525
|
+
return f"{message.get_flattened_text_content()}"
|
|
526
|
+
|
|
527
|
+
@staticmethod
|
|
528
|
+
def _format_internal_message_for_query_chat_history(
|
|
529
|
+
message: InternalCopilotRequestChatMessage,
|
|
530
|
+
) -> str:
|
|
531
|
+
"""Format internal copilot request message for query chat history."""
|
|
532
|
+
text_content = message.get_flattened_text_content()
|
|
533
|
+
log_content = message.get_flattened_log_content()
|
|
534
|
+
if text_content and log_content:
|
|
535
|
+
return f"{text_content}\nLogs: {log_content}"
|
|
536
|
+
elif text_content:
|
|
537
|
+
return text_content
|
|
538
|
+
elif log_content:
|
|
539
|
+
return f"Logs: {log_content}"
|
|
540
|
+
else:
|
|
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)
|