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.

Files changed (84) hide show
  1. rasa/agents/protocol/a2a/a2a_agent.py +50 -42
  2. rasa/agents/utils.py +27 -5
  3. rasa/agents/validation.py +7 -9
  4. rasa/api.py +1 -2
  5. rasa/builder/copilot/constants.py +4 -1
  6. rasa/builder/copilot/copilot.py +191 -79
  7. rasa/builder/copilot/models.py +306 -116
  8. rasa/builder/copilot/prompts/copilot_system_prompt.jinja2 +33 -12
  9. rasa/builder/copilot/prompts/copilot_training_error_handler_prompt.jinja2 +53 -0
  10. rasa/builder/copilot/prompts/latest_user_message_context_prompt.jinja2 +59 -29
  11. rasa/builder/copilot/telemetry.py +8 -0
  12. rasa/builder/guardrails/policy_checker.py +1 -1
  13. rasa/builder/jobs.py +182 -12
  14. rasa/builder/models.py +12 -3
  15. rasa/builder/service.py +16 -2
  16. rasa/cli/dialogue_understanding_test.py +1 -0
  17. rasa/cli/e2e_test.py +1 -0
  18. rasa/cli/inspect.py +1 -0
  19. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/feedback.yml +46 -0
  20. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/goodbye.yml +9 -0
  21. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/help.yml +8 -0
  22. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/human_handoff.yml +41 -0
  23. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/patterns.yml +32 -0
  24. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/show_faqs.yml +8 -0
  25. rasa/cli/project_templates/finance/domain/general/help.yml +0 -0
  26. rasa/cli/project_templates/telco/data/network/flow_solve_internet_issue.yml +2 -2
  27. rasa/cli/project_templates/telco/domain/network/solve_internet_issue.yml +1 -2
  28. rasa/cli/project_templates/telco/tests/e2e_test_cases/with_stub/network/solve_internet_not_slow.yml +33 -0
  29. rasa/cli/project_templates/telco/tests/e2e_test_cases/with_stub/network/solve_internet_slow.yml +47 -0
  30. rasa/cli/project_templates/telco/tests/e2e_test_cases/without_stub/general/hello.yml +8 -0
  31. rasa/cli/run.py +1 -5
  32. rasa/cli/shell.py +1 -0
  33. rasa/cli/train.py +1 -0
  34. rasa/cli/validation/bot_config.py +7 -2
  35. rasa/core/available_agents.py +65 -55
  36. rasa/core/brokers/kafka.py +5 -1
  37. rasa/core/concurrent_lock_store.py +38 -21
  38. rasa/core/config/available_endpoints.py +0 -3
  39. rasa/core/config/configuration.py +36 -1
  40. rasa/core/constants.py +6 -0
  41. rasa/core/iam_credentials_providers/aws_iam_credentials_providers.py +69 -4
  42. rasa/core/iam_credentials_providers/credentials_provider_protocol.py +2 -1
  43. rasa/core/lock_store.py +4 -0
  44. rasa/core/policies/flows/agent_executor.py +16 -8
  45. rasa/core/redis_connection_factory.py +7 -2
  46. rasa/core/tracker_stores/redis_tracker_store.py +4 -0
  47. rasa/core/tracker_stores/sql_tracker_store.py +3 -1
  48. rasa/dialogue_understanding/commands/start_flow_command.py +10 -3
  49. rasa/dialogue_understanding/commands/utils.py +15 -4
  50. rasa/dialogue_understanding/generator/llm_based_command_generator.py +4 -2
  51. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +4 -4
  52. rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +4 -4
  53. rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +2 -2
  54. rasa/dialogue_understanding_test/du_test_runner.py +2 -2
  55. rasa/e2e_test/e2e_test_runner.py +2 -2
  56. rasa/shared/agents/auth/auth_strategy/oauth2_auth_strategy.py +10 -4
  57. rasa/shared/agents/auth/constants.py +1 -0
  58. rasa/shared/core/flows/steps/call.py +2 -2
  59. rasa/telemetry.py +3 -3
  60. rasa/validator.py +37 -0
  61. rasa/version.py +1 -1
  62. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/METADATA +14 -2
  63. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/RECORD +83 -73
  64. rasa/cli/project_templates/telco/tests/e2e_test_cases/network/solve_internet_issue.yml +0 -57
  65. /rasa/cli/project_templates/{finance/tests/e2e_test_cases → basic/tests/e2e_test_cases/without_stub}/general/hello.yml +0 -0
  66. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{accounts → without_stub/accounts}/check_balance.yml +0 -0
  67. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{accounts → without_stub/accounts}/download_statements.yml +0 -0
  68. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{cards → without_stub/cards}/block_card.yml +0 -0
  69. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/bot_challenge.yml +0 -0
  70. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/feedback.yml +0 -0
  71. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/goodbye.yml +0 -0
  72. /rasa/cli/project_templates/{telco/tests/e2e_test_cases → finance/tests/e2e_test_cases/without_stub}/general/hello.yml +0 -0
  73. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/human_handoff.yml +0 -0
  74. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/patterns.yml +0 -0
  75. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{transfers → without_stub/transfers}/transfer_money.yml +0 -0
  76. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{billing → without_stub/billing}/understand_bill.yml +0 -0
  77. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/bot_challenge.yml +0 -0
  78. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/feedback.yml +0 -0
  79. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/goodbye.yml +0 -0
  80. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/human_handoff.yml +0 -0
  81. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/patterns.yml +0 -0
  82. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/NOTICE +0 -0
  83. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/WHEEL +0 -0
  84. {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
- async for event in self._client.send_message(message):
219
- events_received += 1
220
- agent_output = self._handle_send_message_response(agent_input, event)
221
- if agent_output is not None:
222
- return agent_output
223
- else:
224
- # Not a terminal response, save taskID (in case that's the only
225
- # event, and we need to pool) and continue waiting for next events
226
- if (
227
- isinstance(event, tuple)
228
- and len(event) == 2
229
- and isinstance(event[0], Task)
230
- ):
231
- task_id = event[0].id
232
- continue
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
- async for event in self._client.send_message(test_message):
837
- if (
838
- isinstance(event, Message)
839
- or isinstance(event, tuple)
840
- and len(event) == 2
841
- and isinstance(event[0], Task)
842
- ):
843
- # We got a valid response, health check succeeded
844
- return
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
- event_info = "Unexpected response type during health check"
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.unexpected_response",
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(f"{event_info}: {event}")
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 = AvailableAgents.get_agent_config(agent_id)
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 = AvailableAgents.get_agent_config(agent_id)
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
- agent_info = get_agent_info(event.agent_id)
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", "protocol", "description"]:
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
- with open(config_path, "r") as f:
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
- # Create the agent config using AvailableAgents
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 = AvailableAgents.get_instance(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["copilot_internal"] = "copilot_internal"
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"
@@ -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
- # Split chat history into past messages and latest message
209
- past_messages = [
210
- message
211
- for message in context.copilot_chat_history[:-1]
212
- if message.response_category != ResponseCategory.GUARDRAILS_POLICY_VIOLATION
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
- async def _create_system_message(self) -> Dict[str, Any]:
227
- """Render the correct Jinja template based on desired output_type."""
228
- rendered_prompt = self._system_message_prompt_template.render()
229
- return {"role": ROLE_SYSTEM, "content": rendered_prompt}
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
- """Create the chat history messages for the copilot.
239
+ """Filter and convert past messages to OpenAI format.
236
240
 
237
- Filter out messages with response_category of GUARDRAILS_POLICY_VIOLATION.
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
- past_messages: List of past messages (excluding the latest message).
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
- return [
248
- message.to_openai_format()
249
- for message in past_messages
250
- if message.response_category != ResponseCategory.GUARDRAILS_POLICY_VIOLATION
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
- def _create_last_user_message_with_context(
259
+ return [message.build_openai_message() for message in filtered_messages]
260
+
261
+ def _process_latest_message(
254
262
  self,
255
- latest_message: "CopilotChatMessage",
263
+ latest_message: Any,
256
264
  context: CopilotContext,
257
265
  relevant_documents: List[Document],
258
266
  ) -> Dict[str, Any]:
259
- """Create the last user message with context.
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
- context: The context of the copilot.
268
- relevant_documents: The relevant documents to use in the context.
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
- The last user message with context in the OpenAI format.
275
+ Message in OpenAI format.
276
+
277
+ Raises:
278
+ ValueError: If the message type is not supported.
272
279
  """
273
- last_user_message = copy.deepcopy(latest_message)
274
- context_prompt = self._render_last_user_message_context_prompt(
275
- context, relevant_documents
276
- )
277
- last_user_message.content.insert(
278
- 0, TextContent(type="text", text=context_prompt)
279
- )
280
- return {
281
- "role": ROLE_USER,
282
- "content": [
283
- {"type": "text", "text": content.text}
284
- for content in last_user_message.content
285
- if isinstance(content, TextContent)
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
- result = ""
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: "Copilot Internal Request",
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
- # Only use the last N messages for documentation search
322
- messages_to_include = context.copilot_chat_history[
323
- -COPILOT_DOCUMENTATION_SEARCH_QUERY_HISTORY_MESSAGES:
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 = message.get_text_content().strip()
329
- if text:
330
- result += f"{prefix}: {text}\n"
331
- log_content = message.get_log_content().strip()
332
- if log_content:
333
- result += f"{prefix}: {log_content}\n"
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 result
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)