File size: 6,903 Bytes
125deae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Baseline inference script.
Runs an LLM agent (via OpenAI API) against all 3 tasks and reports scores.

Usage:
    export OPENAI_API_KEY=your_key_here
    python baseline.py

    # For JSON output (used by /baseline endpoint):
    python baseline.py --output-json
"""

import os
import sys
import json
import argparse
import requests

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

API_BASE = os.getenv("ENV_BASE_URL", "http://localhost:7860")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
MODEL = os.getenv("BASELINE_MODEL", "gpt-4o-mini")
MAX_STEPS = 20

SYSTEM_PROMPT = """You are an expert DevOps engineer debugging a broken multi-service development environment.

You will receive the current state of services (healthy/error) and the result of your last action.
Your goal is to diagnose faults and fix them so ALL services reach "healthy" status.

Services: api, worker, database, proxy
Available actions:
  - read_logs: read error logs for a service (do this first when a service is broken)
  - inspect_env: view environment variables for a service
  - edit_env: change an environment variable (requires key and value)
  - restart_service: restart a service after fixing its config
  - run_healthcheck: check current status of a service
  - submit: declare done when all services are healthy

STRATEGY:
1. Look at which services are in "error" state
2. Read logs for broken services to understand the error
3. Inspect env vars of the broken service
4. Edit the wrong/missing env var
5. Restart the service
6. Submit when all services show "healthy"

Respond ONLY with a valid JSON action object. Nothing else. Examples:
{"type": "read_logs", "service": "api"}
{"type": "inspect_env", "service": "database"}  
{"type": "edit_env", "service": "api", "key": "DB_PORT", "value": "5432"}
{"type": "restart_service", "service": "api"}
{"type": "submit"}
"""


def call_openai(messages: list) -> str:
    """Call OpenAI chat completions API."""
    if not OPENAI_API_KEY:
        raise ValueError("OPENAI_API_KEY not set in environment variables.")

    import urllib.request
    payload = {
        "model": MODEL,
        "messages": messages,
        "temperature": 0.2,
        "max_tokens": 200,
    }
    req = urllib.request.Request(
        "https://api.openai.com/v1/chat/completions",
        data=json.dumps(payload).encode(),
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {OPENAI_API_KEY}",
        },
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        data = json.loads(resp.read())
    return data["choices"][0]["message"]["content"].strip()


def obs_to_text(obs: dict) -> str:
    """Convert observation dict to readable text for the LLM."""
    services = obs.get("services", {})
    svc_lines = "\n".join(
        f"  {name}: {s['status']} (port: {s.get('port', 'N/A')})"
        for name, s in services.items()
    )
    last = obs.get("last_action_result", {})
    return (
        f"Step {obs['step']}\n"
        f"Services:\n{svc_lines}\n"
        f"Last action result: {'✓' if last.get('success') else '✗'} {last.get('message', '')}\n"
        f"Done: {obs.get('done', False)}"
    )


def run_episode(task_id: str, verbose: bool = True) -> dict:
    """Run one full episode for a task. Returns grader result."""
    if verbose:
        print(f"\n{'='*60}")
        print(f"Running Task: {task_id}")
        print('='*60)

    # Reset
    resp = requests.post(f"{API_BASE}/reset", params={"task_id": task_id})
    obs = resp.json()

    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    step = 0

    while not obs.get("done", False) and step < MAX_STEPS:
        step += 1
        obs_text = obs_to_text(obs)

        if verbose:
            print(f"\n[Step {step}] Observation:\n{obs_text}")

        messages.append({"role": "user", "content": obs_text})

        # Get action from LLM
        try:
            raw_action = call_openai(messages)
        except Exception as e:
            if verbose:
                print(f"  LLM error: {e}")
            break

        if verbose:
            print(f"  Agent action: {raw_action}")

        # Parse and validate action
        try:
            action_dict = json.loads(raw_action)
        except json.JSONDecodeError:
            if verbose:
                print(f"  Could not parse action JSON: {raw_action}")
            messages.append({"role": "assistant", "content": raw_action})
            messages.append({"role": "user", "content": "Invalid JSON. Respond ONLY with a valid JSON action."})
            continue

        messages.append({"role": "assistant", "content": raw_action})

        # Step environment
        step_resp = requests.post(f"{API_BASE}/step", json=action_dict)
        step_data = step_resp.json()
        obs = step_data["observation"]
        reward = step_data["reward"]
        done = step_data["done"]

        if verbose:
            print(f"  Reward: {reward} | Done: {done}")

        if done:
            break

    # Get grader score
    grader_resp = requests.get(f"{API_BASE}/grader", params={"task_id": task_id})
    grader_data = grader_resp.json()
    result = grader_data["grader_result"]

    if verbose:
        print(f"\n[RESULT] Task {task_id}: score={result['score']} | {result['reason']}")

    return {
        "task_id": task_id,
        "score": result["score"],
        "reason": result["reason"],
        "steps_taken": step,
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--output-json", action="store_true", help="Output scores as JSON")
    parser.add_argument("--task", type=str, default=None, help="Run single task only")
    args = parser.parse_args()

    verbose = not args.output_json
    tasks = [args.task] if args.task else ["task1", "task2", "task3"]

    results = []
    for task_id in tasks:
        try:
            result = run_episode(task_id, verbose=verbose)
            results.append(result)
        except Exception as e:
            results.append({
                "task_id": task_id,
                "score": 0.0,
                "reason": f"Error: {str(e)}",
                "steps_taken": 0,
            })

    avg_score = sum(r["score"] for r in results) / len(results)

    if args.output_json:
        output = {"tasks": results, "average_score": round(avg_score, 3)}
        print(json.dumps(output))
    else:
        print(f"\n{'='*60}")
        print("BASELINE SCORES SUMMARY")
        print('='*60)
        for r in results:
            print(f"  {r['task_id']:10} score={r['score']:.3f}  steps={r['steps_taken']}  {r['reason']}")
        print(f"  {'AVERAGE':10} score={avg_score:.3f}")
        print('='*60)


if __name__ == "__main__":
    main()