Theright07 commited on
Commit
e6d9256
Β·
verified Β·
1 Parent(s): 78d4241

Fix: stop only at [RESULT:, inject results into assistant turn, ACTION: regex fix

Browse files
Files changed (1) hide show
  1. app.py +205 -268
app.py CHANGED
@@ -9,32 +9,24 @@ from huggingface_hub import hf_hub_download
9
  from llama_cpp import Llama
10
 
11
  try:
12
- import git as gitlib
13
- HAS_GIT = True
14
- except ImportError:
15
- HAS_GIT = False
16
-
17
  try:
18
- from duckduckgo_search import DDGS
19
- HAS_DDG = True
20
- except ImportError:
21
- HAS_DDG = False
22
 
23
  WORKSPACE = Path("/workspace")
24
  WORKSPACE.mkdir(exist_ok=True)
25
 
26
- MODEL_REPO = "Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF"
27
- MODEL_FILE = "qwen2.5-coder-1.5b-instruct-q4_k_m.gguf"
28
-
29
  print("Downloading model...")
30
- model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
 
31
  print("Loading model...")
32
  llm = Llama(model_path=model_path, n_ctx=8192,
33
  n_threads=int(os.getenv("LLAMA_THREADS","2")), n_batch=512, verbose=False)
34
  print("Model ready!")
35
 
36
  # ── tools ─────────────────────────────────────────────────────────────────────
37
-
38
  def _safe(p):
39
  path = (WORKSPACE / p).resolve()
40
  if WORKSPACE.resolve() not in path.parents and path != WORKSPACE.resolve():
@@ -44,8 +36,8 @@ def _safe(p):
44
  def tool_write_file(path, content):
45
  try:
46
  p = _safe(path); p.parent.mkdir(parents=True, exist_ok=True)
47
- p.write_text(content, encoding="utf-8")
48
- return f"OK: wrote {len(content)} chars to /workspace/{path}"
49
  except Exception as e: return f"ERROR: {e}"
50
 
51
  def tool_read_file(path):
@@ -78,8 +70,7 @@ def tool_run(command, dir="."):
78
  try:
79
  cwd = _safe(dir)
80
  r = subprocess.run(command, cwd=cwd, shell=True, capture_output=True, text=True, timeout=300)
81
- out = (r.stdout or "")[-2500:]
82
- err = (r.stderr or "")[-1000:]
83
  return f"exit={r.returncode}\n{out}\n{err}".strip()
84
  except subprocess.TimeoutExpired: return "ERROR: timeout after 300s"
85
  except Exception as e: return f"ERROR: {e}"
@@ -100,9 +91,8 @@ def tool_gradle_build(repo, task="assembleDebug"):
100
  if w.exists(): os.chmod(w, 0o755); cmd = f"./gradlew {task} --no-daemon"
101
  else: cmd = f"gradle {task} --no-daemon"
102
  r = subprocess.run(cmd, cwd=cwd, shell=True, capture_output=True, text=True, timeout=900)
103
- out = (r.stdout or "")[-3000:]; err = (r.stderr or "")[-1000:]
104
  apks = glob.glob(f"{cwd}/**/*.apk", recursive=True)
105
- return f"exit={r.returncode}\n{out}\n{err}\nAPKs: {apks}"
106
  except Exception as e: return f"ERROR: {e}"
107
 
108
  def tool_web_search(query, max_results=5):
@@ -134,263 +124,222 @@ def tool_download_file(url, save_as):
134
  req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0"})
135
  with urllib.request.urlopen(req, timeout=30) as resp, open(p,'wb') as f:
136
  f.write(resp.read())
137
- return f"OK: downloaded to /workspace/{save_as} ({p.stat().st_size} bytes)"
138
  except Exception as e: return f"ERROR: {e}"
139
 
140
  TOOLS = {
141
- "write_file": (tool_write_file, "path, content", "Create or overwrite a file"),
142
- "read_file": (tool_read_file, "path", "Read file contents"),
143
- "edit_file": (tool_edit_file, "path, find, replace", "Find and replace text in file"),
144
- "delete_file": (tool_delete_file, "path", "Delete file or directory"),
145
- "list_files": (tool_list_files, "path='.'", "List all files in workspace"),
146
- "run": (tool_run, "command, dir='.'", "Execute any shell command"),
147
- "git_clone": (tool_git_clone, "url, dest=''", "Clone a git repository"),
148
- "gradle_build": (tool_gradle_build, "repo, task='assembleDebug'", "Build Android APK"),
149
- "web_search": (tool_web_search, "query, max_results=5", "Search the web"),
150
- "read_url": (tool_read_url, "url", "Fetch URL content"),
151
- "download_file":(tool_download_file,"url, save_as", "Download file from URL"),
152
  }
153
 
154
- # ── SYSTEM PROMPT with few-shot examples ─────────────────────────────────────
155
- # Key insight: Qwen 1.5B needs CONCRETE EXAMPLES to learn tool use
156
- # We show it exactly what good behavior looks like
157
-
158
- SYSTEM = """You are an AI agent running INSIDE a Linux container. You have a real filesystem at /workspace and real tools.
159
 
160
- YOU MUST USE TOOLS. Never say "I can't" or "I'm sorry". You CAN do everything listed below.
161
 
162
- AVAILABLE TOOLS (call them by writing ACTION lines):
163
- ACTION: write_file(path, content)
164
- ACTION: read_file(path)
165
- ACTION: edit_file(path, find, replace)
166
- ACTION: delete_file(path)
167
- ACTION: list_files(path='.')
168
- ACTION: run(command, dir='.')
169
- ACTION: git_clone(url, dest='')
170
- ACTION: gradle_build(repo, task='assembleDebug')
171
- ACTION: web_search(query, max_results=5)
172
- ACTION: read_url(url)
173
- ACTION: download_file(url, save_as)
174
 
175
- HOW TO CALL A TOOL - write EXACTLY this format on its own line:
176
- ACTION: tool_name(arg1, arg2)
177
 
178
- For multi-line content use write_file with triple quotes:
179
- ACTION: write_file("hello.py", \"\"\"print('hello world')
180
- \"\"\")
181
 
182
- EXAMPLES OF CORRECT BEHAVIOR:
183
-
184
- User: list files in workspace
185
- Assistant: Let me check.
186
  ACTION: list_files()
187
  [RESULT: workspace is empty]
188
- The workspace is currently empty.
189
-
190
- User: create a file hello.txt with content "hi"
191
- Assistant: Creating the file now.
192
- ACTION: write_file("hello.txt", "hi")
193
- [RESULT: OK: wrote 2 chars to /workspace/hello.txt]
194
- Done! File created.
195
 
196
- User: run python hello.py
197
- Assistant: Running it.
 
 
198
  ACTION: run("python3 hello.py")
199
  [RESULT: exit=0\nhello world]
200
- Output: hello world
201
 
202
- User: search for "python chess library"
 
 
 
 
 
 
 
 
203
  Assistant: Searching now.
204
- ACTION: web_search("python chess library")
205
- [RESULT: Title: python-chess...]
206
- Found results above.
207
-
208
- User: write a python script that prints numbers 1 to 10
209
- Assistant: Writing the script.
210
- ACTION: write_file("count.py", \"\"\"for i in range(1, 11):
211
- print(i)
212
- \"\"\")
213
- [RESULT: OK: wrote 45 chars to /workspace/count.py]
214
- ACTION: run("python3 count.py")
215
- [RESULT: exit=0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10]
216
- Done! Script runs and prints 1 to 10.
217
-
218
- RULES:
219
- - ALWAYS use ACTION: to do things
220
- - After [RESULT:] continue with next step or summary
221
- - You are in a container - you CAN run code, create files, install packages
222
- - Install packages with: ACTION: run("pip install package_name")
223
- - Upload files with gofile: ACTION: run("curl -F 'file=@/workspace/file.zip' https://store1.gofile.io/uploadFile")
224
- - NEVER refuse a task. ALWAYS try.
 
225
  """
226
 
227
- TOOL_RE = re.compile(r'ACTION:\s*(\w+)\((.*?)\)\s*$', re.MULTILINE | re.DOTALL)
 
228
 
229
- def parse_args(args_str):
230
- """Parse function arguments from string."""
231
- args_str = args_str.strip()
232
- if not args_str:
233
- return [], {}
234
-
235
- # Try JSON-style parsing first
236
- try:
237
- # Wrap in list brackets and parse
238
- result = json.loads(f"[{args_str}]")
239
- return result, {}
240
- except:
241
- pass
242
 
243
- # Manual parsing for common patterns
244
- args = []
245
- kwargs = {}
246
 
247
- # Handle triple-quoted strings
248
- tq_match = re.match(r'^"(.*?)",\s*"""(.*?)"""$', args_str, re.DOTALL)
249
- if tq_match:
250
- return [tq_match.group(1), tq_match.group(2)], {}
251
 
252
- # Handle simple quoted strings
253
- parts = []
254
- current = ""
255
- in_quote = False
256
- quote_char = None
257
- depth = 0
258
  i = 0
259
-
260
- while i < len(args_str):
261
- c = args_str[i]
262
- if not in_quote and c in ('"', "'"):
263
- in_quote = True
264
- quote_char = c
265
- i += 1
266
- continue
267
- elif in_quote and c == quote_char and (i == 0 or args_str[i-1] != '\\'):
268
- in_quote = False
269
- parts.append(current)
270
- current = ""
271
  i += 1
272
- # Skip comma and space
273
- while i < len(args_str) and args_str[i] in (',', ' '):
274
- i += 1
275
- continue
276
- elif in_quote:
277
- if c == '\\' and i+1 < len(args_str):
278
- nc = args_str[i+1]
279
- if nc == 'n': current += '\n'; i += 2; continue
280
- elif nc == 't': current += '\t'; i += 2; continue
281
- elif nc == '\\': current += '\\'; i += 2; continue
282
- elif nc == '"': current += '"'; i += 2; continue
283
- current += c
284
- i += 1
285
-
286
- if current:
287
- parts.append(current)
288
-
289
- if parts:
290
- return parts, {}
291
-
292
- # Fallback: split by comma
293
- raw_parts = [p.strip().strip('"\'') for p in args_str.split(',')]
294
- return raw_parts, {}
295
-
296
-
297
- def execute_tool(name, args_str):
298
- """Execute a tool and return result string."""
299
- if name not in TOOLS:
300
- return f"ERROR: Unknown tool '{name}'. Available: {', '.join(TOOLS.keys())}"
301
-
302
- fn = TOOLS[name][0]
303
 
304
  try:
305
- args, kwargs = parse_args(args_str)
306
- result = fn(*args, **kwargs)
307
- return str(result)
308
  except Exception as e:
309
- # Try with raw string as first arg
310
  try:
311
- result = fn(args_str.strip('"\''))
312
- return str(result)
313
  except Exception as e2:
314
- return f"ERROR calling {name}: {e} | {e2}"
315
 
316
 
317
  def run_agent_sync(message, history):
318
- """Run agent, return list of SSE event dicts."""
319
- # Build messages with few-shot in system
320
  msgs = [{"role": "system", "content": SYSTEM}]
321
-
322
  for h in history:
323
  if isinstance(h, (list, tuple)) and len(h) == 2:
324
  if h[0]: msgs.append({"role": "user", "content": str(h[0])})
325
  if h[1]: msgs.append({"role": "assistant", "content": str(h[1])})
326
-
327
  msgs.append({"role": "user", "content": message})
328
-
329
  events = []
330
  full_response = ""
331
-
332
- for step in range(12): # up to 12 tool calls
333
- # Generate response, stop at ACTION: line end
 
334
  out = llm.create_chat_completion(
335
  messages=msgs,
336
  temperature=0.1,
337
- max_tokens=800,
338
- stop=["\n[RESULT:", "\nACTION:"], # stop before result injection or next action
339
  )
340
-
341
- reply = out["choices"][0]["message"]["content"]
342
  finish = out["choices"][0].get("finish_reason", "stop")
343
-
344
- # If stopped before ACTION:, check if there's a pending action in reply
345
- # Re-generate with higher limit to get the action
346
- if finish == "stop" and "ACTION:" not in reply and step == 0:
347
- # Model gave pure text, check if it should have used a tool
348
- # Give it one more chance with explicit hint
349
- full_response += reply
350
- events.append({"type": "token", "text": reply})
351
- break
352
-
353
- # Find ACTION: in reply
354
- action_match = TOOL_RE.search(reply)
355
-
356
  if not action_match:
357
- # Pure text response - stream it
358
- full_response += reply
359
- events.append({"type": "token", "text": reply})
360
-
361
- # If finish_reason is stop and no more actions, we're done
362
- if finish == "stop":
363
- break
364
- continue
365
-
366
- # Text before the ACTION line
367
- action_pos = reply.rfind("ACTION:")
368
- pre_text = reply[:action_pos].rstrip()
369
- if pre_text:
370
- full_response += pre_text + "\n"
371
- events.append({"type": "token", "text": pre_text + "\n"})
372
-
373
  tool_name = action_match.group(1)
374
- args_str = action_match.group(2)
375
-
376
  # Emit tool_start
377
- events.append({"type": "tool_start", "tool": tool_name, "args": {
378
- "args": args_str[:200] if len(args_str) > 200 else args_str
379
- }})
380
-
381
  # Execute tool
382
- result = execute_tool(tool_name, args_str)
383
- result_short = result[:2000]
384
-
385
- events.append({"type": "tool_result", "tool": tool_name, "result": result_short})
386
-
387
- # Add to conversation context
388
- assistant_turn = reply + f"\n[RESULT: {result_short}]"
389
- msgs.append({"role": "assistant", "content": assistant_turn})
390
- msgs.append({"role": "user", "content": "Continue."})
391
-
392
- full_response += f"[Tool: {tool_name}] {result_short[:100]}\n"
393
-
 
 
 
 
 
 
 
 
 
 
 
394
  events.append({"type": "done", "full": full_response})
395
  return events
396
 
@@ -404,67 +353,57 @@ async def stream_agent(message, history):
404
 
405
 
406
  # ── FastAPI ───────────────────────────────────────────────────────────────────
407
- app = FastAPI(title="Android Build Agent")
408
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
409
 
410
  HTML = r"""<!DOCTYPE html>
411
  <html>
412
- <head>
413
- <title>AI Agent</title>
414
- <meta name="viewport" content="width=device-width,initial-scale=1">
415
  <style>
416
  *{box-sizing:border-box;margin:0;padding:0}
417
  body{font-family:'Segoe UI',system-ui,sans-serif;background:#0d1117;color:#e6edf3;height:100vh;display:flex;flex-direction:column}
418
- #hdr{padding:12px 18px;background:#161b22;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
419
  #hdr h1{font-size:16px;color:#58a6ff;font-weight:700}
420
  .badge{font-size:11px;background:#1f6feb22;color:#58a6ff;border:1px solid #1f6feb55;padding:2px 8px;border-radius:10px}
421
- .dot{width:8px;height:8px;background:#3fb950;border-radius:50%;flex-shrink:0;animation:pulse 2s infinite}
422
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
423
  #chat{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:8px}
424
  .msg{max-width:82%;padding:10px 14px;border-radius:12px;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;font-size:14px}
425
  .user{align-self:flex-end;background:#1f6feb;color:#fff;border-radius:12px 12px 2px 12px}
426
  .bot{align-self:flex-start;background:#161b22;border:1px solid #30363d;border-radius:2px 12px 12px 12px}
427
- .tc{align-self:flex-start;max-width:88%;background:#0d1f3c;border:1px solid #1f6feb55;border-radius:8px;padding:8px 12px;font-size:12px;font-family:monospace}
428
- .tr{align-self:flex-start;max-width:88%;background:#071a07;border:1px solid #3fb95055;border-radius:8px;padding:8px 12px;font-size:12px;font-family:monospace;max-height:180px;overflow-y:auto}
429
  .lbl{font-size:10px;color:#8b949e;margin-bottom:3px;text-transform:uppercase;letter-spacing:.5px}
430
  #bot-area{padding:10px 14px;background:#161b22;border-top:1px solid #30363d}
431
  #row{display:flex;gap:8px;align-items:flex-end}
432
  #inp{flex:1;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#e6edf3;padding:10px 14px;font-size:14px;resize:none;min-height:44px;max-height:120px;outline:none;font-family:inherit}
433
  #inp:focus{border-color:#58a6ff}
434
  #btn{background:#1f6feb;color:#fff;border:none;border-radius:8px;padding:10px 16px;cursor:pointer;font-size:14px;font-weight:600;height:44px}
435
- #btn:hover{background:#388bfd}
436
- #btn:disabled{background:#21262d;color:#555;cursor:not-allowed}
437
  .hint{font-size:11px;color:#6e7681;margin-top:6px}
438
- </style>
439
- </head>
440
  <body>
441
- <div id="hdr">
442
- <div class="dot"></div>
443
- <h1>πŸ€– AI Agent</h1>
444
- <span class="badge">Qwen2.5-Coder</span>
445
- <span class="badge">11 Tools</span>
446
- <span class="badge">Linux Container</span>
447
- </div>
448
  <div id="chat">
449
- <div class="msg bot">Namaste! Main ek AI Agent hoon jo Linux container mein run ho raha hoon.
450
 
451
  Mere paas ye tools hain:
452
  πŸ“ write_file, read_file, edit_file, delete_file, list_files
453
- ⚑ run β€” koi bhi shell command (python, pip, curl, etc.)
454
  πŸ“¦ git_clone β€” GitHub repo clone
455
  πŸ—οΈ gradle_build β€” Android APK build
456
- πŸ” web_search β€” DuckDuckGo
457
- 🌐 read_url β€” URL content
458
- ⬇️ download_file β€” file download
459
 
460
- Mujhe kuch bhi banane, chalane, ya dhundhne ko kaho!</div>
461
  </div>
462
  <div id="bot-area">
463
- <div id="row">
464
- <textarea id="inp" placeholder="e.g. Python se chess game banao aur gofile par upload karo" rows="1"></textarea>
465
- <button id="btn" onclick="send()">Send ➀</button>
466
- </div>
467
- <div class="hint">πŸ’‘ Try: "Python se chess ka game banao" | "Search for Android tutorial" | "List files" | "pip install requests aur test karo"</div>
468
  </div>
469
  <script>
470
  const chat=document.getElementById('chat'),inp=document.getElementById('inp'),btn=document.getElementById('btn');
@@ -472,20 +411,19 @@ let history=[];
472
  inp.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
473
  inp.addEventListener('input',()=>{inp.style.height='auto';inp.style.height=Math.min(inp.scrollHeight,120)+'px'});
474
  function sc(){chat.scrollTop=chat.scrollHeight}
475
- function mkEl(cls,html,par){const e=document.createElement('div');e.className=cls;if(html)e.innerHTML=html;(par||chat).appendChild(e);sc();return e}
476
 
477
  async function send(){
478
  const msg=inp.value.trim();if(!msg||btn.disabled)return;
479
- mkEl('msg user','').textContent=msg;
480
  inp.value='';inp.style.height='auto';btn.disabled=true;
481
- const bd=mkEl('msg bot','');bd.textContent='⏳ Thinking...';
482
  let botTxt='',firstTok=true,curTool=null;
483
  try{
484
  const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},
485
  body:JSON.stringify({message:msg,history})});
486
  if(!r.ok)throw new Error('HTTP '+r.status);
487
- const reader=r.body.getReader(),dec=new TextDecoder();
488
- let buf='';
489
  while(true){
490
  const{done,value}=await reader.read();if(done)break;
491
  buf+=dec.decode(value,{stream:true});
@@ -498,13 +436,13 @@ async function send(){
498
  botTxt+=d.text;bd.textContent=botTxt;sc();
499
  }else if(d.type==='tool_start'){
500
  if(firstTok){bd.textContent='';firstTok=false}
501
- const args=typeof d.args==='object'?JSON.stringify(d.args.args||d.args,null,1):String(d.args);
502
- curTool=mkEl('tc','<div class="lbl">πŸ”§ Tool Call</div><b>'+d.tool+'</b><br><span style="color:#8b949e;font-size:11px">'+args.replace(/</g,'&lt;').substring(0,300)+'</span>');
503
  botTxt='';bd.textContent='';
504
  }else if(d.type==='tool_result'){
505
- if(curTool){const tr=mkEl('tr','<div class="lbl">βœ… Result: '+d.tool+'</div>',null);tr.firstChild.insertAdjacentText('afterend',(d.result||'').substring(0,1000).replace(/</g,'&lt;'));chat.appendChild(tr);sc();}
506
  curTool=null;
507
- const nb=mkEl('msg bot','');nb.textContent='';
508
  Object.defineProperty(bd,'textContent',{get:()=>nb.textContent,set:v=>{nb.textContent=v}});
509
  botTxt='';firstTok=false;
510
  }else if(d.type==='error'){
@@ -518,8 +456,7 @@ async function send(){
518
  if(bd.textContent==='⏳ Thinking...')bd.textContent='(done)';
519
  btn.disabled=false;inp.focus();
520
  }
521
- </script>
522
- </body></html>"""
523
 
524
  @app.get("/", response_class=HTMLResponse)
525
  async def root(): return HTMLResponse(HTML)
 
9
  from llama_cpp import Llama
10
 
11
  try:
12
+ import git as gitlib; HAS_GIT = True
13
+ except: HAS_GIT = False
 
 
 
14
  try:
15
+ from duckduckgo_search import DDGS; HAS_DDG = True
16
+ except: HAS_DDG = False
 
 
17
 
18
  WORKSPACE = Path("/workspace")
19
  WORKSPACE.mkdir(exist_ok=True)
20
 
 
 
 
21
  print("Downloading model...")
22
+ model_path = hf_hub_download(repo_id="Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF",
23
+ filename="qwen2.5-coder-1.5b-instruct-q4_k_m.gguf")
24
  print("Loading model...")
25
  llm = Llama(model_path=model_path, n_ctx=8192,
26
  n_threads=int(os.getenv("LLAMA_THREADS","2")), n_batch=512, verbose=False)
27
  print("Model ready!")
28
 
29
  # ── tools ─────────────────────────────────────────────────────────────────────
 
30
  def _safe(p):
31
  path = (WORKSPACE / p).resolve()
32
  if WORKSPACE.resolve() not in path.parents and path != WORKSPACE.resolve():
 
36
  def tool_write_file(path, content):
37
  try:
38
  p = _safe(path); p.parent.mkdir(parents=True, exist_ok=True)
39
+ p.write_text(str(content), encoding="utf-8")
40
+ return f"OK: wrote {len(str(content))} chars to /workspace/{path}"
41
  except Exception as e: return f"ERROR: {e}"
42
 
43
  def tool_read_file(path):
 
70
  try:
71
  cwd = _safe(dir)
72
  r = subprocess.run(command, cwd=cwd, shell=True, capture_output=True, text=True, timeout=300)
73
+ out = (r.stdout or "")[-2500:]; err = (r.stderr or "")[-1000:]
 
74
  return f"exit={r.returncode}\n{out}\n{err}".strip()
75
  except subprocess.TimeoutExpired: return "ERROR: timeout after 300s"
76
  except Exception as e: return f"ERROR: {e}"
 
91
  if w.exists(): os.chmod(w, 0o755); cmd = f"./gradlew {task} --no-daemon"
92
  else: cmd = f"gradle {task} --no-daemon"
93
  r = subprocess.run(cmd, cwd=cwd, shell=True, capture_output=True, text=True, timeout=900)
 
94
  apks = glob.glob(f"{cwd}/**/*.apk", recursive=True)
95
+ return f"exit={r.returncode}\n{(r.stdout or '')[-3000:]}\n{(r.stderr or '')[-1000:]}\nAPKs: {apks}"
96
  except Exception as e: return f"ERROR: {e}"
97
 
98
  def tool_web_search(query, max_results=5):
 
124
  req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0"})
125
  with urllib.request.urlopen(req, timeout=30) as resp, open(p,'wb') as f:
126
  f.write(resp.read())
127
+ return f"OK: saved to /workspace/{save_as} ({p.stat().st_size} bytes)"
128
  except Exception as e: return f"ERROR: {e}"
129
 
130
  TOOLS = {
131
+ "write_file": (tool_write_file, ["path","content"], "Create or overwrite a file"),
132
+ "read_file": (tool_read_file, ["path"], "Read file contents"),
133
+ "edit_file": (tool_edit_file, ["path","find","replace"], "Find and replace in file"),
134
+ "delete_file": (tool_delete_file, ["path"], "Delete file or directory"),
135
+ "list_files": (tool_list_files, ["path"], "List workspace files"),
136
+ "run": (tool_run, ["command","dir"], "Execute shell command"),
137
+ "git_clone": (tool_git_clone, ["url","dest"], "Clone git repository"),
138
+ "gradle_build": (tool_gradle_build, ["repo","task"], "Build Android APK"),
139
+ "web_search": (tool_web_search, ["query","max_results"], "Search the web"),
140
+ "read_url": (tool_read_url, ["url"], "Fetch URL content"),
141
+ "download_file": (tool_download_file, ["url","save_as"], "Download file from URL"),
142
  }
143
 
144
+ # ── SYSTEM PROMPT ─────────────────────────────────────────────────────────────
145
+ # Key: few-shot examples teach the model the EXACT pattern
146
+ # The model sees: user asks -> assistant writes ACTION: -> sees [RESULT:] -> continues
 
 
147
 
148
+ SYSTEM = """You are an AI agent inside a Linux container. You have tools. You MUST use them.
149
 
150
+ TOOLS:
151
+ - write_file(path, content) - create file
152
+ - read_file(path) - read file
153
+ - edit_file(path, find, replace) - edit file
154
+ - delete_file(path) - delete file
155
+ - list_files(path=".") - list files
156
+ - run(command, dir=".") - run shell command
157
+ - git_clone(url, dest="") - clone repo
158
+ - gradle_build(repo, task="assembleDebug") - build APK
159
+ - web_search(query) - search web
160
+ - read_url(url) - fetch URL
161
+ - download_file(url, save_as) - download file
162
 
163
+ TO USE A TOOL write exactly:
164
+ ACTION: tool_name("arg1", "arg2")
165
 
166
+ EXAMPLES:
 
 
167
 
168
+ User: list workspace files
169
+ Assistant: Checking workspace.
 
 
170
  ACTION: list_files()
171
  [RESULT: workspace is empty]
172
+ Workspace is empty.
 
 
 
 
 
 
173
 
174
+ User: create hello.py that prints hello
175
+ Assistant: Creating file.
176
+ ACTION: write_file("hello.py", "print('hello world')")
177
+ [RESULT: OK: wrote 22 chars to /workspace/hello.py]
178
  ACTION: run("python3 hello.py")
179
  [RESULT: exit=0\nhello world]
180
+ Done. Output: hello world
181
 
182
+ User: install requests and test it
183
+ Assistant: Installing requests.
184
+ ACTION: run("pip install requests -q")
185
+ [RESULT: exit=0\nSuccessfully installed requests]
186
+ ACTION: run("python3 -c \\"import requests; print(requests.get('https://httpbin.org/get').status_code)\\"")
187
+ [RESULT: exit=0\n200]
188
+ Installed and working.
189
+
190
+ User: search for python chess library
191
  Assistant: Searching now.
192
+ ACTION: web_search("python chess library pygame")
193
+ [RESULT: Title: python-chess\nURL: https://python-chess.readthedocs.io\nSnippet: A chess library for Python]
194
+ Found: python-chess library.
195
+
196
+ User: make a chess game in python and upload to gofile
197
+ Assistant: Step 1: Install dependencies.
198
+ ACTION: run("pip install pygame -q")
199
+ [RESULT: exit=0\nSuccessfully installed pygame]
200
+ Step 2: Write chess game.
201
+ ACTION: write_file("chess.py", "# chess game code here")
202
+ [RESULT: OK: wrote file]
203
+ Step 3: Upload to gofile.
204
+ ACTION: run("curl -s -F 'file=@/workspace/chess.py' https://store1.gofile.io/uploadFile")
205
+ [RESULT: exit=0\n{"status":"ok","data":{"downloadPage":"https://gofile.io/d/abc123"}}]
206
+ Uploaded! Download: https://gofile.io/d/abc123
207
+
208
+ IMPORTANT RULES:
209
+ 1. ALWAYS use ACTION: for any task involving files, code, search, commands
210
+ 2. NEVER say "I cannot" or "I'm sorry" - you CAN do everything
211
+ 3. You are in a container with internet access, python3, pip, curl, git
212
+ 4. After [RESULT:] always continue - do next step or summarize
213
+ 5. For uploads use: ACTION: run("curl -s -F 'file=@/workspace/FILE' https://store1.gofile.io/uploadFile")
214
  """
215
 
216
+ # Regex to find ACTION: lines
217
+ ACTION_RE = re.compile(r'ACTION:\s*(\w+)\(([^)]*(?:\([^)]*\)[^)]*)*)\)', re.DOTALL)
218
 
219
+ def call_tool(name, args_raw):
220
+ """Parse args and call tool."""
221
+ if name not in TOOLS:
222
+ return f"ERROR: Unknown tool '{name}'. Available: {', '.join(TOOLS.keys())}"
 
 
 
 
 
 
 
 
 
223
 
224
+ fn = TOOLS[name][0]
225
+ args_raw = args_raw.strip()
 
226
 
227
+ if not args_raw:
228
+ try: return str(fn())
229
+ except: return str(fn("."))
 
230
 
231
+ # Parse quoted string arguments
232
+ args = []
 
 
 
 
233
  i = 0
234
+ while i < len(args_raw):
235
+ # Skip whitespace and commas
236
+ while i < len(args_raw) and args_raw[i] in ' ,\t\n':
 
 
 
 
 
 
 
 
 
237
  i += 1
238
+ if i >= len(args_raw):
239
+ break
240
+
241
+ if args_raw[i] in ('"', "'"):
242
+ # Quoted string
243
+ q = args_raw[i]; i += 1; s = ""
244
+ while i < len(args_raw):
245
+ c = args_raw[i]
246
+ if c == '\\' and i+1 < len(args_raw):
247
+ nc = args_raw[i+1]
248
+ if nc == 'n': s += '\n'; i += 2; continue
249
+ elif nc == 't': s += '\t'; i += 2; continue
250
+ elif nc in ('"', "'", '\\'): s += nc; i += 2; continue
251
+ if c == q:
252
+ i += 1; break
253
+ s += c; i += 1
254
+ args.append(s)
255
+ else:
256
+ # Unquoted - read until comma
257
+ j = i
258
+ while j < len(args_raw) and args_raw[j] != ',':
259
+ j += 1
260
+ args.append(args_raw[i:j].strip())
261
+ i = j
 
 
 
 
 
 
 
262
 
263
  try:
264
+ return str(fn(*args))
 
 
265
  except Exception as e:
 
266
  try:
267
+ return str(fn(args_raw.strip('"').strip("'")))
 
268
  except Exception as e2:
269
+ return f"ERROR: {e} | {e2}"
270
 
271
 
272
  def run_agent_sync(message, history):
273
+ """Run agent loop, return list of SSE event dicts."""
 
274
  msgs = [{"role": "system", "content": SYSTEM}]
 
275
  for h in history:
276
  if isinstance(h, (list, tuple)) and len(h) == 2:
277
  if h[0]: msgs.append({"role": "user", "content": str(h[0])})
278
  if h[1]: msgs.append({"role": "assistant", "content": str(h[1])})
 
279
  msgs.append({"role": "user", "content": message})
280
+
281
  events = []
282
  full_response = ""
283
+ accumulated = "" # full assistant turn so far
284
+
285
+ for step in range(15):
286
+ # Generate next chunk - stop ONLY at [RESULT: so model writes ACTION: freely
287
  out = llm.create_chat_completion(
288
  messages=msgs,
289
  temperature=0.1,
290
+ max_tokens=700,
291
+ stop=["[RESULT:"], # stop when model tries to write its own result
292
  )
293
+ chunk = out["choices"][0]["message"]["content"]
 
294
  finish = out["choices"][0].get("finish_reason", "stop")
295
+
296
+ # Find ACTION: in this chunk
297
+ action_match = ACTION_RE.search(chunk)
298
+
 
 
 
 
 
 
 
 
 
299
  if not action_match:
300
+ # Pure text - emit and done
301
+ full_response += chunk
302
+ events.append({"type": "token", "text": chunk})
303
+ break
304
+
305
+ # Emit text before ACTION:
306
+ action_pos = chunk.rfind("ACTION:")
307
+ pre = chunk[:action_pos].rstrip()
308
+ if pre:
309
+ full_response += pre + "\n"
310
+ events.append({"type": "token", "text": pre + "\n"})
311
+
 
 
 
 
312
  tool_name = action_match.group(1)
313
+ args_raw = action_match.group(2)
314
+
315
  # Emit tool_start
316
+ events.append({"type": "tool_start", "tool": tool_name,
317
+ "args": {"args": args_raw[:200]}})
318
+
 
319
  # Execute tool
320
+ result = call_tool(tool_name, args_raw)
321
+ result_str = str(result)[:2000]
322
+
323
+ events.append({"type": "tool_result", "tool": tool_name, "result": result_str})
324
+
325
+ # Build assistant turn with result injected
326
+ accumulated += pre + "\n" if pre else ""
327
+ accumulated += f"ACTION: {tool_name}({args_raw})\n[RESULT: {result_str}]\n"
328
+
329
+ # Update messages: replace last assistant turn or append
330
+ if msgs and msgs[-1]["role"] == "assistant":
331
+ msgs[-1]["content"] = accumulated
332
+ else:
333
+ msgs.append({"role": "assistant", "content": accumulated})
334
+
335
+ full_response += f"[{tool_name}] {result_str[:80]}\n"
336
+
337
+ # Continue generating after result
338
+ # Add continuation prompt
339
+ if msgs[-1]["role"] != "user":
340
+ # Keep same assistant turn - model will continue from where it left off
341
+ pass
342
+
343
  events.append({"type": "done", "full": full_response})
344
  return events
345
 
 
353
 
354
 
355
  # ── FastAPI ───────────────────────────────────────────────────────────────────
356
+ app = FastAPI(title="AI Agent")
357
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
358
 
359
  HTML = r"""<!DOCTYPE html>
360
  <html>
361
+ <head><title>AI Agent</title><meta name="viewport" content="width=device-width,initial-scale=1">
 
 
362
  <style>
363
  *{box-sizing:border-box;margin:0;padding:0}
364
  body{font-family:'Segoe UI',system-ui,sans-serif;background:#0d1117;color:#e6edf3;height:100vh;display:flex;flex-direction:column}
365
+ #hdr{padding:12px 18px;background:#161b22;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:10px}
366
  #hdr h1{font-size:16px;color:#58a6ff;font-weight:700}
367
  .badge{font-size:11px;background:#1f6feb22;color:#58a6ff;border:1px solid #1f6feb55;padding:2px 8px;border-radius:10px}
368
+ .dot{width:8px;height:8px;background:#3fb950;border-radius:50%;animation:pulse 2s infinite}
369
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
370
  #chat{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:8px}
371
  .msg{max-width:82%;padding:10px 14px;border-radius:12px;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;font-size:14px}
372
  .user{align-self:flex-end;background:#1f6feb;color:#fff;border-radius:12px 12px 2px 12px}
373
  .bot{align-self:flex-start;background:#161b22;border:1px solid #30363d;border-radius:2px 12px 12px 12px}
374
+ .tc{align-self:flex-start;max-width:90%;background:#0d1f3c;border:1px solid #1f6feb55;border-radius:8px;padding:8px 12px;font-size:12px;font-family:monospace}
375
+ .tr{align-self:flex-start;max-width:90%;background:#071a07;border:1px solid #3fb95055;border-radius:8px;padding:8px 12px;font-size:12px;font-family:monospace;max-height:180px;overflow-y:auto}
376
  .lbl{font-size:10px;color:#8b949e;margin-bottom:3px;text-transform:uppercase;letter-spacing:.5px}
377
  #bot-area{padding:10px 14px;background:#161b22;border-top:1px solid #30363d}
378
  #row{display:flex;gap:8px;align-items:flex-end}
379
  #inp{flex:1;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#e6edf3;padding:10px 14px;font-size:14px;resize:none;min-height:44px;max-height:120px;outline:none;font-family:inherit}
380
  #inp:focus{border-color:#58a6ff}
381
  #btn{background:#1f6feb;color:#fff;border:none;border-radius:8px;padding:10px 16px;cursor:pointer;font-size:14px;font-weight:600;height:44px}
382
+ #btn:hover{background:#388bfd}#btn:disabled{background:#21262d;color:#555;cursor:not-allowed}
 
383
  .hint{font-size:11px;color:#6e7681;margin-top:6px}
384
+ </style></head>
 
385
  <body>
386
+ <div id="hdr"><div class="dot"></div><h1>πŸ€– AI Agent</h1>
387
+ <span class="badge">Qwen2.5-Coder</span><span class="badge">11 Tools</span><span class="badge">Linux Container</span></div>
 
 
 
 
 
388
  <div id="chat">
389
+ <div class="msg bot">Namaste! Main Linux container mein run ho raha hoon πŸ€–
390
 
391
  Mere paas ye tools hain:
392
  πŸ“ write_file, read_file, edit_file, delete_file, list_files
393
+ ⚑ run β€” koi bhi shell command (python3, pip, curl, etc.)
394
  πŸ“¦ git_clone β€” GitHub repo clone
395
  πŸ—οΈ gradle_build β€” Android APK build
396
+ πŸ” web_search β€” DuckDuckGo search
397
+ 🌐 read_url, download_file
 
398
 
399
+ Kuch bhi banao, chalao, ya dhundho!</div>
400
  </div>
401
  <div id="bot-area">
402
+ <div id="row">
403
+ <textarea id="inp" placeholder="e.g. Python se chess game banao aur gofile par upload karo" rows="1"></textarea>
404
+ <button id="btn" onclick="send()">Send ➀</button>
405
+ </div>
406
+ <div class="hint">πŸ’‘ "Python se chess game banao" | "pip install flask aur hello world app banao" | "Search Android tutorial"</div>
407
  </div>
408
  <script>
409
  const chat=document.getElementById('chat'),inp=document.getElementById('inp'),btn=document.getElementById('btn');
 
411
  inp.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
412
  inp.addEventListener('input',()=>{inp.style.height='auto';inp.style.height=Math.min(inp.scrollHeight,120)+'px'});
413
  function sc(){chat.scrollTop=chat.scrollHeight}
414
+ function mk(cls,par){const e=document.createElement('div');e.className=cls;(par||chat).appendChild(e);sc();return e}
415
 
416
  async function send(){
417
  const msg=inp.value.trim();if(!msg||btn.disabled)return;
418
+ const um=mk('msg user');um.textContent=msg;
419
  inp.value='';inp.style.height='auto';btn.disabled=true;
420
+ const bd=mk('msg bot');bd.textContent='⏳ Thinking...';
421
  let botTxt='',firstTok=true,curTool=null;
422
  try{
423
  const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},
424
  body:JSON.stringify({message:msg,history})});
425
  if(!r.ok)throw new Error('HTTP '+r.status);
426
+ const reader=r.body.getReader(),dec=new TextDecoder();let buf='';
 
427
  while(true){
428
  const{done,value}=await reader.read();if(done)break;
429
  buf+=dec.decode(value,{stream:true});
 
436
  botTxt+=d.text;bd.textContent=botTxt;sc();
437
  }else if(d.type==='tool_start'){
438
  if(firstTok){bd.textContent='';firstTok=false}
439
+ const args=d.args&&d.args.args?d.args.args:JSON.stringify(d.args);
440
+ curTool=mk('tc');curTool.innerHTML='<div class="lbl">πŸ”§ Tool Call</div><b>'+d.tool+'</b><br><span style="color:#8b949e;font-size:11px">'+String(args).substring(0,300).replace(/</g,'&lt;')+'</span>';
441
  botTxt='';bd.textContent='';
442
  }else if(d.type==='tool_result'){
443
+ if(curTool){const tr=mk('tr');tr.innerHTML='<div class="lbl">βœ… Result: '+d.tool+'</div><pre style="white-space:pre-wrap;font-size:11px">'+String(d.result||'').substring(0,1000).replace(/</g,'&lt;')+'</pre>';}
444
  curTool=null;
445
+ const nb=mk('msg bot');nb.textContent='';
446
  Object.defineProperty(bd,'textContent',{get:()=>nb.textContent,set:v=>{nb.textContent=v}});
447
  botTxt='';firstTok=false;
448
  }else if(d.type==='error'){
 
456
  if(bd.textContent==='⏳ Thinking...')bd.textContent='(done)';
457
  btn.disabled=false;inp.focus();
458
  }
459
+ </script></body></html>"""
 
460
 
461
  @app.get("/", response_class=HTMLResponse)
462
  async def root(): return HTMLResponse(HTML)