diff --git a/src/client/content/config/tabs/settings.py b/src/client/content/config/tabs/settings.py index 519c75fb..cb6e25b1 100644 --- a/src/client/content/config/tabs/settings.py +++ b/src/client/content/config/tabs/settings.py @@ -4,6 +4,7 @@ This script allows importing/exporting configurations using Streamlit (`st`). """ + # spell-checker:ignore streamlit mvnw obaas ollama vllm import time @@ -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 @@ -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(): @@ -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: @@ -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 = {} @@ -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]) @@ -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", @@ -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={ @@ -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): @@ -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)) @@ -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 @@ -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: @@ -289,7 +327,8 @@ 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: @@ -297,30 +336,62 @@ def spring_ai_obaas(src_dir, file_name, provider, ll_config, embed_config): 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 @@ -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 @@ -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 diff --git a/src/client/mcp/rag/README.md b/src/client/mcp/rag/README.md index 561cebed..625d43c1 100644 --- a/src/client/mcp/rag/README.md +++ b/src/client/mcp/rag/README.md @@ -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" ] } } @@ -120,14 +120,17 @@ uv run rag_base_optimizer_config_mcp.py * Set `Local` with `Remote client` line in `/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') ``` @@ -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. diff --git a/src/client/mcp/rag/rag_base_optimizer_config_mcp.py b/src/client/mcp/rag/rag_base_optimizer_config_mcp.py new file mode 100644 index 00000000..d1c22739 --- /dev/null +++ b/src/client/mcp/rag/rag_base_optimizer_config_mcp.py @@ -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') \ No newline at end of file diff --git a/src/client/spring_ai/README.md b/src/client/spring_ai/README.md index 19b39819..28f56e71 100644 --- a/src/client/spring_ai/README.md +++ b/src/client/spring_ai/README.md @@ -1,5 +1,21 @@ # Spring AI template +## Prerequisites + +Before using the AI commands, make sure you have a developer token from OpenAI. + +Create an account at [OpenAI Signup](https://platform.openai.com/signup) and generate the token at [API Keys](https://platform.openai.com/account/api-keys). + +The Spring AI project defines a configuration property named `spring.ai.openai.api-key` that you should set to the value of the `API Key` obtained from `openai.com`. + +Exporting an environment variable is one way to set that configuration property. +```shell +export SPRING_AI_OPENAI_API_KEY= +``` + +Setting the API key is all you need to run the application. +However, you can find more information on setting started in the [Spring AI reference documentation section on OpenAI Chat](https://docs.spring.io/spring-ai/reference/api/clients/openai-chat.html). + ## How to run: Prepare two configurations in the `Oracle ai optimizer and toolkit`, based on vector stores created using this kind of configuration: @@ -155,95 +171,171 @@ npx @modelcontextprotocol/inspector * Test a call to `getRag` Tool. -## Oracle Backend for Microservices and AI -* Add in `application-obaas.yml` the **OPENAI_API_KEY**, if the deployement is based on the OpenAI LLM services: -``` - openai: - base-url: - api-key: +## Oracle Backend for Microservices and AI (rel. 1.4.0) + +To simplify as much as possible the process, configure the Oracle Backend for Microservices and AI Autonomous DB to run the AI Optimizer and toolkit. In this way, you can get smoothly the vectorstore created to be copied as a dedicated version for the microservice running. If you prefer to run the microservice in another user schema, before the step **5.** execute the steps described at **Other deployment options** chapter. + +* Create a user/schema via oractl. First open a tunnel: +```bash +kubectl -n obaas-admin port-forward svc/obaas-admin 8080:8080 ``` -* Build, depending the provider ``: +* run `oractl` and connect with the provided credentials + +* create a namespace to host the AI Optimizer and Toolkit : +```bash +namespace create --namespace ``` -mvn clean package -DskipTests -P -Dspring-boot.run.profiles=obaas + +* create the datastore, saving the password provided: +```bash +datastore create --namespace --username --id ``` -* Set, one time only, the ollama server running in the **Oracle Backend for Microservices and AI**. Prepare an `ollama-values.yaml`: +* For the AI Optimizer and Toolkit local startup, setting this env variables in startup: + +```bash +DB_USERNAME= +DB_PASSWORD= +DB_DSN="" +DB_WALLET_PASSWORD= +TNS_ADMIN= ``` + +NOTE: if you need to access to the Autonomus Database backing the platform as admin, execute: +```bash +kubectl -n application get secret -db-secrets -o jsonpath='{.data.db\.password}' | base64 -d; echo +``` +to do, for example: +```bash +DROP USER vectorusr CASCADE; +``` + +Then proceed as described in following steps: + +1. Create an `ollama-values.yaml` to be used with **helm** to provision an Ollama server. This step requires you have a GPU node pool provisioned with the Oracle Backend for Microservices and AI. Include in the models list to pull the model used in your Spring Boot microservice. Example: + +```yaml ollama: gpu: enabled: true type: 'nvidia' number: 1 models: - - llama3.1 - - llama3.2 - - mxbai-embed-large - - nomic-embed-text + pull: + - llama3.1 + - llama3.2 + - mxbai-embed-large + - nomic-embed-text nodeSelector: node.kubernetes.io/instance-type: VM.GPU.A10.1 ``` -* execute: -``` -kubectl create ns ollama -helm install ollama ollama-helm/ollama --namespace ollama --values ollama-values.yaml +2. Execute the helm chart provisioning: + +```bash +helm upgrade --install ollama ollama-helm/ollama \ + --namespace ollama \ + --create-namespace \ + --values ollama-values.yaml ``` -* check: + +Check if the deployment is working at the end of process. +You should get this kind of output: + +```bash +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace ollama -l "app.kubernetes.io/name=ollama,app.kubernetes.io/instance=ollama" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace ollama $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace ollama port-forward $POD_NAME 8080:$CONTAINER_PORT ``` + +3. check all: +* run: +```bash kubectl -n ollama exec svc/ollama -- ollama ls ``` it should be: -``` +```bash NAME ID SIZE MODIFIED nomic-embed-text:latest 0a109f422b47 274 MB 3 minutes ago mxbai-embed-large:latest 468836162de7 669 MB 3 minutes ago llama3.1:latest a80c4f17acd5 2.0 GB 3 minutes ago ``` * test a single LLM: -``` +```bash kubectl -n ollama exec svc/ollama -- ollama run "llama3.1" "what is spring boot?" ``` -* **NOTE**: The Microservices will access to the ADB23ai on which the vector store table should be created as done in the local desktop example shown before. To access the ai-explorer running on **Oracle Backend for Microservices and AI** and create the same configuration, let's do: - * tunnel: - ``` - kubectl -n ai-explorer port-forward svc/ai-explorer 8181:8501 - ``` - * on localhost: - ``` - http://localhost:8181/ai-sandbox - ``` +NOTICE: for network issue related to huge model download, the process could stuck. Repeat it, or choose to pull manually just for test, removing from the helm chart the `models` part in `ollama-values.yaml`. -* Deploy with `oractl` on a new schema `vector`: - * tunnel: - ``` - kubectl -n obaas-admin port-forward svc/obaas-admin 8080:8080 - ``` - - * oractl: - ``` - create --app-name vector_search - bind --app-name vector_search --service-name myspringai --username vector - ``` +To remove it and repeat: +* get the ollama [POD_ID] stuck: +```bash +kubectl get pods -n ollama +``` -* the `bind` will create the new user, if not exists, but to have the `_SPRINGAI` table compatible with SpringAI Oracle vector store adapter, the microservices need to access to the vector store table created by the ai-explorer with user ADMIN on ADB: +* the uninstall: +```bash +helm uninstall ollama --namespace ollama +kubectl delete pod -n ollama --grace-period=0 --force +kubectl delete pod -n ollama --all --grace-period=0 --force +kubectl delete namespace ollama ``` -GRANT SELECT ON ADMIN. TO vector; + +* install helm chart without models + +* connect to the pod to pull manually: +```bash +kubectl exec -it -n ollama -- bash ``` -* then deploy: + +* run: +```bash +ollama pull llama3.2 +ollama pull mxbai-embed-large ``` -deploy --app-name vector_search --service-name myspringai --artifact-path /target/myspringai-0.0.1-SNAPSHOT.jar --image-version 0.0.1 --java-version ghcr.io/oracle/graalvm-native-image-obaas:21 --service-profile obaas + +* Build, depending the provider ``: + +```bash +mvn clean package -DskipTests -P -Dspring-boot.run.profiles=obaas ``` -* test: + +4. Connect via oractl to deploy the microservice, if not yet done: + +* First open a tunnel: +```bash +kubectl -n obaas-admin port-forward svc/obaas-admin 8080:8080 ``` -kubectl -n vector_search port-forward svc/myspringai 9090:8080 +* run `oractl` and connect with the provided credentials + +5. Execute the deployment: + +```bash +artifact create --namespace --workload --imageVersion 0.0.1 --file + +image create --namespace --workload --imageVersion 0.0.1 + +workload create --namespace --imageVersion 0.0.1 --id --cpuRequest 100m --framework SPRING_BOOT + +binding create --namespace --datastore --workload --framework SPRING_BOOT ``` -* from shell: + +6. Let's test: +* open a tunnel: + +```bash +kubectl -n port-forward svc/ 9090:8080 ``` + +* test via curl. Example: + +```bash curl -N http://localhost:9090/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your_api_key" \ @@ -253,37 +345,114 @@ curl -N http://localhost:9090/v1/chat/completions \ "stream": false }' ``` -it should return: + +7. Open to external access via APISIX Gateway: + +* get the Kubernetes [EXTERNAL-IP] address: + +```bash +kubectl -n ingress-nginx get svc ingress-nginx-controller ``` -{ - "choices": [ - { - "message": { - "content": "Based on the provided documents, it seems that a specific development environment (IDE) is recommended for running the example.\n\nIn document \"67D5C08DF7F7480F\", it states: \"This guide uses IntelliJ Idea community version to create and update the files for this application.\" (page 17)\n\nHowever, there is no information in the provided documents that explicitly prohibits using other development environments. In fact, one of the articles mentions \"Application. Use these instructions as a reference.\" without specifying any particular IDE.\n\nTherefore, while it appears that IntelliJ Idea community version is recommended, I couldn't find any definitive statement ruling out the use of other development environments entirely.\n\nIf you'd like to run the example with a different environment, it might be worth investigating further or consulting additional resources. Sorry if this answer isn't more conclusive!" - } - } - ] -} + +* get the APISIX password: + +```bash +kubectl get secret -n apisix apisix-dashboard -o jsonpath='{.data.conf\.yaml}' | base64 -d | grep 'password:'; echo ``` +* connect to APISIX console: -## Prerequisites +```bash +kubectl port-forward -n apisix svc/apisix-dashboard 8090:80 +``` +and provide the credentials at local url http://localhost:8090/, [admin]/[Password] -Before using the AI commands, make sure you have a developer token from OpenAI. +* Create a route to access the microservice: -Create an account at [OpenAI Signup](https://platform.openai.com/signup) and generate the token at [API Keys](https://platform.openai.com/account/api-keys). +```bash +Name: +Path: /v1/chat/completions* +Algorithm: Round Robin +Upstream Type: Node +Targets: + Host:..svc.cluster.local + Port: 8080 +``` -The Spring AI project defines a configuration property named `spring.ai.openai.api-key` that you should set to the value of the `API Key` obtained from `openai.com`. +8. Test the access to the public IP. Example: +```bash +curl -N http:///v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_api_key" \ + -d '{ + "model": "server", + "messages": [{"role": "user", "content": "Can I use any kind of development environment to run the example?"}], + "stream": false + }' +``` -Exporting an environment variable is one way to set that configuration property. -```shell -export SPRING_AI_OPENAI_API_KEY= + + +### Other deployment options + +If you want to run on another schema instead the [OPTIMIZER_USER], you should add a few steps. + +1. Connect to the backend via oractl: + +* First open a tunnel: +```bash +kubectl -n obaas-admin port-forward svc/obaas-admin 8080:8080 ``` +* Run `oractl` and connect with the provided credentials -Setting the API key is all you need to run the application. -However, you can find more information on setting started in the [Spring AI reference documentation section on OpenAI Chat](https://docs.spring.io/spring-ai/reference/api/clients/openai-chat.html). +* Create a dedicated namespace for the microservice: +```bash +namespace create --namespace +``` + +* Create a dedicated user/schema for the microservice, providing a [MS_USER_PWD] to execute the command: +```bash +datastore create --namespace --username --id +``` +2. Connect to the Autonomous DB instance via the [OPTIMIZER_USER]/[OPTIMIZER_USER_PASSWORD] +* Grant access to the microservice user to copy the vectorstore used: + +```bash +GRANT SELECT ON ""."" TO ; +``` + +3. Then proceed from the step 5. as usual, changing: + +* **** -> **** +* **** -> **** +* **** -> **** + + +### Cleanup env + +* First open a tunnel: +```bash +kubectl -n obaas-admin port-forward svc/obaas-admin 8080:8080 +``` + +* Run `oractl` and connect with the provided credentials: + +```bash +workload list --namespace +workload delete --namespace --id myspringai +image list +image delete --imageId +artifact list +artifact delete --artifactId +``` +* disconnect [OPTIMIZER_USER] from DB (the Optimizer server) and finally with **oractl**: + +```bash +datastore delete --namespace --id optimizerds +namespace delete optimizerns +``` diff --git a/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/AIController.java b/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/AIController.java index 1468c41b..63aabbbb 100644 --- a/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/AIController.java +++ b/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/AIController.java @@ -53,6 +53,7 @@ class AIController { private final OracleVectorStore vectorStore; private final EmbeddingModel embeddingModel; private final String legacyTable; + private final String userTable; private final String contextInstr; private final String searchType; private final int TOPK; @@ -76,6 +77,7 @@ class AIController { OracleVectorStore vectorStore, JdbcTemplate jdbcTemplate, String legacyTable, + String userTable, String contextInstr, String searchType, int TOPK) { @@ -86,6 +88,7 @@ class AIController { this.chatClient = chatClient; this.embeddingModel = embeddingModel; this.legacyTable = legacyTable; + this.userTable = userTable; this.contextInstr = contextInstr; this.searchType = searchType; this.TOPK = TOPK; @@ -132,18 +135,19 @@ public void insertData() { } else { // RUNNING in OBAAS logger.info("Running on OBaaS with user: " + user); + logger.info("copying langchain table from schema/user: " + userTable); sql = "INSERT INTO " + user + "." + newTable + " (ID, CONTENT, METADATA, EMBEDDING) " + - "SELECT ID, TEXT, METADATA, EMBEDDING FROM ADMIN." + legacyTable; + "SELECT ID, TEXT, METADATA, EMBEDDING FROM "+ userTable+"." + legacyTable; } // Execute the insert logger.info("doesExist" + user + ": " + helper.doesTableExist(newTable, user,this.jdbcTemplate)); if (helper.countRecordsInTable(newTable, user,this.jdbcTemplate) == 0) { // First microservice execution - logger.info("Table " + user + "." + newTable + " doesn't exist: create from ADMIN/USER." + legacyTable); + logger.info("Table " + user + "." + newTable + " doesn't exist: create from "+userTable+"." + legacyTable); jdbcTemplate.update(sql); } else { // Table conversion already done - logger.info("Table +" + newTable + " exists: drop before if you want use with new contents " + legacyTable); + logger.info("Table " + user+"."+newTable + " exists: drop before if you want use with new contents " + userTable + "." + legacyTable); } } diff --git a/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Config.java b/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Config.java index 62bcc883..2c6bf037 100644 --- a/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Config.java +++ b/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Config.java @@ -33,6 +33,12 @@ public String modelOllamaAI(@Value("${spring.ai.ollama.chat.options.model}") Str public String legacyTable(@Value("${aims.vectortable.name}") String table) { return table; } + + @Bean + public String userTable(@Value("${aims.vectortable.user}") String user) { + return user; + } + @Bean public String contextInstr(@Value("${aims.sys_instr}") String instr) { diff --git a/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Helper.java b/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Helper.java index 6ab80289..892c531c 100644 --- a/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Helper.java +++ b/src/client/spring_ai/src/main/java/org/springframework/ai/openai/samples/helloworld/Helper.java @@ -86,9 +86,9 @@ public String generateRandomToken(int length) { public String getModel(String modelOpenAI, String modelOllamaAI) { String modelId = "custom"; - if (!"".equals(modelOpenAI)) { + if (!"_".equals(modelOpenAI)) { modelId = modelOpenAI; - } else if (!"".equals(modelOllamaAI)) { + } else if (!"_".equals(modelOllamaAI)) { modelId = modelOllamaAI; } return modelId; diff --git a/src/client/spring_ai/src/main/resources/application-dev.yml b/src/client/spring_ai/src/main/resources/application-dev.yml index 638c1e56..0bf3e1d7 100644 --- a/src/client/spring_ai/src/main/resources/application-dev.yml +++ b/src/client/spring_ai/src/main/resources/application-dev.yml @@ -12,9 +12,7 @@ spring: version: 1.0.0 type: SYNC request-timeout: 120 - instructions: >- - Use this tool to answer any question that may benefit from - up-to-date or domain-specific information. + instructions: "Use this tool to answer any question that may benefit from up-to-date or domain-specific information." capabilities: tool: true resource: true @@ -23,8 +21,8 @@ spring: vectorstore: oracle: distance-type: ${DISTANCE_TYPE} - remove-existing-vector-store-table: true - initialize-schema: true + remove-existing-vector-store-table: True + initialize-schema: True index-type: ${INDEX_TYPE} openai: base-url: {OPENAI_URL} @@ -57,6 +55,7 @@ aims: sys_instr: ${SYS_INSTR} vectortable: name: ${VECTOR_STORE} + user: ${USER_TABLE} rag_params: search_type: Similarity top_k: ${TOP_K} diff --git a/src/client/spring_ai/templates/obaas.yaml b/src/client/spring_ai/templates/obaas.yaml index 28945e92..60a71eb5 100644 --- a/src/client/spring_ai/templates/obaas.yaml +++ b/src/client/spring_ai/templates/obaas.yaml @@ -1,6 +1,3 @@ -server: - servlet: - context-path: /v1 spring: datasource: url: ${{spring.datasource.url]}} @@ -10,12 +7,12 @@ spring: vectorstore: oracle: distance-type: {vector_search[distance_metric]} - remove-existing-vector-store-table: true - initialize-schema: true + remove-existing-vector-store-table: True + initialize-schema: True index-type: {vector_search[index_type]} openai: - base-url: \"{ll_model[api_base]}\" - api-key: \"{ll_model[api_key]}\" + base-url: {ll_model[api_base]} + api-key: {ll_model[api_key]} chat: options: temperature: {ll_model[temperature]} @@ -36,14 +33,15 @@ spring: frequency-penalty: {ll_model[frequency_penalty]} num-predict: {ll_model[max_tokens]} top-p: {ll_model[top_p]} - model: \"{ll_model[id]}\" + model: {ll_model[id]} embedding: - options: - model: \"{vector_search[id]}\" + options: + model: {vector_search[id]} aims: sys_instr: \"{sys_prompt}\" vectortable: name: {vector_search[vector_store]} - rag_params: + user: {database_config[user]} + rag_params: search_type: Similarity top_k: {vector_search[top_k]} diff --git a/src/client/spring_ai/templates/start.sh b/src/client/spring_ai/templates/start.sh new file mode 100644 index 00000000..05964c50 --- /dev/null +++ b/src/client/spring_ai/templates/start.sh @@ -0,0 +1,46 @@ +# Set Values +export PROVIDER="{provider}" + +if [[ "{provider}" == "ollama" ]]; then + PREFIX="OL"; UNSET_PREFIX="OP" + export OPENAI_CHAT_MODEL="" + unset OPENAI_EMBEDDING_MODEL + unset OPENAI_URL + export OLLAMA_BASE_URL="{ll_model[api_base]}" + export OLLAMA_CHAT_MODEL="{ll_model[id]}" + export OLLAMA_EMBEDDING_MODEL="{vector_search[id]}" +else + PREFIX="OP"; UNSET_PREFIX="OL" + export OPENAI_CHAT_MODEL="{ll_model[id]}" + export OPENAI_EMBEDDING_MODEL="{vector_search[id]}" + export OPENAI_URL="{ll_model[api_base]}" + export OLLAMA_CHAT_MODEL="" + unset OLLAMA_EMBEDDING_MODEL +fi + +TEMPERATURE="{ll_model[temperature]}" +FREQUENCY_PENALTY="{ll_model[frequency_penalty]}" +PRESENCE_PENALTY="{ll_model[presence_penalty]}" +MAX_TOKENS="{ll_model[max_tokens]}" +TOP_P="{ll_model[top_p]}" +COMMON_VARS=("TEMPERATURE" "FREQUENCY_PENALTY" "PRESENCE_PENALTY" "MAX_TOKENS" "TOP_P") + +# Loop through the common variables and export them +for var in "${{COMMON_VARS[@]}}"; do + export ${{PREFIX}}_${{var}}="${{!var}}" + unset ${{UNSET_PREFIX}}_${{var}} +done + +# env_vars +export SPRING_AI_OPENAI_API_KEY=${{OPENAI_API_KEY}} +export DB_DSN="jdbc:oracle:thin:@{database_config[dsn]}" +export DB_USERNAME="{database_config[user]}" +export DB_PASSWORD="{database_config[password]}" +export DISTANCE_TYPE="{vector_search[distance_metric]}" +export INDEX_TYPE="{vector_search[index_type]}" +export SYS_INSTR="{sys_prompt}" +export TOP_K="{vector_search[top_k]}" + +export VECTOR_STORE="{vector_search[vector_store]}" +export USER_TABLE=$DB_USERNAME +mvn spring-boot:run -P {provider} \ No newline at end of file diff --git a/tests/client/content/config/tabs/test_settings.py b/tests/client/content/config/tabs/test_settings.py index e9467520..7450cd26 100644 --- a/tests/client/content/config/tabs/test_settings.py +++ b/tests/client/content/config/tabs/test_settings.py @@ -365,33 +365,29 @@ def test_spring_ai_obaas_shell_template(self): assert "You are a helpful assistant." in result assert "{'model': 'gpt-4'}" in result - def test_spring_ai_obaas_yaml_template(self): - """Test spring_ai_obaas function with YAML template""" + def test_spring_ai_obaas_non_yaml_file(self): + """Test spring_ai_obaas with non-YAML file""" from client.content.config.tabs.settings import spring_ai_obaas + mock_state = SimpleNamespace( + client_settings={ + "prompts": {"sys": "Basic Example"}, + "database": {"alias": "DEFAULT"} + }, + prompt_configs=[{"name": "Basic Example", "category": "sys", "prompt": "You are a helpful assistant."}] + ) + mock_template_content = "Provider: {provider}\nPrompt: {sys_prompt}\nLLM: {ll_model}\nEmbed: {vector_search}\nDB: {database_config}" - mock_session_state = self._create_mock_session_state() - mock_template_content = textwrap.dedent(""" - spring: - ai: - openai: - api-key: test - ollama: - base-url: http://localhost:11434 - prompt: {sys_prompt} - """) - - with patch("client.content.config.tabs.settings.state", mock_session_state): - with patch("client.content.config.tabs.settings.st_common.state_configs_lookup") as mock_lookup: - with patch("builtins.open", mock_open(read_data=mock_template_content)): + with patch('client.content.config.tabs.settings.state', mock_state): + with patch('client.content.config.tabs.settings.st_common.state_configs_lookup') as mock_lookup: + with patch('builtins.open', mock_open(read_data=mock_template_content)): mock_lookup.return_value = {"DEFAULT": {"user": "test_user"}} src_dir = Path("/test/path") - result = spring_ai_obaas( - src_dir, "obaas.yaml", "openai", {"model": "gpt-4"}, {"model": "text-embedding-ada-002"} - ) + result = spring_ai_obaas(src_dir, "start.sh", "openai", {"model": "gpt-4"}, {"model": "text-embedding-ada-002"}) - assert "spring:" in result - assert "ollama:" not in result # Should be removed for openai provider + assert "Provider: openai" in result + assert "You are a helpful assistant." in result + assert "{'model': 'gpt-4'}" in result def test_spring_ai_zip_creation(self): """Test spring_ai_zip function creates proper ZIP file"""