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.
- solace_agent_mesh/agents/global/actions/plotly_graph.py +48 -22
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add/copy_from_plugin.py +8 -6
- solace_agent_mesh/cli/commands/build.py +15 -0
- solace_agent_mesh/cli/commands/init/ai_provider_step.py +45 -28
- solace_agent_mesh/cli/commands/init/broker_step.py +1 -4
- solace_agent_mesh/cli/commands/init/create_config_file_step.py +8 -0
- solace_agent_mesh/cli/commands/init/init.py +50 -37
- solace_agent_mesh/cli/commands/plugin/build.py +52 -10
- solace_agent_mesh/cli/commands/run.py +2 -2
- solace_agent_mesh/cli/main.py +14 -8
- solace_agent_mesh/common/prompt_templates.py +1 -3
- solace_agent_mesh/common/utils.py +88 -19
- solace_agent_mesh/config_portal/__init__.py +0 -0
- solace_agent_mesh/config_portal/backend/__init__.py +0 -0
- solace_agent_mesh/config_portal/backend/common.py +35 -0
- solace_agent_mesh/config_portal/backend/server.py +233 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DRPGOzHj.js +42 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/components-ZIfdTbrV.js +191 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/entry.client-DX1misIU.js +19 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/index-BJHAE5s4.js +17 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-8147e469.js +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-DgMDqKDc.js +10 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-hhS5izs8.css +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/favicon.ico +0 -0
- solace_agent_mesh/config_portal/frontend/static/client/index.html +7 -0
- solace_agent_mesh/configs/orchestrator.yaml +1 -1
- solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +4 -0
- solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +28 -15
- solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +19 -5
- solace_agent_mesh/orchestrator/orchestrator_main.py +11 -5
- solace_agent_mesh/orchestrator/orchestrator_prompt.py +78 -74
- solace_agent_mesh/templates/solace-agent-mesh-default.yaml +9 -0
- solace_agent_mesh-0.2.1.dist-info/METADATA +172 -0
- {solace_agent_mesh-0.2.0.dist-info → solace_agent_mesh-0.2.1.dist-info}/RECORD +38 -26
- solace_agent_mesh/common/prompt_templates_unused_delete.py +0 -161
- solace_agent_mesh-0.2.0.dist-info/METADATA +0 -209
- {solace_agent_mesh-0.2.0.dist-info → solace_agent_mesh-0.2.1.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-0.2.0.dist-info → solace_agent_mesh-0.2.1.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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)
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
455
|
-
current_param_value.append(content_before_close
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
481
|
+
current_param_value.append(line)
|
|
465
482
|
|
|
466
483
|
else:
|
|
467
|
-
|
|
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 >, <, 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("<", "<")
|
|
619
|
+
param_value = param_value.replace(">", ">")
|
|
620
|
+
param_value = param_value.replace("&", "&")
|
|
621
|
+
param_value = param_value.replace(""", '"')
|
|
622
|
+
param_value = param_value.replace("'", "'")
|
|
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
|
+
|