mirror of
https://github.com/hoshikawa2/agent_oci_automation.git
synced 2026-03-06 02:10:37 +00:00
refactoring. now the solution is chat
This commit is contained in:
@@ -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).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐞 Troubleshooting
|
## 🐞 Troubleshooting
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user