Spaces:
Sleeping
Sleeping
Fix: stop only at [RESULT:, inject results into assistant turn, ACTION: regex fix
Browse files
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 |
-
|
| 14 |
-
except ImportError:
|
| 15 |
-
HAS_GIT = False
|
| 16 |
-
|
| 17 |
try:
|
| 18 |
-
from duckduckgo_search import DDGS
|
| 19 |
-
|
| 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=
|
|
|
|
| 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{
|
| 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:
|
| 138 |
except Exception as e: return f"ERROR: {e}"
|
| 139 |
|
| 140 |
TOOLS = {
|
| 141 |
-
"write_file":
|
| 142 |
-
"read_file":
|
| 143 |
-
"edit_file":
|
| 144 |
-
"delete_file":
|
| 145 |
-
"list_files":
|
| 146 |
-
"run":
|
| 147 |
-
"git_clone":
|
| 148 |
-
"gradle_build":
|
| 149 |
-
"web_search":
|
| 150 |
-
"read_url":
|
| 151 |
-
"download_file":(tool_download_file,"url,
|
| 152 |
}
|
| 153 |
|
| 154 |
-
# ββ SYSTEM PROMPT
|
| 155 |
-
# Key
|
| 156 |
-
#
|
| 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 |
-
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
|
| 175 |
-
|
| 176 |
-
ACTION: tool_name(arg1, arg2)
|
| 177 |
|
| 178 |
-
|
| 179 |
-
ACTION: write_file("hello.py", \"\"\"print('hello world')
|
| 180 |
-
\"\"\")
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
User: list files in workspace
|
| 185 |
-
Assistant: Let me check.
|
| 186 |
ACTION: list_files()
|
| 187 |
[RESULT: workspace is empty]
|
| 188 |
-
|
| 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:
|
| 197 |
-
Assistant:
|
|
|
|
|
|
|
| 198 |
ACTION: run("python3 hello.py")
|
| 199 |
[RESULT: exit=0\nhello world]
|
| 200 |
-
Output: hello world
|
| 201 |
|
| 202 |
-
User:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
Assistant: Searching now.
|
| 204 |
-
ACTION: web_search("python chess library")
|
| 205 |
-
[RESULT: Title: python-chess..
|
| 206 |
-
Found
|
| 207 |
-
|
| 208 |
-
User:
|
| 209 |
-
Assistant:
|
| 210 |
-
ACTION:
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
| 225 |
"""
|
| 226 |
|
| 227 |
-
|
|
|
|
| 228 |
|
| 229 |
-
def
|
| 230 |
-
"""Parse
|
| 231 |
-
|
| 232 |
-
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
kwargs = {}
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
return [tq_match.group(1), tq_match.group(2)], {}
|
| 251 |
|
| 252 |
-
#
|
| 253 |
-
|
| 254 |
-
current = ""
|
| 255 |
-
in_quote = False
|
| 256 |
-
quote_char = None
|
| 257 |
-
depth = 0
|
| 258 |
i = 0
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 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 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 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 |
-
|
| 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 |
-
|
| 312 |
-
return str(result)
|
| 313 |
except Exception as e2:
|
| 314 |
-
return f"ERROR
|
| 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 |
-
|
| 333 |
-
|
|
|
|
| 334 |
out = llm.create_chat_completion(
|
| 335 |
messages=msgs,
|
| 336 |
temperature=0.1,
|
| 337 |
-
max_tokens=
|
| 338 |
-
stop=["
|
| 339 |
)
|
| 340 |
-
|
| 341 |
-
reply = out["choices"][0]["message"]["content"]
|
| 342 |
finish = out["choices"][0].get("finish_reason", "stop")
|
| 343 |
-
|
| 344 |
-
#
|
| 345 |
-
|
| 346 |
-
|
| 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
|
| 358 |
-
full_response +=
|
| 359 |
-
events.append({"type": "token", "text":
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 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 |
-
|
| 375 |
-
|
| 376 |
# Emit tool_start
|
| 377 |
-
events.append({"type": "tool_start", "tool": tool_name,
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
# Execute tool
|
| 382 |
-
result =
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
events.append({"type": "tool_result", "tool": tool_name, "result":
|
| 386 |
-
|
| 387 |
-
#
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 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="
|
| 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
|
| 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%;
|
| 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:
|
| 428 |
-
.tr{align-self:flex-start;max-width:
|
| 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 |
-
|
| 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 |
-
|
| 450 |
|
| 451 |
Mere paas ye tools hain:
|
| 452 |
π write_file, read_file, edit_file, delete_file, list_files
|
| 453 |
-
β‘ run β koi bhi shell command (
|
| 454 |
π¦ git_clone β GitHub repo clone
|
| 455 |
ποΈ gradle_build β Android APK build
|
| 456 |
-
π web_search β DuckDuckGo
|
| 457 |
-
π read_url
|
| 458 |
-
β¬οΈ download_file β file download
|
| 459 |
|
| 460 |
-
|
| 461 |
</div>
|
| 462 |
<div id="bot-area">
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 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
|
| 476 |
|
| 477 |
async function send(){
|
| 478 |
const msg=inp.value.trim();if(!msg||btn.disabled)return;
|
| 479 |
-
|
| 480 |
inp.value='';inp.style.height='auto';btn.disabled=true;
|
| 481 |
-
const bd=
|
| 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=
|
| 502 |
-
curTool=
|
| 503 |
botTxt='';bd.textContent='';
|
| 504 |
}else if(d.type==='tool_result'){
|
| 505 |
-
if(curTool){const tr=
|
| 506 |
curTool=null;
|
| 507 |
-
const nb=
|
| 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,'<')+'</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,'<')+'</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)
|