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,638 @@
1
+ """Grab-bag of common utility functions."""
2
+
3
+ import re
4
+ import yaml
5
+ import xml.etree.ElementTree as ET
6
+ from solace_ai_connector.common.log import log
7
+
8
+ from ..services.file_service import FileService
9
+
10
+
11
+ def escape_special_characters(xml_string):
12
+ """
13
+ Escapes special characters in an XML string.
14
+ """
15
+ # Escape & if it's not part of an entity like &
16
+ xml_string = re.sub(r"&(?!amp;|lt;|gt;|apos;|quot;)", "&", xml_string)
17
+ return xml_string
18
+
19
+
20
+ def extract_and_ignore_tag_content(xml_string, tag):
21
+ """
22
+ Extracts a tag and ignores its content.
23
+ """
24
+ # Find the first <data> tag and the last </data> tag
25
+ start_index = xml_string.find(f"<{tag}")
26
+ end_index = xml_string.rfind(f"</{tag}>")
27
+
28
+ if start_index == -1 or end_index == -1:
29
+ # If no <data> or </data> is found, return the original string and empty data content
30
+ return xml_string, {}
31
+
32
+ # Extract attributes of the first <data> tag
33
+ open_tag_end = xml_string.find(">", start_index) + 1
34
+ data_open_tag = xml_string[start_index:open_tag_end]
35
+
36
+ # Extract the content of the first <data> tag
37
+ open_tag_xml_string = data_open_tag + f"</{tag}>"
38
+ data_attributes = xml_to_dict(open_tag_xml_string, None)[tag]
39
+
40
+ # Extract content between the first <data> and the last </data>
41
+ data_content = xml_string[open_tag_end:end_index].strip()
42
+
43
+ # Remove the <data>...</data> section from the XML string
44
+ xml_string = xml_string[:start_index] + xml_string[end_index + len(f"</{tag}>") :]
45
+
46
+ data_attributes[tag] = data_content
47
+
48
+ return xml_string, data_attributes
49
+
50
+
51
+ def xml_to_dict(xml_string, ignore_content_tags=[], strip_uniquifier_tag_prefix=False):
52
+ """
53
+ Converts an XML string to a dictionary.
54
+
55
+ Parameters:
56
+ - xml_string (str): The XML string.
57
+ - ignore_content_tags (list): A list of tags whose content should be ignored (content is returned as a string).
58
+ """
59
+ # Extract the <data> block, its content, and its attributes
60
+ ignore_contents = {}
61
+ if ignore_content_tags and len(ignore_content_tags) > 0:
62
+ for escape_tag in ignore_content_tags:
63
+ xml_string, data_content = extract_and_ignore_tag_content(
64
+ xml_string, escape_tag
65
+ )
66
+ ignore_contents[escape_tag] = data_content
67
+
68
+ # Escape special characters in the XML string
69
+ xml_string = escape_special_characters(xml_string)
70
+
71
+ def parse_element(element):
72
+ parsed_dict = {}
73
+
74
+ # Get the tag of the element
75
+ element_tag = element.tag
76
+ if strip_uniquifier_tag_prefix:
77
+ element_tag = element_tag.split("_", 1)[-1]
78
+
79
+ # Add element attributes
80
+ if element.attrib:
81
+ parsed_dict.update(element.attrib)
82
+
83
+ # If the element has children, recursively parse them
84
+ if list(element):
85
+ for child in element:
86
+ tag = child.tag
87
+ if strip_uniquifier_tag_prefix:
88
+ tag = tag.split("_", 1)[-1]
89
+ child_dict = parse_element(child)
90
+ if tag in parsed_dict:
91
+ # If tag already exists, convert it into a list of values
92
+ if not isinstance(parsed_dict[tag], list):
93
+ parsed_dict[tag] = [parsed_dict[tag]]
94
+ parsed_dict[tag].append(child_dict[tag])
95
+ else:
96
+ parsed_dict[tag] = child_dict[tag]
97
+
98
+ # If the element contains text, store it
99
+ if element.text and element.text.strip():
100
+ parsed_dict[element_tag] = element.text.strip()
101
+
102
+ return {element_tag: parsed_dict}
103
+
104
+ # Parse the remaining XML string
105
+ root = ET.fromstring(xml_string)
106
+
107
+ # Convert the root element to dictionary
108
+ parsed_dict = parse_element(root)
109
+
110
+ # Add the ignored <data> content and attributes back into the dictionary
111
+ if ignore_contents:
112
+ parsed_dict.update(ignore_contents)
113
+
114
+ return parsed_dict
115
+
116
+
117
+ def parse_yaml_response(response: str) -> dict:
118
+ """Parse a YAML response that came back from an LLM."""
119
+ # Need to deal with the possibility of a None response or the
120
+ # yaml is surrounded by ```yaml...```
121
+ if not response:
122
+ return {}
123
+ if "<response" in response:
124
+ tag_name = re.search(r"<response[^>]*>", response).group(0)
125
+ response = response.split(tag_name, 1)[1]
126
+ response = response.rsplit(f"</{tag_name[1:]}", 1)[0]
127
+ if "```yaml" in response:
128
+ response = response.split("```yaml", 1)[1]
129
+ response = response.rsplit("```", 1)[0]
130
+ return yaml.safe_load(response)
131
+
132
+
133
+ def split_text(text, max_len=2000):
134
+ pattern = r".{1,%d}(?:\n|\.|\s|$)" % max_len
135
+ return re.findall(pattern, text, re.DOTALL)
136
+
137
+
138
+ def parse_file_content(file_xml: str) -> dict:
139
+ """
140
+ Parse the xml tags in the content and return a dictionary of the content.
141
+ """
142
+ ignore_content_tags = ["data"]
143
+ file_dict = xml_to_dict(file_xml, ignore_content_tags)
144
+ dict_keys = list(file_dict.keys())
145
+ top_key = [key for key in dict_keys if key not in ignore_content_tags][0]
146
+
147
+ return {
148
+ "data": file_dict.get("data", {}).get("data", ""),
149
+ "url": file_dict.get(top_key, {}).get("url", {}).get("url", ""),
150
+ "mime_type": file_dict.get(top_key, {}).get("mime_type", ""),
151
+ "name": file_dict.get(top_key, {}).get("name", ""),
152
+ }
153
+
154
+
155
+ def parse_llm_output(llm_output: str) -> dict:
156
+ # The response has <<< and >>> around the parameter string values
157
+ # We want to replace these strings with placeholders that we will
158
+ # replace with the actual values later. This removes long strings from the
159
+ # yaml that might cause parsing issues.
160
+ # We need to save all the strings that we replace so that we can put them back
161
+ # after we parse the yaml. Use a sequence number to create a unique placeholder
162
+ # for each string.
163
+ string_placeholders = {}
164
+ string_count = 0
165
+ sanity = 100
166
+ while "<<<" in llm_output and sanity > 0:
167
+ sanity -= 1
168
+ start = llm_output.index("<<<")
169
+ if not ">>>" in llm_output:
170
+ # set the end to the end of the string
171
+ end = len(llm_output)
172
+ else:
173
+ end = llm_output.index(">>>")
174
+ string = llm_output[start + 3 : end]
175
+ llm_output = (
176
+ llm_output[:start] + f"___STRING{string_count}___" + llm_output[end + 3 :]
177
+ )
178
+ # Also replace an newline characters in the string with a real newline
179
+ string = string.replace("\\n", "\n")
180
+ string_placeholders[f"___STRING{string_count}___"] = string
181
+ string_count += 1
182
+
183
+ # Sometimes the response comes back surrounded by <response-schema> tags
184
+ if "<response-schema>" in llm_output:
185
+ llm_output = llm_output.split("<response-schema>", 1)[1]
186
+ llm_output = llm_output.rsplit("</response-schema>", 1)[0]
187
+
188
+ if "<response>" in llm_output:
189
+ llm_output = llm_output.split("<response>", 1)[1]
190
+ llm_output = llm_output.rsplit("</response>", 1)[0]
191
+
192
+ if "<response-yaml>" in llm_output:
193
+ llm_output = llm_output.split("<response-yaml>", 1)[1]
194
+ llm_output = llm_output.rsplit("</response-yaml>", 1)[0]
195
+
196
+ # The llm_output should be yaml - it might be contained in ```yaml``` tags
197
+ if "```yaml" in llm_output:
198
+ # Get the text between the ```yaml and the final ```
199
+ # Note that there might be nesting of ``` tags, so don't
200
+ # just go to the next ```, but instead find the last one
201
+ llm_output = llm_output.split("```yaml", 1)[1]
202
+ llm_output = llm_output.rsplit("```", 1)[0]
203
+
204
+ obj = None
205
+ try:
206
+ obj = yaml.safe_load(llm_output)
207
+ except Exception:
208
+ # Remove the last line and try again
209
+ llm_output = llm_output.split("\n")[:-1]
210
+ try:
211
+ obj = yaml.safe_load("\n".join(llm_output))
212
+ except Exception:
213
+ return None
214
+
215
+ def replace_placeholder(value):
216
+ # Find all the placeholders in the string and replace them
217
+ def replace(match):
218
+ placeholder = match.group(0)
219
+ if placeholder in string_placeholders:
220
+ return string_placeholders[placeholder]
221
+ return match.group(0)
222
+
223
+ return re.sub(r"___STRING\d+___", replace, value)
224
+
225
+ # Replace the string placeholders in obj with the actual strings
226
+ def replace_strings(obj):
227
+ if isinstance(obj, dict):
228
+ for key, value in obj.items():
229
+ if isinstance(value, str):
230
+ obj[key] = replace_placeholder(value)
231
+ else:
232
+ replace_strings(value)
233
+ elif isinstance(obj, list):
234
+ for i, value in enumerate(obj):
235
+ if isinstance(value, str):
236
+ obj[i] = replace_placeholder(value)
237
+ else:
238
+ replace_strings(value)
239
+
240
+ replace_strings(obj)
241
+ return obj
242
+
243
+
244
+ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
245
+ tp = tag_prefix
246
+ parsed_data = {
247
+ "actions": [],
248
+ "current_subject_starting_id": None,
249
+ "errors": [],
250
+ "reasoning": None,
251
+ "content": [],
252
+ "status_updates": [],
253
+ "send_last_status_update": False,
254
+ }
255
+
256
+ if not response:
257
+ return parsed_data
258
+
259
+ if not tp:
260
+ # Learn the tag prefix from the response
261
+ match = re.search(r"<(t[\d]+_)", response)
262
+ if match:
263
+ tp = match.group(1)
264
+ else:
265
+ tp = ""
266
+
267
+ if not last_chunk:
268
+ response = remove_incomplete_tags_at_end(response)
269
+
270
+ # Parse out the <reasoning> tag - they are always first, so just do a
271
+ # simple search and remove them from the response
272
+ reasoning_match = re.search(
273
+ "<" + tp + r"reasoning>(.*?)</" + tp + "reasoning>", response, re.DOTALL
274
+ )
275
+ if reasoning_match:
276
+ parsed_data["reasoning"] = reasoning_match.group(1).strip()
277
+ response = re.sub(
278
+ "<" + tp + r"reasoning>.*?</" + tp + "reasoning>",
279
+ "",
280
+ response,
281
+ flags=re.DOTALL,
282
+ )
283
+ elif f"<{tp}reasoning>" in response:
284
+ parsed_data["errors"].append("Incomplete <reasoning> tag")
285
+ return parsed_data
286
+ else:
287
+ parsed_data["reasoning"] = None
288
+
289
+ # Get all the <{tp}status_update> tags
290
+ status_updates = []
291
+ status_matches = re.finditer(
292
+ f"<{tp}status_update>(.*?)</{tp}status_update>", response, re.DOTALL
293
+ )
294
+ for match in status_matches:
295
+ status_updates.append(match.group(1).strip())
296
+ response = response.replace(match.group(0), "", 1)
297
+
298
+ # Remove all complete status_update tags
299
+ response = re.sub(
300
+ f"<{tp}status_update>.*?</{tp}status_update>" + r"\s*",
301
+ "",
302
+ response,
303
+ flags=re.DOTALL,
304
+ )
305
+
306
+ # Remove any incomplete status_update tags
307
+ response = re.sub(f"<{tp}status_update>.*?($|<)", "", response, flags=re.DOTALL)
308
+
309
+ if status_updates:
310
+ parsed_data["status_updates"] = status_updates
311
+
312
+ # Parse out <file> tags and other elements
313
+ in_file = False
314
+ current_file = {}
315
+ file_content = []
316
+ current_action = {}
317
+ in_invoke_action = False
318
+ current_param_name = None
319
+ current_param_value = []
320
+ open_tags = []
321
+ current_text = []
322
+
323
+ for line in response.split("\n"):
324
+
325
+ if f"<{tp}current_subject" in line:
326
+ id_match = re.search(r'starting_id\s*=\s*[\'"](\w+)[\'"]\s*\/?>', line)
327
+ if id_match:
328
+ parsed_data["current_subject_starting_id"] = id_match.group(1)
329
+
330
+ elif f"<{tp}file" in line:
331
+ in_file = True
332
+ # We can't guarantee the order of the attributes, so we need to parse them separately
333
+ name_match = re.search(r'name\s*=\s*[\'"]([^\'"]+)[\'"]', line)
334
+ mime_type_match = re.search(r'mime_type\s*=\s*[\'"]([^\'"]+)[\'"]', line)
335
+ current_file = {
336
+ "name": name_match.group(1) if name_match else "",
337
+ "mime_type": mime_type_match.group(1) if mime_type_match else "",
338
+ "url": "",
339
+ "data": "",
340
+ }
341
+ file_start_index = line.index(f"<{tp}file")
342
+ file_line = line[file_start_index:]
343
+ if f"</{tp}file>" in line:
344
+ file_end_index = line.index(f"</{tp}file>")
345
+ file_line = line[: file_end_index + len(f"</{tp}file>")]
346
+ file_content = [file_line]
347
+ current_file = parse_file_content("\n".join(file_content))
348
+ add_content_entry(parsed_data["content"], "file", current_file)
349
+ in_file = False
350
+ current_file = {}
351
+ file_content = []
352
+ else:
353
+ file_content.append(file_line)
354
+
355
+ elif f"</{tp}file>" in line:
356
+ if in_file:
357
+ if current_text:
358
+ add_content_entry(
359
+ parsed_data["content"], "text", current_text, add_newline=True
360
+ )
361
+ current_text = []
362
+ file_end_index = line.index(f"</{tp}file>")
363
+ file_line = line[: file_end_index + len(f"</{tp}file>")]
364
+ file_content.append(file_line)
365
+ current_file = parse_file_content("\n".join(file_content))
366
+ add_content_entry(parsed_data["content"], "file", current_file)
367
+ in_file = False
368
+ current_file = {}
369
+ file_content = []
370
+ else:
371
+ parsed_data["errors"].append("Unmatched </file> tag")
372
+ elif in_file:
373
+ file_content.append(line)
374
+
375
+ elif f"<{tp}invoke_action" in line:
376
+ if in_invoke_action:
377
+ parsed_data["errors"].append("Nested <invoke_action> tags")
378
+ in_invoke_action = True
379
+ open_tags.append("invoke_action")
380
+ current_action = {
381
+ "agent": None,
382
+ "action": None,
383
+ "parameters": {},
384
+ }
385
+
386
+ for attr in ["agent", "action"]:
387
+ attr_match = re.search(rf'{attr}\s*=\s*[\'"]([_\-\.\w]+)[\'"]', line)
388
+ if attr_match:
389
+ current_action[attr] = attr_match.group(1)
390
+
391
+ elif f"</{tp}invoke_action>" in line:
392
+ if not in_invoke_action:
393
+ parsed_data["errors"].append("Unmatched </invoke_action> tag")
394
+ else:
395
+ in_invoke_action = False
396
+ if "invoke_action" in open_tags:
397
+ open_tags.remove("invoke_action")
398
+ if current_param_name:
399
+ current_action["parameters"][current_param_name] = "\n".join(
400
+ current_param_value
401
+ ).strip()
402
+ parsed_data["actions"].append(current_action)
403
+ current_action = {}
404
+ current_param_name = None
405
+ current_param_value = []
406
+
407
+ elif in_invoke_action and f"<{tp}parameter" in line:
408
+ if current_param_name:
409
+ current_action["parameters"][current_param_name] = "\n".join(
410
+ current_param_value
411
+ ).strip()
412
+ current_param_value = []
413
+
414
+ param_name_match = re.search(r'name\s*=\s*[\'"](\w+)[\'"]', line)
415
+ if param_name_match:
416
+ current_param_name = param_name_match.group(1)
417
+ open_tags.append("parameter")
418
+
419
+ # Handle content on the same line as opening tag
420
+ content_after_open = re.search(
421
+ r">(.*?)(?:</" + tp + "parameter>|$)", line
422
+ )
423
+ if content_after_open:
424
+ initial_content = content_after_open.group(1).strip()
425
+ if initial_content:
426
+ current_param_value.append(initial_content)
427
+
428
+ # Check if parameter closes on same line
429
+ if f"</{tp}parameter>" in line:
430
+ current_action["parameters"][current_param_name] = "\n".join(
431
+ current_param_value
432
+ ).strip()
433
+ current_param_name = None
434
+ current_param_value = []
435
+ if "parameter" in open_tags:
436
+ open_tags.remove("parameter")
437
+ elif line.endswith("/>"):
438
+ current_action["parameters"][current_param_name] = ""
439
+ current_param_name = None
440
+ if "parameter" in open_tags:
441
+ open_tags.remove("parameter")
442
+ elif not ">" in line:
443
+ parsed_data["errors"].append("Incomplete <parameter> tag")
444
+
445
+ elif in_invoke_action and current_param_name:
446
+ if f"</{tp}parameter>" in line:
447
+ # Handle content before closing tag on final line
448
+ content_before_close = re.sub(f"</{tp}parameter>.*", "", line)
449
+ if content_before_close.strip():
450
+ current_param_value.append(content_before_close.strip())
451
+ current_action["parameters"][current_param_name] = "\n".join(
452
+ current_param_value
453
+ ).strip()
454
+ current_param_name = None
455
+ current_param_value = []
456
+ if "parameter" in open_tags:
457
+ open_tags.remove("parameter")
458
+ else:
459
+ current_param_value.append(line.strip())
460
+
461
+ else:
462
+ current_text.append(line)
463
+
464
+ if open_tags:
465
+ parsed_data["errors"].append(f"Unclosed tags: {', '.join(open_tags)}")
466
+
467
+ if in_file:
468
+ content = "\n".join(file_content)
469
+ # Add a status update for this
470
+ parsed_data["status_updates"].append(
471
+ f"File {current_file['name']} loading ({len(content)} characters)..."
472
+ )
473
+ parsed_data["send_last_status_update"] = True
474
+ parsed_data["errors"].append("Unclosed <file> tag")
475
+
476
+ if len(current_text) > 0:
477
+ add_content_entry(parsed_data["content"], "text", current_text)
478
+
479
+ return parsed_data
480
+
481
+
482
+ def remove_incomplete_tags_at_end(text):
483
+ """If the end of the text is in the middle of a <tag> or </tag> then remove it."""
484
+ # remove any open tags at the end
485
+ last_open_tag = text.rfind("<")
486
+ last_open_end_tag = text.rfind("</")
487
+ last_close_tag = text.rfind(">")
488
+ if last_close_tag >= last_open_tag and last_close_tag >= last_open_end_tag:
489
+ return text
490
+ # Knock off the last line
491
+ return text.rsplit("\n", 1)[0]
492
+
493
+
494
+ def add_content_entry(content_list, entry_type, body, add_newline=False):
495
+ if body:
496
+ if entry_type == "text":
497
+ body = clean_text(body)
498
+ if add_newline:
499
+ body += "\n"
500
+ content_list.append({"type": entry_type, "body": body})
501
+
502
+
503
+ def match_solace_topic_level(pattern: str, topic_level: str) -> bool:
504
+ """Match a single topic level with potential * wildcard prefix matching"""
505
+ if pattern == "*":
506
+ return True
507
+ if pattern.endswith("*"):
508
+ return topic_level.startswith(pattern[:-1])
509
+ return pattern == topic_level
510
+
511
+
512
+ def match_solace_topic(subscription: str, topic: str) -> bool:
513
+ """
514
+ Match a Solace topic against a subscription pattern.
515
+ Handles * for prefix matching within a level and > for matching one or more levels.
516
+ Case sensitive matching.
517
+
518
+ Examples:
519
+ match_solace_topic("a/b*/c", "a/bob/c") -> True
520
+ match_solace_topic("a/*/c", "a/b/c") -> True
521
+ match_solace_topic("a/>", "a/b") -> True
522
+ match_solace_topic("a/>", "a/b/c") -> True
523
+ match_solace_topic("a/>", "a") -> False
524
+ """
525
+ if not subscription or not topic:
526
+ return False
527
+
528
+ sub_levels = subscription.split("/")
529
+ topic_levels = topic.split("/")
530
+
531
+ # Handle > wildcard
532
+ if sub_levels[-1] == ">":
533
+ # > must match at least one level
534
+ if len(topic_levels) <= len(sub_levels) - 1:
535
+ return False
536
+ # Match all levels before the >
537
+ return all(
538
+ match_solace_topic_level(sub_levels[i], topic_levels[i])
539
+ for i in range(len(sub_levels) - 1)
540
+ )
541
+
542
+ # Without >, levels must match exactly
543
+ if len(sub_levels) != len(topic_levels):
544
+ return False
545
+
546
+ # Match each level
547
+ return all(
548
+ match_solace_topic_level(sub_levels[i], topic_levels[i])
549
+ for i in range(len(sub_levels))
550
+ )
551
+
552
+
553
+ def clean_text(text_array):
554
+ # Any leading blank lines are removed
555
+ while text_array and not text_array[0]:
556
+ text_array.pop(0)
557
+
558
+ # Look for multiple blank lines in a row and collapse them into a single blank line
559
+ i = 0
560
+ while i < len(text_array) - 1:
561
+ if not text_array[i] and not text_array[i + 1]:
562
+ text_array.pop(i)
563
+ else:
564
+ i += 1
565
+
566
+ return "\n".join(text_array)
567
+
568
+
569
+ def files_to_block_text(files):
570
+ blocks = []
571
+ text = ""
572
+ if files:
573
+ for file in files:
574
+ try:
575
+ block = FileService.get_file_block_by_metadata(file)
576
+ blocks.append(block)
577
+ except Exception as e:
578
+ log.warning("Could not get file block: %s", e)
579
+ if blocks:
580
+ text += (
581
+ "\n\nAttached files:\n\n" if len(blocks) > 1 else "\n\nAttached file:\n\n"
582
+ )
583
+ text += "\n\n".join(blocks)
584
+ return text
585
+
586
+
587
+ def remove_config_parameter(info, parameter_name):
588
+ """
589
+ Remove a parameter from the config of a component.
590
+ """
591
+ if "config_parameters" in info:
592
+ index = info["config_parameters"].index(
593
+ next(
594
+ (
595
+ item
596
+ for item in info["config_parameters"]
597
+ if item.get("name") == parameter_name
598
+ ),
599
+ None,
600
+ )
601
+ )
602
+
603
+ # Remove the parameter from the list
604
+ if index is not None:
605
+ info["config_parameters"].pop(index)
606
+
607
+ return info
608
+
609
+
610
+ def format_agent_response(actions):
611
+ """Format the action response for the AI"""
612
+ ai_response = (
613
+ "Results for the requested actions. NOTE: these are generated internally in the "
614
+ "system. A user was not involved, so there is no need to respond to them or thank them.\n"
615
+ )
616
+ found_ai_response = False
617
+ files = []
618
+ for action in actions:
619
+ found_ai_response = True
620
+ ai_response += f"\n {action.get('action_idx')}: {action.get('agent_name')}.{action.get('action_name')}:\n"
621
+ params = action.get("action_params", {})
622
+ response_files = action.get("response", {}).get("files", [])
623
+ for param, value in params.items():
624
+ if isinstance(value, str):
625
+ ai_response += f" {param}: {value[:100]}\n"
626
+ else:
627
+ ai_response += f" {param}: {value}\n"
628
+ ai_response += " Response:\n"
629
+ ai_response += f" <<<{action.get('response', {'text':'<No response>'}).get('text')}>>>\n"
630
+ if response_files:
631
+ ai_response += " Files:\n"
632
+ for file in response_files:
633
+ ai_response += f" {file.get('url')}\n"
634
+ files.extend(action.get("response", {}).get("files", []))
635
+
636
+ if not found_ai_response:
637
+ return None
638
+ return ai_response, files