wuhp commited on
Commit
29d611a
·
verified ·
1 Parent(s): 03a7726

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +526 -229
app.py CHANGED
@@ -1,91 +1,294 @@
1
- import re
2
- import json
3
- import time
4
- import requests
5
- import importlib.metadata
6
- import gradio as gr
7
- from huggingface_hub import (
8
- create_repo, upload_file, list_models, constants
9
- )
10
- from huggingface_hub.utils import build_hf_headers, get_session, hf_raise_for_status
11
- from google import genai
12
- from google.genai.types import Tool, GenerateContentConfig, GoogleSearch
13
-
14
- # USER INFO & MODEL LISTING
15
-
16
- def show_profile(profile: gr.OAuthProfile | None) -> str:
17
- return f" Logged in as **{profile.username}**" if profile else "*Not logged in.*"
18
-
19
- def list_private_models(
20
- profile: gr.OAuthProfile | None,
21
- oauth_token: gr.OAuthToken | None
22
- ) -> str:
23
- if not profile or not oauth_token:
24
- return "Please log in to see your models."
25
- models = [
26
- f"{m.id} ({'private' if m.private else 'public'})"
27
- for m in list_models(author=profile.username, token=oauth_token.token)
28
- ]
29
- return "No models found." if not models else "Models:\n\n" + "\n - ".join(models)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- # UTILITIES
32
 
33
- def get_sdk_version(sdk_choice: str) -> str:
34
- pkg = "gradio" if sdk_choice == "gradio" else "streamlit"
35
- try:
36
- return importlib.metadata.version(pkg)
37
- except importlib.metadata.PackageNotFoundError:
38
- return "UNKNOWN"
39
-
40
- def extract_code(text: str) -> str:
41
- blocks = re.findall(r"```(?:\w*\n)?([\s\S]*?)```", text)
42
- return blocks[-1].strip() if blocks else text.strip()
43
-
44
- def classify_errors(logs: str) -> str:
45
- errs = set()
46
- for line in logs.splitlines():
47
- if "SyntaxError" in line:
48
- errs.add("syntax")
49
- elif "ImportError" in line or "ModuleNotFoundError" in line:
50
- errs.add("import")
51
- elif "Traceback" in line or "Exception" in line:
52
- errs.add("runtime")
53
- return ", ".join(errs) or "unknown"
54
-
55
- # — HF SPACE LOGGING —
56
-
57
- def _get_space_jwt(repo_id: str) -> str:
58
- url = f"{constants.ENDPOINT}/api/spaces/{repo_id}/jwt"
59
- r = get_session().get(url, headers=build_hf_headers())
60
- hf_raise_for_status(r)
61
- return r.json()["token"]
62
-
63
- def fetch_logs(repo_id: str, level: str) -> str:
64
- jwt = _get_space_jwt(repo_id)
65
- url = f"https://api.hf.space/v1/{repo_id}/logs/{level}"
66
- lines = []
67
- with get_session().get(url, headers=build_hf_headers(token=jwt), stream=True) as resp:
68
- hf_raise_for_status(resp)
69
- for raw in resp.iter_lines():
70
- if raw.startswith(b"data: "):
71
- try:
72
- ev = json.loads(raw[len(b"data: "):].decode())
73
- ts, txt = ev.get("timestamp",""), ev.get("data","")
74
- lines.append(f"[{ts}] {txt}")
75
- except:
76
- continue
77
- return "\n".join(lines)
78
-
79
- def check_iframe(url: str, timeout: int = 5) -> bool:
80
- try:
81
- return requests.get(url, timeout=timeout).status_code == 200
82
- except:
83
- return False
84
 
85
- # CORE LOOP WITH TWO AGENTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  def handle_user_message(
88
- history,
89
  sdk_choice: str,
90
  gemini_api_key: str,
91
  grounding_enabled: bool,
@@ -95,148 +298,227 @@ def handle_user_message(
95
  oauth_token: gr.OAuthToken | None
96
  ):
97
  if not profile or not oauth_token:
98
- return history + [{"role":"assistant","content":"⚠️ Please log in first."}], "", "", "<p>No Space yet.</p>"
99
 
100
- client = genai.Client(api_key=gemini_api_key)
101
- code_fn = "app.py" if sdk_choice=="gradio" else "streamlit_app.py"
102
- repo_id = f"{profile.username}/{profile.username}-auto-space"
103
  iframe_url = f"https://huggingface.co/spaces/{repo_id}"
104
-
105
- # SYSTEM PROMPTS
106
- system_code = {
107
- "role":"system",
108
- "content":(
109
- "You are **Code‑Gen Agent**, a proactive AI developer. Your sole responsibility is to author "
110
- f"and correct the entire `{code_fn}` file in a single markdown code block—no extra commentary. "
111
- "You have permission to edit files, push updates to the HF Space, and optimize code. "
112
- "After each push, await build & run logs before making further changes."
113
- )
114
- }
115
- system_debug = {
116
- "role":"system",
117
- "content":(
118
- "You are **Debug Agent**, a meticulous code reviewer. You can read all files, logs, and the app "
119
- "preview, but you **cannot** modify or push code. Your task is to analyze the latest code + logs "
120
- "and return concise, actionable feedback or “All clear.”"
121
- )
 
 
 
 
 
 
 
122
  }
123
 
124
- # initialize each agent’s conversation
125
- code_chat = [system_code] + history[:]
126
- debug_chat = [system_debug] + history[:]
127
-
128
- build_logs = run_logs = ""
129
- backoff = 1
130
-
131
- for attempt in range(1, 7):
132
- tools = [Tool(google_search=GoogleSearch())] if grounding_enabled else []
133
- cfg = GenerateContentConfig(
134
- tools=tools,
135
- response_modalities=["TEXT"],
136
- temperature=temperature,
137
- max_output_tokens=max_output_tokens,
138
- )
 
 
 
139
 
140
- # --- 1) Code‑Gen generates or updates code ---
141
- resp_code = client.models.generate_content(
142
- model="gemini-2.5-flash-preview-04-17",
143
- contents=[m["content"] for m in code_chat],
144
- config=cfg
145
- )
146
- code = extract_code(resp_code.text)
147
- code_chat.append({"role":"assistant","content":code})
148
- debug_chat.append({"role":"assistant","content":code})
149
-
150
- # quick syntax check
151
- try:
152
- compile(code, code_fn, "exec")
153
- except SyntaxError as e:
154
- code_chat.append({
155
- "role":"user",
156
- "content": f"SyntaxError caught: {e}. Please correct `{code_fn}` only."
157
- })
158
- time.sleep(backoff); backoff = min(backoff*2, 30)
159
- continue
160
-
161
- # write & push to HF Space
162
- sdk_version = get_sdk_version(sdk_choice)
163
- files = {
164
- code_fn: code,
165
- "README.md": f"""---
166
- title: Wuhp Auto Space
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  emoji: 🐢
168
  sdk: {sdk_choice}
169
  sdk_version: {sdk_version}
170
- app_file: {code_fn}
171
  pinned: false
172
  ---
173
- """,
174
- "requirements.txt": "pandas\n" + ("streamlit\n" if sdk_choice=="streamlit" else "gradio\n")
175
- }
176
- for fn, content in files.items():
177
- with open(fn, "w") as f:
178
- f.write(content)
179
-
180
- create_repo(repo_id=repo_id, token=oauth_token.token,
181
- exist_ok=True, repo_type="space", space_sdk=sdk_choice)
182
- for fn in files:
183
- upload_file(
184
- path_or_fileobj=fn, path_in_repo=fn,
185
- repo_id=repo_id, token=oauth_token.token,
186
- repo_type="space"
187
- )
188
-
189
- # fetch logs
190
- build_logs = fetch_logs(repo_id, "build")
191
- run_logs = fetch_logs(repo_id, "run")
192
- err_types = classify_errors(build_logs + "\n" + run_logs)
193
-
194
- # --- 2) Debug‑Agent reviews code & logs ---
195
- debug_input = (
196
- f"🏷 **Attempt {attempt}**\n"
197
- f"Error types: {err_types}\n\n"
198
- f"**Build logs:**\n{build_logs}\n\n"
199
- f"**Run logs:**\n{run_logs}\n\n"
200
- "If there are no errors, reply “All clear.” Otherwise, list your recommended changes."
201
- )
202
- debug_chat.append({"role":"user","content":debug_input})
203
- resp_debug = client.models.generate_content(
204
- model="gemini-2.5-flash-preview-04-17",
205
- contents=[m["content"] for m in debug_chat],
206
- config=cfg
207
- )
208
- feedback = resp_debug.text.strip()
209
- debug_chat.append({"role":"assistant","content":feedback})
210
-
211
- # check for success
212
- if "ERROR" not in build_logs.upper() and \
213
- "ERROR" not in run_logs.upper() and \
214
- check_iframe(iframe_url):
215
- break
216
-
217
- # feed debug feedback back to Code‑Gen
218
- code_chat.append({
219
- "role":"user",
220
- "content": f"🔧 Debug feedback:\n{feedback}\nPlease output the full corrected `{code_fn}` code block only."
221
- })
222
- time.sleep(backoff); backoff = min(backoff*2, 30)
223
-
224
- # prepare UI outputs
225
- messages = [
226
- {"role": m["role"], "content": m["content"]}
227
- for m in code_chat if m["role"] != "system"
228
- ]
229
- iframe_html = (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  f'<iframe src="{iframe_url}" width="100%" height="500px"></iframe>'
231
- + ("" if check_iframe(iframe_url)
232
- else "<p style='color:red;'>⚠️ iframe not responding.</p>")
233
  )
234
- return messages, build_logs, run_logs, iframe_html
235
 
236
- # SIMPLE UI WITH HIGHER MAX TOKENS
 
 
 
 
 
 
 
 
 
 
237
 
238
- with gr.Blocks(title="HF Space Auto‑Builder") as demo:
239
- gr.Markdown("## 🐢 HF Space Auto‑Builder\n1) Sign in  2) Enter prompt  3) Watch code, logs & preview")
240
 
241
  login_btn = gr.LoginButton(variant="huggingface", size="lg")
242
  status_md = gr.Markdown("*Not logged in.*")
@@ -246,37 +528,52 @@ with gr.Blocks(title="HF Space Auto‑Builder") as demo:
246
  login_btn.click(show_profile, inputs=None, outputs=status_md)
247
  login_btn.click(list_private_models, inputs=None, outputs=models_md)
248
 
249
- sdk_choice = gr.Radio(["gradio","streamlit"], value="gradio", label="SDK")
250
- api_key = gr.Textbox(label="Gemini API Key", type="password")
251
- grounding = gr.Checkbox(label="Enable grounding", value=False)
252
- temp = gr.Slider(0,1,value=0.2, label="Temperature")
253
- max_tokens = gr.Number(value=2048, label="Max output tokens (set up to Gemini’s limit)")
 
 
254
 
255
- chatbot = gr.Chatbot(type="messages")
256
- user_in = gr.Textbox(placeholder="Your prompt", label="Prompt", lines=1)
257
- send_btn = gr.Button("Send")
258
- build_box = gr.Textbox(label="Build logs", lines=5, interactive=False)
259
- run_box = gr.Textbox(label="Run logs", lines=5, interactive=False)
260
- preview = gr.HTML("<p>No Space yet.</p>")
261
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  send_btn.click(
263
  fn=handle_user_message,
264
- inputs=[chatbot, sdk_choice, api_key, grounding, temp, max_tokens],
265
- outputs=[chatbot, build_box, run_box, preview]
266
  )
267
  user_in.submit(
268
- fn=handle_user_message,
269
- inputs=[chatbot, sdk_choice, api_key, grounding, temp, max_tokens],
270
- outputs=[chatbot, build_box, run_box, preview]
271
  )
272
 
273
- refresh_btn = gr.Button("Refresh Logs")
274
  refresh_btn.click(
275
  fn=lambda profile, token: (
276
  fetch_logs(f"{profile.username}/{profile.username}-auto-space", "build"),
277
  fetch_logs(f"{profile.username}/{profile.username}-auto-space", "run")
278
  ),
 
279
  outputs=[build_box, run_box]
280
  )
281
 
282
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
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
+
21
+ 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
+
31
+ 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
+
44
+ SYSTEM_DEBUG = {
45
+ "role": "system",
46
+ "content": (
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,
 
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.*")
 
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)