File size: 8,046 Bytes
ed41ca2
 
2b16d5d
ed41ca2
d011d31
 
 
 
ed41ca2
 
 
 
 
 
 
2b16d5d
d011d31
 
ed41ca2
2b16d5d
ed41ca2
 
2b16d5d
ed41ca2
 
 
 
d011d31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed41ca2
 
2b16d5d
d011d31
2b16d5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed41ca2
d011d31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed41ca2
2b16d5d
ed41ca2
 
2b16d5d
 
ed41ca2
2b16d5d
ed41ca2
 
2b16d5d
 
 
 
ed41ca2
 
d011d31
2b16d5d
ed41ca2
 
2b16d5d
ed41ca2
d011d31
 
 
 
 
ed41ca2
 
2b16d5d
d011d31
2b16d5d
 
d011d31
2b16d5d
 
 
 
 
ed41ca2
2b16d5d
ed41ca2
 
2b16d5d
 
ed41ca2
 
2b16d5d
 
ed41ca2
2b16d5d
ed41ca2
2b16d5d
 
ed41ca2
 
 
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
"""
inference.py — OpenEnv standard inference entry point for Repair Strategy System V2.
Produces structured output required by the OpenEnv validator.

Checklist Compliance:
1. Environment variables: API_BASE_URL, MODEL_NAME, HF_TOKEN, LOCAL_IMAGE_NAME
2. OpenAI client configured via these variables.
3. [START], [STEP], [END] tags strictly followed.
"""

from __future__ import annotations

import sys
import os
import argparse
import random
import json
from openai import OpenAI

# Ensure the repo root is on the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from env_v2 import RepairEnvV2, COMPONENTS
from models_v2 import Action, ActionType, ComponentTarget
from tasks_v2 import TASKS

# ---------------------------------------------------------------------------
# OpenEnv Configuration (Pre-Submission Checklist)
# ---------------------------------------------------------------------------

API_BASE_URL = os.getenv("API_BASE_URL", "https://api-inference.huggingface.co/v1/")
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o")
HF_TOKEN = os.getenv("HF_TOKEN") # NO DEFAULT
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")

# Initialize OpenAI client
# Note: If HF_TOKEN is missing, client creation might succeed but calls will fail.
# We handle this by falling back to the baseline agent.
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN) if HF_TOKEN else None

# ---------------------------------------------------------------------------
# Agents
# ---------------------------------------------------------------------------

class InferenceAgent:
    """Baseline deterministic agent for reliable evaluation."""
    def __init__(self):
        self.diagnosed_roots = set()
        self.inspected = set()
        self.fixed_roots = set()
        self.wait_ticks = 0
        self._rng = random.Random(42)

    def pick_action(self, obs) -> Action:
        # Flaw: 20% of the time it just randomly restarts something
        if self._rng.random() < 0.20:
            target = self._rng.choice(["api", "queue", "cache"])
            return Action(action_type=ActionType.restart_service, target=ComponentTarget(target))

        # 1. If we know any root causes we haven't fixed yet, fix them.
        pending_roots = self.diagnosed_roots - self.fixed_roots
        if pending_roots:
            target = list(pending_roots)[0]
            self.fixed_roots.add(target)
            self.wait_ticks = 2
            
            if target == "database":
                return Action(action_type=ActionType.repair_database, target=ComponentTarget.database)
            elif target == "cache":
                return Action(action_type=ActionType.clear_cache, target=ComponentTarget.cache)
            else:
                return Action(action_type=ActionType.restart_service, target=ComponentTarget(target))
                
        # Cooldown period
        if not pending_roots and self.fixed_roots:
            if self.wait_ticks > 0:
                self.wait_ticks -= 1
                return Action(action_type=ActionType.no_op, target=ComponentTarget.api)
            
            worst = max(obs.metrics.items(), key=lambda x: x[1].error_rate)
            if worst[1].error_rate > 0.0:
                if worst[0] == "database":
                    return Action(action_type=ActionType.repair_database, target=ComponentTarget.database)
                elif worst[0] == "cache":
                    return Action(action_type=ActionType.clear_cache, target=ComponentTarget.cache)
                else:
                    return Action(action_type=ActionType.restart_service, target=ComponentTarget(worst[0]))
            return Action(action_type=ActionType.no_op, target=ComponentTarget.api) 
                
        # 2. Pick most suspicious component to inspect
        suspicious = None
        highest_err = -1.0
        
        for comp, m in obs.metrics.items():
            if comp not in self.inspected and m.error_rate > highest_err:
                highest_err = m.error_rate
                suspicious = comp
                
        if suspicious and highest_err > 0.1:
            self.inspected.add(suspicious)
            action_map = {
                "database": ActionType.inspect_database,
                "api": ActionType.inspect_api,
                "cache": ActionType.inspect_cache,
                "queue": ActionType.inspect_queue
            }
            return Action(action_type=action_map[suspicious], target=ComponentTarget(suspicious))
            
        # Fallback: just restart highest error rate
        worst = max(obs.metrics.items(), key=lambda x: x[1].error_rate)
        if worst[1].error_rate > 0.1:
            if worst[0] == "database":
                return Action(action_type=ActionType.repair_database, target=ComponentTarget.database)
            elif worst[0] == "cache":
                return Action(action_type=ActionType.clear_cache, target=ComponentTarget.cache)
            else:
                return Action(action_type=ActionType.restart_service, target=ComponentTarget(worst[0]))
                
        return Action(action_type=ActionType.no_op, target=ComponentTarget.api)

def llm_agent(obs, task_config: dict, step: int) -> Action:
    """LLM-based agent using the OpenAI client."""
    if not client:
        # If no client, we can't call LLM
        return None

    # Logic similar to previous version...
    obs_dict = obs.model_dump() if hasattr(obs, 'model_dump') else obs
    system_prompt = "You are an SRE agent debugging a distributed system. Respond with JSON: {'action_type': '...', 'target': '...'}"
    
    try:
        completion = client.chat.completions.create(
            model=MODEL_NAME,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": f"Step: {step}\\nObservation: {json.dumps(obs_dict)}"}
            ],
            response_format={"type": "json_object"}
        )
        data = json.loads(completion.choices[0].message.content)
        return Action(
            action_type=ActionType(data.get("action_type", "no_op")),
            target=ComponentTarget(data.get("target", "database"))
        )
    except Exception:
        return None

# ---------------------------------------------------------------------------
# Runner logic
# ---------------------------------------------------------------------------

def run_task(task_id: str):
    """Run a single task and print structured output for the validator."""
    if task_id not in TASKS:
        return

    config = TASKS[task_id]
    
    # 1. Start tag
    print(f"[START] task={task_id}", flush=True)
    
    env = RepairEnvV2()
    obs = env.reset(task_config=config, task_id=task_id)
    baseline_agent = InferenceAgent()
    
    done = False
    step = 0
    
    while not done:
        # Try LLM agent first, fallback to baseline
        action = llm_agent(obs, config, step)
        if action is None:
            action = baseline_agent.pick_action(obs)
        
        obs, reward, done, info = env.step(action)
        step += 1
        
        # Update baseline agent state
        res = info.get("inspect_result")
        if res and res["is_likely_root_cause"] and res["confidence"] >= 0.7:
            baseline_agent.diagnosed_roots.add(res["component"])
            
        # 2. Step tag
        print(f"[STEP] step={step} reward={reward.value:.3f}", flush=True)

    # 3. End tag
    result = env.get_episode_result()
    print(f"[END] task={task_id} score={result.score:.4f} steps={result.steps_taken}", flush=True)

def main():
    parser = argparse.ArgumentParser(description="OpenEnv inference runner")
    parser.add_argument("--task", help="Run specific task (easy, medium, hard, multi_root)")
    args = parser.parse_args()

    if args.task:
        tasks_to_run = [args.task]
    else:
        tasks_to_run = ["easy", "medium", "hard", "multi_root"]

    for tid in tasks_to_run:
        run_task(tid)

if __name__ == "__main__":
    main()