diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -74,7 +74,7 @@ def _get_space_jwt(repo_id: str) -> str: hf_raise_for_status(r) # Raises HTTPError for bad responses (e.g. 404 if repo doesn't exist) return r.json()["token"] -# Modified: Removed 'token' parameter and enhanced error reporting +# Modified: Removed 'token' parameter def fetch_logs(repo_id: str, level: str) -> str: """Fetches build or run logs from an HF Space.""" try: @@ -86,13 +86,9 @@ def fetch_logs(repo_id: str, level: str) -> str: # Kept: Still need to pass the fetched JWT to the logs API request headers headers = build_hf_headers(token=jwt) # Use a timeout for the request - # Using stream=True and iterating lines to avoid large log fetch issues - with get_session().get(url, headers=headers, stream=True, timeout=15) as resp: # Increased timeout slightly + with get_session().get(url, headers=headers, stream=True, timeout=10) as resp: hf_raise_for_status(resp) # Read lines with a timeout - # Note: iter_lines might block indefinitely if the stream is kept alive without data. - # A more robust approach might involve asyncio/threading with finer-grained timeouts - # or reading chunks manually, but for a simple poller, this is often sufficient. for raw in resp.iter_lines(decode_unicode=True, chunk_size=512): if raw is None: # handle keep-alive or similar continue @@ -102,65 +98,29 @@ def fetch_logs(repo_id: str, level: str) -> str: ts, txt = ev.get("timestamp","N/A"), ev.get("data","") lines.append(f"[{ts}] {txt}") except json.JSONDecodeError: - # Handle lines that look like data but aren't valid JSON - lines.append(f"[Non-JSON Data] {raw}") + lines.append(f"Error decoding log line: {raw}") except Exception as e: - lines.append(f"[Log Process Error] {raw} - {e}") - # Add checks for potential end-of-stream or error indicators in SSE format - elif raw == "event: end": - print(f"Detected SSE 'event: end' for {level} logs.") - break # Exit loop if end signal is found - elif raw.startswith("event: error"): - print(f"Detected SSE 'event: error' in stream for {level} logs: {raw}") - # Optionally try parsing the error data that follows - try: - # Assuming error data follows event line - error_data_raw = next(resp.iter_lines(decode_unicode=True, chunk_size=512)) - if error_data_raw.startswith("data: "): - err_ev = json.loads(error_data_raw[len("data: "):]) - lines.append(f"[Stream Error] {err_ev.get('message', error_data_raw)}") - else: - lines.append(f"[Stream Error] {raw} - {error_data_raw}") - except StopIteration: - lines.append(f"[Stream Error] {raw} - No data followed.") - except json.JSONDecodeError: - lines.append(f"[Stream Error] {raw} - Data not JSON.") - break # Usually break on stream error - - return "\n".join(lines) if lines else f"No {level} logs found yet." # Indicate if empty - + lines.append(f"Unexpected error processing log line: {raw} - {e}") + return "\n".join(lines) except requests.exceptions.Timeout: - return f"ERROR_TIMEOUT: Timeout fetching {level} logs after 15s." + return f"Error: Timeout fetching {level} logs." except requests.exceptions.RequestException as e: - # Catch 401 specifically if build_hf_headers() failed to find credentials for JWT - if e.response is not None and e.response.status_code == 401: - # More specific message for auth failure *to get JWT* - return f"ERROR_AUTH_FAILED_JWT: Authentication failed when requesting logs token. Ensure HF_TOKEN env var is set on the Space running this app." - # Catch 401 from the logs stream request itself (less likely if JWT worked) - elif e.response is not None and e.response.status_code == 401: - return f"ERROR_AUTH_FAILED_STREAM: Authentication failed during logs stream. JWT might be invalid. Details: {e}" - # Catch 404 specifically (e.g., Space/logs not found yet) - elif e.response is not None and e.response.status_code == 404: - return f"ERROR_NOT_FOUND: Logs endpoint not found. Space might not be built or logs unavailable yet. Details: {e}" - # Catch other HTTP errors - elif e.response is not None: - return f"ERROR_HTTP_{e.response.status_code}: HTTP error fetching {level} logs. Details: {e}" - # Catch other request errors (connection, etc.) - return f"ERROR_REQUEST: Request error fetching {level} logs. Details: {e}" + # Catch 401 specifically if build_hf_headers() failed to find credentials + if e.response and e.response.status_code == 401: + return f"Error fetching {level} logs: Authentication failed. Ensure HF_TOKEN env var is set on the Space or you are logged in via huggingface-cli where the app is running." + return f"Error fetching {level} logs: {e}" except Exception as e: - return f"ERROR_UNEXPECTED: An unexpected error occurred while fetching {level} logs: {e}" + return f"An unexpected error occurred while fetching logs: {e}" def check_iframe(url: str, timeout: int = 10) -> bool: - """Checks if the iframe URL is reachable and returns a 2xx or 3xx status code.""" + """Checks if the iframe URL is reachable and returns a 200 status code.""" # For public spaces, simple request should suffice, no special headers needed here try: response = requests.get(url, timeout=timeout) - # Check for 200 or 300 series redirects, which often indicate success for Space URLs - return 200 <= response.status_code < 400 + return response.status_code == 200 except requests.exceptions.RequestException: - # Any request exception (timeout, connection error, etc.) means it's not accessible - return False + return False # Any request exception (timeout, connection error, etc.) means it's not accessible # --- AGENT PROMPTS --- @@ -169,14 +129,14 @@ SYSTEM_ORCHESTRATOR = { "content": ( "You are **Orchestrator Agent**, the project manager. " "Your role is to guide the development process from user request to a deployed HF Space application. " - "You will analyze the current project state (requirements, plan, files, logs, feedback, status, attempt_count, log_error_state, iframe_ok) " # Added relevant state keys + "You will analyze the current project state (requirements, plan, files, logs, feedback, status, attempt_count) " "and decide the *single* next step/task for the team. " "Output *only* the name of the next task from the following list: " "'PLANNING', 'CODING - {task_description}', 'PUSHING', 'LOGGING', 'DEBUGGING', 'COMPLETE', 'FAILED'. " "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'). " - "Analyze the debug feedback and logs (including log_error_state) carefully to decide the appropriate coding task description or if the project should be FAILED." - "If the debug feedback indicates 'All clear' and iframe_ok is true, transition to 'COMPLETE'." - "If a critical log_error_state exists (like authentication failure), or if maximum attempts are reached while errors persist, transition to 'FAILED'." + "Analyze the debug feedback and logs carefully to decide the appropriate coding task description." + "If the debug feedback indicates 'All clear', transition to 'COMPLETE'." + "If maximum attempts are reached or a critical error occurs, transition to 'FAILED'." ) } @@ -196,9 +156,8 @@ SYSTEM_CODEGEN = { "content": ( "You are **Code‑Gen Agent**, a proactive AI developer. " "Your sole responsibility is to author and correct code files based on the plan and the assigned task. " - "You will receive the full project state, including the requirements, plan, existing files, debug feedback, logs, and status indicators like iframe_ok and log_error_state. " # Added relevant state keys + "You will receive the full project state, including the requirements, plan, existing files, and debug feedback. " "Based on the current task assigned by the Orchestrator ('{current_task}'), write or modify the necessary code *only* in the specified file(s). " - "Analyze feedback, logs, and error states to understand what needs fixing or implementing." "Output the *full content* of the updated file(s) in markdown code blocks. " "Each code block must be immediately preceded by the filename in backticks on its own line. " "Use this format exactly: `` `filename.ext` ``\\n```\\ncode goes here\\n```\n" @@ -212,20 +171,18 @@ SYSTEM_DEBUG = { "role": "system", "content": ( "You are **Debug Agent**, a meticulous code reviewer and tester. " - "You have access to the full project state: requirements, plan, code files, build logs, run logs, iframe_status, log_error_state, and error_types_found. " # Explicitly list relevant keys - "Your task is to analyze the logs and code in the context of the plan and requirements." - "Pay very close attention to the log_error_state and the content of build/run logs for specific errors (SyntaxError, ImportError, runtime errors)." + "You have access to the full project state: requirements, plan, code files, build logs, and run logs. " + "Your task is to analyze the logs and code in the context of the plan and requirements. " "Identify errors, potential issues, missing features based on the plan, and suggest concrete improvements or fixes for the Code-Gen agent. " - "If log_error_state indicates a critical issue like authentication failure, report that first." - "Also check if the implemented features align with the plan and if the iframe_status indicates the app is running." - "If the log_error_state is 'None', the iframe_status is 'Responding OK', the error_types_found is 'none', and the logs don't contain clear ERROR/FATAL messages, state 'All clear. Project appears complete.' as the *first line* of your feedback." + "Pay close attention to the build and run logs for specific errors (SyntaxError, ImportError, runtime errors). " + "Also check if the implemented features align with the plan." + "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." "Otherwise, provide actionable feedback, referencing file names and line numbers where possible. Format feedback clearly." - "Example feedback:\n'Log Error: Authentication failed. Ensure HF_TOKEN secret is set.'\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.' " + "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.' " "Do NOT write or suggest large code blocks directly in your feedback. Focus on *what* needs fixing/adding and *why*." ) } - # --- AGENT RUNNER HELPER --- # MODIFIED AGAIN: Combine system prompt into user message for Gemini generate_content API @@ -237,34 +194,9 @@ def run_agent(client, model_name, system_prompt_template, user_input_state, conf except KeyError as e: print(f"Error formatting system prompt: Missing key {e}. Prompt template: {system_prompt_template['content']}") return f"ERROR: Internal agent error - Missing key {e} for prompt formatting." - except Exception as e: - print(f"Unexpected error formatting system prompt: {e}. Prompt template: {system_prompt_template['content']}") - return f"ERROR: Internal agent error - Unexpected formatting error: {e}" - # Prepare the message content by formatting the project state - # Only include relevant keys to avoid prompt length issues and guide the agent - relevant_state = { - 'requirements': user_input_state.get('requirements', 'None'), - 'plan': user_input_state.get('plan', 'None'), - 'files': {k: v[:500] + '...' if len(v) > 500 else v for k, v in user_input_state.get('files', {}).items()}, # Truncate file content - 'build_logs': user_input_state.get('build_logs', 'No build logs.'), - 'run_logs': user_input_state.get('run_logs', 'No run logs.'), - 'feedback': user_input_state.get('feedback', 'None'), - 'current_task': user_input_state.get('current_task', 'Unknown'), - 'status': user_input_state.get('status', 'Unknown'), - 'attempt_count': user_input_state.get('attempt_count', 0), - 'sdk_choice': user_input_state.get('sdk_choice', 'Unknown'), - 'sdk_version': user_input_state.get('sdk_version', 'Unknown'), - 'repo_id': user_input_state.get('repo_id', 'Unknown'), - 'iframe_url': user_input_state.get('iframe_url', 'Unknown'), - 'main_app_file': user_input_state.get('main_app_file', 'Unknown'), - 'iframe_ok': user_input_state.get('iframe_ok', False), # Added for agents to see iframe status - 'log_error_state': user_input_state.get('log_error_state', 'None'), # Added for agents to see log errors - 'error_types_found': user_input_state.get('error_types_found', 'none'), # Added for agents to see classified errors - - } - user_message_content = "Project State:\n" + json.dumps(relevant_state, indent=2) + user_message_content = "Project State:\n" + json.dumps(user_input_state, indent=2) model_to_use = model_name @@ -274,28 +206,21 @@ def run_agent(client, model_name, system_prompt_template, user_input_state, conf Content(role="user", parts=[Part(text=system_prompt + "\n\n" + user_message_content)]) ] - # print("Sending prompt to Gemini:\n---\n" + messages[0].parts[0].text + "\n---") # Debugging prompt content - response = client.models.generate_content( model=model_to_use, contents=messages, # Pass the list of Content objects config=config ) - if not response.candidates or not response.candidates[0].content or not response.candidates[0].content.parts: + if not response.candidates or not response.candidates[0].content: print("Agent returned no candidates or empty content.") if response.prompt_feedback and response.prompt_feedback.block_reason: block_reason = response.prompt_feedback.block_reason print(f"Prompt was blocked. Reason: {block_reason}") return f"ERROR: Agent response blocked by safety filters. Reason: {block_reason.name}" - # Check for empty parts list even if content exists - if response.candidates and response.candidates[0].content and not response.candidates[0].content.parts: - return "ERROR: Agent returned content with no parts." return f"ERROR: Agent returned no response content." - # Concatenate parts to get the full response text - response_text = "".join([part.text for part in response.candidates[0].content.parts if hasattr(part, 'text')]) - + response_text = "".join([part.text for part in response.candidates[0].content.parts]) # Log the raw agent output for debugging print(f"--- Raw Agent Response --- ({model_to_use})") @@ -310,12 +235,7 @@ def run_agent(client, model_name, system_prompt_template, user_input_state, conf if hasattr(e, 'response') and e.response is not None: try: error_json = e.response.json() - # Check if error_json is a dict and has 'error' key - if isinstance(error_json, dict) and 'error' in error_json: - error_details = json.dumps(error_json['error'], indent=2) - else: - # Fallback to full json or text - error_details = json.dumps(error_json, indent=2) if isinstance(error_json, dict) else e.response.text + error_details = json.dumps(error_json, indent=2) except: try: error_details = e.response.text @@ -324,50 +244,47 @@ def run_agent(client, model_name, system_prompt_template, user_input_state, conf return f"ERROR: Agent failed - {error_details}" - # --- AGENT FUNCTIONS (called by Orchestrator) --- # These functions now expect only the response text from run_agent -# They modify the project_state dictionary directly def run_planner(client, project_state, config): print("Orchestrator: Running Planner Agent...") - # Pass relevant state keys input_state_for_planner = { "requirements": project_state['requirements'], "sdk_choice": project_state['sdk_choice'], "main_app_file": project_state['main_app_file'], - "files": project_state['files'], # Pass existing files state - # Other keys are not strictly needed for planning but passed via run_agent's filter + "files": project_state['files'] } response_text = run_agent( client=client, model_name="gemini-2.5-flash-preview-04-17", system_prompt_template=SYSTEM_ARCHITECT, - user_input_state=project_state, # Pass full state, run_agent filters + user_input_state=input_state_for_planner, config=config, ) if response_text.startswith("ERROR:"): project_state['status_message'] = response_text - # Add error to feedback for debugging - project_state['feedback'] = project_state.get('feedback', '') + "\n\nPlanner Failed: " + response_text - project_state['chat_history'].append({"role": "assistant", "content": f"⚠️ Planner failed: {response_text}"}) return False project_state['plan'] = response_text print("Orchestrator: Planner Output Received.") project_state['status_message'] = "Planning complete." - # Add plan to history if it's new or updated - plan_message = f"**Plan:**\n{project_state['plan']}" - if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != plan_message.strip(): - project_state['chat_history'].append({"role": "assistant", "content": plan_message}) + project_state['chat_history'].append({"role": "assistant", "content": f"**Plan:**\n{project_state['plan']}"}) return True # Modified: Implemented more flexible code block parsing and logging def run_codegen(client, project_state, config): print(f"Orchestrator: Running Code-Gen Agent for task: {project_state['current_task']}...") - # Pass relevant state keys - run_agent filters - input_state_for_codegen = project_state + input_state_for_codegen = { + "current_task": project_state['current_task'], + "requirements": project_state['requirements'], + "plan": project_state['plan'], + "files": project_state['files'], + "feedback": project_state['feedback'] or 'None', + "sdk_choice": project_state['sdk_choice'], + "main_app_file": project_state['main_app_file'] + } response_text = run_agent( client=client, model_name="gemini-2.5-flash-preview-04-17", @@ -378,8 +295,8 @@ def run_codegen(client, project_state, config): if response_text.startswith("ERROR:"): project_state['status_message'] = response_text - project_state['feedback'] = project_state.get('feedback', '') + "\n\nCode-Gen Failed: " + response_text - project_state['chat_history'].append({"role": "assistant", "content": f"⚠️ Code-Gen failed: {response_text}"}) + project_state['feedback'] = project_state.get('feedback', '') + "\n\n" + response_text + project_state['chat_history'].append({"role": "assistant", "content": response_text}) return False files_updated = {} @@ -388,41 +305,31 @@ def run_codegen(client, project_state, config): # Modified Regex: Catches ```lang filename\ncode``` OR `filename`\n```lang\ncode``` # Using named groups and DOTALL flag pattern = re.compile( - # Case 1: ```lang filename\ncode``` or ```filename\ncode``` (optional lang) - r"```(?:\w+)?\s*(?P[\w\-/]+?\.\w+)\s*\n(?P[\s\S]+?)```" - # Case 2: `filename`\n```lang\ncode``` or `filename`\n```\ncode``` (optional lang) - r"|`(?P[\w\-/]+?\.\w+)`\s*\n```(?:\w*)\n(?P[\s\S]+?)```", # Corrected group name to + r"```(?:\w+)?\s*(?P[\w\-/]+?\.\w+)\s*\n(?P[\s\S]+?)```" # Case 1: ```lang filename\ncode``` + r"|`(?P[\w\-/]+?\.\w+)`\s*\n```(?:\w*)\n(?P[\s\S]+?)```", # Case 2: `filename`\n```lang\ncode``` re.DOTALL # Allow . to match newlines ) matches = pattern.finditer(response_text) - # Check if any matches were found at all + # Check if any matches were found at all by iterating once match_found = False extracted_blocks = [] for m in matches: match_found = True filename = m.group('fname1') or m.group('fname2') - # Corrected: Use 'code1' or 'code2' - code_content = m.group('code1') if m.group('code1') is not None else m.group('code2') - extracted_blocks.append((filename, code_content, m.start(), m.end())) # Store start/end for context + # Corrected: Use 'code' for group 2 + code_content = m.group('code1') if m.group('code1') is not None else m.group('code') + extracted_blocks.append((filename, code_content, m.start())) # Process extracted blocks - if not match_found: - print("Code-Gen Agent did not output any code blocks matching the expected format.") - parse_error_msg = "ERROR: Code-Gen Agent failed to output code blocks in the expected `filename`\\n```code``` or ```lang filename\\ncode``` format." - project_state['status_message'] = parse_error_msg - project_state['feedback'] = project_state.get('feedback', '') + "\n\n" + parse_error_msg + "\nRaw Agent Response (no matching blocks detected):\n" + response_text[:2000] + "..." - project_state['chat_history'].append({"role": "assistant", "content": parse_error_msg + "\nSee Debug Feedback for raw response."}) - return False # Indicate failure - - for filename, code_content, start_pos, end_pos in extracted_blocks: + for filename, code_content, start_pos in extracted_blocks: if not filename: - syntax_errors.append(f"Code block found near position {start_pos}-{end_pos} without a clearly parsed filename.") + syntax_errors.append(f"Code block found near position {start_pos} without a clearly parsed filename.") continue if code_content is None: - syntax_errors.append(f"Empty code content parsed for file `{filename}` near position {start_pos}-{end_pos}.") + syntax_errors.append(f"Empty code content parsed for file `{filename}` near position {start_pos}.") continue files_updated[filename] = code_content.strip() @@ -432,13 +339,22 @@ def run_codegen(client, project_state, config): try: compile(code_content, filename, "exec") except SyntaxError as e: - syntax_errors.append(f"Syntax Error in generated `{filename}`: {e} (near position {start_pos}-{end_pos})") + syntax_errors.append(f"Syntax Error in generated `{filename}` near position {start_pos}: {e}") print(f"Syntax Error in generated `{filename}`: {e}") except Exception as e: - syntax_errors.append(f"Unexpected error during syntax check for `{filename}`: {e} (near position {start_pos}-{end_pos})") + syntax_errors.append(f"Unexpected error during syntax check for `{filename}` near position {start_pos}: {e}") print(f"Unexpected error during syntax check for `{filename}`: {e}") - # Handle cases where blocks were found, but none yielded valid files + # Handle cases where no code blocks matched the pattern + if not match_found: + print("Code-Gen Agent did not output any code blocks matching the expected format.") + parse_error_msg = "ERROR: Code-Gen Agent failed to output code blocks in the expected `filename`\\n```code``` or ```lang filename\\ncode``` format." + project_state['status_message'] = parse_error_msg + project_state['feedback'] = project_state.get('feedback', '') + "\n\n" + parse_error_msg + "\nRaw Agent Response (no matching blocks detected):\n" + response_text[:2000] + "..." + project_state['chat_history'].append({"role": "assistant", "content": parse_error_msg + "\nSee Debug Feedback for raw response."}) + return False # Indicate failure + + # Handle cases where blocks were found, but none yielded valid files, or only syntax errors occurred if not files_updated and not syntax_errors: print("Code-Gen Agent outputted text with blocks, but no valid files were parsed.") parse_error_msg = "ERROR: Code-Gen Agent outputted text with blocks, but no valid filenames were parsed from them." @@ -462,18 +378,23 @@ def run_codegen(client, project_state, config): print(f"Orchestrator: Code-Gen Agent updated files: {list(files_updated.keys())}") # Add the generated/updated code content snippet to the chat history for visibility - # Limit size for chat history - code_summary = "\n".join([f"`{fn}`:\n```python\n{code[:400]}{'...' if len(code) > 400 else ''}\n```" for fn, code in files_updated.items()]) - if code_summary: - project_state['chat_history'].append({"role": "assistant", "content": f"**Code Generated/Updated:**\n\n{code_summary}"}) - project_state['status_message'] = f"Code generated/updated: {list(files_updated.keys())}. Ready to push." + code_summary = "\n".join([f"`{fn}`:\n```python\n{code[:500]}{'...' if len(code) > 500 else ''}\n```" for fn, code in files_updated.items()]) + project_state['chat_history'].append({"role": "assistant", "content": f"**Code Generated/Updated:**\n\n{code_summary}"}) + project_state['status_message'] = f"Code generated/updated: {list(files_updated.keys())}" return True def run_debugger(client, project_state, config): print("Orchestrator: Running Debug Agent...") - # Pass relevant state keys - run_agent filters - input_state_for_debugger = project_state + input_state_for_debugger = { + "requirements": project_state['requirements'], + "plan": project_state['plan'], + "files": project_state['files'], + "build_logs": project_state['logs'].get('build', 'No build logs.'), + "run_logs": project_state['logs'].get('run', 'No run logs.'), + "iframe_status": 'Responding OK' if project_state.get('iframe_ok', False) else 'Not responding or check failed.', + "error_types_found": classify_errors(project_state['logs'].get('build', '') + '\n' + project_state['logs'].get('run', '')) + } response_text = run_agent( client=client, model_name="gemini-2.5-flash-preview-04-17", @@ -484,766 +405,410 @@ def run_debugger(client, project_state, config): if response_text.startswith("ERROR:"): project_state['status_message'] = response_text - project_state['feedback'] = project_state.get('feedback', '') + "\n\nDebug Agent Failed: " + response_text - project_state['chat_history'].append({"role": "assistant", "content": f"⚠️ Debug Agent failed: {response_text}"}) + project_state['feedback'] = project_state.get('feedback', '') + "\n\n" + response_text + project_state['chat_history'].append({"role": "assistant", "content": response_text}) return False project_state['feedback'] = response_text print("Orchestrator: Debug Agent Feedback Received.") project_state['status_message'] = "Debug feedback generated." - # Add feedback to history if it's new or updated - feedback_message = f"**Debug Feedback:**\n{project_state['feedback']}" - if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != feedback_message.strip(): - project_state['chat_history'].append({"role": "assistant", "content": feedback_message}) - + project_state['chat_history'].append({"role": "assistant", "content": f"**Debug Feedback:**\n{project_state['feedback']}"}) return True # --- MAIN ORCHESTRATION LOGIC --- -def orchestrate_development(client, project_state: dict, config, oauth_token_token: str | None): +def orchestrate_development(client, project_state, config, oauth_token_token): """Manages the overall development workflow.""" - # This function will run one step of the orchestration loop - # It assumes it's called repeatedly by the Gradio handler until status is not 'In Progress' - - if project_state.get('current_task') is None: # Initial call state - project_state['current_task'] = 'START' - project_state['status'] = 'In Progress' - project_state['status_message'] = 'Initializing...' - project_state['attempt_count'] = 0 - project_state['logs'] = {'build': '', 'run': ''} - project_state['feedback'] = '' - project_state['plan'] = '' - project_state['files'] = {} - project_state['iframe_ok'] = False - project_state['log_error_state'] = 'None' - # Initial message handled in handle_user_message now - # project_state['chat_history'].append({"role": "assistant", "content": "Project initialized. Starting development team."}) - return # Exit after initialization, next call will run the first task - - if project_state['status'] != 'In Progress': - # If already finished or failed, do nothing until reset - print(f"Orchestration loop called, but status is {project_state['status']}. Exiting.") - return - - - print(f"\n--- Attempt {project_state['attempt_count'] + 1} ---") - print(f"Current Task: {project_state['current_task']}") - current_task = project_state['current_task'] - - task_message = f"➡️ Task: {current_task}" - # Only add task message if it's different from the last one - if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != task_message.strip(): - project_state['chat_history'].append({"role": "assistant", "content": task_message}) - - # step_successful is managed within each task block - - if current_task == 'START': - # This state should only be hit once for initialization, handled above. - # If somehow reached here, transition to PLANNING. - print("Orchestrator: START task unexpectedly reached in main loop. Transitioning to PLANNING.") - project_state['current_task'] = 'PLANNING' - project_state['status_message'] = "Transitioning to Planning." - - - elif current_task == 'PLANNING': - step_successful = run_planner(client, project_state, config) - if step_successful: - project_state['current_task'] = 'CODING - Initial Implementation' - else: - # Planning failed (likely Gemini API error), transition to FAILED - project_state['status'] = 'Failed' - project_state['current_task'] = 'FINISHED' - - - elif current_task.startswith('CODING'): - # Ensure minimum files exist before coding - if project_state.get('main_app_file') and project_state['main_app_file'] not in project_state['files']: - print(f"Adding initial stub for {project_state['main_app_file']}...") - project_state['files'][project_state['main_app_file']] = f"# Initial {project_state.get('sdk_choice', 'unknown')} app file\n" - if project_state.get('sdk_choice') == 'gradio': - 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" - elif project_state.get('sdk_choice') == 'streamlit': - 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" - - if 'requirements.txt' not in project_state['files']: - print("Adding initial requirements.txt stub...") - # FIX: Specify gradio>=4.0.0 to get gr.Interval/gr.Timer - req_content = "pandas\n" + (("streamlit\n") if project_state.get('sdk_choice')=="streamlit" else ("gradio>=4.0.0\n" if project_state.get('sdk_choice')=="gradio" else "")) + "google-generativeai\nhuggingface-hub\n" - project_state['files']['requirements.txt'] = req_content - - if 'README.md' not in project_state['files']: - print("Adding initial README.md stub...") - readme_content = f"""--- -title: {project_state.get('repo_id', 'my-app')} + if project_state['current_task'] == 'START': + project_state['current_task'] = 'PLANNING' + project_state['status_message'] = "Starting project: Initializing and moving to Planning." + project_state['chat_history'].append({"role": "assistant", "content": "Project initialized. Starting development team."}) + + + while project_state['status'] == 'In Progress' and project_state['attempt_count'] < 7: + print(f"\n--- Attempt {project_state['attempt_count'] + 1} ---") + print(f"Current Task: {project_state['current_task']}") + current_task = project_state['current_task'] + + task_message = f"➡️ Task: {current_task}" + if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != task_message.strip(): + project_state['chat_history'].append({"role": "assistant", "content": task_message}) + + step_successful = True + + if current_task == 'PLANNING': + step_successful = run_planner(client, project_state, config) + if step_successful: + project_state['current_task'] = 'CODING - Initial Implementation' + if project_state['plan'] and not any("**Plan:**" in msg.get('content', '') for msg in project_state['chat_history']): + project_state['chat_history'].append({"role": "assistant", "content": f"**Plan:**\n{project_state['plan']}"}) + else: + project_state['current_task'] = 'FAILED' + + + elif current_task.startswith('CODING'): + if project_state['main_app_file'] not in project_state['files']: + print(f"Adding initial stub for {project_state['main_app_file']}...") + project_state['files'][project_state['main_app_file']] = f"# Initial {project_state['sdk_choice']} app file\n" + if project_state['sdk_choice'] == 'gradio': + 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" + elif project_state['sdk_choice'] == 'streamlit': + 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" + + if 'requirements.txt' not in project_state['files']: + print("Adding initial requirements.txt stub...") + # FIX: Specify gradio>=4.0.0 to get gr.Interval/gr.Timer + req_content = "pandas\n" + ("streamlit\n" if project_state['sdk_choice']=="streamlit" else "gradio>=4.0.0\n") + "google-generativeai\nhuggingface-hub\n" + project_state['files']['requirements.txt'] = req_content + + if 'README.md' not in project_state['files']: + print("Adding initial README.md stub...") + readme_content = f"""--- +title: {project_state['repo_id']} emoji: 🐢 -sdk: {project_state.get('sdk_choice', 'unknown')} -sdk_version: {project_state.get('sdk_version', 'unknown')} -app_file: {project_state.get('main_app_file', 'app.py')} +sdk: {project_state['sdk_choice']} +sdk_version: {project_state['sdk_version']} +app_file: {project_state['main_app_file']} pinned: false --- -# {project_state.get('repo_id', 'my-app')} +# {project_state['repo_id']} This is an auto-generated HF Space. -**Requirements:** {project_state.get('requirements', 'No requirements provided.')} +**Requirements:** {project_state['requirements']} **Plan:** -{project_state.get('plan', 'No plan generated yet.')} +{project_state['plan']} """ - project_state['files']['README.md'] = readme_content + project_state['files']['README.md'] = readme_content + + step_successful = run_codegen(client, project_state, config) + if step_successful: + project_state['current_task'] = 'PUSHING' + else: + print("Code-Gen step failed. Moving to Debugging.") + project_state['current_task'] = 'DEBUGGING' - step_successful = run_codegen(client, project_state, config) - if step_successful: - project_state['current_task'] = 'PUSHING' - # Clear previous logs and feedback before pushing a new version - project_state['logs'] = {'build': '', 'run': ''} - project_state['feedback'] = '' # Clear feedback from previous debug cycle - project_state['iframe_ok'] = False - project_state['log_error_state'] = 'None' # Reset log error state - # Do NOT reset attempt count here, it's for total cycles + elif current_task == 'PUSHING': + try: + create_repo(repo_id=project_state['repo_id'], token=oauth_token_token, # Token is needed for pushing! + exist_ok=True, repo_type="space", space_sdk=project_state['sdk_choice']) + + files_to_push = { + fn: content + for fn, content in project_state['files'].items() + if fn and fn.strip() + } + print(f"Attempting to push {len(files_to_push)} valid files to {project_state['repo_id']}...") + + for fn, content in files_to_push.items(): + dirpath = os.path.dirname(fn) + if dirpath: + os.makedirs(dirpath, exist_ok=True) + filepath = os.path.join(os.getcwd(), fn) + with open(filepath, "w") as f: + f.write(content) + upload_file( + path_or_fileobj=filepath, path_in_repo=fn, + repo_id=project_state['repo_id'], token=oauth_token_token, # Token is needed for pushing! + repo_type="space" + ) + os.remove(filepath) + + print(f"Pushed {len(files_to_push)} files to {project_state['repo_id']}") + project_state['status_message'] = f"Pushed code to HF Space **{project_state['repo_id']}**. Waiting for build..." + project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) + project_state['current_task'] = 'LOGGING' - else: - print("Code-Gen step failed (likely syntax error or agent error). Moving to Debugging.") - project_state['current_task'] = 'DEBUGGING' # Debugging will analyze the CodeGen failure or agent error - - - elif current_task == 'PUSHING': - if oauth_token_token is None: - project_state['status'] = 'Failed' - project_state['status_message'] = "ERROR: Cannot push without Hugging Face token." - project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) - project_state['current_task'] = 'FINISHED' - print(project_state['status_message']) - return # Exit early on critical error - - try: - repo_id = project_state.get('repo_id') - if not repo_id: - raise ValueError("Repository ID is not set in project state.") - - project_state['status_message'] = f"Creating/Updating HF Space **{repo_id}**..." - # Only add if it's a new message - if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != project_state['status_message'].strip(): + except Exception as e: + step_successful = False + project_state['status'] = 'Failed' + project_state['status_message'] = f"ERROR: Failed to push to HF Space: {e}" project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) - print(project_state['status_message']) + print(project_state['status_message']) + project_state['current_task'] = 'FINISHED' - create_repo(repo_id=repo_id, token=oauth_token_token, - exist_ok=True, repo_type="space", space_sdk=project_state.get('sdk_choice', 'unknown')) - - files_to_push = { - fn: content - for fn, content in project_state.get('files', {}).items() - if fn and fn.strip() # Ensure valid filenames - } - print(f"Attempting to push {len(files_to_push)} valid files to {repo_id}...") - - # Use a temporary directory to avoid cluttering the working directory - import tempfile - step_successful = True # Assume success initially for pushing - with tempfile.TemporaryDirectory() as tmpdir: - temp_file_paths = [] - for fn, content in files_to_push.items(): - # Create necessary subdirectories within the temp dir - full_path = os.path.join(tmpdir, fn) - os.makedirs(os.path.dirname(full_path), exist_ok=True) - try: - with open(full_path, "w", encoding='utf-8') as f: # Specify encoding - f.write(content) - temp_file_paths.append(full_path) - except Exception as e: - print(f"Error writing temporary file {fn}: {e}") - # Decide how to handle: skip file? Fail push? For now, mark push as failed. - step_successful = False # Mark push as failed if any file fails to write - project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError writing file {fn} for push: {e}" - project_state['status_message'] = f"ERROR: Failed to write {fn} locally for push. Details in feedback." - - - if not temp_file_paths and len(files_to_push) > 0: # If files were supposed to be pushed but none were written - print("No valid files prepared for push.") - project_state['status_message'] = "ERROR: No files were prepared for push after Code-Gen. Project Failed." - project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) - project_state['status'] = 'Failed' - project_state['current_task'] = 'FINISHED' - step_successful = False - # No need to return here, let the rest of the block handle the failure state - - - # Upload files one by one IF step_successful is still True after writing - if step_successful: - for fn in files_to_push.keys(): # Iterate over keys to get path_in_repo - filepath = os.path.join(tmpdir, fn) - # Only try to upload if temp file was successfully written and overall push isn't already failed - if filepath in temp_file_paths and step_successful: - try: - upload_file( - path_or_fileobj=filepath, path_in_repo=fn, - repo_id=repo_id, token=oauth_token_token, - repo_type="space" - ) - print(f"Uploaded: {fn}") - except Exception as e: - print(f"Error uploading file {fn}: {e}") - step_successful = False # Mark push as failed if any upload fails - project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError uploading {fn}: {e}" - project_state['status_message'] = f"ERROR: Failed to upload {fn}. Details in feedback." - # Continue trying other files, but mark overall step as failed - elif step_successful: # If step_successful is True but file wasn't written - print(f"Skipping upload for {fn} as it failed to write locally.") - # This case is already covered by the write loop failing step_successful + elif current_task == 'LOGGING': + time.sleep(5) + wait_time = 5 + max_log_wait = 150 + elapsed_log_wait = 0 + logs_fetched = False + iframe_checked = False + + status_logging_message = "Fetching logs and checking iframe..." + if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != status_logging_message.strip(): + project_state['chat_history'].append({"role": "assistant", "content": status_logging_message}) + project_state['status_message'] = status_logging_message + + print("Starting orchestration-based log/iframe check loop...") + while elapsed_log_wait < max_log_wait: + try: + # Modified: Call fetch_logs without the token argument (uses HF_TOKEN env var) + current_build_logs = fetch_logs(project_state['repo_id'], "build") + current_run_logs = fetch_logs(project_state['repo_id'], "run") + current_iframe_ok = check_iframe(project_state['iframe_url']) + + project_state['logs']['build'] = current_build_logs + project_state['logs']['run'] = current_run_logs + project_state['iframe_ok'] = current_iframe_ok + logs_fetched = True + iframe_checked = True # If we checked, record it + + print(f"Orchestration Log/Iframe check at {elapsed_log_wait}s. Build logs len: {len(current_build_logs)}, Run logs len: {len(current_run_logs)}, Iframe OK: {current_iframe_ok}") + + if project_state['iframe_ok'] or \ + "ERROR" in current_build_logs.upper() or "FATAL" in current_build_logs.upper() or \ + elapsed_log_wait >= max_log_wait - wait_time or \ + ("Building" in current_build_logs or len(current_build_logs) > 100) or \ + len(current_run_logs) > 0: + print("Proceeding to Debugging based on logs/iframe status.") + break + else: + print(f"Logs or iframe not ready. Waiting {wait_time}s...") + time.sleep(wait_time) + elapsed_log_wait += wait_time + wait_time = min(wait_time * 1.5, 20) + + except Exception as e: + print(f"Error during orchestration-based log fetching or iframe check: {e}. Will retry.") + time.sleep(wait_time) + elapsed_log_wait += wait_time + wait_time = min(wait_time * 1.5, 20) + + if logs_fetched or iframe_checked: + project_state['status_message'] = "Logs fetched and iframe checked (or timeout reached)." + else: + project_state['status_message'] = "Warning: Could not fetch logs or check iframe status within timeout." + + project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) + project_state['current_task'] = 'DEBUGGING' - if step_successful: - project_state['status_message'] = f"Pushed code to HF Space **{repo_id}**. Build triggered. Waiting for build and logs..." - if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != project_state['status_message'].strip(): - project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) - project_state['current_task'] = 'LOGGING' - # Logs, iframe_ok, log_error_state were reset before push, will be updated by poller + elif current_task == 'DEBUGGING': + step_successful = run_debugger(client, project_state, config) + + if step_successful: + feedback = project_state['feedback'] + iframe_ok = project_state.get('iframe_ok', False) + error_types = classify_errors(project_state['logs'].get('build', '') + '\n' + project_state['logs'].get('run', '')) + + print(f"Debug Analysis - Feedback: {feedback[:100]}... | Iframe OK: {iframe_ok} | Errors: {error_types}") + + is_complete = ("All clear. Project appears complete." in feedback) or \ + (iframe_ok and error_types == "none" and "ERROR" not in feedback.upper() and len(project_state['logs'].get('run', '')) > 10) + + if is_complete: + project_state['status'] = 'Complete' + project_state['current_task'] = 'FINISHED' + project_state['status_message'] = "Debug Agent reports clear. Project appears complete." + elif project_state['attempt_count'] >= 6: + project_state['status'] = 'Failed' + project_state['current_task'] = 'FINISHED' + project_state['status_message'] = f"Max attempts ({project_state['attempt_count']+1}/7) reached after debugging. Project failed." + else: + project_state['current_task'] = 'CODING - Addressing Feedback' + project_state['status_message'] = "Debug Agent found issues. Returning to Coding phase to address feedback." + project_state['attempt_count'] += 1 + backoff_wait = min(project_state['attempt_count'] * 5, 30) + print(f"Waiting {backoff_wait} seconds before next coding attempt...") + time.sleep(backoff_wait) else: - # If any write or upload failed, the push step failed project_state['status'] = 'Failed' project_state['current_task'] = 'FINISHED' - project_state['status_message'] = project_state.get('status_message', f"ERROR: One or more files failed to process or upload to HF Space {repo_id}. See feedback.") - # Ensure the error message is added to chat history if not already there from writing/uploading - if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != project_state['status_message'].strip(): - project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) - print(project_state['status_message']) + elif current_task == 'FINISHED': + pass - except Exception as e: - # Catch errors during repo creation or temp directory handling + else: step_successful = False project_state['status'] = 'Failed' - project_state['status_message'] = f"ERROR: Critical error during PUSHING phase: {e}" + project_state['status_message'] = f"ERROR: Orchestrator entered an unknown task state: {current_task}" project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) print(project_state['status_message']) project_state['current_task'] = 'FINISHED' - elif current_task == 'LOGGING': - # This task primarily waits for the poller to update logs and iframe_ok. - # It checks the state updated by the poller to decide the next step. - # The poller (_update_logs_and_preview) runs periodically and modifies project_state in the background. - - print("Orchestrator: Waiting in LOGGING state. Checking state updated by poller...") - - build_logs = project_state['logs'].get('build', '') - run_logs = project_state['logs'].get('run', '') - iframe_ok = project_state.get('iframe_ok', False) - log_error_state = project_state.get('log_error_state', 'None') - current_status_message = project_state.get('status_message', "Waiting for build/run logs and iframe...") - - # Update status message to reflect waiting state and attempt count - waiting_message_prefix = "Waiting for build/run logs and iframe" - if waiting_message_prefix not in current_status_message: - project_state['status_message'] = f"{waiting_message_prefix} ({project_state['attempt_count']+1}/7 attempts)..." - # Update the last message in chat history if it was a simple task message - if project_state['chat_history'] and project_state['chat_history'][-1].get('content', '').strip().startswith("➡️ Task: LOGGING"): - project_state['chat_history'][-1] = {"role": "assistant", "content": project_state['status_message']} - elif not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != project_state['status_message'].strip(): - project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) - - - # Define conditions to move out of LOGGING - ready_to_debug = False - - if iframe_ok: - print("Orchestrator: Iframe is OK. Moving to Debugging (likely success).") - ready_to_debug = True - project_state['status_message'] = "Iframe check passed. Analyzing build/run logs." - - # Check for explicit log fetching errors - elif log_error_state not in ['None', 'No Logs Yet', 'Iframe Not Ready']: # Any error state that is NOT just 'waiting' - print(f"Orchestrator: Detected log fetching error state: {log_error_state}. Moving to Debugging.") - ready_to_debug = True - project_state['status_message'] = f"Log fetching encountered an error ({log_error_state}). Moving to Debugging." - - # Check for significant logs that indicate build/run started/failed - # Avoid triggering debug if it's just the 'No logs found yet' message - elif not (build_logs.strip() == "No build logs found yet." and run_logs.strip() == "No run logs found yet."): - # Check for actual log content or error indicators within logs - if len(build_logs.strip()) > 50 or len(run_logs.strip()) > 10 or "ERROR" in build_logs.upper() or "FATAL" in build_logs.upper() or "ERROR" in run_logs.upper() or "FATAL" in run_logs.upper(): - print("Orchestrator: Detected significant logs. Moving to Debugging.") - ready_to_debug = True - project_state['status_message'] = "Significant logs detected. Analyzing build/run logs." - - - # Timeout check - Use attempt count as a global timeout for the entire process, - # but also use it to limit time spent *just* in LOGGING if no progress is detected. - if project_state['attempt_count'] >= 6: # Use attempt count as a global timeout trigger - print("Orchestrator: Max attempts reached in LOGGING/overall. Forcing move to Debugging.") - ready_to_debug = True # Force debug to get final feedback before failing - - - if ready_to_debug: - project_state['current_task'] = 'DEBUGGING' - # Add a small delay before debugging starts, giving UI time to update logs - # time.sleep(2) # Moved implicit wait to poller tick - else: - # If not ready to debug, stay in LOGGING. The orchestrator loop will naturally pause between calls. - # Attempt count is incremented globally at the end of the loop if not finished/failed. - pass # Stay in LOGGING, loop will repeat - - - # This task itself doesn't 'fail' based on logs, it transitions based on analysis - # Failure from LOGGING is handled in DEBUGGING or by max attempts. - - - elif current_task == 'DEBUGGING': - step_successful = run_debugger(client, project_state, config) - - if step_successful: - feedback = project_state.get('feedback', '') - iframe_ok = project_state.get('iframe_ok', False) - error_types = classify_errors(project_state['logs'].get('build', '') + '\n' + project_state['logs'].get('run', '')) - log_error_state = project_state.get('log_error_state', 'None') - - print(f"Debug Analysis - Feedback: {feedback[:100]}... | Iframe OK: {iframe_ok} | Errors: {error_types} | Log Error State: {log_error_state}") - - # Define completion conditions - is_complete = False - if "All clear. Project appears complete." in feedback: - is_complete = True # Agent explicitly says it's done - elif iframe_ok and log_error_state == 'None' and error_types == "none" and "ERROR" not in feedback.upper(): - # Check if iframe is okay AND no log fetching error AND logs have no classified errors AND debugger feedback isn't negative - # Also check if the run logs have *some* content, indicating it likely ran - if len(project_state['logs'].get('run', '').strip()) > 20: # Arbitrary minimum length for 'meaningful' run logs - is_complete = True - print("Debug Analysis: Conditions for COMPLETE met.") - else: - print("Debug Analysis: Conditions for COMPLETE not met.") - - - # Define failure conditions - is_failed = False - if project_state['attempt_count'] >= 6: # Global attempt timeout reached (0-indexed, so attempt 7 is >= 6) - is_failed = True - project_state['status_message'] = f"Max attempts ({project_state['attempt_count']+1}/7) reached. Project failed." - print(f"Debug Analysis: Failed due to max attempts ({project_state['attempt_count']+1}).") - elif log_error_state in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Request Error', 'Unexpected Error', 'Poller Unexpected Error', 'HTTP Error']: # Critical log fetching errors - is_failed = True - project_state['status_message'] = f"Project failed due to critical log or iframe fetching error: {log_error_state}" - print(f"Debug Analysis: Failed due to critical log error: {log_error_state}.") - elif ("ERROR" in feedback.upper() or error_types != "none") and project_state['attempt_count'] >= 4: - # If debugger finds errors or classified errors exist after several attempts (e.g., attempts 5, 6, 7) - is_failed = True - project_state['status_message'] = "Project failed: Debugger consistently reported errors or classified errors persist." - print(f"Debug Analysis: Failed due to persistent errors after {project_state['attempt_count']+1} attempts.") - - - if is_complete: - project_state['status'] = 'Complete' - project_state['current_task'] = 'FINISHED' - project_state['status_message'] = project_state.get('status_message', "Project appears complete based on debug analysis and iframe status.") - elif is_failed: + if not step_successful and project_state['status'] == 'In Progress': + print(f"Orchestration step '{current_task}' failed, but status is still 'In Progress'. Transitioning to DEBUGGING or FAILED.") + if project_state['attempt_count'] >= 6: project_state['status'] = 'Failed' + project_state['status_message'] = project_state.get('status_message', f'An unexpected error caused task failure: {current_task}') + project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) project_state['current_task'] = 'FINISHED' - project_state['status_message'] = project_state.get('status_message', "Project failed based on debug analysis or attempt limit.") else: - # If not complete and not failed, return to coding to apply feedback - project_state['current_task'] = 'CODING - Addressing Feedback' - project_state['status_message'] = "Debug Agent found issues. Returning to Coding phase to address feedback." - # Attempt count is incremented at the end of the loop *before* the next cycle. - print(f"Debug Analysis: Issues found. Returning to CODING. Next attempt: {project_state['attempt_count']+2}.") - # No sleep needed here, the loop structure and poller handle timing + project_state['current_task'] = 'DEBUGGING' - else: - # Debugger agent call failed (e.g., API error) - project_state['status'] = 'Failed' - project_state['current_task'] = 'FINISHED' - # Status message set inside run_debugger, contains the error. - - - elif current_task == 'FINISHED': - pass # Stay in finished state - - else: - # Should not happen - unknown state - # step_successful = False # Force failure path - not needed as status is set - project_state['status'] = 'Failed' - project_state['status_message'] = f"ERROR: Orchestrator entered an unknown task state: {current_task}" - project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) - print(project_state['status_message']) - project_state['current_task'] = 'FINISHED' - - - # Increment attempt count at the end of a cycle *if* not finished or failed if project_state['status'] == 'In Progress': - project_state['attempt_count'] += 1 - print(f"Orchestration loop completed one cycle. Incrementing attempt count to {project_state['attempt_count']}.") - # Add a small delay between orchestration steps (optional, the poller also adds implicit delays) - # time.sleep(1) # Adding a 1-second pause between steps - - # Final outcome message when status changes to Complete or Failed - # Only add if it's the final state and the message hasn't been added - if project_state['status'] != 'In Progress' and not any(msg.get('content', '').strip().startswith("**Project Outcome:") for msg in project_state['chat_history']): - final_outcome_message = f"**Project Outcome:** {project_state['status']} - {project_state.get('status_message', 'No specific outcome message.')}" - project_state['chat_history'].append({"role": "assistant", "content": final_outcome_message}) - - if project_state['status'] == 'Complete': - completion_message = f"✅ Application deployed successfully (likely)! Check the preview above: [https://huggingface.co/spaces/{project_state.get('repo_id', '')}](https://huggingface.co/spaces/{project_state.get('repo_id', '')})" - if not any(msg.get('content', '').strip().startswith("✅ Application deployed successfully") for msg in project_state['chat_history']): - project_state['chat_history'].append({"role": "assistant", "content": completion_message}) - elif project_state['status'] == 'Failed': - failure_message = "❌ Project failed to complete. Review logs and feedback for details." - if not any(msg.get('content', '').strip().startswith("❌ Project failed to complete.") for msg in project_state['chat_history']): - project_state['chat_history'].append({"role": "assistant", "content": failure_message}) - - - # orchestrate_development function doesn't return UI outputs directly anymore. - # It updates the project_state dictionary, which is a gr.State, - # and the UI outputs are updated by the poller (_update_logs_and_preview) - # or by the main handle_user_message function after calling orchestrate_development. + project_state['status'] = 'Failed' + project_state['status_message'] = project_state.get('status_message', 'Orchestration loop exited unexpectedly.') + + final_outcome_message = f"**Project Outcome:** {project_state['status']} - {project_state['status_message']}" + if not project_state['chat_history'] or not project_state['chat_history'][-1].get('content', '').strip().startswith("**Project Outcome:"): + project_state['chat_history'].append({"role": "assistant", "content": final_outcome_message}) + + if project_state['status'] == 'Complete': + completion_message = f"✅ Application deployed successfully (likely)! Check the preview above: [https://huggingface.co/spaces/{project_state['repo_id']}](https://huggingface.co/spaces/{project_state['repo_id']})" + if not project_state['chat_history'] or not project_state['chat_history'][-1].get('content', '').strip().startswith("✅ Application deployed successfully"): + project_state['chat_history'].append({"role": "assistant", "content": completion_message}) + elif project_state['status'] == 'Failed': + failure_message = "❌ Project failed to complete. Review logs and feedback for details." + if not project_state['chat_history'] or not project_state['chat_history'][-1].get('content', '').strip().startswith("❌ Project failed to complete."): + project_state['chat_history'].append({"role": "assistant", "content": failure_message}) + return ( + project_state['chat_history'], + project_state['logs'].get('build', 'No build logs.'), + project_state['logs'].get('run', 'No run logs.'), + (f'' + + ("" if project_state.get('iframe_ok') else "

⚠️ iframe not responding or check failed.

")), + project_state['status_message'] + ) # --- Create a unified updater function (for the poller) --- # Place this function *before* the gr.Blocks() definition -def _update_logs_and_preview(profile_token_state: tuple[gr.OAuthProfile | None, gr.OAuthToken | None], - space_name: str, - current_project_state: dict | None - ) -> tuple[str, str, str, dict | None]: +def _update_logs_and_preview(profile_token_state, space_name): """Fetches logs and checks iframe status for the poller.""" - - profile, token = profile_token_state - - # Only poll if logged in and space name is provided + profile, token = profile_token_state # profile_token_state is the state from the login_btn component if not profile or not token or not token.token: - # If not logged in, return placeholder content and the original state - return "Login required", "Login required", "

Please log in.

", current_project_state + return "Login required", "Login required", "

Please log in.

" if not space_name or not space_name.strip(): - # If no space name, return placeholder and original state - return "Enter Space name", "Enter Space name", "

Enter a Space name.

", current_project_state + return "Enter Space name", "Enter Space name", "

Enter a Space name.

" clean = space_name.strip() rid = f"{profile.username}/{clean}" url = f"https://huggingface.co/spaces/{profile.username}/{clean}" - # Initialize project_state if it's the first call or if space name changed while not mid-project - if current_project_state is None: - print(f"Poller: Initializing state for space {rid}") - current_project_state = { - 'repo_id': rid, # Set repo_id based on current UI inputs - 'iframe_url': url, # Set iframe_url based on current UI inputs - 'logs': {'build': 'Polling...', 'run': 'Polling...'}, - 'iframe_ok': False, - 'log_error_state': 'Polling...', # Initial polling state - 'chat_history': [], # Start with empty history for a new space - 'status': 'Not Started', - 'status_message': f'Polling logs for {rid}... Enter requirements to start project.', - 'attempt_count': 0, - 'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': '' - } - # Add an initial message if history is empty - if not current_project_state['chat_history']: - current_project_state['chat_history'].append({"role": "assistant", "content": current_project_state['status_message']}) - - elif current_project_state.get('repo_id') != rid: - # If the UI Space name changes while a project is in progress for a *different* space, - # reset the project state to focus on the new space name. - print(f"Poller detected space name change from {current_project_state.get('repo_id')} to {rid}. Resetting state.") - current_project_state = { - 'repo_id': rid, - 'iframe_url': url, - 'logs': {'build': 'Space name changed, fetching logs...', 'run': 'Space name changed, fetching logs...'}, - 'iframe_ok': False, - 'log_error_state': 'None', # Reset error state - 'chat_history': [{"role": "assistant", "content": f"Space name changed to **{rid}**. Project state reset. Enter requirements to start development on this Space."}], - 'status': 'Not Started', - 'status_message': 'Space name changed. Enter requirements to start.', - 'attempt_count': 0, # Reset attempts for a new project - 'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': '' - } - else: - # If space name matches the one in state, potentially update polling status message - if current_project_state.get('status') in ['Not Started']: - current_status_message = f"Polling logs for {rid}... Current status: {current_project_state.get('status_message', '...')}" - # Avoid flooding chat history with this, just update the status message field - current_project_state['status_message'] = current_status_message - # Ensure there's at least an initial message in history if needed - if not current_project_state['chat_history']: - current_project_state['chat_history'].append({"role": "assistant", "content": current_project_state['status_message']}) - # If status is 'In Progress' and task is 'LOGGING', the orchestrator manages status_message. - # If status is 'Complete' or 'Failed', the final status_message is set. - # Otherwise, keep the current status_message. - - # Fetch logs and check iframe status using the revised functions - # Always attempt to fetch logs and check iframe if a repo_id is set in the state. - # The fetch_logs function handles errors like 401/404/timeout. - if current_project_state.get('repo_id'): - print(f"Poller fetching logs for {rid} (Status: {current_project_state.get('status')}, Task: {current_project_state.get('current_task')})...") - - build_logs_content = fetch_logs(rid, "build") - run_logs_content = fetch_logs(rid, "run") # Fetch run logs regardless of build log status - iframe_status_ok = check_iframe(url) - - # Update log_error_state based on fetch results - # Priority to critical errors > iframe issues > waiting - new_log_error_state = 'None' - - if build_logs_content.startswith("ERROR_AUTH_FAILED_JWT:") or run_logs_content.startswith("ERROR_AUTH_FAILED_JWT:"): - new_log_error_state = 'Auth Failed JWT' - elif build_logs_content.startswith("ERROR_AUTH_FAILED_STREAM:") or run_logs_content.startswith("ERROR_AUTH_FAILED_STREAM:"): - new_log_error_state = 'Auth Failed STREAM' - elif build_logs_content.startswith("ERROR_NOT_FOUND:") or run_logs_content.startswith("ERROR_NOT_FOUND:"): - new_log_error_state = 'Not Found' - elif build_logs_content.startswith("ERROR_TIMEOUT:") or run_logs_content.startswith("ERROR_TIMEOUT:"): - new_log_error_state = 'TIMEOUT' - elif build_logs_content.startswith("ERROR_REQUEST:") or run_logs_content.startswith("ERROR_REQUEST:"): - new_log_error_state = 'Request Error' - elif build_logs_content.startswith("ERROR_HTTP_") or run_logs_content.startswith("ERROR_HTTP_"): - new_log_error_state = 'HTTP Error' - elif build_logs_content.startswith("ERROR_UNEXPECTED:") or run_logs_content.startswith("ERROR_UNEXPECTED:"): - new_log_error_state = 'Poller Unexpected Error' - elif not iframe_status_ok: - # If iframe is not ok, and no explicit log error, assume it's not ready - new_log_error_state = 'Iframe Not Ready' - elif build_logs_content.strip() == "No build logs found yet." and run_logs_content.strip() == "No run logs found yet.": - # If no logs and iframe ok, maybe it's a very simple app? Or a very fast build? - # Let's not mark it as 'No Logs Yet' if iframe is OK. - pass # Keep None - elif (build_logs_content.strip() == "No build logs found yet." or run_logs_content.strip() == "No run logs found yet.") and iframe_status_ok: - # Partial logs, but iframe is OK - pass # Keep None - elif (build_logs_content.strip() != "No build logs found yet." or run_logs_content.strip() != "No run logs found yet.") and not iframe_status_ok: - # Logs are appearing but iframe is not OK - new_log_error_state = 'Iframe Not Ready' # Re-assert iframe not ready if logs appear but it's still down - - - current_project_state['logs']['build'] = build_logs_content - current_project_state['logs']['run'] = run_logs_content - current_project_state['iframe_ok'] = iframe_status_ok - current_project_state['log_error_state'] = new_log_error_state - # Do NOT update chat_history or status_message from the poller here. - # The orchestrator needs to react to the *state* changes, not manage the UI messages directly from poller. - - else: - # If repo_id is not set in state, cannot poll logs - build_logs_content = current_project_state['logs'].get('build', 'Space name not set, cannot poll logs.') - run_logs_content = current_project_state['logs'].get('run', 'Space name not set, cannot poll logs.') - iframe_status_ok = False - current_log_error_state = current_project_state.get('log_error_state', 'Space Not Set') - print("Poller skipping log fetch because repo_id is not set in state.") - - - # Reconstruct preview HTML based on the updated state - preview_html = ( - f'' - if current_project_state.get('iframe_ok') else - f"

⚠️ App preview not loading or check failed ({current_project_state.get('iframe_url', 'URL not set.')})." - f"{' **Authentication Error:** Ensure HF_TOKEN secret is set on the Space running this app.' if current_project_state.get('log_error_state') in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM'] else ''}" - f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if current_project_state.get('log_error_state') == 'Not Found' else ''}" - f"{' **Timeout:** Log fetch timed out.' if current_project_state.get('log_error_state') == 'TIMEOUT' else ''}" - f"{' **Iframe not ready:** Still building or error. Check logs.' if current_project_state.get('log_error_state') == 'Iframe Not Ready' else ''}" - f"{' **Polling Error:** Encountered error fetching logs.' if current_project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error', 'HTTP Error'] else ''}" - f"{' **Space Name Not Set:** Cannot fetch logs or check iframe.' if current_project_state.get('log_error_state') == 'Space Not Set' else ''}" - f"{' Check build logs for details.' if current_project_state.get('log_error_state') not in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Not Found', 'Space Not Set'] else ''}" # Suggest checking logs if no specific critical error - f"

" - ) + try: + # Modified: Call fetch_logs without the token argument (uses HF_TOKEN env var) + b = fetch_logs(rid, "build") + except Exception as e: + b = f"Error fetching build logs: {e}" + print(f"Poller error fetching build logs for {rid}: {e}") + + try: + # Modified: Call fetch_logs without the token argument (uses HF_TOKEN env var) + r = fetch_logs(rid, "run") + except Exception as e: + r = f"Error fetching run logs: {e}" + print(f"Poller error fetching run logs for {rid}: {e}") + try: + # check_iframe does not need authentication for public spaces + ok = check_iframe(url) + except Exception as e: + ok = False + print(f"Poller error checking iframe {url}: {e}") - # Return the updated state along with the UI outputs derived *from* the state - return ( - build_logs_content, # Build logs output - run_logs_content, # Run logs output - preview_html, # Preview HTML output - current_project_state # Updated project state + preview_html = ( + f'' + if ok else + f"

⚠️ iframe not responding yet ({url}). Make sure the Space name is correct and wait for the build to complete. Check build logs for errors.

" ) + return b, r, preview_html # --- MAIN HANDLER (Called by Gradio) --- -# Updated signature to include login state explicitly and project state first -# Note: Gradio 4.x might prefer profile and token as the first two args when auto-injecting. -# Let's stick to the recommended Gradio pattern where auto-injected params come first. +# MOVED THIS FUNCTION BLOCK HERE, BEFORE gr.Blocks() +# Updated signature to include space_name def handle_user_message( - profile: gr.OAuthProfile | None, # (1) - Auto-injected by Gradio - oauth_token: gr.OAuthToken | None, # (2) - Auto-injected by Gradio - project_state: dict | None, # (3) - from gr.State component - user_input: str, # (4) - sdk_choice: str, # (5) - gemini_api_key: str, # (6) - space_name: str, # (7) - grounding_enabled: bool, # (8) - temperature: float, # (9) - max_output_tokens: int, # (10) + history, + user_input: str, + sdk_choice: str, + gemini_api_key: str, + space_name: str, # <-- New parameter for Space name + grounding_enabled: bool, + temperature: float, + max_output_tokens: int, + profile: gr.OAuthProfile | None, + oauth_token: gr.OAuthToken | None # Gradio auto-injects. Keep to pass token to orchestrate_development for PUSHING. ): - # Now the function signature has 10 parameters. - # The inputs list will exclude login_btn to allow auto-injection. - - # Initialize or update project_state based on inputs and previous state - # Decide whether to start a new project or continue - # A new project starts if state is None, or if requirements change - is_new_project_request = ( - project_state is None or # First run - (user_input is not None and user_input.strip() != "" and user_input.strip() != project_state.get('requirements', '').strip()) # New requirements provided - # We could also check if status is 'Complete' or 'Failed' to auto-reset, but letting - # the user input change trigger the reset is more explicit. - ) + if not history or history[-1].get("role") != "user" or history[-1].get("content") != user_input: + history.append({"role": "user", "content": user_input}) - if is_new_project_request: - print("Starting a new project...") - history = [] - clean_space_name = space_name.strip() if space_name else '' - repo_id = f"{profile.username}/{clean_space_name}" if profile and clean_space_name else '' - iframe_url = f"https://huggingface.co/spaces/{profile.username}/{clean_space_name}" if profile and clean_space_name else '' - code_fn = "app.py" if sdk_choice == "gradio" else "streamlit_app.py" - sdk_version = get_sdk_version(sdk_choice) - - project_state = { - 'requirements': user_input.strip() if user_input else '', # Store cleaned requirements - 'plan': '', - 'files': {}, - 'logs': {'build': '', 'run': ''}, - 'feedback': '', - 'current_task': 'START', # Start the orchestration process - 'status': 'In Progress', - 'status_message': 'Initializing...', - 'attempt_count': 0, # Reset attempts for a new project - 'sdk_choice': sdk_choice, - 'sdk_version': sdk_version, - 'repo_id': repo_id, - 'iframe_url': iframe_url, - 'main_app_file': code_fn, - 'chat_history': history, - 'iframe_ok': False, - 'log_error_state': 'None', - } - if user_input and user_input.strip(): - project_state['chat_history'].append({"role": "user", "content": user_input.strip()}) # Add initial user requirement - - else: - # Continue existing project state - print("Continuing existing project...") - # history is part of project_state - no need to copy - # project_state['requirements'] stays the same requirement that started the project - # Update other settings that could change mid-project (though maybe shouldn't) - project_state['sdk_choice'] = sdk_choice - project_state['main_app_file'] = "app.py" if sdk_choice == "gradio" else "streamlit_app.py" - project_state['sdk_version'] = get_sdk_version(sdk_choice) - clean_space_name = space_name.strip() if space_name else '' - # Only update repo_id/iframe_url if the name changes (and state is not 'Not Started') - if project_state.get('repo_id') != f"{profile.username}/{clean_space_name}" and project_state.get('status') != 'Not Started' : - # This case should ideally be handled by the poller's state reset, but double check. - print(f"Handle_user_message detected space name change mid-project from {project_state.get('repo_id')} to {profile.username}/{clean_space_name}. Forcing project reset.") - # Re-initialize as a new project - return handle_user_message(profile, oauth_token, None, user_input, sdk_choice, gemini_api_key, space_name, grounding_enabled, temperature, max_output_tokens) - elif profile and clean_space_name: - project_state['repo_id'] = f"{profile.username}/{clean_space_name}" - project_state['iframe_url'] = f"https://huggingface.co/spaces/{profile.username}/{clean_space_name}" - - # If status is 'Not Started' and user sends input again (maybe after fixing something), - # set task to START to begin orchestration. - if project_state.get('status') == 'Not Started' and user_input and user_input.strip() and project_state.get('current_task') is None: - print("Status was 'Not Started', user sent input. Starting orchestration.") - project_state['current_task'] = 'START' - project_state['status'] = 'In Progress' - project_state['status_message'] = 'Initializing...' - - - # Validation Checks (using received profile/token) - validation_error = None - validation_message = None + if not space_name or not space_name.strip(): + msg = "⚠️ Please enter a Space name." + if not history or history[-1].get("content") != msg: + history.append({"role":"assistant","content":msg}) + return history, "", "", "

Enter a Space name.

", msg + + if not profile or not oauth_token or not oauth_token.token: + error_msg = "⚠️ Please log in first via the Hugging Face button." + if not history or history[-1].get("content") != error_msg: + history.append({"role":"assistant","content":error_msg}) + return history, "", "", "

Please log in.

", "Login required." + + if not gemini_api_key: + error_msg = "⚠️ Please provide your Gemini API Key." + if not history or history[-1].get("content") != error_msg: + history.append({"role":"assistant","content":error_msg}) + return history, "", "", "

Please provide API Key.

", "API Key required." + + if not user_input or user_input.strip() == "": + error_msg = "Please enter requirements for the application." + if not history or history[-1].get("content") != error_msg: + history.append({"role":"assistant","content":error_msg}) + return history, "", "", "

Enter requirements.

", "Waiting for prompt." + + client = genai.Client(api_key=gemini_api_key) + sdk_version = get_sdk_version(sdk_choice) + code_fn = "app.py" if sdk_choice == "gradio" else "streamlit_app.py" + + user_prompt = user_input + + clean_space_name = space_name.strip() + repo_id = f"{profile.username}/{clean_space_name}" + iframe_url = f"https://huggingface.co/spaces/{profile.username}/{clean_space_name}" + + project_state = { + 'requirements': user_prompt, + 'plan': '', + 'files': {}, + 'logs': {'build': '', 'run': ''}, # Initial state, will be updated during orchestration + 'feedback': '', + 'current_task': 'START', + 'status': 'In Progress', + 'status_message': 'Initializing...', + 'attempt_count': 0, + 'sdk_choice': sdk_choice, + 'sdk_version': sdk_version, + 'repo_id': repo_id, + 'iframe_url': iframe_url, + 'main_app_file': code_fn, + 'chat_history': history[:] + } - if not profile or not oauth_token or not hasattr(oauth_token, 'token') or not oauth_token.token: - validation_error = "Login required." - validation_message = "⚠️ Please log in first via the Hugging Face button." - elif not space_name or not space_name.strip(): - validation_error = "Space name required." - validation_message = "⚠️ Please enter a Space name." - elif not gemini_api_key: - validation_error = "API Key required." - validation_message = "⚠️ Please provide your Gemini API Key." - elif not user_input or user_input.strip() == "": - validation_error = "Requirements required." - validation_message = "Please enter requirements for the application." - - - # If there's a validation error, update state and return early - if validation_error: - if project_state.get('status') != 'Failed' or project_state.get('status_message') != validation_message: - # Avoid adding duplicate error messages - project_state['status'] = 'Failed' # Mark as failed due to validation - project_state['status_message'] = validation_message - project_state['current_task'] = 'FINISHED' # End orchestration - if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != validation_message: - project_state['chat_history'].append({"role":"assistant","content":validation_message}) - - # Return current state and UI outputs - return ( - project_state, - project_state.get('chat_history', []), - project_state['logs'].get('build', ''), - project_state['logs'].get('run', ''), - f"

{validation_message}

", # Simple error preview - project_state.get('status_message', 'Validation Failed') - ) - - - # If validation passed and status is 'In Progress' or being set to 'In Progress' ('START' task) - # Note: orchestrate_development handles the 'START' -> 'In Progress' transition internally - if project_state.get('status') == 'In Progress': - client = genai.Client(api_key=gemini_api_key) - - cfg = GenerateContentConfig( - tools=[Tool(google_search=GoogleSearch())] if grounding_enabled else [], - response_modalities=["TEXT"], - temperature=temperature, - max_output_tokens=int(max_output_tokens), - ) + cfg = GenerateContentConfig( + tools=[Tool(google_search=GoogleSearch())] if grounding_enabled else [], + response_modalities=["TEXT"], + temperature=temperature, + max_output_tokens=int(max_output_tokens), + ) + + # Pass the token here for pushing files + final_history, final_build_logs, final_run_logs, final_iframe_html, final_status_message = orchestrate_development( + client, project_state, cfg, oauth_token.token + ) - # Run *one step* of orchestration. orchestrate_development updates project_state directly. - # The function is designed to be called repeatedly by subsequent handle_user_message calls - # triggered by the poller implicitly updating state and thus triggering retries. - try: - orchestrate_development( - client, project_state, cfg, oauth_token.token # Pass the token for pushing - ) - except Exception as e: - # Catch unexpected errors during orchestration step - error_msg = f"FATAL ERROR during orchestration step '{project_state.get('current_task', 'Unknown')}': {e}" - print(error_msg) - project_state['status'] = 'Failed' - project_state['status_message'] = error_msg - if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '') != error_msg: - project_state['chat_history'].append({"role":"assistant","content":error_msg}) - project_state['current_task'] = 'FINISHED' # Ensure state is finished - - - # Return the updated project_state and derived UI outputs - # The UI outputs are derived *from* the state dictionary return ( - project_state, # Return the full state (gr.State will store this) - project_state.get('chat_history', []), # Chatbot output (get from state) - project_state['logs'].get('build', 'No build logs.'), # Build logs output (get from state) - project_state['logs'].get('run', 'No run logs.'), # Run logs output (get from state) - # Reconstruct iframe HTML output (get info from state) - (f'' - if project_state.get('iframe_ok') else - f"

⚠️ App preview not loading or check failed ({project_state.get('iframe_url', 'URL not set.')})." - # Use log_error_state from project_state for more specific messages - f"{' **Authentication Error:** Ensure HF_TOKEN secret is set on the Space running this app.' if project_state.get('log_error_state') in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM'] else ''}" - f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if project_state.get('log_error_state') == 'Not Found' else ''}" - f"{' **Timeout:** Log fetch timed out.' if project_state.get('log_error_state') == 'TIMEOUT' else ''}" - f"{' **Iframe not ready:** Still building or error. Check logs.' if project_state.get('log_error_state') == 'Iframe Not Ready' else ''}" - f"{' **Polling Error:** Encountered error fetching logs.' if project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error', 'HTTP Error'] else ''}" - f"{' **Space Name Not Set:** Cannot fetch logs or check iframe.' if project_state.get('log_error_state') == 'Space Not Set' else ''}" - f"{' Check build logs for details.' if project_state.get('log_error_state') not in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Not Found', 'Space Not Set'] else ''}" # Suggest checking logs if no specific critical error - f"

"), - project_state.get('status_message', '...'), # Status message output (get from state) + final_history, + final_build_logs, + final_run_logs, + final_iframe_html, + final_status_message ) # --- SIMPLE UI WITH HIGHER MAX TOKENS & STATUS DISPLAY --- with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo: - # State variable to hold the project state across interactions - project_state = gr.State(None) # Initialize state as None - 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.") gr.Markdown("1) Log in with Hugging Face. 2) Enter your Gemini API Key. 3) Enter a Space name. 4) Provide app requirements. 5) Click 'Start Development Team' and watch the process.") @@ -1255,21 +820,20 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo: status_md = gr.Markdown("*Not logged in.*") models_md = gr.Markdown() - # Use api_name to make these callable via API if needed, but not strictly necessary for UI load - # Ensure these load on initial page load - demo.load(show_profile, inputs=None, outputs=status_md) # api_name="load_profile" removed - demo.load(list_private_models, inputs=None, outputs=models_md) # api_name="load_models" removed + demo.load(show_profile, inputs=None, outputs=status_md, api_name="load_profile") + demo.load(list_private_models, inputs=None, outputs=models_md, api_name="load_models") - # Ensure these update on login button click login_btn.click( fn=show_profile, - inputs=None, # OAuth auto-injected - outputs=status_md + inputs=None, + outputs=status_md, + api_name="login_profile" ) login_btn.click( fn=list_private_models, - inputs=None, # OAuth auto-injected - outputs=models_md + inputs=None, + outputs=models_md, + api_name="login_models" ) # --- END LOGIN FIX --- @@ -1298,91 +862,59 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo: with gr.Accordion("Logs", open=False): build_box = gr.Textbox(label="Build logs", lines=10, interactive=False, max_lines=20) run_box = gr.Textbox(label="Run logs", lines=10, interactive=False, max_lines=20) + # Manual refresh button REMOVED with gr.Accordion("App Preview", open=True): preview = gr.HTML("

App preview will load here when available.

") # --- Hidden poller (using Timer) --- - # Polls every 2 seconds to update logs and preview log_poller = gr.Timer(value=2, active=True, render=False) # --- End hidden poller --- # handle_user_message is defined ABOVE this block - # The main button and submit handler now receive and return the project_state - # INPUTS: relies on auto-injection of profile/token, then state, then other UI inputs - inputs_list = [ - project_state, # <-- Pass the state in (after auto-injected profile/token) - user_in, # Other UI inputs in order - sdk_choice, - api_key, - space_name, - grounding, - temp, - max_tokens, - # Note: login_btn is NOT in inputs, profile/token are auto-injected - # Note: chatbot is NOT in inputs, its history is managed via project_state['chat_history'] - ] # Total 8 components listed here, plus 2 auto-injected = 10 arguments - - # OUTPUTS: Must match the return values of handle_user_message - outputs_list = [ - project_state, # <-- Return the updated state (gr.State will store this) - chatbot, # Updates the chatbot UI component - build_box, # Updates the build logs textbox - run_box, # Updates the run logs textbox - preview, # Updates the iframe preview - project_status_md # Updates the status markdown - ] # Total 6 outputs - send_btn.click( fn=handle_user_message, - inputs=inputs_list, - outputs=outputs_list - # No api_name needed unless you plan to call this via API directly + inputs=[ + chatbot, + user_in, + sdk_choice, + api_key, + space_name, + grounding, + temp, + max_tokens, + ], + outputs=[chatbot, build_box, run_box, preview, project_status_md] ) user_in.submit( fn=handle_user_message, - inputs=inputs_list, - outputs=outputs_list - # No api_name needed + inputs=[ + chatbot, + user_in, + sdk_choice, + api_key, + space_name, + grounding, + temp, + max_tokens, + ], + outputs=[chatbot, build_box, run_box, preview, project_status_md] ) - # --- Wire the poller to the update function --- - # _update_logs_and_preview is defined ABOVE this block - # The poller needs login state (as tuple), space name, and the current project state - # INPUTS: login_btn state (as tuple), space name, project state - # When login_btn is the *first* component in the inputs list and the function's - # first parameter is a tuple of the correct types, Gradio passes the outputs as a tuple. - poller_inputs = [ - login_btn, # Provides (profile, token) as a tuple because it's first - space_name, - project_state # Passes the current state value - ] # Total 3 components -> function receives 3 arguments (1 tuple + 1 + 1) - # Note: _update_logs_and_preview signature is (profile_token_state, space_name, current_project_state) - - # OUTPUTS: Must match the return values of _update_logs_and_preview - poller_outputs = [ - build_box, # Updates the build logs textbox - run_box, # Updates the run logs textbox - preview, # Updates the iframe preview - project_state # Updates the project state (gr.State) - ] # Total 4 outputs + # Manual refresh handler REMOVED + # --- Wire the poller to that function --- + # _update_logs_and_preview is defined ABOVE this block log_poller.tick( fn=_update_logs_and_preview, - inputs=poller_inputs, - outputs=poller_outputs - # No api_name needed + inputs=[login_btn, space_name], # Pass login button state (for profile/token) and the space_name textbox + outputs=[build_box, run_box, preview] # Update the log textareas and the preview iframe ) # --- End wire poller --- + # Clean up files created during the process when the app stops (optional) - # This part was commented out in the original, keep it commented unless needed # 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"))]) -# Launch the demo -# If running on a Space, server_name='0.0.0.0' and server_port=7860 are standard. -# If running locally, just demo.launch() or specify a port. -if __name__ == "__main__": - # Use `show_error=True` to see the actual Python errors in the UI during development - demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True) \ No newline at end of file +demo.launch(server_name="0.0.0.0", server_port=7860) \ No newline at end of file