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.
- solace_agent_mesh/agents/global/actions/plantuml_diagram.py +9 -2
- solace_agent_mesh/agents/global/actions/plotly_graph.py +70 -46
- solace_agent_mesh/agents/web_request/actions/do_web_request.py +34 -33
- 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/add/gateway.py +162 -9
- solace_agent_mesh/cli/commands/build.py +15 -1
- 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/create_other_project_files_step.py +52 -1
- solace_agent_mesh/cli/commands/init/init.py +50 -37
- solace_agent_mesh/cli/commands/plugin/build.py +60 -9
- solace_agent_mesh/cli/commands/run.py +2 -2
- solace_agent_mesh/cli/config.py +4 -0
- solace_agent_mesh/cli/main.py +14 -8
- solace_agent_mesh/cli/utils.py +7 -2
- solace_agent_mesh/common/constants.py +10 -0
- solace_agent_mesh/common/prompt_templates.py +1 -3
- solace_agent_mesh/common/utils.py +104 -30
- 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/configs/service_embedding.yaml +1 -1
- solace_agent_mesh/configs/service_llm.yaml +1 -1
- solace_agent_mesh/gateway/components/gateway_base.py +7 -1
- solace_agent_mesh/gateway/components/gateway_input.py +8 -5
- solace_agent_mesh/gateway/components/gateway_output.py +12 -3
- solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +4 -0
- solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +43 -12
- 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 +184 -60
- solace_agent_mesh/services/file_service/file_service.py +5 -0
- solace_agent_mesh/services/file_service/file_service_constants.py +1 -1
- solace_agent_mesh/services/file_service/file_transformations.py +11 -1
- solace_agent_mesh/services/file_service/file_utils.py +2 -0
- solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +21 -46
- solace_agent_mesh/services/history_service/history_providers/file_history_provider.py +74 -0
- solace_agent_mesh/services/history_service/history_providers/index.py +40 -0
- solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +19 -156
- solace_agent_mesh/services/history_service/history_providers/mongodb_history_provider.py +66 -0
- solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +40 -140
- solace_agent_mesh/services/history_service/history_providers/sql_history_provider.py +93 -0
- solace_agent_mesh/services/history_service/history_service.py +315 -41
- solace_agent_mesh/services/history_service/long_term_memory/__init__.py +0 -0
- solace_agent_mesh/services/history_service/long_term_memory/long_term_memory.py +399 -0
- solace_agent_mesh/services/llm_service/components/llm_request_component.py +19 -0
- solace_agent_mesh/templates/gateway-config-template.yaml +2 -1
- solace_agent_mesh/templates/gateway-default-config.yaml +3 -3
- solace_agent_mesh/templates/plugin-gateway-default-config.yaml +29 -0
- solace_agent_mesh/templates/rest-api-default-config.yaml +2 -1
- solace_agent_mesh/templates/slack-default-config.yaml +1 -1
- solace_agent_mesh/templates/solace-agent-mesh-default.yaml +9 -0
- solace_agent_mesh/templates/web-default-config.yaml +2 -1
- solace_agent_mesh-0.2.1.dist-info/METADATA +172 -0
- {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.1.dist-info}/RECORD +71 -52
- solace_agent_mesh/common/prompt_templates_unused_delete.py +0 -161
- solace_agent_mesh-0.1.3.dist-info/METADATA +0 -208
- {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.1.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.1.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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)
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
450
|
-
current_param_value.append(content_before_close
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
481
|
+
current_param_value.append(line)
|
|
460
482
|
|
|
461
483
|
else:
|
|
462
|
-
|
|
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 >, <, 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
|
+
|
|
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
|
+
|