p08-sre-agent / src /agent.py
amarshiv86's picture
Upload folder using huggingface_hub
5ac9570 verified
Raw
History Blame Contribute Delete
11.1 kB
"""
P08 · SRE Agent — LangGraph implementation
ReAct pattern: Reason → Act → Observe → repeat until done.
Human-in-the-loop: tools marked requires_approval=True need explicit user confirmation.
"""
import json
import re
from typing import Any
from .tools import TOOLS, ToolResult
# ── Agent state ───────────────────────────────────────────────────────────────
class AgentState:
def __init__(self, query: str):
self.query = query
self.steps: list[dict] = [] # full trace
self.pending_approval: dict | None = None # tool waiting for human
self.final_answer: str = ""
self.done: bool = False
self.error: str = ""
def add_step(self, step_type: str, content: Any):
self.steps.append({"type": step_type, "content": content})
def to_trace(self) -> str:
lines = []
for s in self.steps:
if s["type"] == "thought":
lines.append(f"🤔 **Thought:** {s['content']}")
elif s["type"] == "tool_call":
lines.append(f"🔧 **Tool:** `{s['content']['tool']}` → `{json.dumps(s['content']['args'])}`")
elif s["type"] == "tool_result":
r: ToolResult = s["content"]
if r.success:
lines.append(f"✅ **Result ({r.latency_ms}ms):** ```json\n{json.dumps(r.data, indent=2)}\n```")
else:
lines.append(f"❌ **Error:** {r.error}")
elif s["type"] == "approval_needed":
lines.append(f"⚠️ **Approval needed for:** `{s['content']}`")
elif s["type"] == "approved":
lines.append(f"✅ **Approved:** `{s['content']}`")
elif s["type"] == "rejected":
lines.append(f"🚫 **Rejected:** `{s['content']}`")
return "\n\n".join(lines)
# ── Prompt builder ────────────────────────────────────────────────────────────
def build_system_prompt() -> str:
tool_descriptions = "\n".join(
f"- {name}: {info['description']}"
for name, info in TOOLS.items()
)
return f"""You are an SRE on-call assistant. Answer questions about service health,
SLOs, alerts, and runbooks using the available tools.
Available tools:
{tool_descriptions}
To use a tool, respond with EXACTLY this format (nothing else on that line):
TOOL: tool_name
ARGS: {{"arg1": "value1", "arg2": "value2"}}
To give a final answer, respond with:
ANSWER: your complete answer here
Rules:
- Always use tools to get real data before answering
- Use at most 5 tool calls per query
- If a tool fails, try an alternative approach
- Be concise and actionable in your final answer
- Always include the service name and severity in your answer"""
def build_user_prompt(state: AgentState) -> str:
if not state.steps:
return f"User query: {state.query}"
history = []
for step in state.steps:
if step["type"] == "thought":
history.append(f"Thought: {step['content']}")
elif step["type"] == "tool_call":
tc = step["content"]
history.append(f"Tool called: {tc['tool']} with {tc['args']}")
elif step["type"] == "tool_result":
r: ToolResult = step["content"]
if r.success:
history.append(f"Tool result: {json.dumps(r.data)}")
else:
history.append(f"Tool error: {r.error}")
history_str = "\n".join(history)
return f"User query: {state.query}\n\nHistory:\n{history_str}\n\nContinue:"
# ── Tool call parser ──────────────────────────────────────────────────────────
def parse_tool_call(text: str) -> dict | None:
"""Parse TOOL/ARGS block from model output."""
tool_match = re.search(r"TOOL:\s*(\w+)", text)
args_match = re.search(r"ARGS:\s*(\{.*?\})", text, re.DOTALL)
answer_match = re.search(r"ANSWER:\s*(.+)", text, re.DOTALL)
if answer_match:
return {"type": "answer", "content": answer_match.group(1).strip()}
if tool_match:
tool_name = tool_match.group(1).strip()
args = {}
if args_match:
try:
args = json.loads(args_match.group(1))
except json.JSONDecodeError:
pass
return {"type": "tool_call", "tool": tool_name, "args": args}
return None
# ── Execute tool ──────────────────────────────────────────────────────────────
def execute_tool(tool_name: str, args: dict) -> ToolResult:
if tool_name not in TOOLS:
return ToolResult(
tool=tool_name,
success=False,
data=None,
error=f"Unknown tool '{tool_name}'. Available: {list(TOOLS.keys())}",
)
fn = TOOLS[tool_name]["fn"]
try:
return fn(**args)
except TypeError as e:
return ToolResult(
tool=tool_name,
success=False,
data=None,
error=f"Invalid arguments for {tool_name}: {e}",
)
except Exception as e:
return ToolResult(
tool=tool_name,
success=False,
data=None,
error=f"Tool execution failed: {e}",
)
# ── Main agent class ──────────────────────────────────────────────────────────
class SREAgent:
"""
LangGraph-style ReAct agent with human-in-the-loop approval.
Uses local transformers model — no external API calls.
"""
def __init__(self, pipe):
"""pipe: a transformers text-generation pipeline."""
self.pipe = pipe
self.max_steps = 5
def _call_llm(self, state: AgentState) -> str:
system = build_system_prompt()
user = build_user_prompt(state)
prompt = (
f"<|im_start|>system\n{system}<|im_end|>\n"
f"<|im_start|>user\n{user}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
output = self.pipe(prompt, return_full_text=False)[0]["generated_text"]
# Stop at next turn marker
return output.split("<|im_end|>")[0].strip()
def run(self, query: str) -> AgentState:
"""
Run the agent synchronously.
Tools requiring approval will pause and set state.pending_approval.
Call run_with_approval() to continue after user approves.
"""
state = AgentState(query)
step_count = 0
while not state.done and step_count < self.max_steps:
step_count += 1
# Get LLM decision
llm_output = self._call_llm(state)
parsed = parse_tool_call(llm_output)
if parsed is None:
# No structured output — treat as final answer
state.final_answer = llm_output
state.done = True
break
if parsed["type"] == "answer":
state.final_answer = parsed["content"]
state.done = True
break
if parsed["type"] == "tool_call":
tool_name = parsed["tool"]
args = parsed["args"]
state.add_step("tool_call", {"tool": tool_name, "args": args})
# Check if tool requires human approval
tool_info = TOOLS.get(tool_name, {})
if tool_info.get("requires_approval", False):
state.add_step("approval_needed", f"{tool_name}({args})")
state.pending_approval = {"tool": tool_name, "args": args}
break # Pause for human input
# Execute tool
result = execute_tool(tool_name, args)
state.add_step("tool_result", result)
if step_count >= self.max_steps and not state.done:
state.final_answer = "Reached maximum steps. Based on gathered data, please review the tool results above."
state.done = True
return state
def approve_and_continue(self, state: AgentState) -> AgentState:
"""Continue after human approves a pending tool call."""
if not state.pending_approval:
return state
tool_name = state.pending_approval["tool"]
args = state.pending_approval["args"]
state.add_step("approved", f"{tool_name}({args})")
result = execute_tool(tool_name, args)
state.add_step("tool_result", result)
state.pending_approval = None
# Continue the agent loop
return self.run_continue(state)
def reject_and_continue(self, state: AgentState) -> AgentState:
"""Continue after human rejects a pending tool call."""
if not state.pending_approval:
return state
tool_name = state.pending_approval["tool"]
state.add_step("rejected", tool_name)
state.add_step(
"tool_result",
ToolResult(
tool=tool_name,
success=False,
data=None,
error="Tool call rejected by user",
),
)
state.pending_approval = None
return self.run_continue(state)
def run_continue(self, state: AgentState) -> AgentState:
"""Continue running from an existing state."""
step_count = len(state.steps)
while not state.done and step_count < self.max_steps:
step_count += 1
llm_output = self._call_llm(state)
parsed = parse_tool_call(llm_output)
if parsed is None or parsed["type"] == "answer":
state.final_answer = (
parsed["content"] if parsed and parsed["type"] == "answer"
else llm_output
)
state.done = True
break
if parsed["type"] == "tool_call":
tool_name = parsed["tool"]
args = parsed["args"]
state.add_step("tool_call", {"tool": tool_name, "args": args})
tool_info = TOOLS.get(tool_name, {})
if tool_info.get("requires_approval", False):
state.add_step("approval_needed", f"{tool_name}({args})")
state.pending_approval = {"tool": tool_name, "args": args}
break
result = execute_tool(tool_name, args)
state.add_step("tool_result", result)
if step_count >= self.max_steps and not state.done:
state.final_answer = "Reached maximum steps. Review tool results above."
state.done = True
return state