import re import json import time import requests import importlib.metadata import gradio as gr import os # Needed for writing files from huggingface_hub import ( create_repo, upload_file, list_models, constants ) from huggingface_hub.utils import build_hf_headers, get_session, hf_raise_for_status from google import genai # Import Content and Part types for structured input from google.genai.types import Content, Part from google.genai.types import Tool, GenerateContentConfig, GoogleSearch # --- USER INFO & MODEL LISTING --- def show_profile(profile: gr.OAuthProfile | None) -> str: return f"✅ Logged in as **{profile.username}**" if profile else "*Not logged in.*" def list_private_models( profile: gr.OAuthProfile | None, oauth_token: gr.OAuthToken | None ) -> str: # Gradio injects profile and oauth_token automatically when inputs=None # and the function signature has these parameter types. if not profile or not oauth_token or not hasattr(oauth_token, 'token') or not oauth_token.token: return "Please log in to see your models." try: models = [ f"{m.id} ({'private' if m.private else 'public'})" for m in list_models(author=profile.username, token=oauth_token.token) ] return "No models found." if not models else "Models:\n\n" + "\n - ".join(models) except Exception as e: # Catching potential API errors during model listing return f"Error listing models: {e}" # --- UTILITIES --- def get_sdk_version(sdk_choice: str) -> str: pkg = "gradio" if sdk_choice == "gradio" else "streamlit" try: return importlib.metadata.version(pkg) except importlib.metadata.PackageNotFoundError: return "UNKNOWN" def classify_errors(logs: str) -> str: errs = set() # Convert logs to lower for case-insensitive matching logs_lower = logs.lower() if "syntaxerror" in logs_lower: errs.add("syntax") elif "importerror" in logs_lower or "modulenotfounderror" in logs_lower: errs.add("import") # Catch common error indicators elif "traceback" in logs_lower or "exception" in logs_lower or "error" in logs_lower: errs.add("runtime/generic") # More general error indication return ", ".join(errs) or "none" # --- HF SPACE LOGGING --- # Modified: Removed 'token' parameter def _get_space_jwt(repo_id: str) -> str: """Fetches JWT for Space logs using your local HF credentials (HF_TOKEN/config.json).""" jwt_url = f"{constants.ENDPOINT}/api/spaces/{repo_id}/jwt" # Modified: Called build_hf_headers() without token= argument # This picks up credentials from env var or config.json where the app is running r = get_session().get(jwt_url, headers=build_hf_headers()) 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 def fetch_logs(repo_id: str, level: str) -> str: """Fetches build or run logs from an HF Space.""" try: # Modified: Called _get_space_jwt() without token argument # This will now use the HF_TOKEN env var if available in the Space jwt = _get_space_jwt(repo_id) url = f"https://api.hf.space/v1/{repo_id}/logs/{level}" lines = [] # 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 with get_session().get(url, headers=headers, stream=True, timeout=10) as resp: hf_raise_for_status(resp) # Read lines with a timeout for raw in resp.iter_lines(decode_unicode=True, chunk_size=512): if raw is None: # handle keep-alive or similar continue if raw.startswith("data: "): try: ev = json.loads(raw[len("data: "):]) ts, txt = ev.get("timestamp","N/A"), ev.get("data","") lines.append(f"[{ts}] {txt}") except json.JSONDecodeError: lines.append(f"Error decoding log line: {raw}") except Exception as e: lines.append(f"Unexpected error processing log line: {raw} - {e}") return "\n".join(lines) except requests.exceptions.Timeout: return f"Error: Timeout fetching {level} logs." except requests.exceptions.RequestException as 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"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 200 status code.""" # For public spaces, simple request should suffice, no special headers needed here try: response = requests.get(url, timeout=timeout) return response.status_code == 200 except requests.exceptions.RequestException: return False # Any request exception (timeout, connection error, etc.) means it's not accessible # --- AGENT PROMPTS --- SYSTEM_ORCHESTRATOR = { "role": "system", "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) " "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 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'." ) } SYSTEM_ARCHITECT = { "role": "system", "content": ( "You are **Architect Agent**, the lead planner. " "Given the user requirements and the current project state, your task is to devise or refine the high-level plan for the application. " "Outline the main features, suggest a logical structure, identify potential files (e.g., `app.py`, `utils.py`, `requirements.txt`), and key components needed. " "The target SDK is {sdk_choice}. The main application file should be `{main_app_file}`. " "Output the plan clearly, using bullet points or a simple numbered list. Do NOT write code. Focus only on the plan." ) } SYSTEM_CODEGEN = { "role": "system", "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, 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). " "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" "Provide the *complete* modified code for any file you touch." "Only output the code blocks and their preceding filenames. Do not add extra commentary outside the code blocks." "Crucially, ensure the app uses the specified SDK ({sdk_choice}) and writes its main application logic to the file `{main_app_file}`." # Reinforce main file ) } 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, 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. " "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'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 def run_agent(client, model_name, system_prompt_template, user_input_state, config): """Helper to run a single agent interaction using the project state as input.""" try: # Format the system prompt using state variables system_prompt = system_prompt_template["content"].format(**user_input_state) 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." # Prepare the message content by formatting the project state user_message_content = "Project State:\n" + json.dumps(user_input_state, indent=2) model_to_use = model_name try: # MODIFIED: Combine system prompt and user content into a single 'user' message messages = [ Content(role="user", parts=[Part(text=system_prompt + "\n\n" + user_message_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: 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}" return f"ERROR: Agent returned no response content." 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})") print(response_text[:1000] + ('...' if len(response_text) > 1000 else '')) # Print first 1000 chars print("--------------------------") return response_text.strip() except Exception as e: print(f"Agent call failed: {e}") error_details = str(e) if hasattr(e, 'response') and e.response is not None: try: error_json = e.response.json() error_details = json.dumps(error_json, indent=2) except: try: error_details = e.response.text except: pass return f"ERROR: Agent failed - {error_details}" # --- AGENT FUNCTIONS (called by Orchestrator) --- # These functions now expect only the response text from run_agent def run_planner(client, project_state, config): print("Orchestrator: Running Planner Agent...") 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'] } response_text = run_agent( client=client, model_name="gemini-2.5-flash-preview-04-17", system_prompt_template=SYSTEM_ARCHITECT, user_input_state=input_state_for_planner, config=config, ) if response_text.startswith("ERROR:"): project_state['status_message'] = response_text return False project_state['plan'] = response_text print("Orchestrator: Planner Output Received.") project_state['status_message'] = "Planning complete." 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']}...") 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", system_prompt_template=SYSTEM_CODEGEN, user_input_state=input_state_for_codegen, config=config, ) if response_text.startswith("ERROR:"): project_state['status_message'] = 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 = {} syntax_errors = [] # Modified Regex: Catches ```lang filename\ncode``` OR `filename`\n```lang\ncode``` # Using named groups and DOTALL flag pattern = re.compile( 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 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 '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 for filename, code_content, start_pos in extracted_blocks: if not 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}.") continue files_updated[filename] = code_content.strip() # Quick syntax check for Python files if filename.endswith('.py'): try: compile(code_content, filename, "exec") except SyntaxError as e: 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}` near position {start_pos}: {e}") print(f"Unexpected error during syntax check for `{filename}`: {e}") # 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." project_state['status_message'] = parse_error_msg project_state['feedback'] = project_state.get('feedback', '') + "\n\n" + parse_error_msg + "\nRaw Agent Response (blocks parsed, no filenames):\n" + response_text[:2000] + "..." project_state['chat_history'].append({"role": "assistant", "content": parse_error_msg + "\nSee Debug Feedback for raw response."}) return False if syntax_errors: # If syntax errors found, add them to feedback and signal failure for CodeGen step syntax_error_msg = "ERROR: Code-Gen Agent introduced syntax errors." project_state['feedback'] = syntax_error_msg + "\n" + "\n".join(syntax_errors) + "\n\n" + project_state.get('feedback', '') project_state['status_message'] = syntax_error_msg + " Debugging needed." project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']}) project_state['chat_history'].append({"role": "assistant", "content": "Details:\n" + "\n".join(syntax_errors)}) return False # Indicate failure due to syntax errors project_state['files'].update(files_updated) 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 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...") 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", system_prompt_template=SYSTEM_DEBUG, user_input_state=input_state_for_debugger, config=config, ) if response_text.startswith("ERROR:"): project_state['status_message'] = 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." 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, config, oauth_token_token): """Manages the overall development workflow.""" 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['sdk_choice']} sdk_version: {project_state['sdk_version']} app_file: {project_state['main_app_file']} pinned: false --- # {project_state['repo_id']} This is an auto-generated HF Space. **Requirements:** {project_state['requirements']} **Plan:** {project_state['plan']} """ 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' 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' 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']) project_state['current_task'] = 'FINISHED' 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' 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: project_state['status'] = 'Failed' project_state['current_task'] = 'FINISHED' elif current_task == 'FINISHED': pass else: step_successful = False 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' 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' else: project_state['current_task'] = 'DEBUGGING' if project_state['status'] == 'In Progress': 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, space_name): """Fetches logs and checks iframe status for the poller.""" 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: return "Login required", "Login required", "

Please log in.

" if not space_name or not space_name.strip(): 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}" # Fetch logs and check iframe status using the revised functions 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}") 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) --- # MOVED THIS FUNCTION BLOCK HERE, BEFORE gr.Blocks() # Updated signature to include space_name def handle_user_message( 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. ): if not history or history[-1].get("role") != "user" or history[-1].get("content") != user_input: history.append({"role": "user", "content": user_input}) 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[:] } 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 ) return ( 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: 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.") with gr.Row(): with gr.Column(scale=1): # --- LOGIN BUTTON / PROFILE & MODEL LISTING --- login_btn = gr.LoginButton(variant="huggingface", size="lg") status_md = gr.Markdown("*Not logged in.*") models_md = gr.Markdown() 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") login_btn.click( fn=show_profile, inputs=None, outputs=status_md, api_name="login_profile" ) login_btn.click( fn=list_private_models, inputs=None, outputs=models_md, api_name="login_models" ) # --- END LOGIN FIX --- gr.Markdown("---") sdk_choice = gr.Radio(["gradio","streamlit"], value="gradio", label="SDK", info="Choose the framework for your app.") api_key = gr.Textbox(label="Gemini API Key", type="password", info="Get one from Google AI Studio.") space_name = gr.Textbox( label="Space name", placeholder="e.g. test, example, my-cool-space", info="Becomes username/this-name on HF" ) grounding = gr.Checkbox(label="Enable Google Search (Grounding)", value=False, info="Allow agents to use Google Search.") temp = gr.Slider(0,1,value=0.2, label="Temperature", info="Creativity of agents. Lower is more focused.") max_tokens = gr.Number(value=4096, label="Max Output Tokens", minimum=1000, info="Max length of agent responses (code, feedback, etc.). Recommend 4096+.") with gr.Column(scale=2): project_status_md = gr.Markdown("Waiting for prompt...") chatbot = gr.Chatbot(type="messages", label="Team Communication & Status", show_copy_button=True) user_in = gr.Textbox(placeholder="Describe the application you want to build...", label="Application Requirements", lines=3) send_btn = gr.Button("🚀 Start Development Team") 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) --- log_poller = gr.Timer(value=2, active=True, render=False) # --- End hidden poller --- # handle_user_message is defined ABOVE this block send_btn.click( fn=handle_user_message, 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=[ chatbot, user_in, sdk_choice, api_key, space_name, grounding, temp, max_tokens, ], outputs=[chatbot, build_box, run_box, preview, project_status_md] ) # 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=[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) # 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"))]) demo.launch(server_name="0.0.0.0", server_port=7860)