refactoring. now the solution is chat

This commit is contained in:
2025-10-16 11:01:07 -03:00
parent 06b1c17d5d
commit 1196a0dbb9
7 changed files with 766 additions and 952 deletions

7
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -0,0 +1,24 @@
# Sample guideline, please follow similar structure for guideline with code samples
# 1. Suggest using streams instead of simple loops for better readability.
# <example>
# *Comment:
# Category: Minor
# Issue: Use streams instead of a loop for better readability.
# Code Block:
#
# ```java
# // Calculate squares of numbers
# List<Integer> squares = new ArrayList<>();
# for (int number : numbers) {
# squares.add(number * number);
# }
# ```
# Recommendation:
#
# ```java
# // Calculate squares of numbers
# List<Integer> squares = Arrays.stream(numbers)
# .map(n -> n * n) // Map each number to its square
# .toList();
# ```
# </example>

1183
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +0,0 @@
import sys
import os
import json
import asyncio
from langchain_core.prompts import ChatPromptTemplate
from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import HumanMessage, AIMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
# Configuration File
with open("./config", "r") as f:
config_data = json.load(f)
# Memory Management for the OCI Resource Parameters
class MemoryState:
def __init__(self):
self.messages = []
# Define the language model
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": 2000}
)
# Prompt
prompt = ChatPromptTemplate.from_messages([
("system", """
You are an OCI Operations Agent with access to MCP tools (server `oci-ops`).
Your goal is to provision and manage OCI resources **without requiring the user to know OCIDs**.
INTERACTION RULES:
1) Wait until the user ask to create a resource
2) If all the parameters has the ocid information, create the resource
3) If all the parameters were filled by the user, create the resource
4) If a parameter given is a name and needs to be converted to a OCID, search for it automatically
5) If a parameter is missing, ask for the information
6) Do not wait for a response from creation. Inform "Creation of resource is Done."
IMPORTANT RULES:
1) Never invent OCIDs. Prefer to ask succinct follow-ups.
2) Prefer to reuse defaults from memory when appropriate
OUTPUT STYLE:
- Questions: short, one parameter at a time.
- Show: mini-summary with final values.
- Candidate lists: numbered, with name (type) — ocid — score when available.
"""),
("placeholder", "{messages}")
])
# Run the client with the MCP server
async def main():
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"],
},
},
}
)
tools = await client.get_tools()
if not tools:
print("❌ No MCP tools were loaded. Please check if the server is running.")
return
print("🛠️ Loaded tools:", [t.name for t in tools])
# Creating the LangGraph agent with in-memory state
memory_state = MemoryState()
memory_state.messages = []
agent_executor = create_react_agent(
model=llm,
tools=tools,
prompt=prompt,
)
print("🤖 READY")
while True:
query = input("You: ")
if query.lower() in ["quit", "exit"]:
break
if not query.strip():
continue
memory_state.messages.append(HumanMessage(content=query))
try:
result = await agent_executor.ainvoke({"messages": memory_state.messages})
new_messages = result.get("messages", [])
# Store new messages
memory_state.messages.extend(new_messages)
print("Assist:", new_messages[-1].content)
formatted_messages = prompt.format_messages()
except Exception as e:
print("Error:", e)
# Run the agent with asyncio
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -2,23 +2,37 @@
# ==============================================
# server_mcp.py — MCP Server (FastMCP) for OCI
# ==============================================
# Features
# - SQLite database storing OCI resource OCIDs (name, type, ocid, compartment, tags)
# - Phonetic + fuzzy search (accent-insensitive Soundex + difflib fallback)
# - Tools to: add/update/list/search resources; resolve name→OCID; simple memory KV store
# - Tool to create OCI resources via `oci` CLI (VM example + generic passthrough)
# - Designed for MCP hosts; start with: `python server_mcp.py`
# --------------------------------------------------------------
import asyncio
import json
import os
import re
import shlex
import sqlite3
import subprocess
import sys
import unicodedata
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
import os
import json
import configparser
from mcp.server.fastmcp import FastMCP
# Config File
from mcp.server.fastmcp import FastMCP
import shutil
import configparser, os, json
import oracledb
import json
import oci
import configparser
with open("./config", "r") as f:
config_data = json.load(f)
# FastMCP Server
mcp = FastMCP("oci-ops")
# ------------------------------
@@ -65,6 +79,8 @@ class OCI:
oci_cli = OCI(profile=config_data["oci_profile"])
# -------- OCI config helpers --------
import configparser
def _read_oci_config(profile: Optional[str]) -> Dict[str, str]:
cfg_path = os.path.expanduser("~/.oci/config")
cp = configparser.ConfigParser()
@@ -144,7 +160,7 @@ async def find_subnet(query_text: str) -> dict:
@mcp.tool()
async def list_availability_domains(compartment_ocid: Optional[str] = None) -> Dict[str, Any]:
"""List ADs with `oci iam availability-domain list`."""
"""Lista ADs via `oci iam availability-domain list`."""
cid = compartment_ocid or _tenancy_ocid()
if not cid:
return {"status": "error", "error": "Missing tenancy compartment OCID."}
@@ -172,8 +188,8 @@ async def find_ad(name_or_hint: str, compartment_ocid: Optional[str] = None) ->
best = scored[0]
return {"status": "ok" if best[0] >= 0.6 else "ambiguous", "ad": scored[0][1], "candidates": [n for _, n in scored[:5]]}
async def list_shapes(compartment_ocid: Optional[str] = None, ad: Optional[str] = None) -> Dict[str, Any]:
"""List the shapes with `oci compute shape list --all` (needs compartment; AD is optional)."""
async def _list_shapes_from_oci(compartment_ocid: Optional[str] = None, ad: Optional[str] = None) -> Dict[str, Any]:
"""Lista shapes via `oci compute shape list --all` (precisa compartment; AD melhora a lista)."""
cid = compartment_ocid or _tenancy_ocid()
if not cid:
return {"status": "error", "error": "Missing compartment OCID."}
@@ -188,8 +204,8 @@ async def list_shapes(compartment_ocid: Optional[str] = None, ad: Optional[str]
@mcp.tool()
async def resolve_shape(hint: str, compartment_ocid: Optional[str] = None, ad: Optional[str] = None) -> Dict[str, Any]:
"""Resolve shape informing a name 'e4'find all shapes have e4 like 'VM.Standard.E4.Flex'."""
lst = await list_shapes(compartment_ocid=compartment_ocid, ad=ad)
"""Resolves shape by hint like 'e4'best match type 'VM.Standard.E4.Flex'."""
lst = await _list_shapes_from_oci(compartment_ocid=compartment_ocid, ad=ad)
if lst.get("status") != "ok":
return lst
items = lst["data"]
@@ -198,7 +214,6 @@ async def resolve_shape(hint: str, compartment_ocid: Optional[str] = None, ad: O
for s in items:
name = s.get("shape") or ""
s1 = similarity(q, name)
# bônus para begins-with no sufixo da família
fam = _normalize(name.replace("VM.Standard.", ""))
s1 += 0.2 if fam.startswith(q) or q in fam else 0
scored.append((s1, name))
@@ -208,6 +223,19 @@ async def resolve_shape(hint: str, compartment_ocid: Optional[str] = None, ad: O
best = scored[0]
return {"status": "ok" if best[0] >= 0.6 else "ambiguous", "shape": best[1], "candidates": [n for _, n in scored[:5]]}
@mcp.tool()
async def list_shapes(compartment_ocid: Optional[str] = None, ad: Optional[str] = None) -> Dict[str, Any]:
"""
List all available compute shapes in the given compartment/availability domain.
"""
lst = await _list_shapes_from_oci(compartment_ocid=compartment_ocid, ad=ad)
if lst.get("status") != "ok":
return lst
items = lst["data"]
shapes = [{"shape": s.get("shape"), "ocpus": s.get("ocpus"), "memory": s.get("memoryInGBs")} for s in items]
return {"status": "ok", "data": shapes}
async def list_images(compartment_ocid: Optional[str] = None,
operating_system: Optional[str] = None,
operating_system_version: Optional[str] = None,
@@ -235,29 +263,26 @@ async def resolve_image(query: str,
compartment_ocid: Optional[str] = None,
shape: Optional[str] = None) -> Dict[str, Any]:
"""Find the image by a short name or similarity"""
# heuristic
q = query.strip()
os_name, os_ver = None, None
# examples: "Oracle Linux 9", "OracleLinux 9", "OL9"
if "linux" in q.lower():
os_name = "Oracle Linux"
m = re.search(r"(?:^|\\D)(\\d{1,2})(?:\\D|$)", q)
if m:
os_ver = m.group(1)
# Filter for version
lst = await list_images(compartment_ocid=compartment_ocid, operating_system=os_name, operating_system_version=os_ver)
if lst.get("status") != "ok":
return lst
items = lst["data"]
if not items:
# fallback: sem filtro, listar tudo e fazer fuzzy no display-name
# fallback: no filter, list all and make fuzzy on display-name
lst = await list_images(compartment_ocid=compartment_ocid)
if lst.get("status") != "ok":
return lst
items = lst["data"]
# ranking for display-name and creation date
# rank by similarity of display-name and creation date
ranked = []
for img in items:
dn = img.get("display-name","")
@@ -284,7 +309,7 @@ def _norm(s: str) -> str:
@mcp.tool()
async def find_compartment(query_text: str) -> dict:
"""
Find compartment ocid by the name, the compartment ocid is the identifier field
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])
@@ -307,45 +332,62 @@ async def create_compute_instance(
ssh_authorized_keys_path: Optional[str] = None,
extra_args: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Create an OCI Compute instance via `oci` CLI. Missing parameters should be asked upstream by the agent.
## Example of expected parameters to create a compute instance: ##
compartment-id: ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
subnet-id: ocid1.subnet.oc1.sa-saopaulo-1.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
shape: VM.Standard.E4.Flex
availability-domain: IAfA:SA-SAOPAULO-1-AD-1
image-id: ocid1.image.oc1.sa-saopaulo-1.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
display-name: teste_hoshikawa
shape-config: '{"ocpus": 2, "memoryInGBs": 16}'
"""
Create an OCI Compute instance via `oci` CLI.
Missing parameters should be asked upstream by the agent.
Example:
compartment_id: ocid1.compartment.oc1..aaaa...
subnet_id: ocid1.subnet.oc1.sa-saopaulo-1.aaaa...
shape: VM.Standard.E4.Flex
availability_domain: IAfA:SA-SAOPAULO-1-AD-1
image_id: ocid1.image.oc1.sa-saopaulo-1.aaaa...
display_name: teste_hoshikawa
shape-config: {"ocpus": 2, "memoryInGBs": 16}
"""
# mount shape-config automatically
shape_config = None
if ocpus is not None and memory is not None:
shape_config = json.dumps({"ocpus": ocpus, "memoryInGBs": memory})
args = [
"compute", "instance", "launch",
"--compartment-id", compartment_ocid or "",
"--subnet-id", subnet_ocid or "",
"--shape", shape or "",
"--shape-config", json.dumps({"ocpus": ocpus, "memoryInGBs": memory}),
"--availability-domain", availability_domain or "",
"--image-id", image_ocid or "",
#"--source-details", json.dumps({"sourceType": "image", "imageId": image_ocid or ""}),
]
]
if shape_config:
args += ["--shape-config", shape_config]
if display_name:
args += ["--display-name", display_name]
if ssh_authorized_keys_path:
args += ["--metadata", json.dumps({"ssh_authorized_keys": open(ssh_authorized_keys_path, "r", encoding="utf-8").read()})]
args += ["--metadata", json.dumps({
"ssh_authorized_keys": open(ssh_authorized_keys_path, "r", encoding="utf-8").read()
})]
if extra_args:
args += extra_args
# validate basics
for flag in ["--compartment-id", "--subnet-id", "--shape", "--availability-domain"]:
if "" in [args[args.index(flag)+1]]:
# validação mínima
for flag in ["--compartment-id", "--subnet-id", "--shape", "--availability-domain", "--image-id"]:
if "" in [args[args.index(flag) + 1]]:
return {"status": "error", "error": f"Missing required {flag} value"}
code, out, err = oci_cli.run(args)
if code != 0:
return {"status": "error", "error": err.strip(), "stdout": out}
try:
payload = json.loads(out)
except Exception:
payload = {"raw": out}
return {"status": "ok", "oci_result": payload}
@mcp.tool()

View File

@@ -123,6 +123,20 @@ def check_truncation(response: dict):
pass
return False
def reset_state():
memory_state.messages = []
memory_state.parameters = {
"compartment_id": None,
"subnet_id": None,
"availability_domain": None,
"image_id": None,
"shape": None,
"ocpus": None,
"memoryInGBs": None,
"display_name": None
}
memory_state.candidates = {}
# ----------------------------
# LLM
# ----------------------------
@@ -131,73 +145,182 @@ llm = ChatOCIGenAI(
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}
model_kwargs={"temperature": 0.0, "top_p": 0.0, "max_tokens": 4000}
)
# ----------------------------
# PROMPT
# ----------------------------
system_text = """
system_text = r"""
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)
## PARAMETER TYPES
There are TWO categories of parameters:
### 1. Literal parameters (must always be extracted directly from user text, never candidates):
- display_name
- ocpus
- memoryInGBs
Rules:
- Extract display_name from phrases like "vm chamada X", "nome X", "VM X".
- Extract ocpus from numbers followed by "ocpus", "OCPUs", "cores", "vCPUs".
- Extract memoryInGBs from numbers followed by "GB", "gigabytes", "giga".
- These values must NEVER be null if present in the user request.
- These values must NEVER go into "candidates".
### 2. Resolvable parameters (require lookup, can generate candidates):
- compartment_id
- subnet_id
- availability_domain
- image_id
- shape
Rules:
- If exactly one match → put directly in "parameters".
- If multiple matches → list them in "candidates" for that field.
- If no matches → leave null in "parameters" and add an "ask".
- Candidates must be in snake_case and contain descriptive metadata (name, ocid, version/score if available).
====================
## PIPELINE (MANDATORY)
### STEP 1 — Extract all values literally mentioned
- Parse every candidate value directly from the user request text.
- Do not decide yet whether it is literal or resolvable.
- Example: "create vm called test01 with 2 ocpus and 16 GB memory, image Oracle Linux 9" → extract:
{{ "display_name": "test01", "ocpus": 2, "memoryInGBs": 16, "image": "Oracle Linux 9" }}
### STEP 2 — Classify values into:
- **Literal parameters (always final, never candidates):**
- display_name
- ocpus
- memoryInGBs
- **Resolvable parameters (require OCID lookup or mapping):**
- compartment_id
- subnet_id
- availability_domain
- image_id
- shape
====================
## STEP 3 — Resolve resolvable parameters
- For each resolvable parameter (compartment_id, subnet_id, availability_domain, image_id, shape):
- If exactly one match is found → assign directly in "parameters".
- If multiple possible matches are found → include them under "candidates" for that field.
- If no matches are found → add a concise "ask".
====================
## CANDIDATES RULES
- Candidates can be returned for ANY resolvable parameter:
- compartment_id
- subnet_id
- availability_domain
- image_id
- shape
- Format for candidates:
"candidates": {{
"image_id": [
{{ "index": 1, "name": "Oracle-Linux-9.6-2025.09.16-0", "ocid": "ocid1.image.oc1....", "version": "2025.09.16", "score": 0.98 }},
{{ "index": 2, "name": "Oracle-Linux-9.6-2025.08.31-0", "ocid": "ocid1.image.oc1....", "version": "2025.08.31", "score": 0.96 }}
],
"shape": [
{{ "index": 1, "name": "VM.Standard.E4.Flex", "ocid": "ocid1.shape.oc1....", "score": 0.97 }},
{{ "index": 2, "name": "VM.Standard.A1.Flex", "ocid": "ocid1.shape.oc1....", "score": 0.94 }}
]
}}
- Do not include null values in candidates.
- Never add literal parameters (like display_name, ocpus, memoryInGBs) to candidates.
- Keys in candidates must always be snake_case.
====================
## CANDIDATES STRICT RULES
- Only generate "candidates" if there are MORE THAN ONE possible matches returned by a tool.
- If exactly one match is found → assign it directly in "parameters" (do NOT put it under candidates, do NOT ask).
- If zero matches are found → leave the parameter as null and add an "ask".
- Never ask the user to select an option if only a single match exists.
- For any parameter explicitly given in the user request (e.g., shape "VM.Standard.E4.Flex"):
- Do NOT generate candidates.
- Assume that value as authoritative.
- Only override with a candidate list if the tool fails to resolve it.
- Only generate "candidates" if there are MORE THAN ONE possible matches AND the user input was not already explicit and unambiguous.
- If the user explicitly specifies a resolvable parameter value (e.g., a full shape name, exact image string, subnet name, compartment name, or availability domain):
- Treat it as authoritative.
- Assign it directly to "parameters".
- Do NOT generate candidates and do NOT ask for confirmation.
- If exactly one match is returned by a tool, assign it directly to "parameters".
- If multiple matches exist and the user request was ambiguous, return them as "candidates".
- If no matches exist, leave the parameter as null and add an "ask".
====================
## CANDIDATE HANDLING
- Candidates are used ONLY for resolvable parameters (compartment_id, subnet_id, availability_domain, image_id, shape).
- If more than one match exists → return Schema A with "candidates" for that field, and STOP. Do not also build Schema B in the same turn.
- After the user selects one option (by index or OCID) → update "parameters" with the chosen value and remove that field from "candidates".
- Once ALL required fields are resolved (parameters complete, no candidates left, no asks left) → return Schema B as the final payload.
- Never present the same candidates more than once.
- Never mix Schema A and Schema B in a single response.
====================
## TOOL USAGE AND CANDIDATES
- For every resolvable parameter (compartment_id, subnet_id, availability_domain, image_id, shape):
- Always attempt to resolve using the proper MCP tool:
* find_compartment → for compartment_id
* find_subnet → for subnet_id
* find_ad / list_availability_domains → for availability_domain
* resolve_image / list_images → for image_id
* resolve_shape / list_shapes → for shape
- If the tool returns exactly one match → put the OCID directly in "parameters".
- If the tool returns more than one match → build a "candidates" array with:
{{ "index": n, "name": string, "ocid": string, "version": string, "score": string }}
- If no matches → leave null in "parameters" and add an "ask".
- Candidates MUST always include the **real OCIDs** from tool output.
- Never return plain names like "Oracle Linux 9" or "VM.Standard.E4.Flex" as candidates without the corresponding OCID.
- Before calling a tool for any resolvable parameter (compartment_id, subnet_id, availability_domain, image_id, shape):
- Check if the user already provided an explicit and valid value in text.
- If yes → assign directly, skip candidates, skip further resolution.
- If ambiguous (e.g., "Linux image" without version) → call tool, possibly return candidates.
- If missing entirely → call tool and return ask if nothing is found.
====================
## 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.
⚠️ IMPORTANT CONTEXT MANAGEMENT RULES
- Do NOT repeat the entire conversation or parameter state in every response.
- Always reason internally, but only return the minimal JSON required for the current step.
- Never include past candidates again once they were shown. Keep them only in memory.
- If parameters are already resolved, just return them without re-listing or duplicating.
- Summarize long context internally. Do not expand or re-echo user instructions.
- Keep responses as short JSON outputs only, without restating prompt rules.
"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".
### STEP 4 — Assemble JSON (Schema A if still resolving, Schema B if final)
- Schema A (resolving phase):
{{
"parameters": {{ all snake_case keys }},
"candidates": {{ only if ambiguity > 1 }},
"ask": string (if still missing info)
}}
- Schema B (ready for creation):
{{
"compartmentId": string,
"subnetId": string,
"availabilityDomain": string,
"imageId": string,
"displayName": string,
"shape": string,
"shapeConfig": {{ "ocpus": number, "memoryInGBs": number }}
}}
## OUTPUT CONTRACT
- While resolving:
{{
"parameters": {{ ... }},
"candidates": {{ ... }}, # only if ambiguous
"ask": "..." # only if needed
}}
- When all resolved:
{{
"compartmentId": "...",
"subnetId": "...",
"availabilityDomain": "...",
"imageId": "...",
"displayName": "...",
"shape": "...",
"shapeConfig": {{ "ocpus": <number>, "memoryInGBs": <number> }}
}}
Then return:
{{ "result": "✅ Creation of resource is Done." }}
⚠️ JSON must be strictly valid (RFC8259).
No markdown, no comments, no truncation, no null placeholders.
### STEP 5 — Output contract
- Respond ONLY with one valid JSON object.
- Never output markdown, comments, or explanations.
- Never put literal parameters in "candidates".
- Never leave literal parameters null if present in text.
- Always use snake_case for Schema A and camelCase for Schema B.
"""
prompt = ChatPromptTemplate.from_messages([
@@ -252,13 +375,24 @@ def index():
@app.route("/send", methods=["POST"])
def send():
user_message = request.form["message"]
if user_message.strip().lower() in ["reset", "newvm"]:
reset_state()
return Markup(
f"<div class='message-user'>You: {user_message}</div>"
f"<div class='message-bot'>Assistant: Status reset. You can start a new request.</div>"
)
memory_state.messages.append(HumanMessage(content=user_message))
user_html = f"<div class='message-user'>You: {user_message}</div>"
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}")
state_block = json.dumps({
"parameters": memory_state.parameters,
"candidates": memory_state.candidates
}, ensure_ascii=False)
context_message = AIMessage(content=f"Current known state:\n{state_block}")
result = asyncio.run(agent_executor.ainvoke({
"messages": memory_state.messages + [context_message]
@@ -280,46 +414,50 @@ def send():
parsed = sanitize_json(assistant_reply)
if parsed and "parameters" in parsed:
# atualiza parâmetros
# 🔹 Smart merge: only overwrites if a non-null value came in
for k, v in parsed["parameters"].items():
if v is not None:
if v not in (None, "null", ""):
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))
# injeta um comando estruturado pedindo resolução
cmd = json.dumps({
"type": "resolve",
"missing": missing,
"hint": "Return Schema A JSON only."
})
memory_state.messages.append(HumanMessage(content=cmd))
# adiciona debug_info na resposta enviada ao navegador
# adiciona debug_info à resposta
assistant_reply += "\n\n" + debug_info
# se vieram candidatos
# 🔹 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"<b>Options for {param}:</b><br>"
for c in items:
line = f"{c.get('index')}. {c.get('name')}{c.get('ocid')} — v{c.get('version')} — score {c.get('score')}"
line = f"{c.get('index')}. {c.get('name')}{c.get('ocid')} — v{c.get('version', '')} — score {c.get('score', '')}"
candidates_html += line + "<br>"
ask_text = parsed.get("ask", "Choose an index or provide the OCID.")
assistant_reply = f"{json.dumps({'parameters': memory_state.parameters}, ensure_ascii=False)}<br>{candidates_html}<i>{ask_text}</i>"
assistant_reply = (
f"{json.dumps({'parameters': memory_state.parameters}, ensure_ascii=False)}"
f"<br>{candidates_html}<i>{ask_text}</i>"
)
else:
# 🔹 Se não houver candidatos, zera
memory_state.candidates = {}
else: