Keeby-smilyai commited on
Commit
a3961a8
·
verified ·
1 Parent(s): 1d6374d

Update backend.py

Browse files
Files changed (1) hide show
  1. backend.py +192 -80
backend.py CHANGED
@@ -1,4 +1,3 @@
1
- # backend.py — FINAL, HIERARCHICAL MULTI-AGENT SYSTEM v3.4
2
  import sqlite3
3
  import os
4
  import json
@@ -7,7 +6,12 @@ import concurrent.futures
7
  import traceback
8
  import zipfile
9
  import hashlib
10
- from typing import Optional, Dict, Any
 
 
 
 
 
11
 
12
  import torch
13
  import psutil
@@ -86,7 +90,7 @@ ROLE_PROMPTS = {
86
  "ceo": """You are the CEO. Your job is to create a high-level plan and delegate the first task to your Manager.
87
  Analyze the user's request and provide a list of files to be created.
88
  Then, create the very first task for the Manager to execute.
89
- Respond ONLY with a JSON object with keys "plan" and "initial_task".
90
  Example: {"plan": {"files": ["app.py", "backend.py", "tests/test_app.py"]}, "initial_task": "Start by writing the backend.py file with database functions."}""",
91
 
92
  "manager": """You are the Manager. You receive tasks and questions. Your job is to:
@@ -94,39 +98,124 @@ ROLE_PROMPTS = {
94
  2. After a worker completes a task, assign the NEXT logical task (code another file OR write tests)
95
  3. Only finish when ALL code and tests are complete
96
  4. If unsure, ask the CEO
97
- Available actions: "delegate_to_worker", "answer_worker", "ask_ceo", "finish_project"
98
- Respond ONLY with a JSON object with "action" and "data".
99
- Examples:
100
- - {"action": "delegate_to_worker", "data": {"worker_type": "coder", "task": "Write the full Python code for app.py"}}
101
- - {"action": "delegate_to_worker", "data": {"worker_type": "tester", "task": "Write unit tests for backend.py"}}
102
- - {"action": "finish_project", "data": {}}""",
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  "worker_coder": """You are a Coder. Your only job is to write code based on the Manager's task.
105
- After completing a task, report "task_complete" - DO NOT decide what to do next.
 
106
  The Manager will assign the next file to code.
107
- Available actions: "write_code", "read_file", "ask_manager", "task_complete"
108
- Respond ONLY with a JSON object with "action" and "data".
109
- Example: {"action": "write_code", "data": {"file_path": "backend.py", "code": "import sqlite3..."}}""",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  "worker_tester": """You are a Tester. Your only job is to write tests for existing code.
112
- After completing test writing, report "task_complete".
 
113
  The Manager will decide if more tests are needed or if the project can finish.
114
- Available actions: "write_test", "read_file", "ask_manager", "task_complete"
115
- Respond ONLY with a JSON object with "action" and "data".
116
- Example: {"action": "write_test", "data": {"file_path": "tests/test_backend.py", "code": "import pytest..."}}"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  # ------------------------------ FILE SYSTEM & AI TOOLS ------------------------------
120
  def get_project_dir(user_id, project_id):
121
  path = os.path.join(PROJECT_ROOT, str(user_id), str(project_id)); os.makedirs(path, exist_ok=True); return path
 
122
  def create_file(project_dir, path, content):
123
  full_path = os.path.join(project_dir, path); os.makedirs(os.path.dirname(full_path), exist_ok=True)
124
  with open(full_path, 'w', encoding='utf-8') as f: f.write(content)
 
125
  def read_file(project_dir, path):
126
  full_path = os.path.join(project_dir, path)
127
  try:
128
  with open(full_path, 'r', encoding='utf-8') as f: return f.read()
129
  except FileNotFoundError: return "Error: File not found."
 
130
  def zip_project(project_dir, project_id):
131
  zip_filename = f"project_{project_id}.zip"; zip_path = os.path.join(os.path.dirname(project_dir), zip_filename)
132
  with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
@@ -134,37 +223,25 @@ def zip_project(project_dir, project_id):
134
  for file in files: zf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), project_dir))
135
  return zip_path
136
 
137
- def _extract_code(text: str) -> Optional[str]:
138
- match = re.search(r"```(?:\w*\n)?([\s\S]*?)```", text)
139
- if match: return match.group(1).strip()
140
- return text.strip()
141
-
142
- # --- THIS IS THE FINAL FIX ---
143
- def _extract_json(text: str) -> Optional[Dict[str, Any]]:
144
- """More robust JSON extractor that handles conversational text after the JSON block."""
145
- # First, try to find a markdown block, as it's the most reliable
146
- match = re.search(r"```json\s*([\s\S]*?)\s*```", text)
147
- if match:
148
- json_str = match.group(1)
149
- try:
150
- return json.loads(json_str)
151
- except json.JSONDecodeError:
152
- print(f"Failed to decode JSON from markdown block: {json_str[:200]}...")
153
- # Fall through to the next method if markdown parsing fails
154
- pass
155
-
156
- # If no markdown, find the first '{' and the last '}' to isolate the JSON
157
  try:
158
- start_index = text.find('{')
159
- end_index = text.rfind('}')
160
- if start_index != -1 and end_index != -1 and end_index > start_index:
161
- json_str = text[start_index : end_index + 1]
162
- return json.loads(json_str)
163
- except (json.JSONDecodeError, IndexError):
164
- # This will be the final failure point if no valid JSON is found
165
- print(f"Failed to decode JSON from the entire response: {text[:200]}...")
166
- return None
167
- return None # Return None if no JSON object is found at all
 
 
 
 
 
 
168
 
169
  def generate_with_model(role: str, prompt: str) -> str:
170
  try:
@@ -175,7 +252,7 @@ def generate_with_model(role: str, prompt: str) -> str:
175
  outputs = model.generate(**inputs, max_new_tokens=3072, pad_token_id=tokenizer.eos_token_id, use_cache=True)
176
  return tokenizer.decode(outputs[0][len(inputs.input_ids[0]):], skip_special_tokens=True).strip()
177
  except Exception as e:
178
- print(f"Error during model generation for role {role}: {e}"); return f'{{"action": "error", "data": "{e}"}}'
179
 
180
  # ------------------------------ HIERARCHICAL AGENT ORCHESTRATOR ------------------------------
181
  def run_agent_chain(project_id, user_id, initial_prompt):
@@ -197,12 +274,12 @@ def run_agent_chain(project_id, user_id, initial_prompt):
197
  "current_task": "Start the project.",
198
  "last_action_result": None,
199
  "turn": 0,
200
- "max_turns": 25, # Safety brake
201
  }
202
 
203
  log_step("ORCHESTRATOR", "Briefing the CEO with the user's request.")
204
  ceo_response_text = generate_with_model("ceo", initial_prompt)
205
- ceo_action = _extract_json(ceo_response_text)
206
  if not ceo_action or "plan" not in ceo_action: raise ValueError("CEO failed to provide an initial plan.")
207
 
208
  project_context["file_structure"] = ceo_action["plan"].get("files", [])
@@ -223,55 +300,90 @@ def run_agent_chain(project_id, user_id, initial_prompt):
223
  """
224
 
225
  response_text = generate_with_model(next_agent, prompt)
226
- agent_response = _extract_json(response_text)
227
 
228
- if not agent_response or "action" not in agent_response:
229
  project_context["last_action_result"] = f"Error: Agent {next_agent} returned invalid response. Retrying."
230
  log_step("ORCHESTRATOR", f"Invalid response from {next_agent}. Retrying task.", response_text)
231
  project_context["turn"] += 1
232
  continue
233
 
234
- action = agent_response["action"]
235
- data = agent_response.get("data", {})
236
- thought = data.get("thought", f"Decided to perform action: `{action}`")
237
-
238
- if action == "delegate_to_worker":
239
- worker_type = data.get("worker_type", "coder")
240
- next_agent = f"worker_{worker_type}"
241
- project_context["current_task"] = data.get("task")
242
- project_context["last_action_result"] = f"Task delegated to {next_agent}."
243
- log_step("MANAGER", thought, project_context['last_action_result'])
244
-
245
- elif action == "write_code" or action == "write_test":
246
- file_path = data.get("file_path"); raw_code = data.get("code")
247
- if not file_path or not raw_code:
 
 
 
 
 
 
 
 
 
 
 
248
  project_context["last_action_result"] = "Error: Missing file_path or code in data."
249
  else:
250
- code = _extract_code(raw_code)
251
  create_file(project_dir, file_path, code)
252
  project_context["last_action_result"] = f"Successfully wrote code to {file_path}."
253
- log_step(next_agent.replace("_", " "), thought, project_context['last_action_result'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  elif action == "ask_manager":
 
 
 
 
 
256
  next_agent = "manager"
257
- project_context["current_task"] = data.get("question")
258
- project_context["last_action_result"] = f"A worker has a question."
259
- log_step(next_agent.replace("_", " "), thought, f"Asking Manager: {project_context['current_task']}")
260
-
261
  elif action == "answer_worker":
262
- next_agent = "manager"
263
- project_context["current_task"] = "The worker's question has been answered. What is the next task for a worker?"
264
- project_context["last_action_result"] = f"Manager answered: {data.get('answer')}"
265
- log_step("MANAGER", thought, project_context['last_action_result'])
 
 
266
 
267
  elif action == "task_complete":
268
- next_agent = "manager"
269
- project_context["current_task"] = f"Worker finished task. What is the next high-level step? (e.g., code next file, write tests, or finish project)"
270
  project_context["last_action_result"] = "Worker reported task is complete."
271
- log_step(next_agent.replace("_", " "), thought, project_context['last_action_result'])
 
272
 
273
  elif action == "finish_project":
274
- log_step("MANAGER", thought, "Project is ready for final packaging.")
275
  break
276
 
277
  else:
 
 
1
  import sqlite3
2
  import os
3
  import json
 
6
  import traceback
7
  import zipfile
8
  import hashlib
9
+ from typing import Optional, Dict, Any, List
10
+
11
+ # --- New dependencies for Web Scraping ---
12
+ import requests
13
+ from bs4 import BeautifulSoup
14
+ # ---
15
 
16
  import torch
17
  import psutil
 
90
  "ceo": """You are the CEO. Your job is to create a high-level plan and delegate the first task to your Manager.
91
  Analyze the user's request and provide a list of files to be created.
92
  Then, create the very first task for the Manager to execute.
93
+ Respond ONLY with a JSON object.
94
  Example: {"plan": {"files": ["app.py", "backend.py", "tests/test_app.py"]}, "initial_task": "Start by writing the backend.py file with database functions."}""",
95
 
96
  "manager": """You are the Manager. You receive tasks and questions. Your job is to:
 
98
  2. After a worker completes a task, assign the NEXT logical task (code another file OR write tests)
99
  3. Only finish when ALL code and tests are complete
100
  4. If unsure, ask the CEO
101
+ Available actions: `delegate_coder`, `delegate_tester`, `answer_worker`, `ask_ceo`, `finish_project`
102
+ Respond ONLY with a single tool call on a new line.
103
+
104
+ Example Actions:
105
+
106
+ To delegate a task to a Coder:
107
+ `delegate_coder("Write the full Python code for app.py")`
108
+
109
+ To delegate a task to a Tester:
110
+ `delegate_tester("Write unit tests for backend.py")`
111
+
112
+ To answer a worker's question:
113
+ `answer_worker("The main file is main.py, and it should use Pygame.")`
114
+
115
+ To ask a question to the CEO (e.g., if you are confused):
116
+ `ask_ceo("Should we use a relational database or a NoSQL database for this project?")`
117
+
118
+ To finish the entire project:
119
+ `finish_project()`""",
120
 
121
  "worker_coder": """You are a Coder. Your only job is to write code based on the Manager's task.
122
+ If you need more information or documentation to complete a task, you can use the `scrape_web` tool.
123
+ After completing a task, report back using the `task_complete` tool. DO NOT decide what to do next.
124
  The Manager will assign the next file to code.
125
+ Available actions: `scrape_web`, `write_code`, `read_file`, `ask_manager`, `task_complete`
126
+ Respond ONLY with a single tool call on a new line.
127
+
128
+ Example Actions:
129
+
130
+ To scrape a website for information:
131
+ `scrape_web("https://docs.python.org/3/library/sqlite3.html")`
132
+
133
+ To write code to a file. The second argument is a raw string of the code:
134
+ `write_code("backend.py", "import sqlite3\\n\\ndef init_db():\\n # ...")`
135
+
136
+ To read the contents of an existing file:
137
+ `read_file("main.py")`
138
+
139
+ To ask the Manager a question:
140
+ `ask_manager("What testing framework should I use for this project?")`
141
+
142
+ To report that you have finished your task:
143
+ `task_complete()`""",
144
 
145
  "worker_tester": """You are a Tester. Your only job is to write tests for existing code.
146
+ If you need to look up testing libraries or best practices, use the `scrape_web` tool.
147
+ After completing test writing, report `task_complete`.
148
  The Manager will decide if more tests are needed or if the project can finish.
149
+ Available actions: `scrape_web`, `write_test`, `read_file`, `ask_manager`, `task_complete`
150
+ Respond ONLY with a single tool call on a new line.
151
+
152
+ Example Actions:
153
+
154
+ To scrape a website for information:
155
+ `scrape_web("https://docs.pytest.org/en/stable/")`
156
+
157
+ To write tests to a file. The second argument is a raw string of the test code:
158
+ `write_test("tests/test_backend.py", "import pytest\\n\\ndef test_db_connection():\\n # ...")`
159
+
160
+ To read the contents of an existing file:
161
+ `read_file("app.py")`
162
+
163
+ To ask the Manager a question:
164
+ `ask_manager("Are there any specific edge cases I should focus on for the user login tests?")`
165
+
166
+ To report that you have finished your task:
167
+ `task_complete()`"""
168
  }
169
+ # --------------------------------- NEW PARSER ---------------------------------
170
+ def _extract_tool_call(text: str) -> Optional[Dict[str, Any]]:
171
+ """
172
+ Parses a string for a tool-call like `tool_name("arg1", "arg2")` and extracts
173
+ the function name and its arguments.
174
+ """
175
+ text = text.strip().split("\n")[0] # Only consider the first line
176
+
177
+ match = re.search(r'(\w+)\((.*)\)', text, re.DOTALL)
178
+ if not match:
179
+ return None
180
+
181
+ tool_name = match.group(1)
182
+ args_str = match.group(2)
183
+
184
+ # Simple regex to split arguments, handling quoted strings
185
+ # This is a basic approach and might fail on complex arguments
186
+ args = []
187
+
188
+ # Handle single string arguments
189
+ if re.fullmatch(r'\s*"(.*?)"\s*', args_str, re.DOTALL) or re.fullmatch(r"\s*'(.*?)'\s*", args_str, re.DOTALL):
190
+ args.append(args_str.strip().strip("'\""))
191
+ else:
192
+ # Simple split by comma for multiple args
193
+ for arg in args_str.split(','):
194
+ args.append(arg.strip().strip("'\""))
195
+
196
+ # Convert known numerical args if needed
197
+ try:
198
+ if tool_name in ["delegate_coder", "delegate_tester"]:
199
+ args = [str(arg) for arg in args]
200
+ except (ValueError, IndexError):
201
+ return None # Return None if parsing fails
202
+
203
+ return {"tool_name": tool_name, "args": args}
204
 
205
  # ------------------------------ FILE SYSTEM & AI TOOLS ------------------------------
206
  def get_project_dir(user_id, project_id):
207
  path = os.path.join(PROJECT_ROOT, str(user_id), str(project_id)); os.makedirs(path, exist_ok=True); return path
208
+
209
  def create_file(project_dir, path, content):
210
  full_path = os.path.join(project_dir, path); os.makedirs(os.path.dirname(full_path), exist_ok=True)
211
  with open(full_path, 'w', encoding='utf-8') as f: f.write(content)
212
+
213
  def read_file(project_dir, path):
214
  full_path = os.path.join(project_dir, path)
215
  try:
216
  with open(full_path, 'r', encoding='utf-8') as f: return f.read()
217
  except FileNotFoundError: return "Error: File not found."
218
+
219
  def zip_project(project_dir, project_id):
220
  zip_filename = f"project_{project_id}.zip"; zip_path = os.path.join(os.path.dirname(project_dir), zip_filename)
221
  with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
 
223
  for file in files: zf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), project_dir))
224
  return zip_path
225
 
226
+ def scrape_web(url: str) -> str:
227
+ """Scrapes a URL and returns the clean text content, limited to 5000 chars."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  try:
229
+ headers = {
230
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
231
+ }
232
+ response = requests.get(url, headers=headers, timeout=15)
233
+ response.raise_for_status()
234
+ soup = BeautifulSoup(response.content, 'html.parser')
235
+ for script_or_style in soup(["script", "style"]): script_or_style.decompose()
236
+ text = soup.get_text()
237
+ lines = (line.strip() for line in text.splitlines())
238
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
239
+ text = '\n'.join(chunk for chunk in chunks if chunk)
240
+ return text[:5000]
241
+ except requests.exceptions.RequestException as e:
242
+ return f"Error: Could not retrieve content from URL. {e}"
243
+ except Exception as e:
244
+ return f"Error: An unexpected error occurred during web scraping. {e}"
245
 
246
  def generate_with_model(role: str, prompt: str) -> str:
247
  try:
 
252
  outputs = model.generate(**inputs, max_new_tokens=3072, pad_token_id=tokenizer.eos_token_id, use_cache=True)
253
  return tokenizer.decode(outputs[0][len(inputs.input_ids[0]):], skip_special_tokens=True).strip()
254
  except Exception as e:
255
+ print(f"Error during model generation for role {role}: {e}"); return f"error({e})"
256
 
257
  # ------------------------------ HIERARCHICAL AGENT ORCHESTRATOR ------------------------------
258
  def run_agent_chain(project_id, user_id, initial_prompt):
 
274
  "current_task": "Start the project.",
275
  "last_action_result": None,
276
  "turn": 0,
277
+ "max_turns": 30,
278
  }
279
 
280
  log_step("ORCHESTRATOR", "Briefing the CEO with the user's request.")
281
  ceo_response_text = generate_with_model("ceo", initial_prompt)
282
+ ceo_action = json.loads(ceo_response_text) # CEO still uses JSON for its specific output
283
  if not ceo_action or "plan" not in ceo_action: raise ValueError("CEO failed to provide an initial plan.")
284
 
285
  project_context["file_structure"] = ceo_action["plan"].get("files", [])
 
300
  """
301
 
302
  response_text = generate_with_model(next_agent, prompt)
303
+ tool_call = _extract_tool_call(response_text)
304
 
305
+ if not tool_call:
306
  project_context["last_action_result"] = f"Error: Agent {next_agent} returned invalid response. Retrying."
307
  log_step("ORCHESTRATOR", f"Invalid response from {next_agent}. Retrying task.", response_text)
308
  project_context["turn"] += 1
309
  continue
310
 
311
+ action = tool_call.get("tool_name")
312
+ args = tool_call.get("args", [])
313
+
314
+ log_step(next_agent.replace("_", " "), f"Decided to perform action: `{action}` with args: `{args}`")
315
+
316
+ if action == "delegate_coder":
317
+ if not args:
318
+ project_context["last_action_result"] = "Error: Missing task for delegate_coder."
319
+ next_agent = "manager"
320
+ continue
321
+ next_agent = "worker_coder"
322
+ project_context["current_task"] = args[0]
323
+ project_context["last_action_result"] = f"Task delegated to {next_agent.replace('_', ' ')}."
324
+
325
+ elif action == "delegate_tester":
326
+ if not args:
327
+ project_context["last_action_result"] = "Error: Missing task for delegate_tester."
328
+ next_agent = "manager"
329
+ continue
330
+ next_agent = "worker_tester"
331
+ project_context["current_task"] = args[0]
332
+ project_context["last_action_result"] = f"Task delegated to {next_agent.replace('_', ' ')}."
333
+
334
+ elif action == "write_code":
335
+ if len(args) < 2:
336
  project_context["last_action_result"] = "Error: Missing file_path or code in data."
337
  else:
338
+ file_path = args[0]; code = args[1]
339
  create_file(project_dir, file_path, code)
340
  project_context["last_action_result"] = f"Successfully wrote code to {file_path}."
341
+ next_agent = "manager"
342
+ project_context["current_task"] = f"Worker finished task. What is the next high-level step?"
343
+
344
+ elif action == "write_test":
345
+ if len(args) < 2:
346
+ project_context["last_action_result"] = "Error: Missing file_path or code in data."
347
+ else:
348
+ file_path = args[0]; code = args[1]
349
+ create_file(project_dir, file_path, code)
350
+ project_context["last_action_result"] = f"Successfully wrote tests to {file_path}."
351
+ next_agent = "manager"
352
+ project_context["current_task"] = f"Worker finished task. What is the next high-level step?"
353
+
354
+ elif action == "scrape_web":
355
+ if not args:
356
+ project_context["last_action_result"] = "Error: Missing URL for scrape_web action."
357
+ else:
358
+ url = args[0]
359
+ log_step(next_agent.replace("_", " "), f"Scraping {url}...")
360
+ scraped_content = scrape_web(url)
361
+ project_context["last_action_result"] = f"Scraping result from {url}:\n\n{scraped_content}"
362
+ continue # Do not increment turn or change agent yet
363
 
364
  elif action == "ask_manager":
365
+ if not args:
366
+ project_context["last_action_result"] = "Error: Missing question for ask_manager."
367
+ else:
368
+ project_context["last_action_result"] = f"A worker has a question."
369
+ project_context["current_task"] = args[0]
370
  next_agent = "manager"
371
+
 
 
 
372
  elif action == "answer_worker":
373
+ if not args:
374
+ project_context["last_action_result"] = "Error: Missing answer for answer_worker."
375
+ else:
376
+ project_context["last_action_result"] = f"Manager answered: {args[0]}"
377
+ project_context["current_task"] = "The worker's question has been answered. What is the next task for a worker?"
378
+ next_agent = "manager"
379
 
380
  elif action == "task_complete":
 
 
381
  project_context["last_action_result"] = "Worker reported task is complete."
382
+ project_context["current_task"] = f"Worker finished task. What is the next high-level step?"
383
+ next_agent = "manager"
384
 
385
  elif action == "finish_project":
386
+ log_step("MANAGER", "Project is ready for final packaging.")
387
  break
388
 
389
  else: