File size: 7,869 Bytes
205f6c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13558c9
205f6c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13558c9
205f6c7
 
 
 
 
 
 
 
 
 
 
 
 
 
13558c9
205f6c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
"""
DebugOps inference script
=
Runs the LLM agent (or heuristic fallback) against all three tasks and
emits structured stdout logs in the exact required format:

    [START] task=<name> env=debugops model=<model>
    [STEP]  step=<n> action=<a> reward=<r> done=<bool> error=<null|err>
    [END]   success=<bool> steps=<n> score=<s> rewards=<comma-list>

Environment variables

API_BASE_URL   LLM endpoint  (default: HuggingFace router)
MODEL_NAME     Model ID      (default: Qwen/Qwen2.5-72B-Instruct)
HF_TOKEN       API key (also checked as OPENAI_API_KEY or API_KEY)
"""
from __future__ import annotations
import os
import sys
from typing import Any, Dict, List

try:
    from openai import OpenAI
    _OPENAI_AVAILABLE = True
except ImportError:
    _OPENAI_AVAILABLE = False

from grader.grader import evaluate_episode

# Configuration
API_KEY      = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY") or os.getenv("API_KEY")
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
MODEL_NAME   = os.getenv("MODEL_NAME",   "Qwen/Qwen2.5-72B-Instruct")

VALID_ACTIONS = ["restart_api", "restart_db", "restart_cache", "scale_up", "noop"]

# OpenAI client (spec requires OpenAI client for all LLM calls)
_client: Any = None
if _OPENAI_AVAILABLE and API_KEY:
    _client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)


# Heuristic fallback agent (used when no LLM key or on API error)
def _fallback_agent(state: Dict[str, Any], action_history: List[str]) -> str:
    """
    Rule-based agent driven by log keywords and fix_progress from state.
    Uses fix_progress from the observation to know exactly which step to issue next.
    """
    logs = " ".join(state.get("logs", [])).lower()
    metrics = state.get("metrics", {})
    progress = state.get("fix_progress", 0)

    # Known fix sequences keyed by log signal (ordered by specificity)
    _log_to_seq = [
        ("timeout",     ["scale_up",      "restart_api"]),
        ("upstream",    ["scale_up",      "restart_api"]),
        ("heap",        ["restart_api",   "restart_db"]),
        ("oom",         ["restart_api",   "restart_db"]),
        ("memory",      ["restart_api",   "restart_db"]),
        ("db pool",     ["restart_db",    "scale_up"]),
        ("connections", ["restart_db",    "scale_up"]),
        ("cache miss",  ["restart_cache", "scale_up"]),
        ("cache",       ["restart_cache", "scale_up"]),
        ("db",          ["restart_db",    "scale_up"]),
    ]

    # Identify sequence from logs
    seq = None
    for keyword, candidate_seq in _log_to_seq:
        if keyword in logs:
            seq = candidate_seq
            break

    if seq and progress < len(seq):
        # Issue the next step in the correct sequence
        return seq[progress]

    # Metric-based fallback
    if metrics.get("cpu", 0) > 80 and "scale_up" not in action_history:
        return "scale_up"
    if metrics.get("error_rate", 0) > 0.5 and "restart_api" not in action_history:
        return "restart_api"

    # Try any action not yet used twice
    for a in ["restart_api", "restart_db", "restart_cache", "scale_up"]:
        if action_history.count(a) < 2:
            return a

    return "noop"

# LLM call (OpenAI client as required)
def _call_llm(
    state: Dict[str, Any],
    action_history: List[str],
    reward_history: List[float],
) -> str:
    if _client is None:
        return _fallback_agent(state, action_history)

    # Build a human-readable step history with outcome signals
    step_lines = []
    for i, (a, r) in enumerate(zip(action_history, reward_history)):
        outcome = "✓ progress made" if r > 20 else ("✗ wrong / no effect" if r < -5 else "~ neutral")
        step_lines.append(f"  step {i}: {a:18s} reward={r:>8.1f}  [{outcome}]")
    history_block = "\n".join(step_lines) if step_lines else "  (none yet)"

    services_degraded = [s for s, h in state["services"].items() if h == "degraded"]
    metrics = state["metrics"]

    prompt = f"""You are an expert SRE triaging a production incident. Your goal is to resolve it in as few steps as possible.

SYSTEM STATE (step {state['time_step']}) 
Degraded services : {services_degraded if services_degraded else 'none'}
Metrics           : latency={metrics.get('latency', 0):.0f}ms  error_rate={metrics.get('error_rate', 0):.2%}  cpu={metrics.get('cpu', 0):.0f}%
Metric trend      : {state.get('metric_trend', 'unknown')}
Fix progress      : {state.get('fix_progress', 0)} step(s) completed correctly so far

SYSTEM LOGS 
{chr(10).join('  ' + l for l in state['logs'])}

ACTION HISTORY & OUTCOMES 
{history_block}

INSTRUCTIONS 
Root causes have multi-step fix sequences that MUST be performed in order.
A positive reward means the last action was a correct step — continue the sequence.
A negative reward means the last action was wrong — try something different.
Do NOT repeat an action that already got a negative reward.
If fix_progress increased after your last action, continue to the NEXT step in the sequence.

Choose ONE action from: restart_api, restart_db, restart_cache, scale_up, noop

Respond with ONLY the action name."""

    try:
        response = _client.chat.completions.create(
            model=MODEL_NAME,
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            max_tokens=20,
            timeout=15,
        )
        action = response.choices[0].message.content.strip().lower()
        action = action.split()[0] if action else "noop"
        # strip any punctuation
        action = "".join(c for c in action if c.isalnum() or c == "_")

        if action not in VALID_ACTIONS:
            return _fallback_agent(state, action_history)
        return action

    except Exception:
        return _fallback_agent(state, action_history)


def run_episode(task_name: str = "simple") -> None:
    if task_name == "simple":
        from tasks.task_simple import create_env
        max_steps = 15
    elif task_name == "multi_service":
        from tasks.task_multi_service import create_env
        max_steps = 12
    elif task_name == "critical":
        from tasks.task_critical import create_env
        max_steps = 10
    else:
        raise ValueError(f"Unknown task: {task_name!r}")

    env = create_env()
    state = env.reset()

    print(
        f"[START] task={task_name} env=debugops model={MODEL_NAME}",
        flush=True,
    )

    action_history: List[str] = []
    reward_history: List[float] = []
    episode_logs:   List[Dict[str, Any]] = []
    done = False
    step = 0

    while not done and step < max_steps:
        action = _call_llm(state, action_history, reward_history)

        try:
            next_state, reward, done, info = env.step(action)
            error = "null"
        except Exception as exc:
            next_state, reward, done, info = state, 0.0, True, {}
            error = type(exc).__name__

        action_history.append(action)
        reward_history.append(reward)
        episode_logs.append({"reward": reward, "info": info})

        print(
            f"[STEP] step={step} action={action} reward={round(reward, 3)} "
            f"done={str(done).lower()} error={error}",
            flush=True,
        )

        # Early exit on explicit success flag
        if info.get("success", False):
            done = True

        state = next_state
        step += 1

    result = evaluate_episode(episode_logs, max_steps=max_steps)
    score   = result["score"]                    # already in [0, 1]
    success = result["resolved"]

    print(
        f"[END] success={str(success).lower()} steps={step} "
        f"score={round(score, 3)} "
        f"rewards={','.join(str(round(e['reward'], 3)) for e in episode_logs)}",
        flush=True,
    )


if __name__ == "__main__":
    for task in ["simple", "multi_service", "critical"]:
        run_episode(task)