refactoring. now the solution is chat

This commit is contained in:
2025-10-16 11:41:57 -03:00
parent daccf1f0fd
commit 1491f8e5b8
5 changed files with 70 additions and 32 deletions

View File

@@ -516,6 +516,10 @@ with 2 OCPUs and 16 GB memory
Agent response (Schema A or B depending on resolution). Agent response (Schema A or B depending on resolution).
![img.png](img.png)
![img_1.png](img_1.png)
--- ---
## 🐞 Troubleshooting ## 🐞 Troubleshooting

View File

@@ -204,7 +204,7 @@ async def _list_shapes_from_oci(compartment_ocid: Optional[str] = None, ad: Opti
@mcp.tool() @mcp.tool()
async def resolve_shape(hint: str, compartment_ocid: Optional[str] = None, ad: Optional[str] = None) -> Dict[str, Any]: async def resolve_shape(hint: str, compartment_ocid: Optional[str] = None, ad: Optional[str] = None) -> Dict[str, Any]:
"""Resolves shape by hint like 'e4'best match type 'VM.Standard.E4.Flex'.""" """Resolve shape por dica como 'e4'melhor match tipo 'VM.Standard.E4.Flex'."""
lst = await _list_shapes_from_oci(compartment_ocid=compartment_ocid, ad=ad) lst = await _list_shapes_from_oci(compartment_ocid=compartment_ocid, ad=ad)
if lst.get("status") != "ok": if lst.get("status") != "ok":
return lst return lst
@@ -214,6 +214,7 @@ async def resolve_shape(hint: str, compartment_ocid: Optional[str] = None, ad: O
for s in items: for s in items:
name = s.get("shape") or "" name = s.get("shape") or ""
s1 = similarity(q, name) s1 = similarity(q, name)
# bônus para begins-with no sufixo da família
fam = _normalize(name.replace("VM.Standard.", "")) fam = _normalize(name.replace("VM.Standard.", ""))
s1 += 0.2 if fam.startswith(q) or q in fam else 0 s1 += 0.2 if fam.startswith(q) or q in fam else 0
scored.append((s1, name)) scored.append((s1, name))
@@ -233,6 +234,7 @@ async def list_shapes(compartment_ocid: Optional[str] = None, ad: Optional[str]
return lst return lst
items = lst["data"] items = lst["data"]
# simplificar a saída
shapes = [{"shape": s.get("shape"), "ocpus": s.get("ocpus"), "memory": s.get("memoryInGBs")} for s in items] shapes = [{"shape": s.get("shape"), "ocpus": s.get("ocpus"), "memory": s.get("memoryInGBs")} for s in items]
return {"status": "ok", "data": shapes} return {"status": "ok", "data": shapes}
@@ -263,26 +265,29 @@ async def resolve_image(query: str,
compartment_ocid: Optional[str] = None, compartment_ocid: Optional[str] = None,
shape: Optional[str] = None) -> Dict[str, Any]: shape: Optional[str] = None) -> Dict[str, Any]:
"""Find the image by a short name or similarity""" """Find the image by a short name or similarity"""
# heurística simples para OS/versão
q = query.strip() q = query.strip()
os_name, os_ver = None, None os_name, os_ver = None, None
# exemplos: "Oracle Linux 9", "OracleLinux 9", "OL9"
if "linux" in q.lower(): if "linux" in q.lower():
os_name = "Oracle Linux" os_name = "Oracle Linux"
m = re.search(r"(?:^|\\D)(\\d{1,2})(?:\\D|$)", q) m = re.search(r"(?:^|\\D)(\\d{1,2})(?:\\D|$)", q)
if m: if m:
os_ver = m.group(1) os_ver = m.group(1)
# primeiro: filtro por OS/versão
lst = await list_images(compartment_ocid=compartment_ocid, operating_system=os_name, operating_system_version=os_ver) lst = await list_images(compartment_ocid=compartment_ocid, operating_system=os_name, operating_system_version=os_ver)
if lst.get("status") != "ok": if lst.get("status") != "ok":
return lst return lst
items = lst["data"] items = lst["data"]
if not items: if not items:
# fallback: no filter, list all and make fuzzy on display-name # fallback: sem filtro, listar tudo e fazer fuzzy no display-name
lst = await list_images(compartment_ocid=compartment_ocid) lst = await list_images(compartment_ocid=compartment_ocid)
if lst.get("status") != "ok": if lst.get("status") != "ok":
return lst return lst
items = lst["data"] items = lst["data"]
# rank by similarity of display-name and creation date # rankear por similitude do display-name e data de criação
ranked = [] ranked = []
for img in items: for img in items:
dn = img.get("display-name","") dn = img.get("display-name","")
@@ -309,15 +314,30 @@ def _norm(s: str) -> str:
@mcp.tool() @mcp.tool()
async def find_compartment(query_text: str) -> dict: 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 correct OCID is always in the 'identifier' field.
""" """
structured = f"query compartment resources where displayName =~ '.*{query_text}*.'" structured = f"query compartment resources where displayName =~ '.*{query_text}*.'"
code, out, err = oci_cli.run(["search","resource","structured-search","--query-text", structured]) code, out, err = oci_cli.run([
"search", "resource", "structured-search",
"--query-text", structured
])
if code != 0: if code != 0:
return {"status": "error", "stderr": err, "stdout": out} return {"status": "error", "stderr": err, "stdout": out}
data = json.loads(out) data = json.loads(out)
items = data.get("data", {}).get("items", []) items = data.get("data", {}).get("items", [])
return {"status":"ok","data": items}
results = []
for item in items:
results.append({
"name": item.get("displayName"),
"ocid": item.get("identifier"), # 🔑 este é o OCID correto
"lifecycle_state": item.get("lifecycleState"),
"time_created": item.get("timeCreated")
})
return {"status": "ok", "data": results}
@mcp.tool() @mcp.tool()
async def create_compute_instance( async def create_compute_instance(
@@ -346,7 +366,7 @@ async def create_compute_instance(
shape-config: {"ocpus": 2, "memoryInGBs": 16} shape-config: {"ocpus": 2, "memoryInGBs": 16}
""" """
# mount shape-config automatically # montar shape-config automaticamente
shape_config = None shape_config = None
if ocpus is not None and memory is not None: if ocpus is not None and memory is not None:
shape_config = json.dumps({"ocpus": ocpus, "memoryInGBs": memory}) shape_config = json.dumps({"ocpus": ocpus, "memoryInGBs": memory})

View File

@@ -166,7 +166,7 @@ There are TWO categories of parameters:
- ocpus - ocpus
- memoryInGBs - memoryInGBs
Rules: Rules:
- Extract display_name from phrases like "vm chamada X", "nome X", "VM X". - Extract display_name from phrases like "vm called X", "nome X", "VM X".
- Extract ocpus from numbers followed by "ocpus", "OCPUs", "cores", "vCPUs". - Extract ocpus from numbers followed by "ocpus", "OCPUs", "cores", "vCPUs".
- Extract memoryInGBs from numbers followed by "GB", "gigabytes", "giga". - 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 be null if present in the user request.
@@ -213,6 +213,28 @@ Rules:
- If no matches are found → add a concise "ask". - If no matches are found → add a concise "ask".
==================== ====================
## 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.
====================
## CANDIDATES RULES ## CANDIDATES RULES
- Candidates can be returned for ANY resolvable parameter: - Candidates can be returned for ANY resolvable parameter:
- compartment_id - compartment_id
@@ -234,6 +256,12 @@ Rules:
- Do not include null values in candidates. - Do not include null values in candidates.
- Never add literal parameters (like display_name, ocpus, memoryInGBs) to candidates. - Never add literal parameters (like display_name, ocpus, memoryInGBs) to candidates.
- Keys in candidates must always be snake_case. - Keys in candidates must always be snake_case.
- Ordering rules:
* For image_id → sort by version/date (newest first).
* For shape → sort by score (highest first).
* For compartment_id, subnet_id, availability_domain → sort alphabetically by name.
- After sorting, reindex candidates starting at 1.
- Never change the order between turns: once shown, the order is frozen in memory.
==================== ====================
## CANDIDATES STRICT RULES ## CANDIDATES STRICT RULES
@@ -263,28 +291,7 @@ Rules:
- Once ALL required fields are resolved (parameters complete, no candidates left, no asks left) → return Schema B as the final payload. - 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 present the same candidates more than once.
- Never mix Schema A and Schema B in a single response. - 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.
==================== ====================
⚠️ IMPORTANT CONTEXT MANAGEMENT RULES ⚠️ IMPORTANT CONTEXT MANAGEMENT RULES
@@ -320,7 +327,14 @@ Rules:
- Never output markdown, comments, or explanations. - Never output markdown, comments, or explanations.
- Never put literal parameters in "candidates". - Never put literal parameters in "candidates".
- Never leave literal parameters null if present in text. - Never leave literal parameters null if present in text.
- Always use snake_case for Schema A and camelCase for Schema B.
⚠️ IMPORTANT:
- Use **exclusively** snake_case for Schema A (parameters, candidates, ask).
- Use **exclusively** camelCase for Schema B (final payload for create).
- Never mix both styles in the same JSON.
- If you are in Schema A, do NOT include camelCase keys like `compartmentId` or `shapeConfig`.
- If you are in Schema B, do NOT include snake_case keys like `compartment_id` or `display_name`.
""" """
prompt = ChatPromptTemplate.from_messages([ prompt = ChatPromptTemplate.from_messages([

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
img_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB