solace-agent-mesh 0.0.1__py3-none-any.whl → 0.1.0__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.
- solace_agent_mesh/__init__.py +0 -3
- solace_agent_mesh/agents/__init__.py +0 -0
- solace_agent_mesh/agents/base_agent_component.py +224 -0
- solace_agent_mesh/agents/global/__init__.py +0 -0
- solace_agent_mesh/agents/global/actions/__init__.py +0 -0
- solace_agent_mesh/agents/global/actions/agent_state_change.py +54 -0
- solace_agent_mesh/agents/global/actions/clear_history.py +32 -0
- solace_agent_mesh/agents/global/actions/convert_file_to_markdown.py +160 -0
- solace_agent_mesh/agents/global/actions/create_file.py +70 -0
- solace_agent_mesh/agents/global/actions/error_action.py +45 -0
- solace_agent_mesh/agents/global/actions/plantuml_diagram.py +93 -0
- solace_agent_mesh/agents/global/actions/plotly_graph.py +117 -0
- solace_agent_mesh/agents/global/actions/retrieve_file.py +51 -0
- solace_agent_mesh/agents/global/global_agent_component.py +38 -0
- solace_agent_mesh/agents/image_processing/__init__.py +0 -0
- solace_agent_mesh/agents/image_processing/actions/__init__.py +0 -0
- solace_agent_mesh/agents/image_processing/actions/create_image.py +75 -0
- solace_agent_mesh/agents/image_processing/actions/describe_image.py +115 -0
- solace_agent_mesh/agents/image_processing/image_processing_agent_component.py +23 -0
- solace_agent_mesh/agents/slack/__init__.py +1 -0
- solace_agent_mesh/agents/slack/actions/__init__.py +1 -0
- solace_agent_mesh/agents/slack/actions/post_message.py +177 -0
- solace_agent_mesh/agents/slack/slack_agent_component.py +59 -0
- solace_agent_mesh/agents/web_request/__init__.py +0 -0
- solace_agent_mesh/agents/web_request/actions/__init__.py +0 -0
- solace_agent_mesh/agents/web_request/actions/do_image_search.py +84 -0
- solace_agent_mesh/agents/web_request/actions/do_news_search.py +47 -0
- solace_agent_mesh/agents/web_request/actions/do_suggestion_search.py +34 -0
- solace_agent_mesh/agents/web_request/actions/do_web_request.py +134 -0
- solace_agent_mesh/agents/web_request/actions/download_file.py +69 -0
- solace_agent_mesh/agents/web_request/web_request_agent_component.py +33 -0
- solace_agent_mesh/cli/__init__.py +1 -0
- solace_agent_mesh/cli/commands/__init__.py +0 -0
- solace_agent_mesh/cli/commands/add/__init__.py +3 -0
- solace_agent_mesh/cli/commands/add/add.py +88 -0
- solace_agent_mesh/cli/commands/add/agent.py +110 -0
- solace_agent_mesh/cli/commands/add/copy_from_plugin.py +90 -0
- solace_agent_mesh/cli/commands/add/gateway.py +221 -0
- solace_agent_mesh/cli/commands/build.py +631 -0
- solace_agent_mesh/cli/commands/chat/__init__.py +3 -0
- solace_agent_mesh/cli/commands/chat/chat.py +361 -0
- solace_agent_mesh/cli/commands/config.py +29 -0
- solace_agent_mesh/cli/commands/init/__init__.py +3 -0
- solace_agent_mesh/cli/commands/init/ai_provider_step.py +76 -0
- solace_agent_mesh/cli/commands/init/broker_step.py +102 -0
- solace_agent_mesh/cli/commands/init/builtin_agent_step.py +88 -0
- solace_agent_mesh/cli/commands/init/check_if_already_done.py +13 -0
- solace_agent_mesh/cli/commands/init/create_config_file_step.py +52 -0
- solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +96 -0
- solace_agent_mesh/cli/commands/init/file_service_step.py +73 -0
- solace_agent_mesh/cli/commands/init/init.py +114 -0
- solace_agent_mesh/cli/commands/init/project_structure_step.py +45 -0
- solace_agent_mesh/cli/commands/init/rest_api_step.py +50 -0
- solace_agent_mesh/cli/commands/init/web_ui_step.py +40 -0
- solace_agent_mesh/cli/commands/plugin/__init__.py +3 -0
- solace_agent_mesh/cli/commands/plugin/add.py +98 -0
- solace_agent_mesh/cli/commands/plugin/build.py +217 -0
- solace_agent_mesh/cli/commands/plugin/create.py +117 -0
- solace_agent_mesh/cli/commands/plugin/plugin.py +109 -0
- solace_agent_mesh/cli/commands/plugin/remove.py +71 -0
- solace_agent_mesh/cli/commands/run.py +68 -0
- solace_agent_mesh/cli/commands/visualizer.py +138 -0
- solace_agent_mesh/cli/config.py +81 -0
- solace_agent_mesh/cli/main.py +306 -0
- solace_agent_mesh/cli/utils.py +246 -0
- solace_agent_mesh/common/__init__.py +0 -0
- solace_agent_mesh/common/action.py +91 -0
- solace_agent_mesh/common/action_list.py +37 -0
- solace_agent_mesh/common/action_response.py +327 -0
- solace_agent_mesh/common/constants.py +3 -0
- solace_agent_mesh/common/mysql_database.py +40 -0
- solace_agent_mesh/common/postgres_database.py +79 -0
- solace_agent_mesh/common/prompt_templates.py +30 -0
- solace_agent_mesh/common/prompt_templates_unused_delete.py +161 -0
- solace_agent_mesh/common/stimulus_utils.py +152 -0
- solace_agent_mesh/common/time.py +24 -0
- solace_agent_mesh/common/utils.py +638 -0
- solace_agent_mesh/configs/agent_global.yaml +74 -0
- solace_agent_mesh/configs/agent_image_processing.yaml +82 -0
- solace_agent_mesh/configs/agent_slack.yaml +64 -0
- solace_agent_mesh/configs/agent_web_request.yaml +75 -0
- solace_agent_mesh/configs/conversation_to_file.yaml +56 -0
- solace_agent_mesh/configs/error_catcher.yaml +56 -0
- solace_agent_mesh/configs/monitor.yaml +0 -0
- solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +106 -0
- solace_agent_mesh/configs/monitor_user_feedback.yaml +58 -0
- solace_agent_mesh/configs/orchestrator.yaml +241 -0
- solace_agent_mesh/configs/service_embedding.yaml +81 -0
- solace_agent_mesh/configs/service_llm.yaml +265 -0
- solace_agent_mesh/configs/visualize_websocket.yaml +55 -0
- solace_agent_mesh/gateway/__init__.py +0 -0
- solace_agent_mesh/gateway/components/__init__.py +0 -0
- solace_agent_mesh/gateway/components/gateway_base.py +41 -0
- solace_agent_mesh/gateway/components/gateway_input.py +265 -0
- solace_agent_mesh/gateway/components/gateway_output.py +289 -0
- solace_agent_mesh/gateway/identity/bamboohr_identity.py +18 -0
- solace_agent_mesh/gateway/identity/identity_base.py +10 -0
- solace_agent_mesh/gateway/identity/identity_provider.py +60 -0
- solace_agent_mesh/gateway/identity/no_identity.py +9 -0
- solace_agent_mesh/gateway/identity/passthru_identity.py +9 -0
- solace_agent_mesh/monitors/base_monitor_component.py +26 -0
- solace_agent_mesh/monitors/feedback/user_feedback_monitor.py +75 -0
- solace_agent_mesh/monitors/stim_and_errors/stim_and_error_monitor.py +560 -0
- solace_agent_mesh/orchestrator/__init__.py +0 -0
- solace_agent_mesh/orchestrator/action_manager.py +225 -0
- solace_agent_mesh/orchestrator/components/__init__.py +0 -0
- solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +54 -0
- solace_agent_mesh/orchestrator/components/orchestrator_action_response_component.py +179 -0
- solace_agent_mesh/orchestrator/components/orchestrator_register_component.py +107 -0
- solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +477 -0
- solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +246 -0
- solace_agent_mesh/orchestrator/orchestrator_main.py +166 -0
- solace_agent_mesh/orchestrator/orchestrator_prompt.py +410 -0
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/authorization/providers/base_authorization_provider.py +56 -0
- solace_agent_mesh/services/bamboo_hr_service/__init__.py +3 -0
- solace_agent_mesh/services/bamboo_hr_service/bamboo_hr.py +182 -0
- solace_agent_mesh/services/common/__init__.py +4 -0
- solace_agent_mesh/services/common/auto_expiry.py +45 -0
- solace_agent_mesh/services/common/singleton.py +18 -0
- solace_agent_mesh/services/file_service/__init__.py +14 -0
- solace_agent_mesh/services/file_service/file_manager/__init__.py +0 -0
- solace_agent_mesh/services/file_service/file_manager/bucket_file_manager.py +149 -0
- solace_agent_mesh/services/file_service/file_manager/file_manager_base.py +162 -0
- solace_agent_mesh/services/file_service/file_manager/memory_file_manager.py +64 -0
- solace_agent_mesh/services/file_service/file_manager/volume_file_manager.py +106 -0
- solace_agent_mesh/services/file_service/file_service.py +432 -0
- solace_agent_mesh/services/file_service/file_service_constants.py +54 -0
- solace_agent_mesh/services/file_service/file_transformations.py +131 -0
- solace_agent_mesh/services/file_service/file_utils.py +322 -0
- solace_agent_mesh/services/file_service/transformers/__init__.py +5 -0
- solace_agent_mesh/services/history_service/__init__.py +3 -0
- solace_agent_mesh/services/history_service/history_providers/__init__.py +0 -0
- solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +78 -0
- solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +167 -0
- solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +163 -0
- solace_agent_mesh/services/history_service/history_service.py +139 -0
- solace_agent_mesh/services/llm_service/components/llm_request_component.py +293 -0
- solace_agent_mesh/services/llm_service/components/llm_service_component_base.py +152 -0
- solace_agent_mesh/services/middleware_service/__init__.py +0 -0
- solace_agent_mesh/services/middleware_service/middleware_service.py +20 -0
- solace_agent_mesh/templates/action.py +38 -0
- solace_agent_mesh/templates/agent.py +29 -0
- solace_agent_mesh/templates/agent.yaml +70 -0
- solace_agent_mesh/templates/gateway-config-template.yaml +6 -0
- solace_agent_mesh/templates/gateway-default-config.yaml +28 -0
- solace_agent_mesh/templates/gateway-flows.yaml +81 -0
- solace_agent_mesh/templates/gateway-header.yaml +16 -0
- solace_agent_mesh/templates/gateway_base.py +15 -0
- solace_agent_mesh/templates/gateway_input.py +98 -0
- solace_agent_mesh/templates/gateway_output.py +71 -0
- solace_agent_mesh/templates/plugin-pyproject.toml +30 -0
- solace_agent_mesh/templates/rest-api-default-config.yaml +23 -0
- solace_agent_mesh/templates/rest-api-flows.yaml +80 -0
- solace_agent_mesh/templates/slack-default-config.yaml +9 -0
- solace_agent_mesh/templates/slack-flows.yaml +90 -0
- solace_agent_mesh/templates/solace-agent-mesh-default.yaml +77 -0
- solace_agent_mesh/templates/solace-agent-mesh-plugin-default.yaml +8 -0
- solace_agent_mesh/templates/web-default-config.yaml +5 -0
- solace_agent_mesh/templates/web-flows.yaml +86 -0
- solace_agent_mesh/tools/__init__.py +0 -0
- solace_agent_mesh/tools/components/__init__.py +0 -0
- solace_agent_mesh/tools/components/conversation_formatter.py +111 -0
- solace_agent_mesh/tools/components/file_resolver_component.py +58 -0
- solace_agent_mesh/tools/config/runtime_config.py +26 -0
- solace_agent_mesh-0.1.0.dist-info/METADATA +179 -0
- solace_agent_mesh-0.1.0.dist-info/RECORD +170 -0
- solace_agent_mesh-0.1.0.dist-info/entry_points.txt +3 -0
- solace_agent_mesh-0.0.1.dist-info/licenses/LICENSE.txt → solace_agent_mesh-0.1.0.dist-info/licenses/LICENSE +1 -2
- solace_agent_mesh-0.0.1.dist-info/METADATA +0 -51
- solace_agent_mesh-0.0.1.dist-info/RECORD +0 -5
- {solace_agent_mesh-0.0.1.dist-info → solace_agent_mesh-0.1.0.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
|