Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 105 additions & 24 deletions src/client/content/config/tabs/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

This script allows importing/exporting configurations using Streamlit (`st`).
"""

# spell-checker:ignore streamlit mvnw obaas ollama vllm

import time
Expand Down Expand Up @@ -37,7 +38,12 @@


def _handle_key_comparison(
key: str, current: dict, uploaded: dict, differences: dict, new_path: str, sensitive_keys: set
key: str,
current: dict,
uploaded: dict,
differences: dict,
new_path: str,
sensitive_keys: set,
) -> None:
"""Handle comparison for a single key between current and uploaded settings."""
is_sensitive = key in sensitive_keys
Expand All @@ -57,7 +63,10 @@ def _handle_key_comparison(
# Both present — compare
if is_sensitive:
if current[key] != uploaded[key]:
differences["Value Mismatch"][new_path] = {"current": current[key], "uploaded": uploaded[key]}
differences["Value Mismatch"][new_path] = {
"current": current[key],
"uploaded": uploaded[key],
}
else:
child_diff = compare_settings(current[key], uploaded[key], new_path)
for diff_type, diff_dict in differences.items():
Expand Down Expand Up @@ -101,7 +110,9 @@ def _render_upload_settings_section() -> None:
time.sleep(3)
st.rerun()
else:
st.write("No differences found. The current configuration matches the saved settings.")
st.write(
"No differences found. The current configuration matches the saved settings."
)
except json.JSONDecodeError:
st.error("Error: The uploaded file is not a valid.")
else:
Expand All @@ -116,14 +127,18 @@ def _get_model_configs() -> tuple[dict, dict, str]:
"""
try:
model_lookup = st_common.enabled_models_lookup(model_type="ll")
ll_config = model_lookup[state.client_settings["ll_model"]["model"]] | state.client_settings["ll_model"]
ll_config = (
model_lookup[state.client_settings["ll_model"]["model"]]
| state.client_settings["ll_model"]
)
except KeyError:
ll_config = {}

try:
model_lookup = st_common.enabled_models_lookup(model_type="embed")
embed_config = (
model_lookup[state.client_settings["vector_search"]["model"]] | state.client_settings["vector_search"]
model_lookup[state.client_settings["vector_search"]["model"]]
| state.client_settings["vector_search"]
)
except KeyError:
embed_config = {}
Expand All @@ -140,12 +155,14 @@ def _render_source_code_templates_section() -> None:
logger.info("config found: %s", spring_ai_conf)

if spring_ai_conf == "hybrid":
st.markdown(f"""
st.markdown(
f"""
The current configuration combination of embedding and language models
is currently **not supported** for Spring AI and LangChain MCP templates.
- Language Model: **{ll_config.get("model", "Unset")}**
- Embedding Model: **{embed_config.get("model", "Unset")}**
""")
"""
)
else:
settings = get_settings(state.selected_sensitive_settings)
col_left, col_centre, _ = st.columns([3, 4, 3])
Expand All @@ -161,7 +178,9 @@ def _render_source_code_templates_section() -> None:
if spring_ai_conf != "hosted_vllm":
st.download_button(
label="Download SpringAI",
data=spring_ai_zip(spring_ai_conf, ll_config, embed_config), # Generate zip on the fly
data=spring_ai_zip(
spring_ai_conf, ll_config, embed_config
), # Generate zip on the fly
file_name="spring_ai.zip", # Zip file name
mime="application/zip", # Mime type for zip file
disabled=spring_ai_conf == "hybrid",
Expand All @@ -184,7 +203,10 @@ def get_settings(include_sensitive: bool = False):
if "not found" in str(ex):
# If client settings not found, create them
logger.info("Client settings not found, creating new ones")
api_call.post(endpoint="v1/settings", params={"client": state.client_settings["client"]})
api_call.post(
endpoint="v1/settings",
params={"client": state.client_settings["client"]},
)
settings = api_call.get(
endpoint="v1/settings",
params={
Expand All @@ -211,7 +233,12 @@ def save_settings(settings):

def compare_settings(current, uploaded, path=""):
"""Compare current settings with uploaded settings."""
differences = {"Value Mismatch": {}, "Missing in Uploaded": {}, "Missing in Current": {}, "Override on Upload": {}}
differences = {
"Value Mismatch": {},
"Missing in Uploaded": {},
"Missing in Current": {},
"Override on Upload": {},
}
sensitive_keys = {"api_key", "password", "wallet_password"}

if isinstance(current, dict) and isinstance(uploaded, dict):
Expand All @@ -223,7 +250,9 @@ def compare_settings(current, uploaded, path=""):
if new_path == "client_settings.client" or new_path.endswith(".created"):
continue

_handle_key_comparison(key, current, uploaded, differences, new_path, sensitive_keys)
_handle_key_comparison(
key, current, uploaded, differences, new_path, sensitive_keys
)

elif isinstance(current, list) and isinstance(uploaded, list):
min_len = min(len(current), len(uploaded))
Expand All @@ -240,7 +269,10 @@ def compare_settings(current, uploaded, path=""):
differences["Missing in Current"][new_path] = {"uploaded": uploaded[i]}
else:
if current != uploaded:
differences["Value Mismatch"][path] = {"current": current, "uploaded": uploaded}
differences["Value Mismatch"][path] = {
"current": current,
"uploaded": uploaded,
}

return differences

Expand All @@ -256,13 +288,19 @@ def apply_uploaded_settings(uploaded):
timeout=7200,
)
st.success(response["message"], icon="✅")
state.client_settings = api_call.get(endpoint="v1/settings", params={"client": client_id})
state.client_settings = api_call.get(
endpoint="v1/settings", params={"client": client_id}
)
# Clear States so they are refreshed
for key in ["oci_configs", "model_configs", "database_configs"]:
st_common.clear_state_key(key)
except api_call.ApiError as ex:
st.error(f"Settings for {state.client_settings['client']} - Update Failed", icon="❌")
logger.error("%s Settings Update failed: %s", state.client_settings["client"], ex)
st.error(
f"Settings for {state.client_settings['client']} - Update Failed", icon="❌"
)
logger.error(
"%s Settings Update failed: %s", state.client_settings["client"], ex
)


def spring_ai_conf_check(ll_model: dict, embed_model: dict) -> str:
Expand All @@ -289,38 +327,71 @@ def spring_ai_obaas(src_dir, file_name, provider, ll_config, embed_config):
sys_prompt = next(
item["prompt"]
for item in state.prompt_configs
if item["name"] == state.client_settings["prompts"]["sys"] and item["category"] == "sys"
if item["name"] == state.client_settings["prompts"]["sys"]
and item["category"] == "sys"
)
logger.info("Prompt used in export:\n%s", sys_prompt)
with open(src_dir / "templates" / file_name, "r", encoding="utf-8") as template:
template_content = template.read()

database_lookup = st_common.state_configs_lookup("database_configs", "name")

logger.info(
"Database Legacy User:%s",
database_lookup[state.client_settings["database"]["alias"]]["user"],
)

formatted_content = template_content.format(
provider=provider,
sys_prompt=f"{sys_prompt}",
ll_model=ll_config,
vector_search=embed_config,
database_config=database_lookup[state.client_settings.get("database", {}).get("alias")],
database_config=database_lookup[
state.client_settings.get("database", {}).get("alias")
],
)

if file_name.endswith(".yaml"):
sys_prompt = json.dumps(sys_prompt, indent=True) # Converts it into a valid JSON string (preserving quotes)
sys_prompt = json.dumps(
sys_prompt, indent=True
) # Converts it into a valid JSON string (preserving quotes)

formatted_content = template_content.format(
provider=provider,
sys_prompt=sys_prompt,
ll_model=ll_config,
vector_search=embed_config,
database_config=database_lookup[state.client_settings.get("database", {}).get("alias")],
database_config=database_lookup[
state.client_settings.get("database", {}).get("alias")
],
)

yaml_data = yaml.safe_load(formatted_content)
if provider == "ollama":
del yaml_data["spring"]["ai"]["openai"]
yaml_data["spring"]["ai"]["openai"] = {"chat": {"options": {"model": "_"}}}
if provider == "openai":
del yaml_data["spring"]["ai"]["ollama"]
yaml_data["spring"]["ai"]["ollama"] = {"chat": {"options": {"model": "_"}}}

# check if is formatting a "obaas" template to override openai base url
# that causes an issue in obaas with "/v1"

if (
file_name.find("obaas") != -1
and yaml_data["spring"]["ai"]["openai"]["base-url"].find(
"api.openai.com"
)
!= -1
):
yaml_data["spring"]["ai"]["openai"][
"base-url"
] = "https://api.openai.com"
logger.info(
"in spring_ai_obaas(%s) found openai.base-url and changed with https://api.openai.com",
file_name,
)

formatted_content = yaml.dump(yaml_data)

return formatted_content
Expand Down Expand Up @@ -349,12 +420,20 @@ def spring_ai_zip(provider, ll_config, embed_config):
for filename in filenames:
file_path = os.path.join(foldername, filename)

arc_name = os.path.relpath(file_path, dst_dir) # Make the path relative
arc_name = os.path.relpath(
file_path, dst_dir
) # Make the path relative
zip_file.write(file_path, arc_name)
env_content = spring_ai_obaas(src_dir, "start.sh", provider, ll_config, embed_config)
yaml_content = spring_ai_obaas(src_dir, "obaas.yaml", provider, ll_config, embed_config)
env_content = spring_ai_obaas(
src_dir, "start.sh", provider, ll_config, embed_config
)
yaml_content = spring_ai_obaas(
src_dir, "obaas.yaml", provider, ll_config, embed_config
)
zip_file.writestr("start.sh", env_content.encode("utf-8"))
zip_file.writestr("src/main/resources/application-obaas.yml", yaml_content.encode("utf-8"))
zip_file.writestr(
"src/main/resources/application-obaas.yml", yaml_content.encode("utf-8")
)
zip_buffer.seek(0)
return zip_buffer

Expand Down Expand Up @@ -383,7 +462,9 @@ def langchain_mcp_zip(settings):
for filename in filenames:
file_path = os.path.join(foldername, filename)

arc_name = os.path.relpath(file_path, dst_dir) # Make the path relative
arc_name = os.path.relpath(
file_path, dst_dir
) # Make the path relative
zip_file.write(file_path, arc_name)
zip_buffer.seek(0)
return zip_buffer
Expand Down
17 changes: 10 additions & 7 deletions src/client/mcp/rag/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ If you have already installed Node.js v20.17.0+, it should work.
"command": "npx",
"args": [
"mcp-remote",
"http://127.0.0.1:9090/sse"
"http://127.0.0.1:9090/mcp"
]
}
}
Expand Down Expand Up @@ -120,14 +120,17 @@ uv run rag_base_optimizer_config_mcp.py
* Set `Local` with `Remote client` line in `<PROJECT_DIR>/rag_base_optimizer_config_mcp.py`:

```python
#mcp = FastMCP("rag", port=9090) #Remote client
mcp = FastMCP("rag") #Local
#mcp.run(transport='stdio')
#mcp.run(transport='sse')
mcp.run(transport='streamable-http')
```

* Substitute `stdio` with `sse` line of code:
* Substitute `stdio` with `streamable-http` line of code:

```python
mcp.run(transport='stdio')
#mcp.run(transport='sse')
#mcp.run(transport='sse')
#mcp.run(transport='streamable-http')
```


Expand All @@ -146,9 +149,9 @@ npx @modelcontextprotocol/inspector

* connect the browser to `http://127.0.0.1:6274`

* set the Transport Type to `SSE`
* set the Transport Type to `Streamable HTTP`

* set the `URL` to `http://localhost:9090/sse`
* set the `URL` to `http://localhost:9090/mcp`

* test the tool developed.

Expand Down
77 changes: 77 additions & 0 deletions src/client/mcp/rag/rag_base_optimizer_config_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Copyright (c) 2024, 2025, Oracle and/or its affiliates.
Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
"""
from typing import List
from mcp.server.fastmcp import FastMCP
import os
from dotenv import load_dotenv
#from sentence_transformers import CrossEncoder
#from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
import json
import logging
logger = logging.getLogger(__name__)

logging.basicConfig(
level=logging.INFO,
format="%(name)s - %(levelname)s - %(message)s"
)

from optimizer_utils import rag


logging.info("Successfully imported libraries and modules")

CHUNKS_DIR = "chunks_temp"
data = {}

# Initialize FastMCP server
mcp = FastMCP("rag", port=9090) #Remote client
#mcp = FastMCP("rag") #Local


@mcp.tool()
def rag_tool(question: str) -> str:
"""
Use this tool to answer any question that may benefit from up-to-date or domain-specific information.

Args:
question: the question for which are you looking for an answer

Returns:
JSON string with answer
"""

answer = rag.rag_tool_base(question)

return f"{answer}"

if __name__ == "__main__":

# To dinamically change Tool description: not used but in future maybe
rag_tool_desc=[
f"""
Use this tool to answer any question that may benefit from up-to-date or domain-specific information.

Args:
question: the question for which are you looking for an answer

Returns:
JSON string with answer
"""
]


# Initialize and run the server

# Set optimizer_settings.json file ABSOLUTE path
rag.set_optimizer_settings_path("optimizer_settings.json")

# Change according protocol type

#mcp.run(transport='stdio')
#mcp.run(transport='sse')
mcp.run(transport='streamable-http')
Loading
Loading