11import asyncio
22import os
33import sys
4- import textwrap
54import json
6- import traceback
5+ import subprocess
6+ import time
7+ import re
8+ import httpx
9+ import threading
710from pathlib import Path
8- from typing import List , Dict , Any
911
10- import httpx
1112from dotenv import load_dotenv
1213from openai import OpenAI
1314
14- from cloudenv .client import ClientEnvironment
15- from cloudenv .models import CloudEnvAction , CloudEnvObservation
16-
17- # ---------------------------
18- # PATH FIX
19- # ---------------------------
20-
2115ROOT = Path (__file__ ).parent .resolve ()
2216if str (ROOT ) not in sys .path :
2317 sys .path .insert (0 , str (ROOT ))
2418
2519TASK_DIR = ROOT / "tasks"
2620
27- # ---------------------------
28- # ENV
29- # ---------------------------
21+ from cloudenv .client import ClientEnvironment # noqa
22+ from cloudenv .models import CloudEnvAction , CloudEnvObservation # noqa
3023
3124load_dotenv ()
3225
3831TEMPERATURE = float (os .getenv ("TEMPERATURE" , "0.3" ))
3932MAX_TOKENS = int (os .getenv ("MAX_TOKENS" , "300" ))
4033
41- SERVER_URL = "https ://vishvam10-cloudenv.hf.space "
34+ SERVER_URL = "http ://127.0.0.1:8000 "
4235
4336BENCHMARK = "cloudenv"
4437SUCCESS_THRESHOLD = 0.5
5043
5144TASK_ORDER = ["easy-task" , "medium-task" , "hard-task" ]
5245
53- # ---------------------------
54- # PROMPT (STRICT )
55- # ---------------------------
46+ SYSTEM_PROMPT = (
47+ ( Path ( __file__ ). parent / "system_prompt.txt" ). read_text (). strip ( )
48+ )
5649
57- SYSTEM_PROMPT = """
58- You are a deterministic cloud pipeline repair engine.
5950
60- Return ONLY valid JSON.
61- No explanations.
62- No extra text.
51+ def start_ministack ():
52+ proc = subprocess .Popen (
53+ ["uv" , "run" , "ministack" ],
54+ cwd = ROOT ,
55+ stdout = subprocess .DEVNULL ,
56+ stderr = subprocess .DEVNULL ,
57+ )
58+ time .sleep (3 )
59+ return proc
60+
61+
62+ def start_server ():
63+ proc = subprocess .Popen (
64+ ["uv" , "run" , "server" ],
65+ cwd = ROOT ,
66+ stdout = subprocess .PIPE ,
67+ stderr = subprocess .STDOUT ,
68+ text = True ,
69+ bufsize = 1 ,
70+ )
6371
64- Format:
65- {
66- "service": "...",
67- "operation": "...",
68- "instance_id": null,
69- "payload": {}
70- }
71- """
72+ def stream ():
73+ for line in proc .stdout :
74+ print (f"[SERVER] { line } " , end = "" )
7275
73- PROMPT_PATH = Path (__file__ ).parent / "system_prompt.txt"
74- if PROMPT_PATH .exists ():
75- SYSTEM_PROMPT = PROMPT_PATH .read_text ().strip ()
76+ threading .Thread (target = stream , daemon = True ).start ()
7677
77- # ---------------------------
78- # LOGGING
79- # ---------------------------
78+ url = f"{ SERVER_URL } /docs"
8079
80+ for _ in range (40 ):
81+ try :
82+ r = httpx .get (url , timeout = 1 )
83+ if r .status_code == 200 :
84+ print ("[INFO] server ready" )
85+ return proc
86+ except Exception :
87+ time .sleep (1 )
88+
89+ raise RuntimeError ("Server failed" )
8190
82- def log_start (task : str ):
91+
92+ def log_start (task ):
8393 print (f"[START] task={ task } env={ BENCHMARK } model={ MODEL_NAME } " , flush = True )
8494
8595
86- def log_step (step : int , action : str , reward : float , done : bool ):
96+ def log_step (step , action , reward , done ):
8797 print (
8898 f"[STEP] step={ step } action={ action } reward={ reward :.2f} done={ str (done ).lower ()} " ,
8999 flush = True ,
90100 )
91101
92102
93- def log_end (success : bool , steps : int , score : float , rewards : List [float ]):
94- rewards_str = "," .join (f"{ r :.2f} " for r in rewards )
103+ def log_end (success , steps , score , rewards ):
95104 print (
96- f"[END] success={ str (success ).lower ()} steps={ steps } score={ score :.3f} rewards={ rewards_str } " ,
105+ f"[END] success={ str (success ).lower ()} steps={ steps } score={ score :.3f} rewards={ ',' . join ( f' { r :.2f } ' for r in rewards ) } " ,
97106 flush = True ,
98107 )
99108
100109
101- # ---------------------------
102- # TASK LOADER
103- # ---------------------------
104-
105-
106- def load_task (name : str ):
110+ def load_task (name ):
107111 return json .loads ((TASK_DIR / f"{ name } .json" ).read_text ())
108112
109113
110- # ---------------------------
111- # PIPELINE REGISTER
112- # ---------------------------
113-
114-
115- async def register_pipeline (task_data : Dict [str , Any ]):
114+ async def register_pipeline (task_data ):
116115 url = f"{ SERVER_URL } /api/pipelines"
116+ async with httpx .AsyncClient (timeout = 20 ) as c :
117+ r = await c .post (url , json = task_data )
118+ if r .status_code == 400 :
119+ try :
120+ d = r .json ().get ("detail" , "" )
121+ except Exception :
122+ d = ""
123+ if "already exists" in d :
124+ await c .delete (f"{ url } /{ task_data ['pipeline_id' ]} " )
125+ await c .post (url , json = task_data )
117126
118- async with httpx .AsyncClient (timeout = 20 ) as http :
119- try :
120- r = await http .post (url , json = task_data )
121-
122- if r .status_code == 400 :
123- try :
124- detail = r .json ().get ("detail" , "" )
125- except Exception :
126- detail = ""
127-
128- if "already exists" in detail :
129- await http .delete (f"{ url } /{ task_data ['pipeline_id' ]} " )
130- await http .post (url , json = task_data )
131127
132- except Exception :
133- print ("[ERROR] pipeline register failed" , flush = True )
134- traceback .print_exc ()
128+ def build_prompt (step , state , broken , ideal , history ):
129+ return f"""
130+ STEP { step }
131+ STATE:
132+ { json .dumps (state )[:1200 ]}
133+ BROKEN:
134+ { json .dumps (broken )[:1200 ]}
135+ IDEAL:
136+ { json .dumps (ideal )[:1200 ]}
137+ HISTORY:
138+ { history [- 5 :] if history else "None" }
139+ Return ONLY JSON.
140+ """ .strip ()
135141
136142
137- # ---------------------------
138- # ACTION PARSER
139- # ---------------------------
143+ def call_model (step , state , broken , ideal , history ):
144+ prompt = build_prompt (step , state , broken , ideal , history )
145+ try :
146+ res = client .chat .completions .create (
147+ model = MODEL_NAME ,
148+ messages = [
149+ {"role" : "system" , "content" : SYSTEM_PROMPT },
150+ {"role" : "user" , "content" : prompt },
151+ ],
152+ temperature = TEMPERATURE ,
153+ max_tokens = MAX_TOKENS ,
154+ )
155+ return (res .choices [0 ].message .content or "" ).strip ()
156+ except Exception :
157+ return "{}"
140158
141159
142- def parse_llm_action (raw : str , task_id : str , episode_id : str ) -> CloudEnvAction :
160+ def parse_llm_action (raw , task_id , episode_id ) :
143161 try :
144162 raw = raw .strip ()
145- raw = raw [raw .find ("{" ) : raw .rfind ("}" ) + 1 ]
146- data = json .loads (raw )
147-
163+ m = re .search (r"\{.*\}" , raw , re .S )
164+ if not m :
165+ raise ValueError ("no json" )
166+ data = json .loads (m .group ())
167+ if "action" in data :
168+ data = data ["action" ]
148169 return CloudEnvAction (
149170 task_id = task_id ,
150171 episode_id = episode_id ,
@@ -165,88 +186,28 @@ def parse_llm_action(raw: str, task_id: str, episode_id: str) -> CloudEnvAction:
165186 )
166187
167188
168- # ---------------------------
169- # LLM
170- # ---------------------------
171-
172-
173- def build_prompt (
174- step : int , state : dict , broken : dict , ideal : dict , history : List [str ]
175- ) -> str :
176- return textwrap .dedent (f"""
177- STEP { step }
178-
179- CURRENT STATE:
180- { json .dumps (state )[:1000 ]}
181-
182- BROKEN PIPELINE:
183- { json .dumps (broken )[:1000 ]}
184-
185- IDEAL PIPELINE:
186- { json .dumps (ideal )[:1000 ]}
187-
188- HISTORY:
189- { history [- 5 :] if history else "None" }
190-
191- Return ONLY JSON action.
192- """ ).strip ()
193-
194-
195- def call_model (
196- step : int , state : dict , broken : dict , ideal : dict , history : List [str ]
197- ) -> str :
198- prompt = build_prompt (step , state , broken , ideal , history )
199-
200- try :
201- res = client .chat .completions .create (
202- model = MODEL_NAME ,
203- messages = [
204- {"role" : "system" , "content" : SYSTEM_PROMPT },
205- {"role" : "user" , "content" : prompt },
206- ],
207- temperature = TEMPERATURE ,
208- max_tokens = MAX_TOKENS ,
209- )
210- return (res .choices [0 ].message .content or "" ).strip ()
211-
212- except Exception as e :
213- print (f"[DEBUG] LLM error: { e } " , flush = True )
214- return "{}"
215-
216-
217- # ---------------------------
218- # EPISODE
219- # ---------------------------
220-
221-
222- async def run_episode (task_data : dict ):
223-
189+ async def run_episode (task_data ):
224190 pipeline_id = task_data ["pipeline_id" ]
225191 task_id = pipeline_id
226192 episode_id = f"{ pipeline_id } -episode-1"
227193
228- history : List [str ] = []
229- rewards : List [float ] = []
230-
194+ history = []
195+ rewards = []
231196 steps = 0
232- score = 0.0
233- success = False
234197
235198 log_start (task_id )
236199
237- env = None
200+ env = ClientEnvironment ( base_url = SERVER_URL )
238201
239202 try :
240203 await register_pipeline (task_data )
241204
242- env = ClientEnvironment (base_url = SERVER_URL )
243-
244205 result = await env .initialize_environment (
245206 task_id = task_id ,
246207 episode_id = episode_id ,
247208 )
248209
249- obs : CloudEnvObservation = result .observation
210+ obs = result .observation
250211
251212 for step in range (1 , MAX_STEPS + 1 ):
252213 if obs .done :
@@ -263,8 +224,8 @@ async def run_episode(task_data: dict):
263224 action = parse_llm_action (raw_action , task_id , episode_id )
264225
265226 result = await env .step (action )
266-
267227 obs = result .observation
228+
268229 reward = float (obs .reward or 0.0 )
269230 done = bool (obs .done )
270231
@@ -283,34 +244,29 @@ async def run_episode(task_data: dict):
283244 success = score >= SUCCESS_THRESHOLD
284245
285246 except Exception as e :
286- log_step (steps or 1 , "error" , 0.0 , True )
287- print (f"[ERROR] episode failed: { e } " , flush = True )
247+ print (f"[ERROR] { e } " )
288248 score = 0.0
289249 success = False
290250
291251 finally :
292- if env :
293- try :
294- await env .close ()
295- except Exception :
296- pass
252+ await env .close ()
297253
298254 log_end (success , steps , score , rewards )
299255
300256
301- # ---------------------------
302- # MAIN
303- # ---------------------------
304-
305-
306257async def main ():
307- for name in TASK_ORDER :
308- try :
258+ ministack_server = start_ministack ()
259+ server = start_server ()
260+ try :
261+ for name in TASK_ORDER :
309262 task = load_task (name )
310263 await run_episode (task )
311- except Exception as e :
312- print (f"[DEBUG] task failed { name } : { e } " , flush = True )
313- log_end (False , 0 , 0.0 , [])
264+ finally :
265+ server .terminate ()
266+ server .wait ()
267+
268+ ministack_server .terminate ()
269+ ministack_server .wait ()
314270
315271
316272if __name__ == "__main__" :
0 commit comments