wuhp commited on
Commit
2ccd1c7
·
verified ·
1 Parent(s): 29d611a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +593 -390
app.py CHANGED
@@ -1,20 +1,133 @@
1
- # ... (existing imports and utility functions like show_profile, list_private_models,
2
- # get_sdk_version, extract_code, classify_errors, _get_space_jwt,
3
- # fetch_logs, check_iframe remain the same)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- # --- AGENT PROMPTS & FUNCTIONS ---
 
 
 
 
 
 
 
 
 
 
6
 
7
- # Updated/New System Prompts
8
  SYSTEM_ORCHESTRATOR = {
9
  "role": "system",
10
  "content": (
11
  "You are **Orchestrator Agent**, the project manager. "
12
  "Your role is to guide the development process from user request to a deployed HF Space application. "
13
- "You will delegate tasks to other agents (Architect, Code-Gen, Debug) based on the project state. "
14
- "Your inputs will include the current project state (requirements, plan, files, logs, feedback, status). "
15
- "Your output should be a decision on the next step/task, potentially summarizing information for the next agent."
16
- "Possible next steps could be: 'PLANNING', 'CODING - {task_description}', 'DEBUGGING', 'COMPLETE', 'FAILED'."
17
- "Analyze the feedback and logs carefully to decide the next coding task if debugging is needed."
 
 
 
18
  )
19
  }
20
 
@@ -22,9 +135,10 @@ SYSTEM_ARCHITECT = {
22
  "role": "system",
23
  "content": (
24
  "You are **Architect Agent**, the lead planner. "
25
- "Given the user requirements, your task is to devise a high-level plan for the application. "
26
- "Outline the main features, suggest a file structure (e.g., `app.py`, `utils.py`), and identify key components needed. "
27
- "Output the plan clearly, perhaps using bullet points or a simple structure. Do NOT write code."
 
28
  )
29
  }
30
 
@@ -32,12 +146,13 @@ SYSTEM_CODEGEN = {
32
  "role": "system",
33
  "content": (
34
  "You are **Code‑Gen Agent**, a proactive AI developer. "
35
- "Your sole responsibility is to author and correct code files based on the plan and assigned task. "
36
- "You will receive the full project state, including the plan, existing files, and debug feedback. "
37
- "Based on the current task assigned by the Orchestrator, write or modify the necessary code *only* in the specified file(s). "
38
- "Output the *full content* of the updated file(s) in markdown code blocks, clearly indicating the filename(s)."
39
- "Example output format: \n\n`app.py`\n```python\n# app code here\n```\n\n`utils.py`\n```python\n# utils code here\n```"
40
- "Focus *only* on outputting the code blocks for the files you are modifying/creating. Do not add extra commentary outside the code blocks."
 
41
  )
42
  }
43
 
@@ -47,533 +162,621 @@ SYSTEM_DEBUG = {
47
  "You are **Debug Agent**, a meticulous code reviewer and tester. "
48
  "You have access to the full project state: requirements, plan, code files, build logs, and run logs. "
49
  "Your task is to analyze the logs and code in the context of the plan and requirements. "
50
- "Identify errors, potential issues, missing features based on the plan, and suggest concrete improvements or fixes. "
51
- "If the application appears to be working and meets the plan/requirements (and no significant errors in logs), state 'All clear. Project appears complete.' "
52
- "Otherwise, provide actionable feedback for the Code-Gen agent, referencing file names and line numbers where possible. Format feedback clearly."
53
- "Example feedback: 'Error in `app.py`: ModuleNotFoundError for 'missing_library'. Add 'missing_library' to `requirements.txt`.\nIssue: The 'plot_data' function in `utils.py` doesn't handle empty input data per the plan.' "
54
- "Do NOT write or suggest code directly in your feedback unless it's a very small, inline fix suggestion. Focus on *what* needs fixing/adding and *why*."
 
 
55
  )
56
  }
57
 
58
- # --- AGENT FUNCTIONS ---
59
 
60
- def run_agent(client, model_name, system_prompt, user_input, config, chat_history=None):
61
- """Helper to run a single agent interaction."""
62
- if chat_history is None:
63
- chat_history = []
 
 
 
 
 
 
64
 
65
- messages = chat_history + [{"role": "user", "content": user_input}]
 
 
66
 
67
- # Gemini expects alternating user/model roles starting with user,
68
- # but system prompts are outside this. We'll prepend system instructions
69
- # to the *first* user message if no history exists, or manage roles manually.
70
- # A simpler way for few-turn interactions is to put the system prompt
71
- # at the start of the *first* user message, or use a dedicated system role
72
- # if the model supports it (Gemini sometimes treats the first entry specially).
73
- # Let's stick to prepending system info to the first message for robustness.
74
- if not chat_history:
75
- user_input = system_prompt["content"] + "\n\n" + user_input
76
- messages = [{"role": "user", "content": user_input}] # Start fresh with combined prompt
77
 
78
- # For subsequent turns, just append the new user input
79
- if chat_history:
80
- messages = chat_history + [{"role": "user", "content": user_input}]
 
81
 
 
 
 
 
 
 
82
 
83
- # Ensure messages strictly alternate user/model roles for Gemini
84
- # This is a common pain point. Let's just send the last few relevant messages + system context
85
- # A better approach is to pass the *full* history but carefully structure it.
86
- # For simplicity here, let's just pass the system prompt and the immediate user input,
87
- # letting the agent rely on the *structured* project_state for context, not chat history.
88
- # This shifts complexity from prompt history management to state management.
89
 
90
- # Revised approach: Pass relevant project_state info *as* the user input
91
- # The agent's core prompt (SYSTEM_*) defines its role and what to do with the input state.
92
- full_prompt = f"{system_prompt['content']}\n\nProject State:\n{user_input}"
 
 
93
 
94
- try:
95
- resp = client.models.generate_content(
96
- model=model_name,
97
- contents=[{"role": "user", "content": full_prompt}], # Only send the current state input
98
- config=config
99
- )
100
- resp.raise_for_status() # Raise an exception for bad status codes
101
- return resp.text.strip(), [{"role": "user", "content": full_prompt}, {"role": "model", "content": resp.text.strip()}] # Return response and the turn
102
  except Exception as e:
103
- print(f"Agent failed: {e}")
104
- return f"ERROR: Agent failed - {e}", None # Indicate failure
 
 
 
 
 
 
 
 
 
 
105
 
106
  def run_planner(client, project_state, config):
107
- print("Running Planner Agent...")
108
- input_state_str = f"Requirements: {project_state['requirements']}"
 
 
 
 
 
 
 
109
  response_text, _ = run_agent(
110
  client=client,
111
- model_name="gemini-2.5-flash-preview-04-17", # Or a stronger model if needed for planning
112
- system_prompt=SYSTEM_ARCHITECT,
113
- user_input=input_state_str,
114
  config=config,
115
- # chat_history=project_state.get('planner_chat', []) # Optional: keep chat history per agent
116
  )
117
- # project_state['planner_chat'] = planner_chat # Update chat history
118
 
119
  if response_text.startswith("ERROR:"):
120
- return response_text # Propagate error
 
121
 
122
  project_state['plan'] = response_text
123
- print("Planner Output:", response_state['plan'])
124
- return "Plan generated."
 
125
 
126
  def run_codegen(client, project_state, config):
127
- print(f"Running Code-Gen Agent for task: {project_state['current_task']}...")
128
  # Code-Gen needs requirements, plan, existing files, and debug feedback
129
- input_state_str = (
130
- f"Current Task: {project_state['current_task']}\n\n"
131
- f"Requirements:\n{project_state['requirements']}\n\n"
132
- f"Plan:\n{project_state['plan']}\n\n"
133
- f"Existing Files:\n{json.dumps(project_state['files'], indent=2)}\n\n"
134
- f"Debug Feedback:\n{project_state['feedback'] or 'None'}\n"
135
- )
 
 
136
  response_text, _ = run_agent(
137
  client=client,
138
- model_name="gemini-2.5-flash-preview-04-17", # Or a stronger model
139
- system_prompt=SYSTEM_CODEGEN,
140
- user_input=input_state_str,
141
  config=config,
142
- # chat_history=project_state.get('codegen_chat', []) # Optional
143
  )
144
- # project_state['codegen_chat'] = codegen_chat # Update chat history
145
 
146
  if response_text.startswith("ERROR:"):
147
- return response_text # Propagate error
 
148
 
149
  # Parse the response text to extract code blocks for potentially multiple files
150
  files_updated = {}
151
- # Simple regex to find blocks like `filename`\n```...\n```
152
- # This needs to handle multiple blocks
153
- blocks = re.findall(r"(`[^`]*`)\s*```(?:\w*\n)?([\s\S]*?)```", response_text)
 
154
 
155
  if not blocks:
156
- # If no code blocks are found, maybe the agent just gave commentary or failed
157
- # Treat this as an error or a failure to produce code
158
- print("Code-Gen Agent did not output any code blocks.")
159
- return "ERROR: Code-Gen Agent failed to output code blocks."
160
 
 
161
  for filename_match, code_content in blocks:
162
  filename = filename_match.strip('`').strip()
163
- if filename: # Ensure filename is not empty
164
- files_updated[filename] = code_content.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  if not files_updated:
167
- print("Code-Gen Agent outputted blocks but couldn't parse filenames.")
168
- return "ERROR: Code-Gen Agent outputted blocks but couldn't parse filenames."
 
 
 
 
 
 
 
169
 
170
 
171
  project_state['files'].update(files_updated) # Update existing files or add new ones
172
- print(f"Code-Gen Agent updated files: {list(files_updated.keys())}")
173
 
174
- # Quick syntax check on updated files
175
- for fn, code in files_updated.items():
176
- if fn.endswith('.py'):
177
- try:
178
- compile(code, fn, "exec")
179
- print(f"Syntax check passed for {fn}")
180
- except SyntaxError as e:
181
- print(f"Syntax Error in {fn}: {e}")
182
- return f"ERROR: SyntaxError in {fn}: {e}"
183
 
184
- return "Code generated/updated."
185
 
186
  def run_debugger(client, project_state, config):
187
- print("Running Debug Agent...")
188
- # Debugger needs requirements, plan, files, logs
189
- input_state_str = (
190
- f"Requirements:\n{project_state['requirements']}\n\n"
191
- f"Plan:\n{project_state['plan']}\n\n"
192
- f"Current Files:\n{json.dumps(project_state['files'], indent=2)}\n\n"
193
- f"Build Logs:\n{project_state['logs'].get('build', 'No build logs.')}\n\n"
194
- f"Run Logs:\n{project_state['logs'].get('run', 'No run logs.')}\n"
195
- f"Iframe Check: {'Responded OK' if project_state.get('iframe_ok') else 'Not responding or checked yet.'}"
196
- )
 
197
  response_text, _ = run_agent(
198
  client=client,
199
- model_name="gemini-2.5-flash-preview-04-17", # Flash is good for analysis
200
- system_prompt=SYSTEM_DEBUG,
201
- user_input=input_state_str,
202
  config=config,
203
- # chat_history=project_state.get('debug_chat', []) # Optional
204
  )
205
- # project_state['debug_chat'] = debug_chat # Update chat history
206
 
207
  if response_text.startswith("ERROR:"):
208
- return response_text # Propagate error
 
209
 
210
  project_state['feedback'] = response_text
211
- print("Debug Agent Feedback:", project_state['feedback'])
212
- return "Feedback generated."
213
-
214
- def run_orchestrator(client, project_state, config):
215
- print("Running Orchestrator Agent...")
216
- # The Orchestrator's prompt is implicit in the state it receives and the function logic
217
- # We *could* make the Orchestrator an LLM call too, but a state machine is often more reliable
218
- # for controlling flow. Let's use Python code as the Orchestrator for now.
219
-
220
- status_message = "Orchestrator deciding next step..."
221
-
222
- # Based on current status and feedback, decide next task
223
- if project_state['status'] == 'START':
224
- project_state['current_task'] = 'PLANNING'
225
- project_state['status'] = 'In Progress'
226
- status_message = "Starting project: Planning phase."
227
-
228
- elif project_state['current_task'] == 'PLANNING':
229
- # Assume run_planner was just executed
230
- if project_state['plan']:
231
- project_state['current_task'] = 'CODING - Initial Implementation'
232
- status_message = "Planning complete. Moving to initial coding."
233
- else:
234
- # Planner failed to produce a plan
235
- project_state['status'] = 'Failed'
236
- status_message = "Planning failed. Could not generate project plan."
237
-
238
- elif project_state['current_task'].startswith('CODING'):
239
- # Assume run_codegen was just executed
240
- # Next step is always pushing after coding
241
- project_state['current_task'] = 'PUSHING'
242
- status_message = "Coding phase complete. Preparing to push changes."
243
-
244
- elif project_state['current_task'] == 'PUSHING':
245
- # Assume files were just pushed in the main loop
246
- # Next step is fetching logs
247
- project_state['current_task'] = 'LOGGING'
248
- status_message = "Push complete. Fetching logs."
249
-
250
- elif project_state['current_task'] == 'LOGGING':
251
- # Assume logs were fetched in the main loop
252
- # Next step is debugging
253
- project_state['current_task'] = 'DEBUGGING'
254
- status_message = "Logs fetched. Analyzing with Debug Agent."
255
-
256
- elif project_state['current_task'] == 'DEBUGGING':
257
- # Assume run_debugger was just executed
258
- feedback = project_state['feedback']
259
- build_logs = project_state['logs'].get('build', '')
260
- run_logs = project_state['logs'].get('run', '')
261
- iframe_ok = project_state.get('iframe_ok', False)
262
-
263
- # Analyze feedback and logs to decide next step
264
- if "All clear. Project appears complete." in feedback or \
265
- ("ERROR" not in build_logs.upper() and "ERROR" not in run_logs.upper() and iframe_ok):
266
- project_state['status'] = 'Complete'
267
- project_state['current_task'] = 'FINISHED'
268
- status_message = "Debug Agent reports clear. Project appears complete."
269
- elif project_state['attempt_count'] >= 6: # Max attempts reached
270
- project_state['status'] = 'Failed'
271
- project_state['current_task'] = 'FINISHED'
272
- status_message = f"Max attempts ({project_state['attempt_count']}/6) reached. Project failed."
273
- else:
274
- # Errors or issues found, need more coding/debugging
275
- project_state['current_task'] = 'CODING - Addressing Feedback'
276
- status_message = "Debug Agent found issues. Returning to Coding phase to address feedback."
277
-
278
- else:
279
- # Should not happen, maybe a cleanup or failure state
280
- project_state['status'] = 'Failed'
281
- status_message = f"Orchestrator in unknown state: {project_state['current_task']}"
282
-
283
-
284
- project_state['status_message'] = status_message
285
- print(f"Orchestrator Status: {project_state['status']} - Task: {project_state['current_task']}")
286
 
 
287
 
288
- # --- MAIN HANDLER REVISION ---
289
-
290
- def handle_user_message(
291
- history, # Keep history for chatbot display, but agents use project_state
292
- sdk_choice: str,
293
- gemini_api_key: str,
294
- grounding_enabled: bool,
295
- temperature: float,
296
- max_output_tokens: int,
297
- profile: gr.OAuthProfile | None,
298
- oauth_token: gr.OAuthToken | None
299
- ):
300
- if not profile or not oauth_token:
301
- return history + [{"role":"assistant","content":"⚠️ Please log in first."}], "", "", "<p>No Space yet.</p>", "Waiting for prompt..."
302
 
303
- client = genai.Client(api_key=gemini_api_key)
304
- repo_id = f"{profile.username}/{profile.username}-auto-space"
305
- iframe_url = f"https://huggingface.co/spaces/{repo_id}"
306
- sdk_version = get_sdk_version(sdk_choice)
307
- code_fn = "app.py" if sdk_choice == "gradio" else "streamlit_app.py" # Still need a primary file name
308
 
309
- # Initialize or load project state
310
- # For this implementation, we'll initialize fresh for each new user message
311
- # A more complex app might persist state or load from HF Space
312
- project_state = {
313
- 'requirements': history[-1]['content'] if history else "", # Get the latest user message
314
- 'plan': '',
315
- 'files': {}, # Use a dict to store multiple file contents {filename: code}
316
- 'logs': {'build': '', 'run': ''},
317
- 'feedback': '',
318
- 'current_task': 'START',
319
- 'status': 'In Progress',
320
- 'status_message': 'Initializing...',
321
- 'attempt_count': 0,
322
- 'sdk_choice': sdk_choice,
323
- 'sdk_version': sdk_version,
324
- 'repo_id': repo_id,
325
- 'iframe_url': iframe_url,
326
- 'main_app_file': code_fn, # Keep track of the main app file name convention
327
- # Optional: Per-agent chat histories for more complex interactions
328
- # 'planner_chat': [],
329
- # 'codegen_chat': [],
330
- # 'debug_chat': [],
331
- }
332
-
333
- # Add initial user message to history for chatbot display
334
- if history and history[-1].get("role") == "user":
335
- # This assumes the Gradio chatbot already added the user message
336
- pass # History should already have the user input
337
- else:
338
- # Handle cases where history might be empty or last turn wasn't user
339
- # (e.g. first message, or if previous turn failed)
340
- # This depends on how Gradio handles the initial history input
341
- # For standard Gradio chatbot, history will be like [{"role": "user", "content": "..."}]
342
- pass
343
-
344
-
345
- cfg = GenerateContentConfig(
346
- tools=[Tool(google_search=GoogleSearch())] if grounding_enabled else [],
347
- response_modalities=["TEXT"],
348
- temperature=temperature,
349
- max_output_tokens=max_output_tokens, # Ensure this is high enough for code
350
- )
351
-
352
- # Main Orchestration Loop
353
  while project_state['status'] == 'In Progress' and project_state['attempt_count'] < 7:
354
  print(f"\n--- Attempt {project_state['attempt_count'] + 1} ---")
355
  print(f"Current Task: {project_state['current_task']}")
 
356
  current_task = project_state['current_task']
357
- feedback_message = "" # Message to add to chatbot history about step outcome
 
 
 
 
358
 
359
  if current_task == 'START':
360
- run_orchestrator(client, project_state, cfg) # Move to PLANNING
 
 
 
361
 
362
  elif current_task == 'PLANNING':
363
- result = run_planner(client, project_state, cfg)
364
- feedback_message = f"**Plan:**\n\n{project_state['plan']}"
365
- if result.startswith("ERROR:"):
366
- project_state['status'] = 'Failed'
367
- project_state['status_message'] = result
368
- run_orchestrator(client, project_state, cfg) # Decide next based on plan result
 
369
 
370
- elif current_task.startswith('CODING'):
371
- # If this is the very first coding task, ensure the main app file exists
372
- # even if the planner didn't explicitly list it.
373
- if project_state['main_app_file'] not in project_state['files'] and project_state['attempt_count'] == 0:
374
- project_state['files'][project_state['main_app_file']] = f"# Initial {sdk_choice} app file\n" # Start with something
375
 
376
- result = run_codegen(client, project_state, cfg)
377
- if result.startswith("ERROR:"):
378
- project_state['status'] = 'Failed'
379
- project_state['status_message'] = result
380
- else:
381
- # Show the code that was just generated/updated in the chatbot history
382
- code_output = "\n".join([f"`{fn}`\n```python\n{code}\n```" for fn, code in project_state['files'].items()])
383
- feedback_message = f"**Generated/Updated Code:**\n\n{code_output}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
- run_orchestrator(client, project_state, cfg) # Move to PUSHING or Failed
386
 
387
  elif current_task == 'PUSHING':
388
- # Save and push all files in project_state['files']
389
  try:
390
  # Create/update repo first
391
- create_repo(repo_id=repo_id, token=oauth_token.token,
392
- exist_ok=True, repo_type="space", space_sdk=sdk_choice)
393
 
394
- # Write and upload all files
395
  for fn, content in project_state['files'].items():
396
- # Write to a temporary file or in the current directory
 
397
  with open(fn, "w") as f:
398
  f.write(content)
399
  upload_file(
400
  path_or_fileobj=fn, path_in_repo=fn,
401
- repo_id=repo_id, token=oauth_token.token,
402
  repo_type="space"
403
  )
404
 
405
- # Ensure requirements.txt and README are also pushed, or handled by CodeGen
406
- # For simplicity, let's add boilerplate here if not generated by CodeGen
407
  if 'requirements.txt' not in project_state['files']:
408
- req_content = "pandas\n" + ("streamlit\n" if sdk_choice=="streamlit" else "gradio\n")
409
  with open("requirements.txt", "w") as f:
410
  f.write(req_content)
411
  upload_file(path_or_fileobj="requirements.txt", path_in_repo="requirements.txt",
412
- repo_id=repo_id, token=oauth_token.token, repo_type="space")
 
413
 
 
414
  if 'README.md' not in project_state['files']:
415
  readme_content = f"""---
416
- title: {profile.username}'s Auto Space
417
  emoji: 🐢
418
- sdk: {sdk_choice}
419
- sdk_version: {sdk_version}
420
  app_file: {project_state['main_app_file']}
421
  pinned: false
422
  ---
 
 
 
 
 
 
 
 
423
  """
424
  with open("README.md", "w") as f:
425
  f.write(readme_content)
426
  upload_file(path_or_fileobj="README.md", path_in_repo="README.md",
427
- repo_id=repo_id, token=oauth_token.token, repo_type="space")
 
428
 
429
 
430
- print(f"Pushed {len(project_state['files'])} files to {repo_id}")
431
- feedback_message = f"Pushed code to HF Space **{repo_id}**."
432
- run_orchestrator(client, project_state, cfg) # Move to LOGGING
 
433
 
434
  except Exception as e:
435
- project_state['status'] = 'Failed'
 
436
  project_state['status_message'] = f"ERROR: Failed to push to HF Space: {e}"
437
- feedback_message = project_state['status_message']
438
- print(feedback_message)
 
439
 
440
 
441
  elif current_task == 'LOGGING':
442
- # Fetch logs and check iframe status
443
- try:
444
- project_state['logs']['build'] = fetch_logs(repo_id, "build")
445
- project_state['logs']['run'] = fetch_logs(repo_id, "run")
446
- project_state['iframe_ok'] = check_iframe(iframe_url)
447
- feedback_message = "**Logs Fetched.** Analyzing..."
448
- print("Logs fetched.")
449
- run_orchestrator(client, project_state, cfg) # Move to DEBUGGING
450
- except Exception as e:
451
- project_state['status'] = 'Failed'
452
- project_state['status_message'] = f"ERROR: Failed to fetch logs or check iframe: {e}"
453
- feedback_message = project_state['status_message']
454
- print(feedback_message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
 
456
 
457
  elif current_task == 'DEBUGGING':
458
- result = run_debugger(client, project_state, cfg)
459
- feedback_message = f"**Debug Feedback:**\n\n{project_state['feedback']}"
460
- if result.startswith("ERROR:"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  project_state['status'] = 'Failed'
462
- project_state['status_message'] = result
463
- run_orchestrator(client, project_state, cfg) # Move to Decision/CODING/COMPLETE/FAILED
464
 
465
- elif current_task == 'FINISHED':
466
- # Exit loop state
467
- pass
468
 
469
- else:
470
- # Unknown task
471
- project_state['status'] = 'Failed'
472
- project_state['status_message'] = f"ERROR: Orchestrator entered an unknown task state: {current_task}"
473
- feedback_message = project_state['status_message']
474
- print(feedback_message)
475
 
 
476
 
477
- # Add the outcome of the last step to the chatbot history
478
- if feedback_message:
479
- history.append({"role": "assistant", "content": feedback_message})
480
- # Provide status updates as part of the assistant messages or via the status_message output
481
- history.append({"role": "assistant", "content": f"Project Status: {project_state['status_message']}"})
482
 
 
483
 
484
- # Increment attempt count after a cycle of Coding/Pushing/Logging/Debugging
485
- # Only increment if we went through the core loop steps, not planning or initial start
486
- if current_task in ['DEBUGGING', 'PLANNING']: # End of a major cycle
487
- project_state['attempt_count'] += 1
488
- # Add a small backoff before the next attempt loop starts
489
- if project_state['status'] == 'In Progress':
490
- backoff = min(project_state['attempt_count'] * 5, 30) # Linear or exponential backoff
491
- print(f"Waiting for {backoff} seconds before next attempt...")
492
- time.sleep(backoff)
493
 
494
 
495
- # --- End of Orchestration Loop ---
 
 
 
 
 
496
 
497
- # Prepare final UI outputs
498
- final_messages = history[:] # Use the accumulated history
499
- final_build_logs = project_state['logs'].get('build', '')
500
- final_run_logs = project_state['logs'].get('run', '')
501
 
502
- # Final iframe check and HTML
503
- final_iframe_html = (
504
- f'<iframe src="{iframe_url}" width="100%" height="500px"></iframe>'
505
- + ("" if project_state.get('iframe_ok') else "<p style='color:red;'>⚠️ iframe not responding or checked.</p>")
 
 
 
 
506
  )
507
 
508
- # Add a final status message to the chatbot history
509
- final_messages.append({"role": "assistant", "content": f"**Project Outcome:** {project_state['status']} - {project_state['status_message']}"})
510
- if project_state['status'] == 'Complete':
511
- final_messages.append({"role": "assistant", "content": "✅ Application deployed successfully (likely)! Check the preview above."})
512
- else:
513
- final_messages.append({"role": "assistant", "content": "❌ Project failed to complete. Review logs and feedback for details."})
514
 
515
- return final_messages, final_build_logs, final_run_logs, final_iframe_html, project_state['status_message']
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
- # --- SIMPLE UI WITH HIGHER MAX TOKENS & STATUS DISPLAY —
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
 
520
  with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
521
- gr.Markdown("## 🐢 HF Space Auto‑Builder (Team AI)\n1) Sign in  2) Enter prompt  3) Watch progress, logs & preview")
 
522
 
523
- login_btn = gr.LoginButton(variant="huggingface", size="lg")
524
- status_md = gr.Markdown("*Not logged in.*")
525
- models_md = gr.Markdown()
526
- demo.load(show_profile, inputs=None, outputs=status_md)
527
- demo.load(list_private_models, inputs=None, outputs=models_md)
528
- login_btn.click(show_profile, inputs=None, outputs=status_md)
529
- login_btn.click(list_private_models, inputs=None, outputs=models_md)
530
 
531
  with gr.Row():
532
  with gr.Column(scale=1):
533
- sdk_choice = gr.Radio(["gradio","streamlit"], value="gradio", label="SDK")
534
- api_key = gr.Textbox(label="Gemini API Key", type="password")
535
- grounding = gr.Checkbox(label="Enable grounding", value=False)
536
- temp = gr.Slider(0,1,value=0.2, label="Temperature")
537
- max_tokens = gr.Number(value=4096, label="Max output tokens (recommend >= 4096)", minimum=512) # Increased recommendation
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
  with gr.Column(scale=2):
540
  project_status_md = gr.Markdown("Waiting for prompt...")
541
- chatbot = gr.Chatbot(type="messages", label="Team Communication & Status")
542
- user_in = gr.Textbox(placeholder="Your prompt for the application...", label="Application Requirements", lines=2)
543
- send_btn = gr.Button("Start Development Team")
544
 
545
  # Separate accordions for logs and preview
546
  with gr.Accordion("Logs", open=False):
547
- build_box = gr.Textbox(label="Build logs", lines=5, interactive=False)
548
- run_box = gr.Textbox(label="Run logs", lines=5, interactive=False)
549
- refresh_btn = gr.Button("Refresh Logs Only") # Add refresh button for logs
 
550
 
551
  with gr.Accordion("App Preview", open=True):
552
- preview = gr.HTML("<p>No Space yet.</p>")
553
-
554
 
555
  # Update the button click handler
556
  # It will now return the updated chatbot history, logs, preview, and the project status
557
  send_btn.click(
558
  fn=handle_user_message,
559
- inputs=[chatbot, sdk_choice, api_key, grounding, temp, max_tokens, login_btn, login_btn],
560
  outputs=[chatbot, build_box, run_box, preview, project_status_md]
561
  )
562
  user_in.submit(
563
- fn=handle_user_message,
564
- inputs=[chatbot, sdk_choice, api_key, grounding, temp, max_tokens, login_btn, login_btn],
565
  outputs=[chatbot, build_box, run_box, preview, project_status_md]
566
  )
567
 
568
  # Handler for refreshing logs manually
 
569
  refresh_btn.click(
570
  fn=lambda profile, token: (
571
- fetch_logs(f"{profile.username}/{profile.username}-auto-space", "build"),
572
- fetch_logs(f"{profile.username}/{profile.username}-auto-space", "run")
573
  ),
574
- inputs=[login_btn, login_btn], # Pass profile and token from login button state
575
  outputs=[build_box, run_box]
576
  )
577
 
578
 
579
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
1
+ import re
2
+ import json
3
+ import time
4
+ import requests
5
+ import importlib.metadata
6
+ import gradio as gr
7
+ import os # Needed for writing files
8
+ from huggingface_hub import (
9
+ create_repo, upload_file, list_models, constants
10
+ )
11
+ from huggingface_hub.utils import build_hf_headers, get_session, hf_raise_for_status
12
+ from google import genai
13
+ from google.genai.types import Tool, GenerateContentConfig, GoogleSearch
14
+
15
+ # --- USER INFO & MODEL LISTING ---
16
+
17
+ def show_profile(profile: gr.OAuthProfile | None) -> str:
18
+ return f"✅ Logged in as **{profile.username}**" if profile else "*Not logged in.*"
19
+
20
+ def list_private_models(
21
+ profile: gr.OAuthProfile | None,
22
+ oauth_token: gr.OAuthToken | None
23
+ ) -> str:
24
+ if not profile or not oauth_token:
25
+ return "Please log in to see your models."
26
+ try:
27
+ models = [
28
+ f"{m.id} ({'private' if m.private else 'public'})"
29
+ for m in list_models(author=profile.username, token=oauth_token.token)
30
+ ]
31
+ return "No models found." if not models else "Models:\n\n" + "\n - ".join(models)
32
+ except Exception as e:
33
+ return f"Error listing models: {e}"
34
+
35
+
36
+ # --- UTILITIES ---
37
+
38
+ def get_sdk_version(sdk_choice: str) -> str:
39
+ pkg = "gradio" if sdk_choice == "gradio" else "streamlit"
40
+ try:
41
+ return importlib.metadata.version(pkg)
42
+ except importlib.metadata.PackageNotFoundError:
43
+ return "UNKNOWN"
44
+
45
+ # This utility is no longer needed as parsing is handled in run_codegen
46
+ # def extract_code(text: str) -> str:
47
+ # blocks = re.findall(r"```(?:\w*\n)?([\s\S]*?)```", text)
48
+ # return blocks[-1].strip() if blocks else text.strip()
49
+
50
+ def classify_errors(logs: str) -> str:
51
+ errs = set()
52
+ # Convert logs to lower for case-insensitive matching
53
+ logs_lower = logs.lower()
54
+ if "syntaxerror" in logs_lower:
55
+ errs.add("syntax")
56
+ elif "importerror" in logs_lower or "modulenotfounderror" in logs_lower:
57
+ errs.add("import")
58
+ # Catch common error indicators
59
+ elif "traceback" in logs_lower or "exception" in logs_lower or "error" in logs_lower:
60
+ errs.add("runtime/generic") # More general error indication
61
+
62
+ return ", ".join(errs) or "none"
63
+
64
+ # --- HF SPACE LOGGING ---
65
+
66
+ def _get_space_jwt(repo_id: str, token: str) -> str:
67
+ """Fetches JWT for Space logs using the user's Hf token."""
68
+ url = f"{constants.ENDPOINT}/api/spaces/{repo_id}/jwt"
69
+ headers = build_hf_headers(token=token)
70
+ r = get_session().get(url, headers=headers)
71
+ hf_raise_for_status(r) # Raises HTTPError for bad responses (e.g. 404 if repo doesn't exist)
72
+ return r.json()["token"]
73
+
74
+ def fetch_logs(repo_id: str, level: str, token: str) -> str:
75
+ """Fetches build or run logs from an HF Space."""
76
+ try:
77
+ jwt = _get_space_jwt(repo_id, token)
78
+ url = f"https://api.hf.space/v1/{repo_id}/logs/{level}"
79
+ lines = []
80
+ headers = build_hf_headers(token=jwt)
81
+ # Use a timeout for the request
82
+ with get_session().get(url, headers=headers, stream=True, timeout=10) as resp:
83
+ hf_raise_for_status(resp)
84
+ # Read lines with a timeout
85
+ for raw in resp.iter_lines(decode_unicode=True, chunk_size=512):
86
+ if raw is None: # handle keep-alive or similar
87
+ continue
88
+ if raw.startswith("data: "):
89
+ try:
90
+ ev = json.loads(raw[len("data: "):])
91
+ ts, txt = ev.get("timestamp","N/A"), ev.get("data","")
92
+ lines.append(f"[{ts}] {txt}")
93
+ except json.JSONDecodeError:
94
+ lines.append(f"Error decoding log line: {raw}")
95
+ except Exception as e:
96
+ lines.append(f"Unexpected error processing log line: {raw} - {e}")
97
+ return "\n".join(lines)
98
+ except requests.exceptions.Timeout:
99
+ return f"Error: Timeout fetching {level} logs."
100
+ except requests.exceptions.RequestException as e:
101
+ return f"Error fetching {level} logs: {e}"
102
+ except Exception as e:
103
+ return f"An unexpected error occurred while fetching logs: {e}"
104
+
105
 
106
+ def check_iframe(url: str, timeout: int = 10) -> bool:
107
+ """Checks if the iframe URL is reachable and returns a 200 status code."""
108
+ try:
109
+ # Use a HEAD request for efficiency if only status is needed, but GET is safer for
110
+ # checking if content is served. Let's stick to GET with a timeout.
111
+ response = requests.get(url, timeout=timeout)
112
+ return response.status_code == 200
113
+ except requests.exceptions.RequestException:
114
+ return False # Any request exception (timeout, connection error, etc.) means it's not accessible
115
+
116
+ # --- AGENT PROMPTS ---
117
 
 
118
  SYSTEM_ORCHESTRATOR = {
119
  "role": "system",
120
  "content": (
121
  "You are **Orchestrator Agent**, the project manager. "
122
  "Your role is to guide the development process from user request to a deployed HF Space application. "
123
+ "You will analyze the current project state (requirements, plan, files, logs, feedback, status, attempt_count) "
124
+ "and decide the *single* next step/task for the team. "
125
+ "Output *only* the name of the next task from the following list: "
126
+ "'PLANNING', 'CODING - {task_description}', 'PUSHING', 'LOGGING', 'DEBUGGING', 'COMPLETE', 'FAILED'. "
127
+ "If moving to 'CODING', briefly describe the specific area to focus on (e.g., 'CODING - Initial UI', 'CODING - Adding Data Loading', 'CODING - Fixing Import Errors'). "
128
+ "Analyze the debug feedback and logs carefully to decide the appropriate coding task description."
129
+ "If the debug feedback indicates 'All clear', transition to 'COMPLETE'."
130
+ "If maximum attempts are reached or a critical error occurs, transition to 'FAILED'."
131
  )
132
  }
133
 
 
135
  "role": "system",
136
  "content": (
137
  "You are **Architect Agent**, the lead planner. "
138
+ "Given the user requirements and the current project state, your task is to devise or refine the high-level plan for the application. "
139
+ "Outline the main features, suggest a logical structure, identify potential files (e.g., `app.py`, `utils.py`, `requirements.txt`), and key components needed. "
140
+ "The target SDK is {sdk_choice}. The main application file should be `{main_app_file}`. "
141
+ "Output the plan clearly, using bullet points or a simple numbered list. Do NOT write code. Focus only on the plan."
142
  )
143
  }
144
 
 
146
  "role": "system",
147
  "content": (
148
  "You are **Code‑Gen Agent**, a proactive AI developer. "
149
+ "Your sole responsibility is to author and correct code files based on the plan and the assigned task. "
150
+ "You will receive the full project state, including the requirements, plan, existing files, and debug feedback. "
151
+ "Based on the current task assigned by the Orchestrator ('{current_task}'), write or modify the necessary code *only* in the specified file(s). "
152
+ "Output the *full content* of the updated file(s) in markdown code blocks, clearly indicating the filename(s) immediately before the code block like this: `filename`\n```<language>\ncode goes here\n```"
153
+ "If the task involves creating a new file, include it in the output. If modifying an existing file, provide the *complete* modified code for that file."
154
+ "Ensure the code adheres to the plan and addresses the debug feedback if provided."
155
+ "Only output the code blocks and their preceding filenames. Do not add extra commentary outside the code blocks."
156
  )
157
  }
158
 
 
162
  "You are **Debug Agent**, a meticulous code reviewer and tester. "
163
  "You have access to the full project state: requirements, plan, code files, build logs, and run logs. "
164
  "Your task is to analyze the logs and code in the context of the plan and requirements. "
165
+ "Identify errors, potential issues, missing features based on the plan, and suggest concrete improvements or fixes for the Code-Gen agent. "
166
+ "Pay close attention to the build and run logs for specific errors (SyntaxError, ImportError, runtime errors). "
167
+ "Also check if the implemented features align with the plan."
168
+ "If the application appears to be working based on the logs and iframe check, and seems to meet the plan's core requirements, state 'All clear. Project appears complete.' as the *first line* of your feedback."
169
+ "Otherwise, provide actionable feedback, referencing file names and line numbers where possible. Format feedback clearly."
170
+ "Example feedback:\n'Error in `app.py`: ModuleNotFoundError for 'missing_library'. Add 'missing_library' to `requirements.txt`.'\n'Issue: The plan required a download button, but it's missing in `app.py`.'\n'Suggestion: Check the loop in `utils.py`, it might cause an infinite loop based on run logs.' "
171
+ "Do NOT write or suggest large code blocks directly in your feedback. Focus on *what* needs fixing/adding and *why*."
172
  )
173
  }
174
 
175
+ # --- AGENT RUNNER HELPER ---
176
 
177
+ def run_agent(client, model_name, system_prompt_template, user_input_state, config):
178
+ """Helper to run a single agent interaction using the project state as input."""
179
+ # Format the system prompt using state variables if needed
180
+ try:
181
+ # Use a specific formatting approach that handles missing keys gracefully if needed
182
+ # For our current prompts, direct f-string formatting with known keys is fine
183
+ system_prompt = system_prompt_template["content"].format(**user_input_state)
184
+ except KeyError as e:
185
+ print(f"Error formatting system prompt: Missing key {e}. Prompt template: {system_prompt_template['content']}")
186
+ return f"ERROR: Internal agent error - Missing key {e} for prompt formatting.", None
187
 
188
+ # Prepare the message content. The user_input_state is the core context.
189
+ # We structure it clearly for the agent to read.
190
+ user_message_content = "Project State:\n" + json.dumps(user_input_state, indent=2)
191
 
192
+ messages = [
193
+ {"role": "user", "content": system_prompt + "\n\n" + user_message_content}
194
+ ]
 
 
 
 
 
 
 
195
 
196
+ try:
197
+ # Use a specific, more capable model if needed for complex reasoning, otherwise Flash is fine
198
+ # model_to_use = "gemini-1.5-flash-latest" # Example of using a different model
199
+ model_to_use = model_name # Use the one passed in
200
 
201
+ response = client.models.generate_content(
202
+ model=model_to_use,
203
+ contents=messages,
204
+ config=config
205
+ )
206
+ response.raise_for_status() # Raise an exception for bad status codes
207
 
208
+ # Some models return parts, concatenate them
209
+ response_text = "".join([part.text for part in response.candidates[0].content.parts])
 
 
 
 
210
 
211
+ print(f"--- Agent Response --- ({model_name})")
212
+ # print(response_text) # Careful: can be very long
213
+ print("----------------------")
214
+
215
+ return response_text.strip(), messages + [{"role": "model", "content": response_text.strip()}] # Return response and the turn
216
 
 
 
 
 
 
 
 
 
217
  except Exception as e:
218
+ print(f"Agent call failed: {e}")
219
+ # Attempt to extract error message from response object if possible
220
+ error_details = str(e)
221
+ if hasattr(e, 'response') and e.response is not None:
222
+ try:
223
+ error_details = e.response.text # Get response body for more details
224
+ except:
225
+ pass # Ignore if cannot get text
226
+
227
+ return f"ERROR: Agent failed - {error_details}", None # Indicate failure
228
+
229
+ # --- AGENT FUNCTIONS (called by Orchestrator) ---
230
 
231
  def run_planner(client, project_state, config):
232
+ print("Orchestrator: Running Planner Agent...")
233
+ # Planner needs requirements and basic project info
234
+ input_state_for_planner = {
235
+ "requirements": project_state['requirements'],
236
+ "sdk_choice": project_state['sdk_choice'],
237
+ "main_app_file": project_state['main_app_file'],
238
+ # Include other relevant state like existing files if refining plan
239
+ "files": project_state['files']
240
+ }
241
  response_text, _ = run_agent(
242
  client=client,
243
+ model_name="gemini-1.5-flash-latest", # Flash is good for this, maybe Pro if planning is complex
244
+ system_prompt_template=SYSTEM_ARCHITECT,
245
+ user_input_state=input_state_for_planner,
246
  config=config,
 
247
  )
 
248
 
249
  if response_text.startswith("ERROR:"):
250
+ project_state['status_message'] = response_text
251
+ return False # Indicate failure
252
 
253
  project_state['plan'] = response_text
254
+ print("Orchestrator: Planner Output Received.")
255
+ project_state['status_message'] = "Planning complete."
256
+ return True
257
 
258
  def run_codegen(client, project_state, config):
259
+ print(f"Orchestrator: Running Code-Gen Agent for task: {project_state['current_task']}...")
260
  # Code-Gen needs requirements, plan, existing files, and debug feedback
261
+ input_state_for_codegen = {
262
+ "current_task": project_state['current_task'],
263
+ "requirements": project_state['requirements'],
264
+ "plan": project_state['plan'],
265
+ "files": project_state['files'], # Pass current files so it can modify
266
+ "feedback": project_state['feedback'] or 'None',
267
+ "sdk_choice": project_state['sdk_choice'],
268
+ "main_app_file": project_state['main_app_file'] # Ensure it knows the main file convention
269
+ }
270
  response_text, _ = run_agent(
271
  client=client,
272
+ model_name="gemini-1.5-flash-latest", # Can use Flash, or Pro for more complex coding
273
+ system_prompt_template=SYSTEM_CODEGEN,
274
+ user_input_state=input_state_for_codegen,
275
  config=config,
 
276
  )
 
277
 
278
  if response_text.startswith("ERROR:"):
279
+ project_state['status_message'] = response_text
280
+ return False # Indicate failure
281
 
282
  # Parse the response text to extract code blocks for potentially multiple files
283
  files_updated = {}
284
+ # Regex to find blocks like `filename`\n```[language]\ncode...\n```
285
+ # It captures the filename (group 1) and the code content (group 2)
286
+ # Added handling for optional language tag after the triple backticks
287
+ blocks = re.findall(r"(`[^`]+`)\s*```(?:\w*\n)?([\s\S]*?)```", response_text)
288
 
289
  if not blocks:
290
+ print("Code-Gen Agent did not output any code blocks in expected format.")
291
+ project_state['status_message'] = "ERROR: Code-Gen Agent failed to output code blocks in `filename`\\n```code``` format."
292
+ return False # Indicate failure
 
293
 
294
+ syntax_errors = []
295
  for filename_match, code_content in blocks:
296
  filename = filename_match.strip('`').strip()
297
+ if not filename:
298
+ print(f"Warning: Found a code block but filename was empty: {code_content[:50]}...")
299
+ syntax_errors.append(f"Code block found with empty filename.")
300
+ continue
301
+
302
+ files_updated[filename] = code_content.strip() # Store updated code
303
+
304
+ # Quick syntax check for Python files
305
+ if filename.endswith('.py'):
306
+ try:
307
+ compile(code_content, filename, "exec")
308
+ print(f"Syntax check passed for {filename}")
309
+ except SyntaxError as e:
310
+ syntax_errors.append(f"Syntax Error in {filename}: {e}")
311
+ print(f"Syntax Error in {filename}: {e}")
312
+ except Exception as e:
313
+ syntax_errors.append(f"Unexpected error during syntax check for {filename}: {e}")
314
+ print(f"Unexpected error during syntax check for {filename}: {e}")
315
+
316
 
317
  if not files_updated:
318
+ print("Code-Gen Agent outputted blocks but couldn't parse any valid filenames.")
319
+ project_state['status_message'] = "ERROR: Code-Gen Agent outputted blocks but couldn't parse any valid filenames."
320
+ return False # Indicate failure
321
+
322
+ if syntax_errors:
323
+ # If syntax errors found, add them to feedback and signal failure for CodeGen step
324
+ project_state['feedback'] = "Syntax Errors:\n" + "\n".join(syntax_errors) + "\n\n" + project_state['feedback'] # Prepend errors
325
+ project_state['status_message'] = "ERROR: Code-Gen Agent introduced syntax errors."
326
+ return False # Indicate failure due to syntax errors
327
 
328
 
329
  project_state['files'].update(files_updated) # Update existing files or add new ones
330
+ print(f"Orchestrator: Code-Gen Agent updated files: {list(files_updated.keys())}")
331
 
332
+ # Add the generated/updated code content to the status message for visibility in UI
333
+ code_summary = "\n".join([f"`{fn}`:\n```python\n{code[:200]}...\n```" for fn, code in files_updated.items()]) # Show snippet
334
+ project_state['status_message'] = f"Code generated/updated.\n\nFiles Updated:\n{code_summary}"
 
 
 
 
 
 
335
 
336
+ return True # Indicate success
337
 
338
  def run_debugger(client, project_state, config):
339
+ print("Orchestrator: Running Debug Agent...")
340
+ # Debugger needs requirements, plan, files, logs, and iframe status
341
+ input_state_for_debugger = {
342
+ "requirements": project_state['requirements'],
343
+ "plan": project_state['plan'],
344
+ "files": project_state['files'],
345
+ "build_logs": project_state['logs'].get('build', 'No build logs.'),
346
+ "run_logs": project_state['logs'].get('run', 'No run logs.'),
347
+ "iframe_status": 'Responding OK' if project_state.get('iframe_ok', False) else 'Not responding or check failed.',
348
+ "error_types_found": classify_errors(project_state['logs'].get('build', '') + '\n' + project_state['logs'].get('run', ''))
349
+ }
350
  response_text, _ = run_agent(
351
  client=client,
352
+ model_name="gemini-1.5-flash-latest", # Flash is good for analysis
353
+ system_prompt_template=SYSTEM_DEBUG,
354
+ user_input_state=input_state_for_debugger,
355
  config=config,
 
356
  )
 
357
 
358
  if response_text.startswith("ERROR:"):
359
+ project_state['status_message'] = response_text
360
+ return False # Indicate failure
361
 
362
  project_state['feedback'] = response_text
363
+ print("Orchestrator: Debug Agent Feedback Received.")
364
+ project_state['status_message'] = "Debug feedback generated."
365
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ # --- MAIN ORCHESTRATION LOGIC ---
368
 
369
+ # We use Python functions to represent the state transitions and agent calls
370
+ # This is more deterministic than using an LLM purely for orchestration.
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
+ def orchestrate_development(client, project_state, config, oauth_token_token):
373
+ """Manages the overall development workflow."""
 
 
 
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  while project_state['status'] == 'In Progress' and project_state['attempt_count'] < 7:
376
  print(f"\n--- Attempt {project_state['attempt_count'] + 1} ---")
377
  print(f"Current Task: {project_state['current_task']}")
378
+
379
  current_task = project_state['current_task']
380
+ step_successful = True # Flag to track if the current step completed without error
381
+ # Add current task to history for UI visibility
382
+ project_state['chat_history'].append({"role": "assistant", "content": f"➡️ Task: {current_task}"})
383
+ project_state['status_message'] = f"Executing: {current_task}..."
384
+
385
 
386
  if current_task == 'START':
387
+ # Initial state, move to planning
388
+ project_state['current_task'] = 'PLANNING'
389
+ project_state['status_message'] = "Starting project: Initializing."
390
+
391
 
392
  elif current_task == 'PLANNING':
393
+ step_successful = run_planner(client, project_state, config)
394
+ if step_successful:
395
+ project_state['current_task'] = 'CODING - Initial Implementation' # Move to coding after planning
396
+ # Add plan to chat history for user
397
+ project_state['chat_history'].append({"role": "assistant", "content": f"**Plan:**\n{project_state['plan']}"})
398
+ else:
399
+ project_state['current_task'] = 'FAILED' # Planning failed
400
 
 
 
 
 
 
401
 
402
+ elif current_task.startswith('CODING'):
403
+ # Ensure the main app file exists before coding if it's the first coding step
404
+ if project_state['attempt_count'] == 0 and project_state['current_task'] == 'CODING - Initial Implementation':
405
+ if project_state['main_app_file'] not in project_state['files']:
406
+ project_state['files'][project_state['main_app_file']] = f"# Initial {project_state['sdk_choice']} app file\n" # Start with a basic stub
407
+ if project_state['sdk_choice'] == 'gradio':
408
+ project_state['files'][project_state['main_app_file']] += "import gradio as gr\n\n# Define a simple interface\n# For example: gr.Interface(...).launch()\n"
409
+ elif project_state['sdk_choice'] == 'streamlit':
410
+ project_state['files'][project_state['main_app_file']] += "import streamlit as st\n\n# Your Streamlit app starts here\n# For example: st.write('Hello, world!')\n"
411
+
412
+
413
+ step_successful = run_codegen(client, project_state, config)
414
+
415
+ if step_successful:
416
+ project_state['current_task'] = 'PUSHING' # Always push after attempting to code
417
+ else:
418
+ # Code-gen failed (syntax error, parsing issue, etc.)
419
+ # We'll try debugging/coding again in the next attempt loop iteration if attempts allow
420
+ print("Code-Gen step failed. Incrementing attempt count.")
421
+ project_state['attempt_count'] += 1 # Count Code-Gen failure as an attempt
422
+ # Add the error message from run_codegen to chat history
423
+ project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
424
+ project_state['current_task'] = 'DEBUGGING' # Go to debugging to analyze the failure
425
 
 
426
 
427
  elif current_task == 'PUSHING':
 
428
  try:
429
  # Create/update repo first
430
+ create_repo(repo_id=project_state['repo_id'], token=oauth_token_token,
431
+ exist_ok=True, repo_type="space", space_sdk=project_state['sdk_choice'])
432
 
433
+ # Write and upload all files in project_state['files']
434
  for fn, content in project_state['files'].items():
435
+ # Ensure directories exist for files like utils/data.py
436
+ os.makedirs(os.path.dirname(fn), exist_ok=True)
437
  with open(fn, "w") as f:
438
  f.write(content)
439
  upload_file(
440
  path_or_fileobj=fn, path_in_repo=fn,
441
+ repo_id=project_state['repo_id'], token=oauth_token_token,
442
  repo_type="space"
443
  )
444
 
445
+ # Ensure requirements.txt is handled - CodeGen *could* generate it,
446
+ # but adding boilerplate here guarantees it exists if not generated.
447
  if 'requirements.txt' not in project_state['files']:
448
+ req_content = "pandas\n" + ("streamlit\n" if project_state['sdk_choice']=="streamlit" else "gradio\n") + "google-generativeai\nhuggingface-hub\n"
449
  with open("requirements.txt", "w") as f:
450
  f.write(req_content)
451
  upload_file(path_or_fileobj="requirements.txt", path_in_repo="requirements.txt",
452
+ repo_id=project_state['repo_id'], token=oauth_token_token, repo_type="space")
453
+ project_state['files']['requirements.txt'] = req_content # Add to state for completeness
454
 
455
+ # Ensure README.md is handled
456
  if 'README.md' not in project_state['files']:
457
  readme_content = f"""---
458
+ title: {project_state['repo_id']}
459
  emoji: 🐢
460
+ sdk: {project_state['sdk_choice']}
461
+ sdk_version: {project_state['sdk_version']}
462
  app_file: {project_state['main_app_file']}
463
  pinned: false
464
  ---
465
+ # {project_state['repo_id']}
466
+
467
+ This is an auto-generated HF Space.
468
+
469
+ **Requirements:** {project_state['requirements']}
470
+
471
+ **Plan:**
472
+ {project_state['plan']}
473
  """
474
  with open("README.md", "w") as f:
475
  f.write(readme_content)
476
  upload_file(path_or_fileobj="README.md", path_in_repo="README.md",
477
+ repo_id=project_state['repo_id'], token=oauth_token_token, repo_type="space")
478
+ project_state['files']['README.md'] = readme_content # Add to state
479
 
480
 
481
+ print(f"Pushed {len(project_state['files'])} files to {project_state['repo_id']}")
482
+ project_state['status_message'] = f"Pushed code to HF Space **{project_state['repo_id']}**. Waiting for build..."
483
+ project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
484
+ project_state['current_task'] = 'LOGGING' # Move to fetching logs
485
 
486
  except Exception as e:
487
+ step_successful = False
488
+ project_state['status'] = 'Failed' # Pushing is critical, fail if it fails
489
  project_state['status_message'] = f"ERROR: Failed to push to HF Space: {e}"
490
+ project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
491
+ print(project_state['status_message'])
492
+ project_state['current_task'] = 'FINISHED' # End process
493
 
494
 
495
  elif current_task == 'LOGGING':
496
+ # Wait a moment for build to start
497
+ time.sleep(5) # Initial wait
498
+ wait_time = 5
499
+ max_log_wait = 60 # Maximum total time to wait for logs in this step
500
+ elapsed_log_wait = 0
501
+ logs_fetched = False
502
+
503
+ while elapsed_log_wait < max_log_wait:
504
+ try:
505
+ build_logs = fetch_logs(project_state['repo_id'], "build", oauth_token_token)
506
+ run_logs = fetch_logs(project_state['repo_id'], "run", oauth_token_token)
507
+ project_state['logs']['build'] = build_logs
508
+ project_state['logs']['run'] = run_logs
509
+ project_state['iframe_ok'] = check_iframe(project_state['iframe_url'])
510
+
511
+ logs_fetched = True
512
+ print("Logs fetched. Checking iframe.")
513
+
514
+ # Simple check: if build logs exist AND (contain success or timeout reached)
515
+ # and run logs exist, we can proceed to debugging.
516
+ # More advanced: check for specific build success/failure patterns.
517
+ if logs_fetched and (
518
+ "Building success" in build_logs or elapsed_log_wait >= max_log_wait - wait_time # Assume built if timeout reached
519
+ or "Build complete" in build_logs # Common build tool output
520
+ ) and (run_logs or project_state['iframe_ok']): # Need some sign of runtime or iframe
521
+ break # Exit the log fetching wait loop
522
+ elif "ERROR" in build_logs.upper() or "FATAL" in build_logs.upper():
523
+ print("Build errors detected, proceeding to debugging.")
524
+ break # Proceed to debugging to analyze errors
525
+ else:
526
+ print(f"Logs incomplete or no clear status yet. Waiting {wait_time}s...")
527
+ time.sleep(wait_time)
528
+ elapsed_log_wait += wait_time
529
+ wait_time = min(wait_time * 1.5, 15) # Increase wait time
530
+
531
+
532
+ except Exception as e:
533
+ print(f"Error during log fetching or iframe check: {e}. Will retry.")
534
+ time.sleep(wait_time)
535
+ elapsed_log_wait += wait_time
536
+ wait_time = min(wait_time * 1.5, 15)
537
+
538
+
539
+ if logs_fetched:
540
+ project_state['status_message'] = "Logs fetched and iframe checked."
541
+ project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
542
+ project_state['current_task'] = 'DEBUGGING' # Move to debugging to analyze logs
543
+ else:
544
+ step_successful = False
545
+ project_state['status'] = 'Failed' # Failed to fetch logs within timeout
546
+ project_state['status_message'] = "ERROR: Failed to fetch logs or iframe within timeout."
547
+ project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
548
+ project_state['current_task'] = 'FINISHED' # End process
549
 
550
 
551
  elif current_task == 'DEBUGGING':
552
+ step_successful = run_debugger(client, project_state, config)
553
+
554
+ # Add debug feedback to chat history
555
+ project_state['chat_history'].append({"role": "assistant", "content": f"**Debug Feedback:**\n{project_state['feedback']}"})
556
+
557
+
558
+ if step_successful:
559
+ # Analyze feedback to decide next step
560
+ feedback = project_state['feedback']
561
+ iframe_ok = project_state.get('iframe_ok', False)
562
+ error_types = classify_errors(project_state['logs'].get('build', '') + '\n' + project_state['logs'].get('run', ''))
563
+
564
+ print(f"Debug Analysis - Feedback: {feedback[:100]}... | Iframe OK: {iframe_ok} | Errors: {error_types}")
565
+
566
+
567
+ if "All clear. Project appears complete." in feedback or \
568
+ (iframe_ok and error_types == "none" and "ERROR" not in feedback.upper()):
569
+ # Debugger says it's clear OR iframe is working AND no errors reported in logs AND no ERROR in feedback
570
+ project_state['status'] = 'Complete'
571
+ project_state['current_task'] = 'FINISHED'
572
+ project_state['status_message'] = "Debug Agent reports clear. Project appears complete."
573
+ elif project_state['attempt_count'] >= 6: # Max attempts reached AFTER debugging
574
+ project_state['status'] = 'Failed'
575
+ project_state['current_task'] = 'FINISHED'
576
+ project_state['status_message'] = f"Max attempts ({project_state['attempt_count']+1}/7) reached after debugging. Project failed."
577
+ else:
578
+ # Errors or issues found, need more coding/debugging
579
+ project_state['current_task'] = 'CODING - Addressing Feedback'
580
+ project_state['status_message'] = "Debug Agent found issues. Returning to Coding phase to address feedback."
581
+ project_state['attempt_count'] += 1 # Increment attempt count before looping back to coding
582
+ backoff_wait = min(project_state['attempt_count'] * 5, 30) # Backoff before next coding attempt
583
+ print(f"Waiting {backoff_wait} seconds before next coding attempt...")
584
+ time.sleep(backoff_wait)
585
+
586
+ else:
587
+ # Debugger failed (e.g. API error)
588
  project_state['status'] = 'Failed'
589
+ project_state['current_task'] = 'FINISHED'
590
+ # status_message already set by run_debugger
591
 
 
 
 
592
 
593
+ elif current_task == 'FINISHED':
594
+ # Exit the main loop
595
+ break
 
 
 
596
 
597
+ # --- End of Task Handling ---
598
 
599
+ # If a step failed and didn't already set status to FAILED, mark the Orchestrator logic as failed
600
+ if not step_successful and project_state['status'] == 'In Progress':
601
+ project_state['status'] = 'Failed'
602
+ project_state['status_message'] = project_state.get('status_message', 'An unexpected error occurred during orchestration step.')
603
+ project_state['current_task'] = 'FINISHED' # End process
604
 
605
+ # --- End of Orchestration Loop ---
606
 
607
+ # Final status message if loop exited without explicit FINISHED state
608
+ if project_state['status'] == 'In Progress':
609
+ project_state['status'] = 'Failed'
610
+ project_state['status_message'] = project_state.get('status_message', 'Orchestration loop exited unexpectedly.')
 
 
 
 
 
611
 
612
 
613
+ # Add final outcome message to history
614
+ project_state['chat_history'].append({"role": "assistant", "content": f"**Project Outcome:** {project_state['status']} - {project_state['status_message']}"})
615
+ if project_state['status'] == 'Complete':
616
+ project_state['chat_history'].append({"role": "assistant", "content": "✅ Application deployed successfully (likely)! Check the preview above."})
617
+ else:
618
+ project_state['chat_history'].append({"role": "assistant", "content": "❌ Project failed to complete. Review logs and feedback for details."})
619
 
 
 
 
 
620
 
621
+ # Return final state for UI update
622
+ return (
623
+ project_state['chat_history'],
624
+ project_state['logs'].get('build', 'No build logs.'),
625
+ project_state['logs'].get('run', 'No run logs.'),
626
+ (f'<iframe src="{project_state["iframe_url"]}" width="100%" height="500px"></iframe>'
627
+ + ("" if project_state.get('iframe_ok') else "<p style='color:red;'>⚠️ iframe not responding or checked.</p>")),
628
+ project_state['status_message'] # Return the final status message
629
  )
630
 
 
 
 
 
 
 
631
 
632
+ # --- MAIN HANDLER (Called by Gradio) ---
633
 
634
+ def handle_user_message(
635
+ history, # This is the list of messages in the Gradio Chatbot
636
+ sdk_choice: str,
637
+ gemini_api_key: str,
638
+ grounding_enabled: bool,
639
+ temperature: float,
640
+ max_output_tokens: int,
641
+ profile: gr.OAuthProfile | None,
642
+ oauth_token: gr.OAuthToken | None # We need the token object
643
+ ):
644
+ if not profile or not oauth_token:
645
+ # Append error message to history for display
646
+ error_history = history + [{"role":"assistant","content":"⚠️ Please log in first."}]
647
+ return error_history, "", "", "<p>Please log in.</p>", "Login required."
648
 
649
+ if not gemini_api_key:
650
+ error_history = history + [{"role":"assistant","content":"⚠️ Please provide your Gemini API Key."}]
651
+ return error_history, "", "", "<p>Please provide API Key.</p>", "API Key required."
652
+
653
+
654
+ client = genai.Client(api_key=gemini_api_key)
655
+ repo_id = f"{profile.username}/{profile.username}-auto-space"
656
+ iframe_url = f"https://huggingface.co/spaces/{repo_id}"
657
+ sdk_version = get_sdk_version(sdk_choice)
658
+ code_fn = "app.py" if sdk_choice == "gradio" else "streamlit_app.py" # Standard main file name convention
659
+
660
+ # Get the user's latest prompt from the history
661
+ user_prompt = history[-1]['content'] if history and history[-1].get("role") == "user" else "No prompt provided."
662
+ if user_prompt == "No prompt provided." and len(history) > 0:
663
+ # Handle cases where history might be unusual, try finding last user message
664
+ for msg in reversed(history):
665
+ if msg.get("role") == "user":
666
+ user_prompt = msg["content"]
667
+ break
668
+
669
+
670
+ # Initialize project state for this development session
671
+ # History will be updated throughout and returned at the end
672
+ project_state = {
673
+ 'requirements': user_prompt,
674
+ 'plan': '',
675
+ 'files': {}, # Use a dict to store multiple file contents {filename: code}
676
+ 'logs': {'build': '', 'run': ''},
677
+ 'feedback': '',
678
+ 'current_task': 'START',
679
+ 'status': 'In Progress',
680
+ 'status_message': 'Initializing...',
681
+ 'attempt_count': 0,
682
+ 'sdk_choice': sdk_choice,
683
+ 'sdk_version': sdk_version,
684
+ 'repo_id': repo_id,
685
+ 'iframe_url': iframe_url,
686
+ 'main_app_file': code_fn,
687
+ 'chat_history': history[:] # Use the passed-in history to build upon
688
+ }
689
+
690
+ cfg = GenerateContentConfig(
691
+ tools=[Tool(google_search=GoogleSearch())] if grounding_enabled else [],
692
+ response_modalities=["TEXT"],
693
+ temperature=temperature,
694
+ max_output_tokens=int(max_output_tokens), # Ensure integer
695
+ )
696
+
697
+ # Start the orchestration process
698
+ final_history, final_build_logs, final_run_logs, final_iframe_html, final_status_message = orchestrate_development(
699
+ client, project_state, cfg, oauth_token.token
700
+ )
701
+
702
+ # Return the final state for the UI
703
+ return (
704
+ final_history,
705
+ final_build_logs,
706
+ final_run_logs,
707
+ final_iframe_html,
708
+ final_status_message
709
+ )
710
+
711
+ # --- SIMPLE UI WITH HIGHER MAX TOKENS & STATUS DISPLAY ---
712
 
713
  with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
714
+ gr.Markdown("## 🐢 HF Space Auto‑Builder (Team AI)\nUse AI agents to build and deploy a simple Gradio or Streamlit app on a Hugging Face Space.")
715
+ gr.Markdown("1) Log in with Hugging Face. 2) Enter your Gemini API Key. 3) Provide app requirements. 4) Click 'Start Development Team' and watch the process.")
716
 
 
 
 
 
 
 
 
717
 
718
  with gr.Row():
719
  with gr.Column(scale=1):
720
+ login_btn = gr.LoginButton(variant="huggingface", size="lg")
721
+ status_md = gr.Markdown("*Not logged in.*")
722
+ models_md = gr.Markdown()
723
+ # Load profile/models on startup and login button click
724
+ demo.load(show_profile, inputs=None, outputs=status_md, api_name="load_profile")
725
+ demo.load(list_private_models, inputs=[login_btn, login_btn], outputs=models_md, api_name="load_models")
726
+ login_btn.select(show_profile, inputs=[login_btn], outputs=status_md, api_name="login_profile") # Use select for login_btn
727
+ login_btn.select(list_private_models, inputs=[login_btn, login_btn], outputs=models_md, api_name="login_models") # Pass login_btn state twice to get profile and token
728
+
729
+
730
+ gr.Markdown("---")
731
+
732
+ sdk_choice = gr.Radio(["gradio","streamlit"], value="gradio", label="SDK", info="Choose the framework for your app.")
733
+ api_key = gr.Textbox(label="Gemini API Key", type="password", info="Get one from Google AI Studio.")
734
+ grounding = gr.Checkbox(label="Enable Google Search (Grounding)", value=False, info="Allow agents to use Google Search.")
735
+ temp = gr.Slider(0,1,value=0.2, label="Temperature", info="Creativity of agents. Lower is more focused.")
736
+ max_tokens = gr.Number(value=4096, label="Max Output Tokens", minimum=1000, info="Max length of agent responses (code, feedback, etc.). Recommend 4096+.")
737
 
738
  with gr.Column(scale=2):
739
  project_status_md = gr.Markdown("Waiting for prompt...")
740
+ chatbot = gr.Chatbot(type="messages", label="Team Communication & Status", show_copy_button=True)
741
+ user_in = gr.Textbox(placeholder="Describe the application you want to build...", label="Application Requirements", lines=3)
742
+ send_btn = gr.Button("🚀 Start Development Team")
743
 
744
  # Separate accordions for logs and preview
745
  with gr.Accordion("Logs", open=False):
746
+ build_box = gr.Textbox(label="Build logs", lines=10, interactive=False, max_lines=20)
747
+ run_box = gr.Textbox(label="Run logs", lines=10, interactive=False, max_lines=20)
748
+ # Need login state for refresh button
749
+ refresh_btn = gr.Button("🔄 Refresh Logs Only")
750
 
751
  with gr.Accordion("App Preview", open=True):
752
+ preview = gr.HTML("<p>App preview will load here when available.</p>")
 
753
 
754
  # Update the button click handler
755
  # It will now return the updated chatbot history, logs, preview, and the project status
756
  send_btn.click(
757
  fn=handle_user_message,
758
+ inputs=[chatbot, sdk_choice, api_key, grounding, temp, max_tokens, login_btn, login_btn], # Pass login_btn state twice
759
  outputs=[chatbot, build_box, run_box, preview, project_status_md]
760
  )
761
  user_in.submit(
762
+ fn=handle_user_message,
763
+ inputs=[chatbot, sdk_choice, api_key, grounding, temp, max_tokens, login_btn, login_btn], # Pass login_btn state twice
764
  outputs=[chatbot, build_box, run_box, preview, project_status_md]
765
  )
766
 
767
  # Handler for refreshing logs manually
768
+ # It needs the repo_id (derived from profile) and the auth token
769
  refresh_btn.click(
770
  fn=lambda profile, token: (
771
+ fetch_logs(f"{profile.username}/{profile.username}-auto-space", "build", token.token) if profile and token else "Login required to fetch logs.",
772
+ fetch_logs(f"{profile.username}/{profile.username}-auto-space", "run", token.token) if profile and token else "Login required to fetch logs."
773
  ),
774
+ inputs=[login_btn, login_btn], # Pass login_btn state twice to get profile and token
775
  outputs=[build_box, run_box]
776
  )
777
 
778
 
779
+ # Clean up files created during the process when the app stops (optional, good for Spaces)
780
+ # demo.on_event("close", lambda: [os.remove(f) for f in os.listdir() if os.path.isfile(f) and (f.endswith(".py") or f.endswith(".txt") or f.endswith(".md"))]) # Be careful with this in production
781
+
782
+ demo.launch(server_name="0.0.0.0", server_port=7860)