solace-agent-mesh 0.1.3__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 (73) hide show
  1. solace_agent_mesh/agents/global/actions/plantuml_diagram.py +9 -2
  2. solace_agent_mesh/agents/global/actions/plotly_graph.py +70 -46
  3. solace_agent_mesh/agents/web_request/actions/do_web_request.py +34 -33
  4. solace_agent_mesh/cli/__init__.py +1 -1
  5. solace_agent_mesh/cli/commands/add/copy_from_plugin.py +8 -6
  6. solace_agent_mesh/cli/commands/add/gateway.py +162 -9
  7. solace_agent_mesh/cli/commands/build.py +15 -1
  8. solace_agent_mesh/cli/commands/init/ai_provider_step.py +45 -28
  9. solace_agent_mesh/cli/commands/init/broker_step.py +1 -4
  10. solace_agent_mesh/cli/commands/init/create_config_file_step.py +8 -0
  11. solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +52 -1
  12. solace_agent_mesh/cli/commands/init/init.py +50 -37
  13. solace_agent_mesh/cli/commands/plugin/build.py +60 -9
  14. solace_agent_mesh/cli/commands/run.py +2 -2
  15. solace_agent_mesh/cli/config.py +4 -0
  16. solace_agent_mesh/cli/main.py +14 -8
  17. solace_agent_mesh/cli/utils.py +7 -2
  18. solace_agent_mesh/common/constants.py +10 -0
  19. solace_agent_mesh/common/prompt_templates.py +1 -3
  20. solace_agent_mesh/common/utils.py +104 -30
  21. solace_agent_mesh/config_portal/__init__.py +0 -0
  22. solace_agent_mesh/config_portal/backend/__init__.py +0 -0
  23. solace_agent_mesh/config_portal/backend/common.py +35 -0
  24. solace_agent_mesh/config_portal/backend/server.py +233 -0
  25. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DRPGOzHj.js +42 -0
  26. solace_agent_mesh/config_portal/frontend/static/client/assets/components-ZIfdTbrV.js +191 -0
  27. solace_agent_mesh/config_portal/frontend/static/client/assets/entry.client-DX1misIU.js +19 -0
  28. solace_agent_mesh/config_portal/frontend/static/client/assets/index-BJHAE5s4.js +17 -0
  29. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-8147e469.js +1 -0
  30. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DgMDqKDc.js +10 -0
  31. solace_agent_mesh/config_portal/frontend/static/client/assets/root-hhS5izs8.css +1 -0
  32. solace_agent_mesh/config_portal/frontend/static/client/favicon.ico +0 -0
  33. solace_agent_mesh/config_portal/frontend/static/client/index.html +7 -0
  34. solace_agent_mesh/configs/orchestrator.yaml +1 -1
  35. solace_agent_mesh/configs/service_embedding.yaml +1 -1
  36. solace_agent_mesh/configs/service_llm.yaml +1 -1
  37. solace_agent_mesh/gateway/components/gateway_base.py +7 -1
  38. solace_agent_mesh/gateway/components/gateway_input.py +8 -5
  39. solace_agent_mesh/gateway/components/gateway_output.py +12 -3
  40. solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +4 -0
  41. solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +43 -12
  42. solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +19 -5
  43. solace_agent_mesh/orchestrator/orchestrator_main.py +11 -5
  44. solace_agent_mesh/orchestrator/orchestrator_prompt.py +184 -60
  45. solace_agent_mesh/services/file_service/file_service.py +5 -0
  46. solace_agent_mesh/services/file_service/file_service_constants.py +1 -1
  47. solace_agent_mesh/services/file_service/file_transformations.py +11 -1
  48. solace_agent_mesh/services/file_service/file_utils.py +2 -0
  49. solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +21 -46
  50. solace_agent_mesh/services/history_service/history_providers/file_history_provider.py +74 -0
  51. solace_agent_mesh/services/history_service/history_providers/index.py +40 -0
  52. solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +19 -156
  53. solace_agent_mesh/services/history_service/history_providers/mongodb_history_provider.py +66 -0
  54. solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +40 -140
  55. solace_agent_mesh/services/history_service/history_providers/sql_history_provider.py +93 -0
  56. solace_agent_mesh/services/history_service/history_service.py +315 -41
  57. solace_agent_mesh/services/history_service/long_term_memory/__init__.py +0 -0
  58. solace_agent_mesh/services/history_service/long_term_memory/long_term_memory.py +399 -0
  59. solace_agent_mesh/services/llm_service/components/llm_request_component.py +19 -0
  60. solace_agent_mesh/templates/gateway-config-template.yaml +2 -1
  61. solace_agent_mesh/templates/gateway-default-config.yaml +3 -3
  62. solace_agent_mesh/templates/plugin-gateway-default-config.yaml +29 -0
  63. solace_agent_mesh/templates/rest-api-default-config.yaml +2 -1
  64. solace_agent_mesh/templates/slack-default-config.yaml +1 -1
  65. solace_agent_mesh/templates/solace-agent-mesh-default.yaml +9 -0
  66. solace_agent_mesh/templates/web-default-config.yaml +2 -1
  67. solace_agent_mesh-0.2.1.dist-info/METADATA +172 -0
  68. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.1.dist-info}/RECORD +71 -52
  69. solace_agent_mesh/common/prompt_templates_unused_delete.py +0 -161
  70. solace_agent_mesh-0.1.3.dist-info/METADATA +0 -208
  71. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.1.dist-info}/WHEEL +0 -0
  72. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.1.dist-info}/entry_points.txt +0 -0
  73. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -139,17 +139,28 @@ def parse_file_content(file_xml: str) -> dict:
139
139
  """
140
140
  Parse the xml tags in the content and return a dictionary of the content.
141
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
- }
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
+
149
+ ignore_content_tags = ["data"]
150
+ file_dict = xml_to_dict(file_xml, ignore_content_tags)
151
+ dict_keys = list(file_dict.keys())
152
+ top_key = [key for key in dict_keys if key not in ignore_content_tags][0]
153
+
154
+ return {
155
+ "data": file_dict.get("data", {}).get("data", ""),
156
+ "url": file_dict.get(top_key, {}).get("url", {}).get("url", ""),
157
+ "mime_type": file_dict.get(top_key, {}).get("mime_type", ""),
158
+ "name": file_dict.get(top_key, {}).get("name", ""),
159
+ }
160
+ except Exception as e:
161
+ result = {"data": "", "url": "", "mime_type": "", "name": ""}
162
+ log.error("Error parsing file content: %s", e)
163
+ return result
153
164
 
154
165
 
155
166
  def parse_llm_output(llm_output: str) -> dict:
@@ -160,6 +171,7 @@ def parse_llm_output(llm_output: str) -> dict:
160
171
  # We need to save all the strings that we replace so that we can put them back
161
172
  # after we parse the yaml. Use a sequence number to create a unique placeholder
162
173
  # for each string.
174
+
163
175
  string_placeholders = {}
164
176
  string_count = 0
165
177
  sanity = 100
@@ -241,7 +253,9 @@ def parse_llm_output(llm_output: str) -> dict:
241
253
  return obj
242
254
 
243
255
 
244
- 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
+ ):
245
259
  tp = tag_prefix
246
260
  parsed_data = {
247
261
  "actions": [],
@@ -319,6 +333,7 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
319
333
  current_param_value = []
320
334
  open_tags = []
321
335
  current_text = []
336
+ seen_invoke_action = False
322
337
 
323
338
  for line in response.split("\n"):
324
339
 
@@ -345,7 +360,8 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
345
360
  file_line = line[: file_end_index + len(f"</{tp}file>")]
346
361
  file_content = [file_line]
347
362
  current_file = parse_file_content("\n".join(file_content))
348
- 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)
349
365
  in_file = False
350
366
  current_file = {}
351
367
  file_content = []
@@ -363,7 +379,8 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
363
379
  file_line = line[: file_end_index + len(f"</{tp}file>")]
364
380
  file_content.append(file_line)
365
381
  current_file = parse_file_content("\n".join(file_content))
366
- 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)
367
384
  in_file = False
368
385
  current_file = {}
369
386
  file_content = []
@@ -376,6 +393,7 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
376
393
  if in_invoke_action:
377
394
  parsed_data["errors"].append("Nested <invoke_action> tags")
378
395
  in_invoke_action = True
396
+ seen_invoke_action = True
379
397
  open_tags.append("invoke_action")
380
398
  current_action = {
381
399
  "agent": None,
@@ -398,7 +416,7 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
398
416
  if current_param_name:
399
417
  current_action["parameters"][current_param_name] = "\n".join(
400
418
  current_param_value
401
- ).strip()
419
+ )
402
420
  parsed_data["actions"].append(current_action)
403
421
  current_action = {}
404
422
  current_param_name = None
@@ -406,9 +424,10 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
406
424
 
407
425
  elif in_invoke_action and f"<{tp}parameter" in line:
408
426
  if current_param_name:
409
- current_action["parameters"][current_param_name] = "\n".join(
410
- current_param_value
411
- ).strip()
427
+ param_value = "\n".join(current_param_value)
428
+ current_action["parameters"][current_param_name] = (
429
+ clean_parameter_value(param_value)
430
+ )
412
431
  current_param_value = []
413
432
 
414
433
  param_name_match = re.search(r'name\s*=\s*[\'"](\w+)[\'"]', line)
@@ -421,17 +440,19 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
421
440
  r">(.*?)(?:</" + tp + "parameter>|$)", line
422
441
  )
423
442
  if content_after_open:
424
- initial_content = content_after_open.group(1).strip()
443
+ initial_content = content_after_open.group(1)
425
444
  if initial_content:
426
445
  current_param_value.append(initial_content)
427
446
 
428
447
  # Check if parameter closes on same line
429
448
  if f"</{tp}parameter>" in line:
430
- current_action["parameters"][current_param_name] = "\n".join(
431
- current_param_value
432
- ).strip()
449
+ param_value = "\n".join(current_param_value)
450
+ current_action["parameters"][current_param_name] = (
451
+ clean_parameter_value(param_value)
452
+ )
433
453
  current_param_name = None
434
454
  current_param_value = []
455
+
435
456
  if "parameter" in open_tags:
436
457
  open_tags.remove("parameter")
437
458
  elif line.endswith("/>"):
@@ -446,25 +467,30 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
446
467
  if f"</{tp}parameter>" in line:
447
468
  # Handle content before closing tag on final line
448
469
  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()
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
+ )
454
476
  current_param_name = None
455
477
  current_param_value = []
456
478
  if "parameter" in open_tags:
457
479
  open_tags.remove("parameter")
458
480
  else:
459
- current_param_value.append(line.strip())
481
+ current_param_value.append(line)
460
482
 
461
483
  else:
462
- 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)
463
489
 
464
490
  if open_tags:
465
491
  parsed_data["errors"].append(f"Unclosed tags: {', '.join(open_tags)}")
466
492
 
467
- if in_file:
493
+ if in_file and not seen_invoke_action:
468
494
  content = "\n".join(file_content)
469
495
  # Add a status update for this
470
496
  parsed_data["status_updates"].append(
@@ -476,9 +502,28 @@ def parse_orchestrator_response(response, last_chunk=False, tag_prefix=""):
476
502
  if len(current_text) > 0:
477
503
  add_content_entry(parsed_data["content"], "text", current_text)
478
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
+
479
511
  return parsed_data
480
512
 
481
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
+
482
527
  def remove_incomplete_tags_at_end(text):
483
528
  """If the end of the text is in the middle of a <tag> or </tag> then remove it."""
484
529
  # remove any open tags at the end
@@ -550,6 +595,35 @@ def match_solace_topic(subscription: str, topic: str) -> bool:
550
595
  )
551
596
 
552
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
+
553
627
  def clean_text(text_array):
554
628
  # Any leading blank lines are removed
555
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
+