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.
- 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/assets/web-visualizer/assets/index-C5awueeJ.js +109 -0
- solace_agent_mesh/assets/web-visualizer/assets/index-D0qORgkg.css +1 -0
- solace_agent_mesh/assets/web-visualizer/index.html +14 -0
- solace_agent_mesh/assets/web-visualizer/vite.svg +1 -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 +24 -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.1.dist-info/METADATA +179 -0
- solace_agent_mesh-0.1.1.dist-info/RECORD +174 -0
- solace_agent_mesh-0.1.1.dist-info/entry_points.txt +3 -0
- solace_agent_mesh-0.0.1.dist-info/licenses/LICENSE.txt → solace_agent_mesh-0.1.1.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.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl
|
|
6
|
+
|
|
7
|
+
from solace_ai_connector.common.log import log
|
|
8
|
+
|
|
9
|
+
from ...common.time import ONE_DAY, TEN_MINUTES
|
|
10
|
+
from ..common import AutoExpiry, AutoExpirySingletonMeta
|
|
11
|
+
from .file_manager.bucket_file_manager import BucketFileManager
|
|
12
|
+
from .file_manager.volume_file_manager import VolumeFileManager
|
|
13
|
+
from .file_manager.memory_file_manager import MemoryFileManager
|
|
14
|
+
from .file_manager.file_manager_base import FileManagerBase
|
|
15
|
+
from .file_service_constants import FS_PROTOCOL, INDENT_SIZE, DEFAULT_FILE_MANAGER, BLOCK_IGNORE_KEYS, BLOCK_TAG_KEYS, FS_URL_REGEX
|
|
16
|
+
from .file_transformations import apply_file_transformations
|
|
17
|
+
from .file_utils import starts_with_fs_url
|
|
18
|
+
from ...tools.config.runtime_config import get_service_config
|
|
19
|
+
|
|
20
|
+
FILE_MANAGERS = {
|
|
21
|
+
"bucket": BucketFileManager,
|
|
22
|
+
"volume": VolumeFileManager,
|
|
23
|
+
"memory": MemoryFileManager,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FileServicePermissionError(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# FileService class - Manages file storage and retrieval
|
|
32
|
+
class FileService(AutoExpiry, metaclass=AutoExpirySingletonMeta):
|
|
33
|
+
file_manager: FileManagerBase
|
|
34
|
+
|
|
35
|
+
def __init__(self, config=None, identifier=None) -> None:
|
|
36
|
+
self.identifier = identifier
|
|
37
|
+
self._expiry_thread = None
|
|
38
|
+
config = config or get_service_config("file_service")
|
|
39
|
+
self.service_type = config.get("type", DEFAULT_FILE_MANAGER)
|
|
40
|
+
self.max_time_to_live = config.get("max_time_to_live", ONE_DAY)
|
|
41
|
+
self.expiration_check_interval = config.get(
|
|
42
|
+
"expiration_check_interval", TEN_MINUTES
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if self.service_type not in config.get("config", {}):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Missing configuration for file service type: {self.service_type}"
|
|
48
|
+
)
|
|
49
|
+
self.service_config = config.get("config").get(self.service_type)
|
|
50
|
+
|
|
51
|
+
if self.service_type not in FILE_MANAGERS and not self.service_config.get(
|
|
52
|
+
"module_path"
|
|
53
|
+
):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Unsupported file service type: {self.service_type}. No module_path provided."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if self.service_type in FILE_MANAGERS:
|
|
59
|
+
# Load built-in history provider
|
|
60
|
+
self.file_manager = FILE_MANAGERS[self.service_type](
|
|
61
|
+
self.service_config, self.max_time_to_live
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
try:
|
|
65
|
+
# Load the provider from the module path
|
|
66
|
+
module_name = self.service_type
|
|
67
|
+
module_path = self.service_config.get("module_path")
|
|
68
|
+
module = importlib.import_module(module_path, package=__package__)
|
|
69
|
+
manager_class = getattr(module, module_name)
|
|
70
|
+
if not issubclass(manager_class, FileManagerBase):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Provided class {manager_class} does not inherit from FileManagerBase"
|
|
73
|
+
)
|
|
74
|
+
self.file_manager = manager_class(
|
|
75
|
+
self.service_config, self.max_time_to_live
|
|
76
|
+
)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise ImportError("Unable to load component: " + str(e)) from e
|
|
79
|
+
|
|
80
|
+
# Start the background thread for auto-expiry
|
|
81
|
+
self._start_auto_expiry_thread(self.expiration_check_interval)
|
|
82
|
+
|
|
83
|
+
def _delete_expired_items(self):
|
|
84
|
+
"""Checks all files and deletes those that have exceeded max_time_to_live."""
|
|
85
|
+
all_files_metadata = self.file_manager.list_all_metadata()
|
|
86
|
+
current_time = time.time()
|
|
87
|
+
for metadata in all_files_metadata:
|
|
88
|
+
current_time = time.time()
|
|
89
|
+
expiration_timestamp = metadata["expiration_timestamp"]
|
|
90
|
+
|
|
91
|
+
if current_time > expiration_timestamp:
|
|
92
|
+
try:
|
|
93
|
+
filename, _ = self.get_parsed_url(metadata["url"])
|
|
94
|
+
self.file_manager.delete_by_name(filename)
|
|
95
|
+
log.info(
|
|
96
|
+
f"Deleted expired file: {metadata['url']} {current_time} > {expiration_timestamp}"
|
|
97
|
+
)
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
log.warning(f"File not found: {metadata['url']}")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
log.error(
|
|
102
|
+
f"Failed to delete expired file: {metadata['url']} with error: {e}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _validate_file_url(self, file_url: str):
|
|
106
|
+
if not starts_with_fs_url(file_url):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Invalid URL format. URL must start with '{FS_PROTOCOL}://'"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def list_all_metadata(self, session_id: str):
|
|
112
|
+
all_files_metadata = self.file_manager.list_all_metadata()
|
|
113
|
+
return [
|
|
114
|
+
metadata
|
|
115
|
+
for metadata in all_files_metadata
|
|
116
|
+
if metadata.get("session_id") == session_id
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def validate_access_permission(
|
|
121
|
+
self, filename: str, session_id: str, return_metadata=False
|
|
122
|
+
):
|
|
123
|
+
if not session_id:
|
|
124
|
+
raise ValueError("Invalid session ID used for accessing file")
|
|
125
|
+
metadata = self.file_manager.get_metadata(filename)
|
|
126
|
+
if metadata.get("session_id") != session_id:
|
|
127
|
+
raise FileServicePermissionError(f"Access denied to file: {filename}")
|
|
128
|
+
current_time = time.time()
|
|
129
|
+
if current_time > metadata.get("expiration_timestamp"):
|
|
130
|
+
raise FileServicePermissionError(f"File has expired: {filename}")
|
|
131
|
+
if return_metadata:
|
|
132
|
+
return metadata
|
|
133
|
+
|
|
134
|
+
def get_parsed_url(self, file_url: str):
|
|
135
|
+
self._validate_file_url(file_url)
|
|
136
|
+
|
|
137
|
+
if file_url.startswith("<url>") and file_url.endswith("</url>"):
|
|
138
|
+
file_url = file_url[5:-6].strip()
|
|
139
|
+
|
|
140
|
+
# Parse the URL into its components
|
|
141
|
+
url_parts = urlparse(file_url)
|
|
142
|
+
|
|
143
|
+
filename = url_parts[1]
|
|
144
|
+
|
|
145
|
+
# Get the query parameters
|
|
146
|
+
query = dict(parse_qsl(url_parts[4]))
|
|
147
|
+
return filename, query
|
|
148
|
+
|
|
149
|
+
def upload_from_buffer(
|
|
150
|
+
self,
|
|
151
|
+
buffer: bytes,
|
|
152
|
+
file_name: str,
|
|
153
|
+
session_id: str,
|
|
154
|
+
**kwargs,
|
|
155
|
+
) -> dict:
|
|
156
|
+
"""
|
|
157
|
+
Upload a file from a buffer.
|
|
158
|
+
kwargs are added to metadata
|
|
159
|
+
kwargs are over-written by default metadata if they have the same key.
|
|
160
|
+
The official support kwargs are:
|
|
161
|
+
- schema_yaml: str
|
|
162
|
+
- shape: str
|
|
163
|
+
- data_source: str
|
|
164
|
+
"""
|
|
165
|
+
if type(buffer) == str:
|
|
166
|
+
buffer = buffer.encode("utf-8")
|
|
167
|
+
elif type(buffer) != bytes:
|
|
168
|
+
raise ValueError("Invalid buffer type. Expected bytes or string.")
|
|
169
|
+
|
|
170
|
+
return self.file_manager.upload_from_buffer(
|
|
171
|
+
buffer,
|
|
172
|
+
file_name,
|
|
173
|
+
session_id=session_id,
|
|
174
|
+
**kwargs,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def upload_from_file(self, file_path: str, session_id: str, **kwargs) -> dict:
|
|
178
|
+
"""
|
|
179
|
+
Upload a file from a file path.
|
|
180
|
+
kwargs are added to metadata
|
|
181
|
+
kwargs are over-written by default metadata if they have the same key.
|
|
182
|
+
The official support kwargs are:
|
|
183
|
+
- schema_yaml: str
|
|
184
|
+
- shape: str
|
|
185
|
+
- data_source: str
|
|
186
|
+
"""
|
|
187
|
+
return self.file_manager.upload_from_file(
|
|
188
|
+
file_path, session_id=session_id, **kwargs
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def get_metadata(self, file_url: str) -> dict:
|
|
192
|
+
"""
|
|
193
|
+
Get metadata from a file URL.
|
|
194
|
+
"""
|
|
195
|
+
filename, _ = self.get_parsed_url(file_url)
|
|
196
|
+
return self.file_manager.get_metadata(filename)
|
|
197
|
+
|
|
198
|
+
def download_to_buffer(self, file_url: str, session_id: str) -> bytes:
|
|
199
|
+
"""
|
|
200
|
+
Download a file to a buffer.
|
|
201
|
+
"""
|
|
202
|
+
filename, _ = self.get_parsed_url(file_url)
|
|
203
|
+
self.validate_access_permission(filename, session_id)
|
|
204
|
+
return self.file_manager.download_to_buffer(filename)
|
|
205
|
+
|
|
206
|
+
def download_to_file(self, file_url: str, destination_path: str, session_id: str):
|
|
207
|
+
"""
|
|
208
|
+
Download a file to a destination path.
|
|
209
|
+
"""
|
|
210
|
+
filename, _ = self.get_parsed_url(file_url)
|
|
211
|
+
self.validate_access_permission(filename, session_id)
|
|
212
|
+
return self.file_manager.download_to_file(filename, destination_path)
|
|
213
|
+
|
|
214
|
+
def delete_by_url(self, file_url: str):
|
|
215
|
+
"""
|
|
216
|
+
Delete a file by URL.
|
|
217
|
+
"""
|
|
218
|
+
filename, _ = self.get_parsed_url(file_url)
|
|
219
|
+
return self.file_manager.delete_by_name(filename)
|
|
220
|
+
|
|
221
|
+
def update_file_expiration(self, file_url: str, expiration_timestamp: float):
|
|
222
|
+
"""
|
|
223
|
+
Update the expiration timestamp for a file.
|
|
224
|
+
"""
|
|
225
|
+
filename, _ = self.get_parsed_url(file_url)
|
|
226
|
+
return self.file_manager.update_file_expiration(filename, expiration_timestamp)
|
|
227
|
+
|
|
228
|
+
def get_file_block_by_url(self, file_url: str) -> str:
|
|
229
|
+
"""
|
|
230
|
+
Get file block LLM by URL in a format understandable by the LLM.
|
|
231
|
+
"""
|
|
232
|
+
metadata = self.get_metadata(file_url)
|
|
233
|
+
return self.get_file_block_by_metadata(metadata)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def get_file_block_by_metadata(metadata: dict, tag_prefix: str = "") -> str:
|
|
237
|
+
"""
|
|
238
|
+
Get file block LLM by metadata in a format understandable by the LLM.
|
|
239
|
+
"""
|
|
240
|
+
block = ""
|
|
241
|
+
head = f"<{tag_prefix}file "
|
|
242
|
+
tail = f"\n</{tag_prefix}file>"
|
|
243
|
+
body = ""
|
|
244
|
+
tags = ""
|
|
245
|
+
|
|
246
|
+
def indent(text: str, size=INDENT_SIZE) -> str:
|
|
247
|
+
space = " " * size
|
|
248
|
+
return space + f"\n{space}".join(text.split("\n")).strip()
|
|
249
|
+
|
|
250
|
+
for key, value in metadata.items():
|
|
251
|
+
if key in BLOCK_IGNORE_KEYS or key in BLOCK_TAG_KEYS or value is None:
|
|
252
|
+
continue
|
|
253
|
+
tags += f'{key}="{value}" '
|
|
254
|
+
|
|
255
|
+
if "url" in metadata and metadata["url"]:
|
|
256
|
+
if metadata["url"].startswith(FS_PROTOCOL):
|
|
257
|
+
body += f'<url>\n{indent(metadata["url"])}\n</url>\n'
|
|
258
|
+
else:
|
|
259
|
+
tags += f'url="{metadata["url"]}" '
|
|
260
|
+
|
|
261
|
+
for key in BLOCK_TAG_KEYS:
|
|
262
|
+
if key in metadata and metadata[key] is not None:
|
|
263
|
+
tag = key.replace("_", "-")
|
|
264
|
+
body += f"<{tag}>\n{indent(metadata[key])}\n</{tag}>\n"
|
|
265
|
+
|
|
266
|
+
if body:
|
|
267
|
+
block = head + tags + ">\n" + indent(body) + tail
|
|
268
|
+
else:
|
|
269
|
+
block = head + tags + "/>"
|
|
270
|
+
|
|
271
|
+
return block
|
|
272
|
+
|
|
273
|
+
def resolve_url(self, file_url, session_id: str, return_extra=False) -> bytes | str:
|
|
274
|
+
"""
|
|
275
|
+
Resolve a URL to its actual file URL and applies the necessary transformations (from query parameters)
|
|
276
|
+
|
|
277
|
+
Parameters:
|
|
278
|
+
- file_url (str): The URL to resolve.
|
|
279
|
+
- return_extra (bool): Whether to return original file content and metadata along with the resolved URL.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
- bytes|str: The resolved file content. Applies query transformations if any.
|
|
283
|
+
"""
|
|
284
|
+
filename, queries = self.get_parsed_url(file_url)
|
|
285
|
+
file_metadata = self.validate_access_permission(
|
|
286
|
+
filename, session_id, return_metadata=True
|
|
287
|
+
)
|
|
288
|
+
file_bytes = self.file_manager.download_to_buffer(filename)
|
|
289
|
+
if return_extra:
|
|
290
|
+
return (
|
|
291
|
+
apply_file_transformations(file_bytes, file_metadata, queries),
|
|
292
|
+
file_bytes,
|
|
293
|
+
file_metadata,
|
|
294
|
+
)
|
|
295
|
+
return apply_file_transformations(file_bytes, file_metadata, queries)
|
|
296
|
+
|
|
297
|
+
def resolve_all_resolvable_urls(
|
|
298
|
+
self, text: str, session_id: str, forceResolve=False
|
|
299
|
+
) -> str:
|
|
300
|
+
"""
|
|
301
|
+
Resolve all resolvable URLs in a text
|
|
302
|
+
|
|
303
|
+
Parameters:
|
|
304
|
+
- text (str): The text to resolve URLs in.
|
|
305
|
+
- forceResolve (bool): Whether to force resolve all URLs (if false, only URLs with 'resolve' query parameter set to True will be resolved).
|
|
306
|
+
"""
|
|
307
|
+
cache = {}
|
|
308
|
+
|
|
309
|
+
if not session_id:
|
|
310
|
+
raise ValueError("Invalid session ID used for resolving URLs")
|
|
311
|
+
|
|
312
|
+
def replace_url(match):
|
|
313
|
+
raw_url = match.group()
|
|
314
|
+
url = FileService._clean_url(raw_url)
|
|
315
|
+
try:
|
|
316
|
+
filename, queries = self.get_parsed_url(url)
|
|
317
|
+
|
|
318
|
+
resolvable = queries.get("resolve", False)
|
|
319
|
+
resolvable = (
|
|
320
|
+
resolvable
|
|
321
|
+
if isinstance(resolvable, bool)
|
|
322
|
+
else resolvable.lower() == "true"
|
|
323
|
+
)
|
|
324
|
+
if not resolvable and not forceResolve:
|
|
325
|
+
return raw_url
|
|
326
|
+
|
|
327
|
+
if filename in cache:
|
|
328
|
+
metadata = cache[filename][0]
|
|
329
|
+
file_bytes = cache[filename][1]
|
|
330
|
+
else:
|
|
331
|
+
metadata = self.validate_access_permission(
|
|
332
|
+
filename, session_id, return_metadata=True
|
|
333
|
+
)
|
|
334
|
+
file_bytes = self.file_manager.download_to_buffer(filename)
|
|
335
|
+
cache[filename] = (metadata, file_bytes)
|
|
336
|
+
|
|
337
|
+
response = apply_file_transformations(file_bytes, metadata, queries)
|
|
338
|
+
# Convert type to string
|
|
339
|
+
if type(response) == bytes:
|
|
340
|
+
response = response.decode("utf-8", "ignore")
|
|
341
|
+
elif type(response) == str:
|
|
342
|
+
pass
|
|
343
|
+
else:
|
|
344
|
+
response = json.dumps(response)
|
|
345
|
+
# If initial URl was in quotes, return the response in quotes
|
|
346
|
+
if raw_url.startswith('"') or raw_url.startswith("'"):
|
|
347
|
+
response = f"{raw_url[0]}{response}"
|
|
348
|
+
|
|
349
|
+
if raw_url.endswith('"') or raw_url.endswith("'"):
|
|
350
|
+
response = f"{response}{raw_url[-1]}"
|
|
351
|
+
elif raw_url.endswith("',") or raw_url.endswith('",'):
|
|
352
|
+
response = f"{response}{''.join(raw_url[-2:])}"
|
|
353
|
+
# If initial URL ends with a comma, return the response with a comma
|
|
354
|
+
elif raw_url.endswith(","):
|
|
355
|
+
response = f"{response},"
|
|
356
|
+
return response
|
|
357
|
+
except FileServicePermissionError as e:
|
|
358
|
+
raise e
|
|
359
|
+
except Exception as e:
|
|
360
|
+
log.error(f"Failed to resolve URL: {raw_url} with error: {e}")
|
|
361
|
+
raise e
|
|
362
|
+
|
|
363
|
+
return re.sub(FS_URL_REGEX, replace_url, text)
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def get_urls_from_text(text: str) -> list:
|
|
367
|
+
"""
|
|
368
|
+
Get all file URLs from a file block or text
|
|
369
|
+
"""
|
|
370
|
+
urls = []
|
|
371
|
+
|
|
372
|
+
def append_url(match):
|
|
373
|
+
raw_url = match.group()
|
|
374
|
+
url = FileService._clean_url(raw_url)
|
|
375
|
+
urls.append(url)
|
|
376
|
+
return raw_url
|
|
377
|
+
|
|
378
|
+
re.sub(FS_URL_REGEX, append_url, text)
|
|
379
|
+
return urls
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def _clean_url(url: str) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Clean up a URL by removing any extra characters
|
|
385
|
+
"""
|
|
386
|
+
url = url.strip()
|
|
387
|
+
if url.endswith(","):
|
|
388
|
+
url = url[:-1]
|
|
389
|
+
if url.startswith('"') or url.startswith("'"):
|
|
390
|
+
url = url[1:]
|
|
391
|
+
if url.startswith("<url>"):
|
|
392
|
+
url = url[5:-6].strip()
|
|
393
|
+
if url.endswith('"') or url.endswith("'") or url.endswith(","):
|
|
394
|
+
url = url[:-1]
|
|
395
|
+
return url
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def get_query_params_from_url(url: str) -> dict:
|
|
399
|
+
"""
|
|
400
|
+
Get query parameters from a URL
|
|
401
|
+
"""
|
|
402
|
+
# Parse the URL into its components
|
|
403
|
+
url_parts = urlparse(url)
|
|
404
|
+
|
|
405
|
+
# Get the query parameters
|
|
406
|
+
query = dict(parse_qsl(url_parts[4]))
|
|
407
|
+
return query
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def add_query_params_to_url(url, params):
|
|
411
|
+
"""
|
|
412
|
+
Add query parameters to a URL
|
|
413
|
+
"""
|
|
414
|
+
# Parse the URL into its components
|
|
415
|
+
url_parts = list(urlparse(url))
|
|
416
|
+
|
|
417
|
+
# Get existing query parameters and update them with the new params
|
|
418
|
+
query = dict(parse_qsl(url_parts[4]))
|
|
419
|
+
query.update(params)
|
|
420
|
+
|
|
421
|
+
queries = {}
|
|
422
|
+
for key, value in query.items():
|
|
423
|
+
if type(value) == str:
|
|
424
|
+
queries[key] = value
|
|
425
|
+
else:
|
|
426
|
+
queries[key] = json.dumps(value)
|
|
427
|
+
|
|
428
|
+
# Encode updated query parameters back into the URL
|
|
429
|
+
url_parts[4] = urlencode(queries)
|
|
430
|
+
|
|
431
|
+
# Rebuild the URL with the updated query string
|
|
432
|
+
return urlunparse(url_parts)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
FS_PROTOCOL = "amfs"
|
|
2
|
+
"""
|
|
3
|
+
mesh file service protocol.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
META_FILE_EXTENSION = ".metadata"
|
|
7
|
+
"""
|
|
8
|
+
Extension for metadata files.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
INDENT_SIZE = 2
|
|
12
|
+
"""
|
|
13
|
+
Indent size for XML representation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
DEFAULT_FILE_MANAGER = "volume"
|
|
17
|
+
"""
|
|
18
|
+
Default file manager to use.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
BLOCK_IGNORE_KEYS = [
|
|
23
|
+
"session_id",
|
|
24
|
+
"upload_timestamp",
|
|
25
|
+
"url",
|
|
26
|
+
]
|
|
27
|
+
"""
|
|
28
|
+
Keys to ignore from metadata file attribute while generating file block.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
BLOCK_TAG_KEYS = [
|
|
32
|
+
"data",
|
|
33
|
+
"schema-yaml",
|
|
34
|
+
"schema_yaml",
|
|
35
|
+
"shape",
|
|
36
|
+
"data-source",
|
|
37
|
+
"data_source",
|
|
38
|
+
]
|
|
39
|
+
"""
|
|
40
|
+
Keys to be treated as tags in the file block, and not file attributes.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
FS_URL_REGEX = r"""<url>\s*({protocol}:\/\/.+?)\s*<\/url>|{protocol}:\/\/[^\s\?]+(\s|$)|{protocol}:\/\/[^\n\?]+\?.*?(?=\n|$)|\"({protocol}:\/\/[^\"]+?)(\"|\n)|'({protocol}:\/\/[^']+?)('|\n)""".format(
|
|
44
|
+
protocol=FS_PROTOCOL
|
|
45
|
+
)
|
|
46
|
+
"""
|
|
47
|
+
Regex pattern to match FS URLs in a text. \n
|
|
48
|
+
Matches if: \n
|
|
49
|
+
- Starts with <url> ends with </url>
|
|
50
|
+
- Starts with FS_PROTOCOL:// has no ? and ends with space or newline or end of string \n
|
|
51
|
+
- Starts with FS_PROTOCOL:// has ? and ends with space or newline or end of string \n
|
|
52
|
+
- Starts with "FS_PROTOCOL:// ends with " or newline \n
|
|
53
|
+
- Starts with 'FS_PROTOCOL:// ends with ' or newline \n
|
|
54
|
+
"""
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
import zipfile
|
|
5
|
+
import gzip
|
|
6
|
+
import io
|
|
7
|
+
|
|
8
|
+
from solace_ai_connector.common.log import log
|
|
9
|
+
from .transformers import TRANSFORMERS
|
|
10
|
+
|
|
11
|
+
QUERY_OPTIONS = {
|
|
12
|
+
"encoding": ["zip", "gzip", "base64", "datauri"],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for tr in TRANSFORMERS:
|
|
16
|
+
for key, value in tr.queries.items():
|
|
17
|
+
QUERY_OPTIONS[key] = value.get("type")
|
|
18
|
+
|
|
19
|
+
LLM_QUERY_OPTIONS = {
|
|
20
|
+
**QUERY_OPTIONS,
|
|
21
|
+
"resolve": "bool",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def encode_file(file: bytes, encoding: str, mime_type: str, file_name=str) -> bytes:
|
|
26
|
+
"""
|
|
27
|
+
Encode a file using the specified encoding.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
- file (bytes): The file content as bytes.
|
|
31
|
+
- encoding (str): The encoding to use ('zip', 'gzip', 'base64', 'datauri').
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
- bytes: The encoded file content.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
if encoding == "zip":
|
|
38
|
+
# Create a BytesIO buffer to hold the zip file
|
|
39
|
+
zip_buffer = io.BytesIO()
|
|
40
|
+
# Create a zip file in the buffer
|
|
41
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
42
|
+
# Add the file to the ZIP archive
|
|
43
|
+
zip_file.writestr(file_name, file)
|
|
44
|
+
# Get the byte buffer of the zip file
|
|
45
|
+
zip_buffer.seek(0) # Move to the beginning of the buffer
|
|
46
|
+
return zip_buffer.read()
|
|
47
|
+
|
|
48
|
+
elif encoding == "gzip":
|
|
49
|
+
# Create a BytesIO buffer to hold the gzip file
|
|
50
|
+
gzip_buffer = io.BytesIO()
|
|
51
|
+
# Create a gzip file in the buffer
|
|
52
|
+
with gzip.GzipFile(fileobj=gzip_buffer, mode="wb") as gzip_file:
|
|
53
|
+
# Write the file content to the gzip archive
|
|
54
|
+
gzip_file.write(file)
|
|
55
|
+
return gzip_buffer.getvalue()
|
|
56
|
+
|
|
57
|
+
elif encoding == "base64":
|
|
58
|
+
return base64.b64encode(file).decode("utf-8")
|
|
59
|
+
|
|
60
|
+
elif encoding == "datauri":
|
|
61
|
+
return f"data:{mime_type};base64,{base64.b64encode(file).decode('utf-8')}"
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
log.error("Failed to encode file: %s", e)
|
|
65
|
+
return file
|
|
66
|
+
|
|
67
|
+
return file
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def apply_file_transformations(
|
|
71
|
+
file: bytes, metadata: dict, transformations: dict = {}
|
|
72
|
+
) -> bytes | str:
|
|
73
|
+
"""
|
|
74
|
+
Apply transformations to a file.
|
|
75
|
+
|
|
76
|
+
Parameters:
|
|
77
|
+
- file (bytes): The file content as bytes.
|
|
78
|
+
- metadata (dict): The file metadata.
|
|
79
|
+
- transformations (dict): The transformations to apply.
|
|
80
|
+
"""
|
|
81
|
+
if not transformations:
|
|
82
|
+
return file
|
|
83
|
+
text_mime_type_regex = r"text/.*|.*csv|.*json|.*xml|.*yaml|.*x-yaml|.*txt"
|
|
84
|
+
mime_type = metadata.get("mime_type", "")
|
|
85
|
+
name = metadata.get("name", "unknown")
|
|
86
|
+
other = {
|
|
87
|
+
"mime_type": mime_type,
|
|
88
|
+
"name": name,
|
|
89
|
+
}
|
|
90
|
+
data = file
|
|
91
|
+
if not re.match(text_mime_type_regex, mime_type):
|
|
92
|
+
for transformer in TRANSFORMERS:
|
|
93
|
+
if transformer.is_binary_transformer:
|
|
94
|
+
data = transformer.transform(file, data, transformations, other)
|
|
95
|
+
|
|
96
|
+
# Should be last transformation
|
|
97
|
+
if transformations.get("encoding"):
|
|
98
|
+
try:
|
|
99
|
+
byte_data = data if isinstance(data, bytes) else data.encode("utf-8")
|
|
100
|
+
return encode_file(
|
|
101
|
+
byte_data, transformations.get("encoding"), mime_type, name
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
log.error("Failed to encode to base64: %s", e)
|
|
105
|
+
return file
|
|
106
|
+
return data
|
|
107
|
+
else:
|
|
108
|
+
# File is text-based
|
|
109
|
+
# Convert bytes to string if of type bytes
|
|
110
|
+
decoded_data = file.decode("utf-8") if isinstance(file, bytes) else file
|
|
111
|
+
data = decoded_data
|
|
112
|
+
|
|
113
|
+
for transformer in TRANSFORMERS:
|
|
114
|
+
if transformer.is_text_transformer:
|
|
115
|
+
data = transformer.transform(file, data, transformations, other)
|
|
116
|
+
|
|
117
|
+
# Should be last transformation
|
|
118
|
+
if transformations.get("encoding"):
|
|
119
|
+
try:
|
|
120
|
+
byte_data = data if isinstance(data, bytes) else data.encode("utf-8")
|
|
121
|
+
return encode_file(
|
|
122
|
+
byte_data, transformations["encoding"], mime_type, name
|
|
123
|
+
)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
log.error("Failed to encode to base64: %s", e)
|
|
126
|
+
return file
|
|
127
|
+
|
|
128
|
+
if not isinstance(data, str):
|
|
129
|
+
data = json.dumps(data)
|
|
130
|
+
|
|
131
|
+
return data
|