solace-agent-mesh 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (176) hide show
  1. solace_agent_mesh/__init__.py +0 -3
  2. solace_agent_mesh/agents/__init__.py +0 -0
  3. solace_agent_mesh/agents/base_agent_component.py +224 -0
  4. solace_agent_mesh/agents/global/__init__.py +0 -0
  5. solace_agent_mesh/agents/global/actions/__init__.py +0 -0
  6. solace_agent_mesh/agents/global/actions/agent_state_change.py +54 -0
  7. solace_agent_mesh/agents/global/actions/clear_history.py +32 -0
  8. solace_agent_mesh/agents/global/actions/convert_file_to_markdown.py +160 -0
  9. solace_agent_mesh/agents/global/actions/create_file.py +70 -0
  10. solace_agent_mesh/agents/global/actions/error_action.py +45 -0
  11. solace_agent_mesh/agents/global/actions/plantuml_diagram.py +93 -0
  12. solace_agent_mesh/agents/global/actions/plotly_graph.py +117 -0
  13. solace_agent_mesh/agents/global/actions/retrieve_file.py +51 -0
  14. solace_agent_mesh/agents/global/global_agent_component.py +38 -0
  15. solace_agent_mesh/agents/image_processing/__init__.py +0 -0
  16. solace_agent_mesh/agents/image_processing/actions/__init__.py +0 -0
  17. solace_agent_mesh/agents/image_processing/actions/create_image.py +75 -0
  18. solace_agent_mesh/agents/image_processing/actions/describe_image.py +115 -0
  19. solace_agent_mesh/agents/image_processing/image_processing_agent_component.py +23 -0
  20. solace_agent_mesh/agents/slack/__init__.py +1 -0
  21. solace_agent_mesh/agents/slack/actions/__init__.py +1 -0
  22. solace_agent_mesh/agents/slack/actions/post_message.py +177 -0
  23. solace_agent_mesh/agents/slack/slack_agent_component.py +59 -0
  24. solace_agent_mesh/agents/web_request/__init__.py +0 -0
  25. solace_agent_mesh/agents/web_request/actions/__init__.py +0 -0
  26. solace_agent_mesh/agents/web_request/actions/do_image_search.py +84 -0
  27. solace_agent_mesh/agents/web_request/actions/do_news_search.py +47 -0
  28. solace_agent_mesh/agents/web_request/actions/do_suggestion_search.py +34 -0
  29. solace_agent_mesh/agents/web_request/actions/do_web_request.py +134 -0
  30. solace_agent_mesh/agents/web_request/actions/download_file.py +69 -0
  31. solace_agent_mesh/agents/web_request/web_request_agent_component.py +33 -0
  32. solace_agent_mesh/assets/web-visualizer/assets/index-C5awueeJ.js +109 -0
  33. solace_agent_mesh/assets/web-visualizer/assets/index-D0qORgkg.css +1 -0
  34. solace_agent_mesh/assets/web-visualizer/index.html +14 -0
  35. solace_agent_mesh/assets/web-visualizer/vite.svg +1 -0
  36. solace_agent_mesh/cli/__init__.py +1 -0
  37. solace_agent_mesh/cli/commands/__init__.py +0 -0
  38. solace_agent_mesh/cli/commands/add/__init__.py +3 -0
  39. solace_agent_mesh/cli/commands/add/add.py +88 -0
  40. solace_agent_mesh/cli/commands/add/agent.py +110 -0
  41. solace_agent_mesh/cli/commands/add/copy_from_plugin.py +90 -0
  42. solace_agent_mesh/cli/commands/add/gateway.py +221 -0
  43. solace_agent_mesh/cli/commands/build.py +631 -0
  44. solace_agent_mesh/cli/commands/chat/__init__.py +3 -0
  45. solace_agent_mesh/cli/commands/chat/chat.py +361 -0
  46. solace_agent_mesh/cli/commands/config.py +29 -0
  47. solace_agent_mesh/cli/commands/init/__init__.py +3 -0
  48. solace_agent_mesh/cli/commands/init/ai_provider_step.py +76 -0
  49. solace_agent_mesh/cli/commands/init/broker_step.py +102 -0
  50. solace_agent_mesh/cli/commands/init/builtin_agent_step.py +88 -0
  51. solace_agent_mesh/cli/commands/init/check_if_already_done.py +13 -0
  52. solace_agent_mesh/cli/commands/init/create_config_file_step.py +52 -0
  53. solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +96 -0
  54. solace_agent_mesh/cli/commands/init/file_service_step.py +73 -0
  55. solace_agent_mesh/cli/commands/init/init.py +114 -0
  56. solace_agent_mesh/cli/commands/init/project_structure_step.py +45 -0
  57. solace_agent_mesh/cli/commands/init/rest_api_step.py +50 -0
  58. solace_agent_mesh/cli/commands/init/web_ui_step.py +40 -0
  59. solace_agent_mesh/cli/commands/plugin/__init__.py +3 -0
  60. solace_agent_mesh/cli/commands/plugin/add.py +98 -0
  61. solace_agent_mesh/cli/commands/plugin/build.py +217 -0
  62. solace_agent_mesh/cli/commands/plugin/create.py +117 -0
  63. solace_agent_mesh/cli/commands/plugin/plugin.py +109 -0
  64. solace_agent_mesh/cli/commands/plugin/remove.py +71 -0
  65. solace_agent_mesh/cli/commands/run.py +68 -0
  66. solace_agent_mesh/cli/commands/visualizer.py +138 -0
  67. solace_agent_mesh/cli/config.py +81 -0
  68. solace_agent_mesh/cli/main.py +306 -0
  69. solace_agent_mesh/cli/utils.py +246 -0
  70. solace_agent_mesh/common/__init__.py +0 -0
  71. solace_agent_mesh/common/action.py +91 -0
  72. solace_agent_mesh/common/action_list.py +37 -0
  73. solace_agent_mesh/common/action_response.py +327 -0
  74. solace_agent_mesh/common/constants.py +3 -0
  75. solace_agent_mesh/common/mysql_database.py +40 -0
  76. solace_agent_mesh/common/postgres_database.py +79 -0
  77. solace_agent_mesh/common/prompt_templates.py +30 -0
  78. solace_agent_mesh/common/prompt_templates_unused_delete.py +161 -0
  79. solace_agent_mesh/common/stimulus_utils.py +152 -0
  80. solace_agent_mesh/common/time.py +24 -0
  81. solace_agent_mesh/common/utils.py +638 -0
  82. solace_agent_mesh/configs/agent_global.yaml +74 -0
  83. solace_agent_mesh/configs/agent_image_processing.yaml +82 -0
  84. solace_agent_mesh/configs/agent_slack.yaml +64 -0
  85. solace_agent_mesh/configs/agent_web_request.yaml +75 -0
  86. solace_agent_mesh/configs/conversation_to_file.yaml +56 -0
  87. solace_agent_mesh/configs/error_catcher.yaml +56 -0
  88. solace_agent_mesh/configs/monitor.yaml +0 -0
  89. solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +106 -0
  90. solace_agent_mesh/configs/monitor_user_feedback.yaml +58 -0
  91. solace_agent_mesh/configs/orchestrator.yaml +241 -0
  92. solace_agent_mesh/configs/service_embedding.yaml +81 -0
  93. solace_agent_mesh/configs/service_llm.yaml +265 -0
  94. solace_agent_mesh/configs/visualize_websocket.yaml +55 -0
  95. solace_agent_mesh/gateway/__init__.py +0 -0
  96. solace_agent_mesh/gateway/components/__init__.py +0 -0
  97. solace_agent_mesh/gateway/components/gateway_base.py +41 -0
  98. solace_agent_mesh/gateway/components/gateway_input.py +265 -0
  99. solace_agent_mesh/gateway/components/gateway_output.py +289 -0
  100. solace_agent_mesh/gateway/identity/bamboohr_identity.py +18 -0
  101. solace_agent_mesh/gateway/identity/identity_base.py +10 -0
  102. solace_agent_mesh/gateway/identity/identity_provider.py +60 -0
  103. solace_agent_mesh/gateway/identity/no_identity.py +9 -0
  104. solace_agent_mesh/gateway/identity/passthru_identity.py +9 -0
  105. solace_agent_mesh/monitors/base_monitor_component.py +26 -0
  106. solace_agent_mesh/monitors/feedback/user_feedback_monitor.py +75 -0
  107. solace_agent_mesh/monitors/stim_and_errors/stim_and_error_monitor.py +560 -0
  108. solace_agent_mesh/orchestrator/__init__.py +0 -0
  109. solace_agent_mesh/orchestrator/action_manager.py +225 -0
  110. solace_agent_mesh/orchestrator/components/__init__.py +0 -0
  111. solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +54 -0
  112. solace_agent_mesh/orchestrator/components/orchestrator_action_response_component.py +179 -0
  113. solace_agent_mesh/orchestrator/components/orchestrator_register_component.py +107 -0
  114. solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +477 -0
  115. solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +246 -0
  116. solace_agent_mesh/orchestrator/orchestrator_main.py +166 -0
  117. solace_agent_mesh/orchestrator/orchestrator_prompt.py +410 -0
  118. solace_agent_mesh/services/__init__.py +0 -0
  119. solace_agent_mesh/services/authorization/providers/base_authorization_provider.py +56 -0
  120. solace_agent_mesh/services/bamboo_hr_service/__init__.py +3 -0
  121. solace_agent_mesh/services/bamboo_hr_service/bamboo_hr.py +182 -0
  122. solace_agent_mesh/services/common/__init__.py +4 -0
  123. solace_agent_mesh/services/common/auto_expiry.py +45 -0
  124. solace_agent_mesh/services/common/singleton.py +18 -0
  125. solace_agent_mesh/services/file_service/__init__.py +14 -0
  126. solace_agent_mesh/services/file_service/file_manager/__init__.py +0 -0
  127. solace_agent_mesh/services/file_service/file_manager/bucket_file_manager.py +149 -0
  128. solace_agent_mesh/services/file_service/file_manager/file_manager_base.py +162 -0
  129. solace_agent_mesh/services/file_service/file_manager/memory_file_manager.py +64 -0
  130. solace_agent_mesh/services/file_service/file_manager/volume_file_manager.py +106 -0
  131. solace_agent_mesh/services/file_service/file_service.py +432 -0
  132. solace_agent_mesh/services/file_service/file_service_constants.py +54 -0
  133. solace_agent_mesh/services/file_service/file_transformations.py +131 -0
  134. solace_agent_mesh/services/file_service/file_utils.py +322 -0
  135. solace_agent_mesh/services/file_service/transformers/__init__.py +5 -0
  136. solace_agent_mesh/services/history_service/__init__.py +3 -0
  137. solace_agent_mesh/services/history_service/history_providers/__init__.py +0 -0
  138. solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +78 -0
  139. solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +167 -0
  140. solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +163 -0
  141. solace_agent_mesh/services/history_service/history_service.py +139 -0
  142. solace_agent_mesh/services/llm_service/components/llm_request_component.py +293 -0
  143. solace_agent_mesh/services/llm_service/components/llm_service_component_base.py +152 -0
  144. solace_agent_mesh/services/middleware_service/__init__.py +0 -0
  145. solace_agent_mesh/services/middleware_service/middleware_service.py +20 -0
  146. solace_agent_mesh/templates/action.py +38 -0
  147. solace_agent_mesh/templates/agent.py +29 -0
  148. solace_agent_mesh/templates/agent.yaml +70 -0
  149. solace_agent_mesh/templates/gateway-config-template.yaml +6 -0
  150. solace_agent_mesh/templates/gateway-default-config.yaml +28 -0
  151. solace_agent_mesh/templates/gateway-flows.yaml +81 -0
  152. solace_agent_mesh/templates/gateway-header.yaml +16 -0
  153. solace_agent_mesh/templates/gateway_base.py +15 -0
  154. solace_agent_mesh/templates/gateway_input.py +98 -0
  155. solace_agent_mesh/templates/gateway_output.py +71 -0
  156. solace_agent_mesh/templates/plugin-pyproject.toml +30 -0
  157. solace_agent_mesh/templates/rest-api-default-config.yaml +24 -0
  158. solace_agent_mesh/templates/rest-api-flows.yaml +80 -0
  159. solace_agent_mesh/templates/slack-default-config.yaml +9 -0
  160. solace_agent_mesh/templates/slack-flows.yaml +90 -0
  161. solace_agent_mesh/templates/solace-agent-mesh-default.yaml +77 -0
  162. solace_agent_mesh/templates/solace-agent-mesh-plugin-default.yaml +8 -0
  163. solace_agent_mesh/templates/web-default-config.yaml +5 -0
  164. solace_agent_mesh/templates/web-flows.yaml +86 -0
  165. solace_agent_mesh/tools/__init__.py +0 -0
  166. solace_agent_mesh/tools/components/__init__.py +0 -0
  167. solace_agent_mesh/tools/components/conversation_formatter.py +111 -0
  168. solace_agent_mesh/tools/components/file_resolver_component.py +58 -0
  169. solace_agent_mesh/tools/config/runtime_config.py +26 -0
  170. solace_agent_mesh-0.1.1.dist-info/METADATA +179 -0
  171. solace_agent_mesh-0.1.1.dist-info/RECORD +174 -0
  172. solace_agent_mesh-0.1.1.dist-info/entry_points.txt +3 -0
  173. solace_agent_mesh-0.0.1.dist-info/licenses/LICENSE.txt → solace_agent_mesh-0.1.1.dist-info/licenses/LICENSE +1 -2
  174. solace_agent_mesh-0.0.1.dist-info/METADATA +0 -51
  175. solace_agent_mesh-0.0.1.dist-info/RECORD +0 -5
  176. {solace_agent_mesh-0.0.1.dist-info → solace_agent_mesh-0.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,26 @@
1
+ """Base class for all monitor components in the Solace Agent Mesh.
2
+
3
+ Monitors are passive listeners that observe and track events in the mesh. They may
4
+ trigger agent actions but are not agents themselves.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional
8
+ from solace_ai_connector.components.component_base import ComponentBase
9
+ from solace_ai_connector.common.message import Message
10
+
11
+
12
+ class BaseMonitorComponent(ComponentBase):
13
+ """Base class for monitor components.
14
+
15
+ Provides common functionality for monitoring mesh events and interacting
16
+ with agents when needed.
17
+ """
18
+
19
+ def __init__(self, module_info: Optional[Dict] = None, **kwargs: Any) -> None:
20
+ """Initialize the monitor component.
21
+
22
+ Args:
23
+ module_info: Optional configuration dictionary for the module.
24
+ **kwargs: Additional keyword arguments passed to parent class.
25
+ """
26
+ super().__init__(module_info or {}, **kwargs)
@@ -0,0 +1,75 @@
1
+ """Monitor component that tracks user feedback for responses.
2
+
3
+ This component:
4
+ 1. Listens to feedback events from the user
5
+ 3. Creates .feedback file with the feedback contents
6
+ """
7
+
8
+ from ..base_monitor_component import BaseMonitorComponent
9
+ from typing import Dict, Optional
10
+
11
+ from solace_ai_connector.common.log import log
12
+ from solace_ai_connector.common.message import Message
13
+ from ...services.file_service import FileService
14
+ from ...common.constants import SOLACE_AGENT_MESH_SYSTEM_SESSION_ID
15
+ from ...common.time import ONE_DAY
16
+
17
+ import json
18
+ import time
19
+
20
+ info = {
21
+ "class_name": "UserFeedbackMonitor",
22
+ "description": "Monitor that tracks user feedback and generates a feedback file",
23
+ "config_parameters": [
24
+ ],
25
+ }
26
+
27
+ DEFAULT_STIMULUS_TTL = 7 * ONE_DAY;
28
+ class UserFeedbackMonitor(BaseMonitorComponent):
29
+ """Monitor that tracks user feedback and generates a feedback file"""
30
+
31
+ def __init__(self, module_info: Optional[Dict] = None, **kwargs):
32
+ module_info = module_info or {}
33
+ module_info.update(info)
34
+ super().__init__(module_info, **kwargs)
35
+ self.feedback_ttl = self.get_config("feedback_ttl", DEFAULT_STIMULUS_TTL)
36
+
37
+ def invoke(self, message: Message, data: Dict):
38
+
39
+ """Create a feedback file with the feedback contents"""
40
+
41
+ # Extract stimulus UUID and session ID from feedback data
42
+ feedback_data = data.get("data", {})
43
+ stimulus_uuid = feedback_data.get("stimulus_uuid", "unknown")
44
+
45
+ # Create file content
46
+ file_content = json.dumps(data, indent=2)
47
+
48
+ # Upload file to the file service
49
+ file_service = FileService()
50
+ file_name = f"{stimulus_uuid}_feedback.json"
51
+ file_service.upload_from_buffer(
52
+ file_content.encode("utf-8"),
53
+ file_name=file_name,
54
+ session_id=SOLACE_AGENT_MESH_SYSTEM_SESSION_ID,
55
+ data_source="User Feedback Monitor",
56
+ expiration_timestamp=time.time() + self.feedback_ttl,
57
+ )
58
+
59
+ # File the related stimulus files and update the expiration date
60
+
61
+ # First get all of the metadata
62
+ metadata = file_service.list_all_metadata(
63
+ session_id=SOLACE_AGENT_MESH_SYSTEM_SESSION_ID,
64
+ )
65
+
66
+ for meta in metadata:
67
+ if meta["name"].endswith(f"{stimulus_uuid}.md") or meta["name"].endswith(f"{stimulus_uuid}.stim"):
68
+ try:
69
+ file_service.update_file_expiration(
70
+ meta["url"],
71
+ expiration_timestamp=time.time() + self.feedback_ttl,
72
+ )
73
+ except Exception as e:
74
+ log.error(f"Error updating expiration for file {meta['url']}: {str(e)}")
75
+ return data
@@ -0,0 +1,560 @@
1
+ """Monitor component that tracks stimulus events and errors.
2
+
3
+ This component:
4
+ 1. Listens to all Solace Agent Mesh events
5
+ 2. Tracks events and errors for each stimulus
6
+ 3. Creates .stim and .md files when stimuli complete
7
+ 4. Triggers notifications to agents when errors occur
8
+ """
9
+
10
+ import time
11
+ from typing import Dict, List, Optional, Tuple
12
+ from datetime import datetime
13
+
14
+ import yaml
15
+ import json
16
+ import base64
17
+ from solace_ai_connector.common.log import log
18
+ from solace_ai_connector.common.message import Message
19
+
20
+ from ..base_monitor_component import BaseMonitorComponent
21
+ from ...common.stimulus_utils import describe_stimulus
22
+ from ...common.constants import SOLACE_AGENT_MESH_SYSTEM_SESSION_ID
23
+ from ...services.file_service import FileService
24
+ from ...common.time import FIVE_MINUTES
25
+
26
+
27
+ info = {
28
+ "class_name": "StimAndErrorMonitor",
29
+ "description": "Monitor that tracks stimulus events/errors and generates summary files",
30
+ "config_parameters": [
31
+ {
32
+ "name": "stimulus_ttl",
33
+ "required": False,
34
+ "description": "Time-to-live for stimulus state in seconds (default: 3600)",
35
+ "type": "integer",
36
+ "default": 20,
37
+ },
38
+ {
39
+ "name": "notification_flow_name",
40
+ "required": True,
41
+ "description": "Flow to send notifications to. This flow must be defined within the same instance as this monitor",
42
+ "type": "string",
43
+ },
44
+ {
45
+ "name": "error_format",
46
+ "required": False,
47
+ "description": "Format for error messages (default: text)",
48
+ "type": "string",
49
+ "default": "markdown",
50
+ },
51
+ {
52
+ "name": "notification_mode",
53
+ "required": False,
54
+ "description": "Mode for sending notifications. One of 'errors', 'all', 'none (default: errors)",
55
+ "type": "string",
56
+ "default": "errors",
57
+ },
58
+ ],
59
+ }
60
+
61
+
62
+ DEFAULT_STIMULUS_TTL = FIVE_MINUTES
63
+
64
+
65
+ class StimAndErrorMonitor(BaseMonitorComponent):
66
+ """Monitor that tracks stimulus events/errors and generates summary files."""
67
+
68
+ SLACK_POST_MESSAGE_SCOPE = "slack:post_message:create"
69
+
70
+ def __init__(self, module_info: Optional[Dict] = None, **kwargs):
71
+ """Initialize the monitor.
72
+
73
+ Args:
74
+ module_info: Optional configuration dictionary.
75
+ **kwargs: Additional keyword arguments.
76
+ """
77
+ module_info = module_info or {}
78
+ module_info.update(info)
79
+ super().__init__(module_info, **kwargs)
80
+ self.stimulus_ttl = self.get_config("stimulus_ttl", DEFAULT_STIMULUS_TTL)
81
+ self.notification_flow_name = self.get_config(
82
+ "notification_flow_name",
83
+ )
84
+ self.error_format = self.get_config("error_format", "markdown")
85
+ self.notification_mode = self.get_config("notification_mode", "errors")
86
+
87
+ def _get_stimulus_cache_key(self, stimulus_uuid: str) -> str:
88
+ """Generate cache key for a stimulus.
89
+
90
+ Args:
91
+ stimulus_uuid: The UUID of the stimulus.
92
+
93
+ Returns:
94
+ The cache key string.
95
+ """
96
+ return f"monitor:stimulus_state:{stimulus_uuid}"
97
+
98
+ def _get_stimulus_state(self, stimulus_uuid: str) -> Dict:
99
+ """Get current state for a stimulus from cache.
100
+
101
+ Args:
102
+ stimulus_uuid: The UUID of the stimulus.
103
+
104
+ Returns:
105
+ Dictionary containing stimulus state with events, errors and metadata.
106
+ """
107
+ cache_key = self._get_stimulus_cache_key(stimulus_uuid)
108
+ state = self.cache_service.get_data(cache_key) or {
109
+ "events": [],
110
+ "errors": [],
111
+ "metadata": {},
112
+ }
113
+ return state
114
+
115
+ def _save_stimulus_state(self, stimulus_uuid: str, state: Dict) -> None:
116
+ """Save stimulus state to cache.
117
+
118
+ Args:
119
+ stimulus_uuid: The UUID of the stimulus.
120
+ state: Dictionary containing the state to save.
121
+ """
122
+ cache_key = self._get_stimulus_cache_key(stimulus_uuid)
123
+ self.cache_service.add_data(
124
+ cache_key, state, expiry=self.stimulus_ttl, component=self
125
+ )
126
+
127
+ def add_system_event(self, message: Message) -> None:
128
+ """Add a system event to stimulus state.
129
+
130
+ Args:
131
+ message: The Message object containing the system event.
132
+ """
133
+ user_props = message.get_user_properties() or {}
134
+ topic = message.get_topic()
135
+ stimulus_uuid = user_props.get("stimulus_uuid")
136
+
137
+ if not stimulus_uuid:
138
+ return
139
+
140
+ if "/stimulus/error/" in topic:
141
+ error_info = message.get_payload() or {}
142
+
143
+ if "error_message" in error_info:
144
+ # Solace Agent Mesh error format
145
+ error_message = error_info.get("error_message", "Unknown error")
146
+ error_source = error_info.get("source", "Unknown source")
147
+ if "error" in error_info:
148
+ # solace-ai-connector error format
149
+ error = error_info.get("error", {})
150
+ error_text = error.get("text", "Unknown error")
151
+ location = error_info.get("location")
152
+ location_str = "Unknown"
153
+ error_original_message = error_info.get("message", None)
154
+ if location:
155
+ location_str = (
156
+ f"{location.get('instance', '')}"
157
+ f".{location.get('flow', '')}"
158
+ f".{location.get('component', '')}"
159
+ f"[{location.get('component_idx', '0')}]"
160
+ )
161
+
162
+ error_message = (
163
+ f"\n Error in component {location_str}"
164
+ f"\n Exception name: {error.get('exception', 'Unknown')}"
165
+ f"\n Exception info: {error_text}"
166
+ f"\n Stack trace: {error.get('traceback', '--none--')}\n"
167
+ )
168
+
169
+ error_source = (
170
+ f"\n Instance: {location.get('instance', 'Unknown')}"
171
+ f"\n Flow: {location.get('flow', 'Unknown')}"
172
+ f"\n Component: {location.get('component', 'Unknown')}"
173
+ f"\n Component Index: {location.get('component_idx', '0')}\n"
174
+ )
175
+
176
+ self.add_error(
177
+ stimulus_uuid,
178
+ error_message=error_message,
179
+ error_source=error_source,
180
+ user_properties=user_props,
181
+ )
182
+
183
+ state = self._get_stimulus_state(stimulus_uuid)
184
+
185
+ # Add the event
186
+ state["events"].append(
187
+ {
188
+ "topic": topic,
189
+ "payload": message.get_payload(),
190
+ "user_properties": user_props,
191
+ "timestamp": time.time(),
192
+ }
193
+ )
194
+
195
+ self._save_stimulus_state(stimulus_uuid, state)
196
+
197
+ # If the event's topic contains "responseComplete", mark the stimulus as complete
198
+ if "/responseComplete/" in topic:
199
+ self.handle_stimulus_complete(stimulus_uuid, state, session_id=SOLACE_AGENT_MESH_SYSTEM_SESSION_ID)
200
+
201
+ def add_error(
202
+ self,
203
+ stimulus_uuid: str,
204
+ error_message: str,
205
+ error_source: str,
206
+ user_properties: Optional[Dict] = None,
207
+ ) -> None:
208
+ """Add an error to stimulus state and notify.
209
+
210
+ Args:
211
+ stimulus_uuid: The UUID of the stimulus.
212
+ error_message: The error message.
213
+ error_source: The source of the error.
214
+ user_properties: Optional user properties.
215
+ """
216
+ state = self._get_stimulus_state(stimulus_uuid)
217
+
218
+ # Add the error
219
+ error = {
220
+ "message": error_message,
221
+ "source": error_source,
222
+ "timestamp": time.time(),
223
+ "user_properties": user_properties or {},
224
+ }
225
+ state["errors"].append(error)
226
+
227
+ self._save_stimulus_state(stimulus_uuid, state)
228
+
229
+ error_text = None
230
+ error_blocks = None
231
+ if self.error_format == "slack":
232
+ error_blocks = self._format_error_message_slack(
233
+ stimulus_uuid, error_message, error_source
234
+ )
235
+ elif self.error_format == "markdown":
236
+ error_text = (
237
+ f"__Error detected for stimulus **{stimulus_uuid}**__\n\n"
238
+ "*Error Message*\n"
239
+ f"{error_message}\n\n"
240
+ "*Source*\n"
241
+ f"{error_source}"
242
+ )
243
+ else:
244
+ error_text = f"Error detected in stimulus {stimulus_uuid} from {error_source}: {error_message}"
245
+
246
+ # Send notification about the error
247
+ self.send_to_flow(
248
+ self.notification_flow_name,
249
+ Message(
250
+ payload={
251
+ "is_last": False,
252
+ "correlation_id": stimulus_uuid,
253
+ "text": error_text,
254
+ "blocks": error_blocks,
255
+ },
256
+ user_properties={
257
+ "originator_scopes": [self.SLACK_POST_MESSAGE_SCOPE],
258
+ "session_id": user_properties.get("session_id"),
259
+ },
260
+ ),
261
+ )
262
+
263
+ def _format_error_message_markdown(
264
+ self, stimulus_uuid: str, error_message: str, error_source: str
265
+ ) -> str:
266
+ """Format an error message as a markdown string.
267
+
268
+ Args:
269
+ stimulus_uuid: The UUID of the stimulus.
270
+ error_message: The error message.
271
+ error_source: The source of the error.
272
+
273
+ Returns:
274
+ The formatted error message.
275
+ """
276
+
277
+ return (
278
+ f"__Error detected for stimulus **{stimulus_uuid}**__\n\n"
279
+ "*Error Message*\n"
280
+ f"{error_message}\n\n"
281
+ "*Source*\n"
282
+ f"{error_source}"
283
+ )
284
+
285
+ def _stimulus_has_error(self, state: Dict, is_timeout: bool) -> bool:
286
+ """Check if a stimulus state contains an error.
287
+
288
+ Args:
289
+ state: The state of the stimulus.
290
+ is_timeout: Whether this was a timeout completion.
291
+
292
+ Returns:
293
+ True if the stimulus state contains an error, False otherwise.
294
+ """
295
+ if is_timeout:
296
+ return True
297
+
298
+ return bool(state.get("errors"))
299
+
300
+ def _format_error_message_slack(
301
+ self, stimulus_uuid: str, error_message: str, error_source: str
302
+ ) -> List[Dict]:
303
+ """Format an error message as a Slack message.
304
+
305
+ Args:
306
+ stimulus_uuid: The UUID of the stimulus.
307
+ error_message: The error message.
308
+ error_source: The source of the error.
309
+
310
+ Returns:
311
+ The formatted error message in Slack blocks.
312
+
313
+ """
314
+
315
+ return [
316
+ {
317
+ "type": "context",
318
+ "elements": [
319
+ {
320
+ "type": "mrkdwn",
321
+ "text": f":grimacing: Error detected for stimulus *{stimulus_uuid}*",
322
+ }
323
+ ],
324
+ },
325
+ {
326
+ "type": "section",
327
+ "text": {"type": "mrkdwn", "text": "*Error Message*"},
328
+ },
329
+ {"type": "section", "text": {"type": "mrkdwn", "text": error_message}},
330
+ {"type": "section", "text": {"type": "mrkdwn", "text": "*Source*"}},
331
+ {"type": "section", "text": {"type": "mrkdwn", "text": error_source}},
332
+ ]
333
+
334
+ def _create_stimulus_files(
335
+ self,
336
+ stimulus_uuid: str,
337
+ state: Dict,
338
+ is_timeout: bool = False,
339
+ session_id: str = None,
340
+ ) -> Tuple[str, str]:
341
+ """Create .stim and .md files for a completed stimulus.
342
+
343
+ Args:
344
+ stimulus_uuid: The UUID of the stimulus.
345
+ state: The current state of the stimulus.
346
+ is_timeout: Whether this was a timeout completion.
347
+ session_id: Session ID for file service operations.
348
+
349
+ Returns:
350
+ Tuple of (stim_file_url, md_file_url).
351
+
352
+ Raises:
353
+ ValueError: If required file creation fails.
354
+ """
355
+ try:
356
+ # Get user email from the first event's user properties
357
+ identity = None
358
+ if state["events"]:
359
+ identity = (
360
+ state["events"][0]
361
+ .get("user_properties", {})
362
+ .get("user_info", {})
363
+ .get("email", "unknown")
364
+ )
365
+ # If no user email is found, use the identity
366
+ if not identity:
367
+ identity = state["events"][0].get("user_properties", {}).get("identity", "unknown")
368
+
369
+ # Create timestamp
370
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
371
+
372
+ # Base filename
373
+ base_filename = f"{identity}_{timestamp}_{stimulus_uuid}"
374
+
375
+ # Add completion status to state
376
+ state["completion_status"] = "timeout" if is_timeout else "complete"
377
+
378
+ # Get the last event and get the available files from the user properties
379
+ available_files = (
380
+ state["events"][-1]
381
+ .get("user_properties", {})
382
+ .get("available_files", None)
383
+ )
384
+ try:
385
+ available_files = json.loads(available_files)
386
+ except Exception as e:
387
+ log.error("Failed to parse available files: %s", str(e))
388
+ available_files = []
389
+
390
+ # If there are available files, go to the file service and get the file content
391
+ if available_files:
392
+ file_service = FileService()
393
+ for file in available_files:
394
+ if "url" not in file:
395
+ if "data" in file:
396
+ file["content"] = file["data"]
397
+ file["id"] = file.get("name", "no file name")
398
+ else:
399
+ try:
400
+ file["content"] = file_service.download_to_buffer(
401
+ file["url"], session_id
402
+ )
403
+ file["id"] = file["url"]
404
+ except Exception as e:
405
+ # This can happen if the file has timed out
406
+ log.debug(
407
+ "Failed to download file from %s: %s",
408
+ file["url"],
409
+ str(e),
410
+ )
411
+ file["content"] = b"File no longer exists"
412
+ file["mime_type"] = "text/plain"
413
+ mime_type = file.get("mime_type", "")
414
+ if (
415
+ file.get("content")
416
+ and not mime_type.startswith("application")
417
+ and not mime_type.startswith("text")
418
+ ):
419
+ # base64 encode binary files
420
+ file["content"] = base64.b64encode(file["content"]).decode(
421
+ "utf-8"
422
+ )
423
+ elif type(file["content"]) == bytes:
424
+ file["content"] = file["content"].decode("utf-8")
425
+
426
+ # Create .stim file
427
+ stim_data = state.copy()
428
+ stim_data["files"] = available_files
429
+ stim_content = yaml.dump(stim_data, sort_keys=False)
430
+ file_service = FileService()
431
+ stim_meta = file_service.upload_from_buffer(
432
+ stim_content.encode("utf-8"),
433
+ f"{base_filename}.stim",
434
+ session_id=session_id,
435
+ data_source="Monitor - Stimulus and Error - Stimulus File",
436
+ )
437
+
438
+ # Create .md file using stimulus_utils
439
+ md_content = describe_stimulus(stimulus_uuid, state, is_timeout=is_timeout)
440
+ md_meta = file_service.upload_from_buffer(
441
+ md_content.encode("utf-8"),
442
+ f"{base_filename}.md",
443
+ session_id=session_id,
444
+ data_source="Monitor - Stimulus and Error - Description File",
445
+ )
446
+
447
+ return stim_meta["url"], md_meta["url"]
448
+
449
+ except Exception as e:
450
+ log.error("Failed to create stimulus files: %s", str(e))
451
+ raise ValueError(f"Failed to create stimulus files: {str(e)}")
452
+
453
+ def handle_stimulus_complete(
454
+ self,
455
+ stimulus_uuid: str,
456
+ stimulus_state: Dict = None,
457
+ is_timeout: bool = False,
458
+ session_id: Optional[str] = None,
459
+ ) -> None:
460
+ """Handle stimulus completion or timeout.
461
+
462
+ Args:
463
+ stimulus_uuid: The UUID of the completed stimulus.
464
+ stimulus_state: Optional pre-fetched state.
465
+ is_timeout: Whether this was a timeout completion.
466
+ session_id: Optional session ID for file operations.
467
+ """
468
+ try:
469
+ # Get current state
470
+ state = stimulus_state or self._get_stimulus_state(stimulus_uuid)
471
+
472
+ # Create .stim and .md files
473
+ stim_url, md_url = self._create_stimulus_files(
474
+ stimulus_uuid, state, is_timeout, session_id=session_id
475
+ )
476
+
477
+ if self.notification_mode == "none":
478
+ return
479
+
480
+ if self.notification_mode == "errors" and not self._stimulus_has_error(
481
+ state, is_timeout
482
+ ):
483
+ return
484
+
485
+ user_properties = (
486
+ state["events"][0].get("user_properties") if state["events"] else {}
487
+ )
488
+
489
+ # Send notification about completion
490
+ self.send_to_flow(
491
+ self.notification_flow_name,
492
+ Message(
493
+ payload={
494
+ "is_last": False,
495
+ "correlation_id": stimulus_uuid,
496
+ "text": f"Stimulus {stimulus_uuid} {'timed out' if is_timeout else 'completed'}",
497
+ },
498
+ user_properties={
499
+ "originator_scopes": ["slack:post_message:create"],
500
+ "session_id": session_id,
501
+ },
502
+ ),
503
+ )
504
+ self.send_to_flow(
505
+ self.notification_flow_name,
506
+ Message(
507
+ payload={
508
+ "is_last": True,
509
+ "correlation_id": stimulus_uuid,
510
+ "text": "Stimulus Files:",
511
+ "files": [md_url, stim_url],
512
+ },
513
+ user_properties={
514
+ "originator_scopes": ["slack:post_message:create"],
515
+ "session_id": SOLACE_AGENT_MESH_SYSTEM_SESSION_ID,
516
+ },
517
+ ),
518
+ )
519
+
520
+ except Exception as e:
521
+ log.error(
522
+ "Error handling stimulus completion for %s: %s", stimulus_uuid, str(e)
523
+ )
524
+ finally:
525
+ # Remove stimulus from cache after successful completion
526
+ cache_key = self._get_stimulus_cache_key(stimulus_uuid)
527
+ self.cache_service.remove_data(cache_key)
528
+
529
+ def handle_cache_expiry_event(self, event: Dict) -> None:
530
+ """Handle cache expiry events for stimulus state.
531
+
532
+ Args:
533
+ event: Dictionary containing cache expiry event details.
534
+ """
535
+ # Extract stimulus UUID from cache key
536
+ cache_key = event.get("key", "")
537
+ if not cache_key.startswith("monitor:stimulus_state:"):
538
+ return
539
+
540
+ stimulus_uuid = cache_key.split(":")[-1]
541
+
542
+ state = event.get("expired_data", {})
543
+
544
+ # Mark stimulus as complete with timeout
545
+ self.handle_stimulus_complete(
546
+ stimulus_uuid=stimulus_uuid,
547
+ stimulus_state=state,
548
+ is_timeout=True,
549
+ session_id=SOLACE_AGENT_MESH_SYSTEM_SESSION_ID,
550
+ )
551
+
552
+ def invoke(self, message: Message, data: Dict) -> None:
553
+ """Process incoming messages.
554
+
555
+ Args:
556
+ message: The incoming Solace message.
557
+ data: Additional message data/context.
558
+ """
559
+ self.add_system_event(message)
560
+ return data
File without changes