diff --git a/README.md b/README.md index c286d80..feb853a 100644 --- a/README.md +++ b/README.md @@ -516,6 +516,10 @@ with 2 OCPUs and 16 GB memory Agent response (Schema A or B depending on resolution). +![img.png](img.png) +![img_1.png](img_1.png) + + --- ## 🐞 Troubleshooting diff --git a/files/server_mcp.py b/files/server_mcp.py index 5677d43..ba2970c 100644 --- a/files/server_mcp.py +++ b/files/server_mcp.py @@ -204,7 +204,7 @@ async def _list_shapes_from_oci(compartment_ocid: Optional[str] = None, ad: Opti @mcp.tool() 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) if lst.get("status") != "ok": return lst @@ -214,6 +214,7 @@ 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)) @@ -233,6 +234,7 @@ async def list_shapes(compartment_ocid: Optional[str] = None, ad: Optional[str] return lst items = lst["data"] + # simplificar a saída shapes = [{"shape": s.get("shape"), "ocpus": s.get("ocpus"), "memory": s.get("memoryInGBs")} for s in items] return {"status": "ok", "data": shapes} @@ -263,26 +265,29 @@ 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""" + # heurística simples para OS/versão q = query.strip() os_name, os_ver = None, None + # exemplos: "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) + # primeiro: filtro por OS/versão 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: 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) if lst.get("status") != "ok": return lst items = lst["data"] - # rank by similarity of display-name and creation date + # rankear por similitude do display-name e data de criação ranked = [] for img in items: dn = img.get("display-name","") @@ -309,15 +314,30 @@ 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 correct OCID is always in 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]) + code, out, err = oci_cli.run([ + "search", "resource", "structured-search", + "--query-text", structured + ]) if code != 0: - return {"status":"error","stderr": err, "stdout": out} + return {"status": "error", "stderr": err, "stdout": out} + data = json.loads(out) - items = data.get("data",{}).get("items",[]) - return {"status":"ok","data": items} + items = data.get("data", {}).get("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() async def create_compute_instance( @@ -346,7 +366,7 @@ async def create_compute_instance( shape-config: {"ocpus": 2, "memoryInGBs": 16} """ - # mount shape-config automatically + # montar shape-config automaticamente shape_config = None if ocpus is not None and memory is not None: shape_config = json.dumps({"ocpus": ocpus, "memoryInGBs": memory}) diff --git a/files/webchat.py b/files/webchat.py index 06272b4..da62da4 100644 --- a/files/webchat.py +++ b/files/webchat.py @@ -166,7 +166,7 @@ There are TWO categories of parameters: - ocpus - memoryInGBs 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 memoryInGBs from numbers followed by "GB", "gigabytes", "giga". - 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". ==================== +## 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 can be returned for ANY resolvable parameter: - compartment_id @@ -234,6 +256,12 @@ Rules: - 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. +- 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 @@ -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. - 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. ==================== ⚠️ IMPORTANT CONTEXT MANAGEMENT RULES @@ -320,7 +327,14 @@ Rules: - 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. + +⚠️ 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([ diff --git a/img.png b/img.png new file mode 100644 index 0000000..40be122 Binary files /dev/null and b/img.png differ diff --git a/img_1.png b/img_1.png new file mode 100644 index 0000000..dadde1c Binary files /dev/null and b/img_1.png differ