# app.py # IMPORTANT: These two lines MUST be at the very top, before any other imports. # They patch asyncio to work cooperatively with gevent workers. import gevent.monkey gevent.monkey.patch_all(asyncio=True) import asyncio from flask import Flask, request, jsonify from proxy_lite import Runner, RunnerConfig from proxy_lite.config import RecorderConfig # Needed for explicit instantiation import os import logging # Configure logging for better visibility in Hugging Face Space logs logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) app = Flask(__name__) # Global runner instance. It will be initialized once per worker process/greenlet # upon the first request to ensure correct event loop binding. _runner = None async def initialize_runner(): """Initializes the Proxy-lite Runner asynchronously.""" global _runner if _runner is None: logger.info("Initializing Proxy-lite Runner...") # Retrieve Hugging Face API Token from environment variables (set as Space Secret) hf_api_token = os.environ.get("HF_API_TOKEN") if not hf_api_token: logger.error("HF_API_TOKEN environment variable not set. Cannot initialize Runner.") raise ValueError("HF_API_TOKEN environment variable not set. Please set it as a Space secret.") # --- EXPLICITLY CREATE RECORDER_CONFIG --- # This addresses the PermissionError by ensuring the root_path is explicitly set # and passed as a Pydantic object, rather than relying on nested dict parsing. recorder_config_instance = RecorderConfig(root_path="/tmp/proxy_lite_runs") # --- END EXPLICIT RECORDER_CONFIG --- # --- EXPLICITLY CREATE RUNNER_CONFIG --- # Pass all configuration parameters as keyword arguments to the Pydantic model. config = RunnerConfig( environment={ "name": "webbrowser", "homepage": "https://www.google.com", "headless": True, # Keep headless for server environment }, solver={ "name": "simple", "agent": { "name": "proxy_lite", "client": { "name": "convergence", "model_id": "convergence-ai/proxy-lite-3b", "api_base": "https://api-inference.huggingface.co/models/convergence-ai/proxy-lite-3b", "api_key": hf_api_token } } }, recorder=recorder_config_instance # Pass the explicitly created instance ) # --- END EXPLICIT RUNNER_CONFIG --- # Instantiate the Runner, passing the config as a keyword argument (Pydantic style) _runner = Runner(config=config) logger.info("Proxy-lite Runner initialized successfully.") return _runner def run_async_task(coro): """ Helper to run async coroutines in a synchronous context (like Flask routes). Ensures an event loop exists for the current thread/greenlet and runs the coroutine. This addresses the "no current event loop" errors. """ try: # Try to get the running loop (for current thread/greenlet) loop = asyncio.get_running_loop() except RuntimeError: # If no loop is running, create a new one for this thread/greenlet loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # Run the coroutine until it completes return loop.run_until_complete(coro) @app.route('/run_proxy_task', methods=['POST']) def run_proxy_task_endpoint(): """API endpoint to receive a task and execute it with Proxy-lite.""" data = request.json task = data.get('task') if not task: logger.warning("Received request without 'task' field. Returning 400.") return jsonify({"error": "No 'task' provided in request body"}), 400 logger.info(f"Received task for proxy-lite: '{task}'") try: # Ensure runner is initialized (and event loop handled) before use runner = run_async_task(initialize_runner()) # Run the task using the proxy-lite runner result = run_async_task(runner.run(task)) logger.info(f"Proxy-lite task completed. Output: {result[:200]}...") # Log truncated output # Return the result as a JSON response return jsonify({"output": result}) except Exception as e: logger.exception(f"Error processing task '{task}':") # Logs full traceback for debugging return jsonify({"error": f"An error occurred: {str(e)}. Check logs for details."}), 500 @app.route('/') def root(): """Basic root endpoint for health check and user info.""" logger.info("Root endpoint accessed.") return "Proxy-lite API is running. Send POST requests to /run_proxy_task with a 'task' in JSON body." if __name__ == '__main__': # During local development, ensure HF_API_TOKEN is set manually if not os.environ.get("HF_API_TOKEN"): logger.error("HF_API_TOKEN environment variable is not set. Please set it for local testing.") exit(1) logger.info("Starting Flask development server on 0.0.0.0:7860...") # Hugging Face Spaces expects binding to 0.0.0.0 and listening on port 7860 app.run(host='0.0.0.0', port=7860, debug=True) # debug=True for local testing, disable for production