solace-agent-mesh 0.2.0__py3-none-any.whl → 0.2.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 (40) hide show
  1. solace_agent_mesh/agents/global/actions/plotly_graph.py +48 -22
  2. solace_agent_mesh/cli/__init__.py +1 -1
  3. solace_agent_mesh/cli/commands/add/copy_from_plugin.py +8 -6
  4. solace_agent_mesh/cli/commands/build.py +15 -0
  5. solace_agent_mesh/cli/commands/init/ai_provider_step.py +45 -28
  6. solace_agent_mesh/cli/commands/init/broker_step.py +1 -4
  7. solace_agent_mesh/cli/commands/init/create_config_file_step.py +8 -0
  8. solace_agent_mesh/cli/commands/init/init.py +50 -37
  9. solace_agent_mesh/cli/commands/plugin/build.py +52 -10
  10. solace_agent_mesh/cli/commands/run.py +2 -2
  11. solace_agent_mesh/cli/main.py +14 -8
  12. solace_agent_mesh/common/prompt_templates.py +1 -3
  13. solace_agent_mesh/common/utils.py +88 -19
  14. solace_agent_mesh/config_portal/__init__.py +0 -0
  15. solace_agent_mesh/config_portal/backend/__init__.py +0 -0
  16. solace_agent_mesh/config_portal/backend/common.py +35 -0
  17. solace_agent_mesh/config_portal/backend/server.py +233 -0
  18. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DRPGOzHj.js +42 -0
  19. solace_agent_mesh/config_portal/frontend/static/client/assets/components-ZIfdTbrV.js +191 -0
  20. solace_agent_mesh/config_portal/frontend/static/client/assets/entry.client-DX1misIU.js +19 -0
  21. solace_agent_mesh/config_portal/frontend/static/client/assets/index-BJHAE5s4.js +17 -0
  22. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-8147e469.js +1 -0
  23. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DgMDqKDc.js +10 -0
  24. solace_agent_mesh/config_portal/frontend/static/client/assets/root-hhS5izs8.css +1 -0
  25. solace_agent_mesh/config_portal/frontend/static/client/favicon.ico +0 -0
  26. solace_agent_mesh/config_portal/frontend/static/client/index.html +7 -0
  27. solace_agent_mesh/configs/orchestrator.yaml +1 -1
  28. solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +4 -0
  29. solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +28 -15
  30. solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +19 -5
  31. solace_agent_mesh/orchestrator/orchestrator_main.py +11 -5
  32. solace_agent_mesh/orchestrator/orchestrator_prompt.py +78 -74
  33. solace_agent_mesh/templates/solace-agent-mesh-default.yaml +9 -0
  34. solace_agent_mesh-0.2.1.dist-info/METADATA +172 -0
  35. {solace_agent_mesh-0.2.0.dist-info → solace_agent_mesh-0.2.1.dist-info}/RECORD +38 -26
  36. solace_agent_mesh/common/prompt_templates_unused_delete.py +0 -161
  37. solace_agent_mesh-0.2.0.dist-info/METADATA +0 -209
  38. {solace_agent_mesh-0.2.0.dist-info → solace_agent_mesh-0.2.1.dist-info}/WHEEL +0 -0
  39. {solace_agent_mesh-0.2.0.dist-info → solace_agent_mesh-0.2.1.dist-info}/entry_points.txt +0 -0
  40. {solace_agent_mesh-0.2.0.dist-info → solace_agent_mesh-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -140,6 +140,12 @@ def parse_file_content(file_xml: str) -> dict:
140
140
  Parse the xml tags in the content and return a dictionary of the content.
141
141
  """
142
142
  try:
143
+ # It is possible that the content of the file mistakenly has t###_ prefixes on the
144
+ # tags, so we need to strip them off. This is a bit of a hack to work around LLM errors
145
+
146
+ file_xml = re.sub(r"<t\d+_", "<", file_xml)
147
+ file_xml = re.sub(r"</t\d+_", "</", file_xml)
148
+
143
149
  ignore_content_tags = ["data"]
144
150
  file_dict = xml_to_dict(file_xml, ignore_content_tags)
145
151
  dict_keys = list(file_dict.keys())
@@ -165,6 +171,7 @@ def parse_llm_output(llm_output: str) -> dict:
165
171
  # We need to save all the strings that we replace so that we can put them back
166
172
  # after we parse the yaml. Use a sequence number to create a unique placeholder
167
173
  # for each string.
174
+
168
175
  string_placeholders = {}
169
176
  string_count = 0
170
177
  sanity = 100
@@ -246,7 +253,9 @@ def parse_llm_output(llm_output: str) -> dict:
246
253
  return obj
247
254
 
248
255
 
249
- def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
256
+ def parse_orchestrator_response(
257
+ response, last_chunk=False, tag_prefix="", check_reasoning=True
258
+ ):
250
259
  tp = tag_prefix
251
260
  parsed_data = {
252
261
  "actions": [],
@@ -324,6 +333,7 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
324
333
  current_param_value = []
325
334
  open_tags = []
326
335
  current_text = []
336
+ seen_invoke_action = False
327
337
 
328
338
  for line in response.split("\n"):
329
339
 
@@ -350,7 +360,8 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
350
360
  file_line = line[: file_end_index + len(f"</{tp}file>")]
351
361
  file_content = [file_line]
352
362
  current_file = parse_file_content("\n".join(file_content))
353
- add_content_entry(parsed_data["content"], "file", current_file)
363
+ if not seen_invoke_action:
364
+ add_content_entry(parsed_data["content"], "file", current_file)
354
365
  in_file = False
355
366
  current_file = {}
356
367
  file_content = []
@@ -368,7 +379,8 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
368
379
  file_line = line[: file_end_index + len(f"</{tp}file>")]
369
380
  file_content.append(file_line)
370
381
  current_file = parse_file_content("\n".join(file_content))
371
- add_content_entry(parsed_data["content"], "file", current_file)
382
+ if not seen_invoke_action:
383
+ add_content_entry(parsed_data["content"], "file", current_file)
372
384
  in_file = False
373
385
  current_file = {}
374
386
  file_content = []
@@ -381,6 +393,7 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
381
393
  if in_invoke_action:
382
394
  parsed_data["errors"].append("Nested <invoke_action> tags")
383
395
  in_invoke_action = True
396
+ seen_invoke_action = True
384
397
  open_tags.append("invoke_action")
385
398
  current_action = {
386
399
  "agent": None,
@@ -403,7 +416,7 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
403
416
  if current_param_name:
404
417
  current_action["parameters"][current_param_name] = "\n".join(
405
418
  current_param_value
406
- ).strip()
419
+ )
407
420
  parsed_data["actions"].append(current_action)
408
421
  current_action = {}
409
422
  current_param_name = None
@@ -411,9 +424,10 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
411
424
 
412
425
  elif in_invoke_action and f"<{tp}parameter" in line:
413
426
  if current_param_name:
414
- current_action["parameters"][current_param_name] = "\n".join(
415
- current_param_value
416
- ).strip()
427
+ param_value = "\n".join(current_param_value)
428
+ current_action["parameters"][current_param_name] = (
429
+ clean_parameter_value(param_value)
430
+ )
417
431
  current_param_value = []
418
432
 
419
433
  param_name_match = re.search(r'name\s*=\s*[\'"](\w+)[\'"]', line)
@@ -426,17 +440,19 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
426
440
  r">(.*?)(?:</" + tp + "parameter>|$)", line
427
441
  )
428
442
  if content_after_open:
429
- initial_content = content_after_open.group(1).strip()
443
+ initial_content = content_after_open.group(1)
430
444
  if initial_content:
431
445
  current_param_value.append(initial_content)
432
446
 
433
447
  # Check if parameter closes on same line
434
448
  if f"</{tp}parameter>" in line:
435
- current_action["parameters"][current_param_name] = "\n".join(
436
- current_param_value
437
- ).strip()
449
+ param_value = "\n".join(current_param_value)
450
+ current_action["parameters"][current_param_name] = (
451
+ clean_parameter_value(param_value)
452
+ )
438
453
  current_param_name = None
439
454
  current_param_value = []
455
+
440
456
  if "parameter" in open_tags:
441
457
  open_tags.remove("parameter")
442
458
  elif line.endswith("/>"):
@@ -451,25 +467,30 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
451
467
  if f"</{tp}parameter>" in line:
452
468
  # Handle content before closing tag on final line
453
469
  content_before_close = re.sub(f"</{tp}parameter>.*", "", line)
454
- if content_before_close.strip():
455
- current_param_value.append(content_before_close.strip())
456
- current_action["parameters"][current_param_name] = "\n".join(
457
- current_param_value
458
- ).strip()
470
+ if content_before_close:
471
+ current_param_value.append(content_before_close)
472
+ param_value = "\n".join(current_param_value)
473
+ current_action["parameters"][current_param_name] = (
474
+ clean_parameter_value(param_value)
475
+ )
459
476
  current_param_name = None
460
477
  current_param_value = []
461
478
  if "parameter" in open_tags:
462
479
  open_tags.remove("parameter")
463
480
  else:
464
- current_param_value.append(line.strip())
481
+ current_param_value.append(line)
465
482
 
466
483
  else:
467
- current_text.append(line)
484
+ # NOTE that we are intentionally ignoring all output text that occurs
485
+ # after any <invoke_action> tag. It has been told to never do this and
486
+ # if it does, then there is a good chance it is hallucinating responses
487
+ if not seen_invoke_action:
488
+ current_text.append(line)
468
489
 
469
490
  if open_tags:
470
491
  parsed_data["errors"].append(f"Unclosed tags: {', '.join(open_tags)}")
471
492
 
472
- if in_file:
493
+ if in_file and not seen_invoke_action:
473
494
  content = "\n".join(file_content)
474
495
  # Add a status update for this
475
496
  parsed_data["status_updates"].append(
@@ -481,9 +502,28 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
481
502
  if len(current_text) > 0:
482
503
  add_content_entry(parsed_data["content"], "text", current_text)
483
504
 
505
+ # Final check - if there is no reasoning, then the LLM is not complying with the
506
+ # request and we should return an error
507
+ if check_reasoning and not parsed_data["reasoning"]:
508
+ parsed_data["errors"].append("No <t###_reasoning> tag found")
509
+ parsed_data["content"] = []
510
+
484
511
  return parsed_data
485
512
 
486
513
 
514
+ def strip_text_after_invoke_action(text):
515
+ """
516
+ Remove any text after the last </invoke_action> tag.
517
+ This is to prevent hallucinations from the LLM.
518
+ """
519
+ # Find the last instance of </t\d+_invoke_action> regexp and remove everything after it.
520
+ matches = list(re.finditer(r"</t\d+_invoke_action>", text))
521
+ if matches:
522
+ last_match_end = matches[-1].end()
523
+ return text[:last_match_end]
524
+ return text
525
+
526
+
487
527
  def remove_incomplete_tags_at_end(text):
488
528
  """If the end of the text is in the middle of a <tag> or </tag> then remove it."""
489
529
  # remove any open tags at the end
@@ -555,6 +595,35 @@ def match_solace_topic(subscription: str, topic: str) -> bool:
555
595
  )
556
596
 
557
597
 
598
+ def clean_parameter_value(param_value):
599
+ """
600
+ Cleans a parameter value by:
601
+ 1. Removing CDATA wrapper if present
602
+ 2. Resolving XML entities like &gt;, &lt;, etc.
603
+
604
+ Parameters:
605
+ - param_value (str): The parameter value that might contain a CDATA wrapper
606
+ and/or XML entities
607
+
608
+ Returns:
609
+ - str: The cleaned parameter value
610
+ """
611
+ if isinstance(param_value, str):
612
+ # Remove CDATA wrapper if present
613
+ cdata_match = re.match(r"\s*<!\[CDATA\[(.*?)\]\]>\s*$", param_value, re.DOTALL)
614
+ if cdata_match:
615
+ param_value = cdata_match.group(1)
616
+
617
+ # Resolve XML entities
618
+ param_value = param_value.replace("&lt;", "<")
619
+ param_value = param_value.replace("&gt;", ">")
620
+ param_value = param_value.replace("&amp;", "&")
621
+ param_value = param_value.replace("&quot;", '"')
622
+ param_value = param_value.replace("&apos;", "'")
623
+
624
+ return param_value
625
+
626
+
558
627
  def clean_text(text_array):
559
628
  # Any leading blank lines are removed
560
629
  while text_array and not text_array[0]:
File without changes
File without changes
@@ -0,0 +1,35 @@
1
+ default_options = {
2
+ "namespace": "",
3
+ "config_dir": "configs",
4
+ "module_dir": "modules",
5
+ "env_file": ".env",
6
+ "build_dir": "build",
7
+ "broker_type": "solace",
8
+ "broker_url": "ws://localhost:8008",
9
+ "broker_vpn": "default",
10
+ "broker_username": "default",
11
+ "broker_password": "default",
12
+ "container_engine": "docker",
13
+ "llm_model_name": "openai/gpt-4o",
14
+ "llm_endpoint_url": "https://api.openai.com/v1",
15
+ "llm_api_key": "",
16
+ "embedding_model_name": "openai/text-embedding-ada-002",
17
+ "embedding_endpoint_url": "https://api.openai.com/v1",
18
+ "embedding_api_key": "",
19
+ "embedding_service_enabled": False,
20
+ "built_in_agent": ["web_request"],
21
+ "file_service_provider": "volume",
22
+ "file_service_config": ["directory=/tmp/solace-agent-mesh"],
23
+ "env_var": [],
24
+ "rest_api_enabled": True,
25
+ "rest_api_server_input_port": "5050",
26
+ "rest_api_server_host": "127.0.0.1",
27
+ "rest_api_server_input_endpoint": "/api/v1/request",
28
+ "rest_api_gateway_name": "rest-api",
29
+ "webui_enabled": True,
30
+ "webui_listen_port": "5001",
31
+ "webui_host": "localhost",
32
+ "dev_mode": True,
33
+ }
34
+
35
+ CONTAINER_RUN_COMMAND = " run -d -p 8080:8080 -p 55554:55555 -p 8008:8008 -p 1883:1883 -p 8000:8000 -p 5672:5672 -p 9000:9000 -p 2222:2222 --shm-size=2g --env username_admin_globalaccesslevel=admin --env username_admin_password=admin --name=solace solace/solace-pubsub-standard"
@@ -0,0 +1,233 @@
1
+ import sys
2
+ from flask import Flask, jsonify, request, send_from_directory, send_file
3
+ from flask_cors import CORS
4
+ import os
5
+ import sys
6
+ from solace_agent_mesh.config_portal.backend.common import default_options, CONTAINER_RUN_COMMAND
7
+ from cli.utils import get_formatted_names
8
+ import shutil
9
+ import litellm
10
+
11
+ litellm.suppress_debug_info = True
12
+
13
+ #disable flask startup banner
14
+ import logging
15
+ log = logging.getLogger('werkzeug')
16
+ log.disabled = True
17
+ cli = sys.modules['flask.cli']
18
+ cli.show_server_banner = lambda *x: None
19
+
20
+ def create_app(shared_config=None):
21
+ """Factory function that creates the Flask application with configuration injected"""
22
+ app = Flask(__name__)
23
+ CORS(app, resources={r"/api/*": {"origins": ["http://localhost:5174", "http://127.0.0.1:5174"]}})
24
+
25
+ EXCLUDE_OPTIONS = ["config_dir", "module_dir", "env_file", "build_dir", "container_engine", "rest_api_enabled",
26
+ "rest_api_server_input_port", "rest_api_server_host", "rest_api_server_input_endpoint",
27
+ "rest_api_gateway_name", "webui_enabled", "webui_listen_port", "webui_host"]
28
+
29
+ @app.route('/api/default_options', methods=['GET'])
30
+ def get_default_options():
31
+ """Endpoint that returns the default options for form initialization"""
32
+ path = request.args.get('path', 'advanced')
33
+
34
+ modified_default_options = default_options.copy()
35
+
36
+ # Base exclusions for all paths
37
+ base_exclude_options = EXCLUDE_OPTIONS.copy()
38
+
39
+ # Additional exclusions for quick path
40
+ quick_path_exclude_options = [
41
+ "namespace", "broker_type", "broker_url", "broker_vpn",
42
+ "broker_username", "broker_password", "container_engine",
43
+ "built_in_agent", "file_service_provider", "file_service_config"
44
+ ]
45
+
46
+ # Apply exclusions based on path
47
+ exclude_options = base_exclude_options.copy()
48
+ if path == 'quick':
49
+ exclude_options.extend(quick_path_exclude_options)
50
+
51
+ # Remove excluded options
52
+ for option in exclude_options:
53
+ modified_default_options.pop(option, None)
54
+
55
+ return jsonify({
56
+ "default_options": modified_default_options,
57
+ "status": "success"
58
+ })
59
+
60
+ @app.route('/api/save_config', methods=['POST'])
61
+ def save_config():
62
+ """
63
+ Endpoint that accepts configuration data from the frontend,
64
+ merges it with default options, and updates the shared configuration.
65
+ """
66
+ try:
67
+ received_data = request.json
68
+ force = received_data.pop('force', False)
69
+
70
+ if not received_data:
71
+ return jsonify({"status": "error", "message": "No data received"}), 400
72
+
73
+ complete_config = default_options.copy()
74
+
75
+ # Update with the received data
76
+ for key, value in received_data.items():
77
+ if key in complete_config or key:
78
+ complete_config[key] = value
79
+
80
+ config_directory = complete_config["config_dir"]
81
+ formatted_name = get_formatted_names(complete_config["rest_api_gateway_name"])
82
+ gateway_directory = os.path.join(
83
+ config_directory, "gateways", formatted_name["SNAKE_CASE_NAME"]
84
+ )
85
+
86
+ #Handle the case where the gateway directory for rest already exists
87
+ if os.path.exists(gateway_directory) and not force:
88
+ return jsonify({"status": "ask_confirmation", "message": f"Gateway directory {gateway_directory} already exists, it will be overwritten."}), 400
89
+ elif os.path.exists(gateway_directory) and force:
90
+ shutil.rmtree(gateway_directory)
91
+
92
+ # Update the shared configuration if it exists
93
+ if shared_config is not None:
94
+ for key, value in complete_config.items():
95
+ shared_config[key] = value
96
+
97
+ return jsonify({
98
+ "status": "success",
99
+ })
100
+
101
+ except Exception as e:
102
+ return jsonify({"status": "error", "message": str(e)}), 500
103
+
104
+ @app.route('/api/test_llm_config', methods=['POST'])
105
+ def test_llm_config():
106
+ """
107
+ Endpoint that tests the LLM configuration given by the users"""
108
+ llm_config = request.json
109
+
110
+ #check for all values
111
+ if not llm_config.get("model") or not llm_config.get("api_key") or not llm_config.get("base_url"):
112
+ return jsonify({"status": "error", "message": "Please provide all the required values"}), 400
113
+
114
+ try:
115
+ response = litellm.completion(
116
+ model=llm_config.get("model"),
117
+ api_key=llm_config.get("api_key"),
118
+ base_url=llm_config.get("base_url"),
119
+ messages=[{"role":"user","content": "Say OK"}]
120
+ )
121
+ message = response.get("choices")[0].get("message")
122
+
123
+ if message is not None:
124
+ return jsonify({"status": "success", "message": message.content}), 200
125
+ else:
126
+ raise ValueError("No response from LLM")
127
+ except Exception:
128
+ return jsonify({"status": "error", "message": "No response from LLM."}), 400
129
+
130
+
131
+
132
+ @app.route('/api/runcontainer', methods=['POST'])
133
+ def runcontainer():
134
+ try:
135
+ data = request.json or {}
136
+
137
+ # Check if the user has podman or docker installed
138
+ has_podman = shutil.which("podman") is not None
139
+ has_docker = shutil.which("docker") is not None
140
+
141
+ if not has_podman and not has_docker:
142
+ return jsonify({
143
+ "status": "error",
144
+ "message": "You need to have either podman or docker installed to use the container broker."
145
+ }), 400
146
+
147
+ # Determine which container engine to use
148
+ container_engine = data.get('container_engine')
149
+
150
+ # If both are available, default to podman
151
+ if not container_engine and has_podman and has_docker:
152
+ container_engine = "podman"
153
+ # If only one is available, use that one
154
+ elif not container_engine:
155
+ container_engine = "podman" if has_podman else "docker"
156
+
157
+ # Validate the container engine selection
158
+ if container_engine not in ["podman", "docker"]:
159
+ return jsonify({
160
+ "status": "error",
161
+ "message": f"Invalid container engine: {container_engine}. Must be 'podman' or 'docker'."
162
+ }), 400
163
+
164
+ if container_engine == "podman" and not has_podman:
165
+ return jsonify({
166
+ "status": "error",
167
+ "message": "Podman was selected but is not installed on this system."
168
+ }), 400
169
+
170
+ if container_engine == "docker" and not has_docker:
171
+ return jsonify({
172
+ "status": "error",
173
+ "message": "Docker was selected but is not installed on this system."
174
+ }), 400
175
+
176
+ # Run command for the container start
177
+ command = container_engine + CONTAINER_RUN_COMMAND
178
+
179
+ # Execute the command and capture exit code
180
+ response_status = os.system(command)
181
+
182
+ if response_status != 0:
183
+ return jsonify({
184
+ "status": "error",
185
+ "message": f"Failed to start container. Exit code: {response_status}"
186
+ }), 500
187
+
188
+ return jsonify({
189
+ "status": "success",
190
+ "message": f"Successfully started Solace PubSub+ broker container using {container_engine}",
191
+ "container_engine": container_engine
192
+ })
193
+
194
+ except Exception as e:
195
+ return jsonify({"status": "error", "message": str(e)}), 500
196
+
197
+
198
+ @app.route('/api/shutdown', methods=['POST'])
199
+ def shutdown():
200
+ """Kills this Flask process immediately"""
201
+ response = jsonify({"message": "Server shutting down...", "status": "success"})
202
+ os._exit(0)
203
+ return response
204
+
205
+
206
+
207
+ @app.route("/assets/<path:path>")
208
+ def serve_assets(path):
209
+ return send_from_directory("../frontend/static/client/assets", path)
210
+
211
+ @app.route("/static/client/<path:path>")
212
+ def serve_client_files(path):
213
+ return send_from_directory("../frontend/static/client", path)
214
+
215
+ @app.route("/", defaults={"path": ""})
216
+ def serve(path):
217
+ return send_file("../frontend/static/client/index.html")
218
+
219
+ @app.route("/<path:path>")
220
+ def serve_files(path):
221
+ if path.endswith(('.png', '.jpg', '.jpeg', '.gif', '.ico')):
222
+ return send_from_directory("../frontend/static/client", path)
223
+ return send_file("../frontend/static/client/index.html")
224
+
225
+ return app
226
+
227
+ def run_flask(host="127.0.0.1", port=5002, shared_config=None):
228
+ """
229
+ Run the Flask development server with dependency-injected shared configuration.
230
+ """
231
+ app = create_app(shared_config)
232
+ app.run(host=host, port=port, debug=False, use_reloader=False)
233
+