mirror of
https://github.com/hoshikawa2/agent_oci_automation.git
synced 2026-03-06 10:11:02 +00:00
refactoring. now the solution is chat
This commit is contained in:
274
files/webchat.py
274
files/webchat.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user