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,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