diff --git a/files/server_mcp.py b/files/server_mcp.py
index 9d542ca..3de087d 100644
--- a/files/server_mcp.py
+++ b/files/server_mcp.py
@@ -284,7 +284,7 @@ def _norm(s: str) -> str:
@mcp.tool()
async def find_compartment(query_text: str) -> dict:
"""
- Find compartment ocid by the name
+ Find compartment ocid by the name, the compartment ocid is the identifier field
"""
structured = f"query compartment resources where displayName =~ '.*{query_text}*.'"
code, out, err = oci_cli.run(["search","resource","structured-search","--query-text", structured])
diff --git a/files/templates/chat.html b/files/templates/chat.html
new file mode 100644
index 0000000..13ac421
--- /dev/null
+++ b/files/templates/chat.html
@@ -0,0 +1,143 @@
+
+
+
+
+ OCI WebChat
+
+
+
+
+💻 OCI WebChat
+
+
+
+
+
+
\ No newline at end of file
diff --git a/files/webchat.py b/files/webchat.py
new file mode 100644
index 0000000..4d6677a
--- /dev/null
+++ b/files/webchat.py
@@ -0,0 +1,337 @@
+# -*- coding: utf-8 -*-
+import os
+import sys
+import json
+import asyncio
+from flask import Flask, render_template, request
+from markupsafe import Markup
+import re
+
+from langchain_core.prompts import ChatPromptTemplate
+from langchain_core.messages import HumanMessage, AIMessage
+from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI
+from langgraph.prebuilt import create_react_agent
+from langchain_mcp_adapters.client import MultiServerMCPClient
+
+# ----------------------------
+# CONFIG
+# ----------------------------
+with open("./config", "r") as f:
+ config_data = json.load(f)
+
+app = Flask(__name__)
+
+# ----------------------------
+# MEMORY STATE
+# ----------------------------
+class MemoryState:
+ def __init__(self):
+ self.messages = []
+ self.parameters = {
+ "compartment_id": None,
+ "subnet_id": None,
+ "availability_domain": None,
+ "image_id": None,
+ "shape": None,
+ "ocpus": None,
+ "memoryInGBs": None,
+ "display_name": None,
+ }
+ self.candidates = {} # <- novo
+
+memory_state = MemoryState()
+
+def safe_json_extract(text: str):
+ try:
+ match = re.search(r"\{.*\}", text, re.DOTALL)
+ if match:
+ cleaned = match.group(0).strip()
+ # If the JSON comes truncated (missing closing braces)
+ if cleaned.count("{") > cleaned.count("}"):
+ cleaned += "}" * (cleaned.count("{") - cleaned.count("}"))
+ print("⚠️ JSON truncated, added closing brace(s).")
+ return json.loads(cleaned)
+ except Exception as e:
+ print("⚠️ Failed to parse JSON:", e)
+ return None
+
+def sanitize_json(text: str):
+ """
+ Try to fix and parse malformed JSON from LLM responses.
+ """
+ if not text:
+ return None
+
+ # Removes extra spaces/lines
+ cleaned = text.strip()
+
+ # If text comes outside of JSON, extract only the object
+ match = re.search(r'\{.*\}', cleaned, re.DOTALL)
+ if match:
+ cleaned = match.group(0)
+
+ # Remove commas before closing object/array
+ cleaned = re.sub(r',(\s*[}\]])', r'\1', cleaned)
+
+ # Balance brackets []
+ open_brackets = cleaned.count('[')
+ close_brackets = cleaned.count(']')
+ if open_brackets > close_brackets:
+ cleaned += "]" * (open_brackets - close_brackets)
+ elif close_brackets > open_brackets:
+ # remove extra brackets at the end
+ cleaned = cleaned.rstrip("]")
+
+ # Balance braces {}
+ open_braces = cleaned.count('{')
+ close_braces = cleaned.count('}')
+ if open_braces > close_braces:
+ cleaned += "}" * (open_braces - close_braces)
+ elif close_braces > open_braces:
+ # removes leftover keys at the end
+ cleaned = cleaned.rstrip("}")
+
+ try:
+ return json.loads(cleaned)
+ except Exception as e:
+ print("⚠️ Still invalid after sanitization:", e)
+ print("🔎 Sanitized content:", cleaned[:500])
+ print("🔎 Original:", text)
+ return None
+
+def validate_payload(params):
+ """Checks if all mandatory parameters are filled in"""
+ required = ["compartment_id", "subnet_id", "availability_domain",
+ "image_id", "shape", "ocpus", "memoryInGBs", "display_name"]
+ missing = [r for r in required if not params.get(r)]
+ return missing
+
+def check_truncation(response: dict):
+ """
+ VChecks if the response was truncated due to token limit.
+ The object returned by OCI Generative AI contains token usage.
+ """
+ try:
+ usage = response.get("usage", {})
+ if usage:
+ completion_tokens = usage.get("completion_tokens", 0)
+ max_allowed = llm.model_kwargs.get("max_tokens", 0)
+ if completion_tokens >= max_allowed:
+ print("⚠️ Response possibly truncated by max_tokens.")
+ return True
+ except Exception:
+ pass
+ return False
+
+# ----------------------------
+# LLM
+# ----------------------------
+llm = ChatOCIGenAI(
+ model_id="cohere.command-r-08-2024",
+ service_endpoint=config_data["llm_endpoint"],
+ compartment_id=config_data["compartment_id"],
+ auth_profile=config_data["oci_profile"],
+ model_kwargs={"temperature": 0.1, "top_p": 0.75, "max_tokens": 4000}
+)
+
+# ----------------------------
+# PROMPT
+# ----------------------------
+
+system_text = """
+You are an **OCI Operations Agent** with access to MCP tools (server `oci-ops`).
+Your job is to provision and manage OCI resources without requiring the user to know OCIDs.
+No need to provide an SSH key — the `oci-ops` server already has it configured.
+
+====================
+## TOOLS
+- `create_compute_instance` → Create a new Compute instance
+- `resolve_image` / `list_images` → Resolve or list images
+- `resolve_shape` / `list_shapes` → Resolve or list shapes
+- `find_subnet` → Find subnet
+- `find_compartment` → Find compartment
+- `find_ad` / `list_availability_domains` → Resolve or list availability domains
+- `oci_cli_passthrough` → Run raw OCI CLI (expert use only)
+====================
+
+## RULES
+- Parameters: compartment_id, subnet_id, availability_domain, image_id, shape, ocpus, memoryInGBs, display_name.
+- Use **snake_case** for parameters at all times.
+- Only when ALL parameters are resolved → build the `create_compute_instance` payload using **camelCase**.
+- If ambiguous (>1 results) → return in "candidates" with this format:
+- Always use snake_case for "parameters": compartment_id, subnet_id, availability_domain, image_id, shape, ocpus, memoryInGBs, display_name.
+- Only when calling `create_compute_instance`, convert to camelCase: compartmentId, subnetId, availabilityDomain, imageId, displayName, shape, shapeConfig.
+- Never mix snake_case and camelCase in the same JSON object.
+
+"candidates": {{
+ "image_id": [
+ {{ "index": 1, "name": "Oracle-Linux-9.6-2025.09.16-0", "ocid": "ocid1.image.oc1....", "version": "2025.09.16", "score": 0.99 }},
+ {{ "index": 2, "name": "Oracle-Linux-9.6-2025.08.31-0", "ocid": "ocid1.image.oc1....", "version": "2025.08.31", "score": 0.97 }}
+ ]
+}}
+
+- Do not include null/None values in candidates.
+- If no matches → just return "ask".
+- If exactly one → assign directly in "parameters".
+
+## OUTPUT CONTRACT
+- While resolving:
+{{
+ "parameters": {{ ... }},
+ "candidates": {{ ... }}, # only if ambiguous
+ "ask": "..." # only if needed
+}}
+
+- When all resolved:
+{{
+ "compartmentId": "...",
+ "subnetId": "...",
+ "availabilityDomain": "...",
+ "imageId": "...",
+ "displayName": "...",
+ "shape": "...",
+ "shapeConfig": {{ "ocpus": , "memoryInGBs": }}
+}}
+
+Then return:
+{{ "result": "✅ Creation of resource is Done." }}
+
+⚠️ JSON must be strictly valid (RFC8259).
+No markdown, no comments, no truncation, no null placeholders.
+"""
+
+prompt = ChatPromptTemplate.from_messages([
+ ("system", system_text),
+ ("placeholder", "{messages}")
+])
+
+# ----------------------------
+# MCP TOOLS
+# ----------------------------
+client = MultiServerMCPClient(
+ {
+ "oci-ops": {
+ "command": sys.executable,
+ "args": ["server_mcp.py"],
+ "transport": "stdio",
+ "env": {
+ "PATH": os.environ.get("PATH", "") + os.pathsep + os.path.expanduser("~/.local/bin"),
+ "OCI_CLI_BIN": config_data["OCI_CLI_BIN"],
+ "OCI_CLI_PROFILE": config_data["oci_profile"],
+ },
+ },
+ }
+)
+
+async def load_tools():
+ tools = await client.get_tools()
+ if not tools:
+ print("❌ No MCP tools loaded")
+ else:
+ print("🛠️ Loaded tools:", [t.name for t in tools])
+ return tools
+
+tools = asyncio.get_event_loop().run_until_complete(load_tools())
+
+# ----------------------------
+# AGENT EXECUTOR
+# ----------------------------
+agent_executor = create_react_agent(
+ model=llm,
+ tools=tools,
+ prompt=prompt,
+)
+
+# ----------------------------
+# FLASK ROUTES
+# ----------------------------
+@app.route("/")
+def index():
+ return render_template("chat.html")
+
+@app.route("/send", methods=["POST"])
+def send():
+ user_message = request.form["message"]
+ memory_state.messages.append(HumanMessage(content=user_message))
+ user_html = f"You: {user_message}
"
+
+ try:
+ # injeta estado atual na conversa
+ params_json = json.dumps({"parameters": memory_state.parameters}, indent=2)
+ context_message = AIMessage(content=f"Current known parameters:\n{params_json}")
+
+ result = asyncio.run(agent_executor.ainvoke({
+ "messages": memory_state.messages + [context_message]
+ }))
+
+ debug_info = ""
+
+ if check_truncation(result):
+ debug_info += "\n\n⚠️ Warning: Response truncated by max_tokens limit."
+
+ new_messages = result.get("messages", [])
+
+ if new_messages:
+ memory_state.messages.extend(new_messages)
+ assistant_reply = new_messages[-1].content
+
+ parsed = safe_json_extract(assistant_reply)
+ if not parsed: # fallback se falhar
+ parsed = sanitize_json(assistant_reply)
+
+ if parsed and "parameters" in parsed:
+ # atualiza parâmetros
+ for k, v in parsed["parameters"].items():
+ if v is not None:
+ memory_state.parameters[k] = v
+ print("📌 Current status:", memory_state.parameters)
+
+ missing = validate_payload(memory_state.parameters)
+ if not missing:
+ print("✅ All parameters filled in. The agent should now create the VM..")
+ else:
+ print("⚠️ Faltando parâmetros:", missing)
+ if not missing:
+ debug_info += "\n✅ All parameters filled in. The agent should now create the VM.."
+ else:
+ debug_info += f"\n⚠️ Missing parameters: {missing}"
+
+ if missing:
+ auto_followup = f"Please resolve the following missing parameters: {missing}"
+ memory_state.messages.append(HumanMessage(content=auto_followup))
+
+ # adiciona debug_info na resposta enviada ao navegador
+ assistant_reply += "\n\n" + debug_info
+
+ # se vieram candidatos
+ if parsed and "candidates" in parsed and parsed["candidates"]:
+ memory_state.candidates = parsed["candidates"]
+ print("🔍 Candidates found:", memory_state.candidates)
+
+ # monta bloco HTML de candidatos
+ candidates_html = ""
+ for param, items in memory_state.candidates.items():
+ candidates_html += f"Options for {param}:
"
+ for c in items:
+ line = f"{c.get('index')}. {c.get('name')} — {c.get('ocid')} — v{c.get('version')} — score {c.get('score')}"
+ candidates_html += line + "
"
+
+ ask_text = parsed.get("ask", "Choose an index or provide the OCID.")
+ assistant_reply = f"{json.dumps({'parameters': memory_state.parameters}, ensure_ascii=False)}
{candidates_html}{ask_text}"
+
+ else:
+ memory_state.candidates = {}
+
+ else:
+ assistant_reply = "⚠️ No response from agent."
+ except Exception as e:
+ assistant_reply = f"⚠️ Erro: {e}"
+
+ bot_html = f"Assistant: {assistant_reply}
"
+ return Markup(user_html + bot_html)
+
+# ----------------------------
+# MAIN
+# ----------------------------
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=8080, debug=True)
\ No newline at end of file