Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,91 +1,294 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
from
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
|
31 |
-
#
|
32 |
|
33 |
-
def
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
101 |
-
|
102 |
-
repo_id = f"{profile.username}/{profile.username}-auto-space"
|
103 |
iframe_url = f"https://huggingface.co/spaces/{repo_id}"
|
104 |
-
|
105 |
-
#
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
}
|
123 |
|
124 |
-
#
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
|
|
|
|
|
|
139 |
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
emoji: 🐢
|
168 |
sdk: {sdk_choice}
|
169 |
sdk_version: {sdk_version}
|
170 |
-
app_file: {
|
171 |
pinned: false
|
172 |
---
|
173 |
-
"""
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
230 |
f'<iframe src="{iframe_url}" width="100%" height="500px"></iframe>'
|
231 |
-
+ ("" if
|
232 |
-
else "<p style='color:red;'>⚠️ iframe not responding.</p>")
|
233 |
)
|
234 |
-
return messages, build_logs, run_logs, iframe_html
|
235 |
|
236 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
|
|
|
|
254 |
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
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 |
-
|
269 |
-
inputs=[chatbot, sdk_choice, api_key, grounding, temp, max_tokens],
|
270 |
-
outputs=[chatbot, build_box, run_box, preview]
|
271 |
)
|
272 |
|
273 |
-
|
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 |
-
|
|
|
|
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)
|