Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,20 +1,133 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
14 |
-
"
|
15 |
-
"
|
16 |
-
"
|
17 |
-
"
|
|
|
|
|
|
|
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
|
26 |
-
"Outline the main features, suggest a
|
27 |
-
"
|
|
|
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 |
-
"
|
40 |
-
"
|
|
|
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 |
-
"
|
52 |
-
"
|
53 |
-
"
|
54 |
-
"
|
|
|
|
|
55 |
)
|
56 |
}
|
57 |
|
58 |
-
# --- AGENT
|
59 |
|
60 |
-
def run_agent(client, model_name,
|
61 |
-
"""Helper to run a single agent interaction."""
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
-
|
|
|
|
|
66 |
|
67 |
-
|
68 |
-
|
69 |
-
|
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 |
-
|
79 |
-
|
80 |
-
|
|
|
81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
|
83 |
-
|
84 |
-
|
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 |
-
|
91 |
-
|
92 |
-
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
|
106 |
def run_planner(client, project_state, config):
|
107 |
-
print("Running Planner Agent...")
|
108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
response_text, _ = run_agent(
|
110 |
client=client,
|
111 |
-
model_name="gemini-
|
112 |
-
|
113 |
-
|
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 |
-
|
|
|
121 |
|
122 |
project_state['plan'] = response_text
|
123 |
-
print("Planner Output
|
124 |
-
|
|
|
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 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
|
|
|
|
136 |
response_text, _ = run_agent(
|
137 |
client=client,
|
138 |
-
model_name="gemini-
|
139 |
-
|
140 |
-
|
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 |
-
|
|
|
148 |
|
149 |
# Parse the response text to extract code blocks for potentially multiple files
|
150 |
files_updated = {}
|
151 |
-
#
|
152 |
-
#
|
153 |
-
|
|
|
154 |
|
155 |
if not blocks:
|
156 |
-
|
157 |
-
|
158 |
-
|
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:
|
164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
|
166 |
if not files_updated:
|
167 |
-
print("Code-Gen Agent outputted blocks but couldn't parse filenames.")
|
168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
#
|
175 |
-
for fn, code in files_updated.items()
|
176 |
-
|
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
|
185 |
|
186 |
def run_debugger(client, project_state, config):
|
187 |
-
print("Running Debug Agent...")
|
188 |
-
# Debugger needs requirements, plan, files, logs
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
|
|
197 |
response_text, _ = run_agent(
|
198 |
client=client,
|
199 |
-
model_name="gemini-
|
200 |
-
|
201 |
-
|
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 |
-
|
|
|
209 |
|
210 |
project_state['feedback'] = response_text
|
211 |
-
print("Debug Agent Feedback
|
212 |
-
|
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 |
-
#
|
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 |
-
|
304 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
358 |
|
359 |
if current_task == 'START':
|
360 |
-
|
|
|
|
|
|
|
361 |
|
362 |
elif current_task == 'PLANNING':
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
project_state['
|
368 |
-
|
|
|
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 |
-
|
377 |
-
|
378 |
-
|
379 |
-
project_state['
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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=
|
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 |
-
#
|
|
|
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=
|
402 |
repo_type="space"
|
403 |
)
|
404 |
|
405 |
-
# Ensure requirements.txt
|
406 |
-
#
|
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=
|
|
|
413 |
|
|
|
414 |
if 'README.md' not in project_state['files']:
|
415 |
readme_content = f"""---
|
416 |
-
title: {
|
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=
|
|
|
428 |
|
429 |
|
430 |
-
print(f"Pushed {len(project_state['files'])} files to {repo_id}")
|
431 |
-
|
432 |
-
|
|
|
433 |
|
434 |
except Exception as e:
|
435 |
-
|
|
|
436 |
project_state['status_message'] = f"ERROR: Failed to push to HF Space: {e}"
|
437 |
-
|
438 |
-
print(
|
|
|
439 |
|
440 |
|
441 |
elif current_task == 'LOGGING':
|
442 |
-
#
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
455 |
|
456 |
|
457 |
elif current_task == 'DEBUGGING':
|
458 |
-
|
459 |
-
|
460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
461 |
project_state['status'] = 'Failed'
|
462 |
-
project_state['
|
463 |
-
|
464 |
|
465 |
-
elif current_task == 'FINISHED':
|
466 |
-
# Exit loop state
|
467 |
-
pass
|
468 |
|
469 |
-
|
470 |
-
#
|
471 |
-
|
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 |
-
#
|
478 |
-
if
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
|
|
|
483 |
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
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 |
-
#
|
503 |
-
|
504 |
-
|
505 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
517 |
|
518 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
519 |
|
520 |
with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
|
521 |
-
gr.Markdown("## 🐢 HF Space Auto‑Builder (Team AI)\
|
|
|
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 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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="
|
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=
|
548 |
-
run_box = gr.Textbox(label="Run logs", lines=
|
549 |
-
|
|
|
550 |
|
551 |
with gr.Accordion("App Preview", open=True):
|
552 |
-
preview = gr.HTML("<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 |
-
|
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
|
575 |
outputs=[build_box, run_box]
|
576 |
)
|
577 |
|
578 |
|
579 |
-
|
|
|
|
|
|
|
|
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)
|