Spaces:
Sleeping
Sleeping
| """ | |
| environments/trace_env/app.py | |
| FastAPI server β the OpenEnv-standard HTTP interface for TraceEnv. | |
| Endpoints follow the OpenEnv spec: | |
| POST /reset β TraceObservation | |
| POST /step β {observation, reward, done, info} | |
| GET /state β EpisodeState | |
| GET /health β {"status": "ok"} | |
| POST /analyse_image β Full VLM image analysis (LLaMA 4 Scout) | |
| Deploy on HuggingFace Spaces or run locally: | |
| uvicorn environments.trace_env.app:app --reload --port 8000 | |
| """ | |
| from fastapi import FastAPI, HTTPException, UploadFile, File, Form | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import Optional | |
| import yaml, os, logging | |
| logger = logging.getLogger(__name__) | |
| try: | |
| from .core.env import TraceEnv | |
| from .core.schemas import TraceAction, TraceObservation | |
| except ImportError: | |
| from core.env import TraceEnv | |
| from core.schemas import TraceAction, TraceObservation | |
| # ββ Load config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CONFIG_PATH = os.environ.get("TRACE_CONFIG", "configs/env_config.yaml") | |
| try: | |
| with open(CONFIG_PATH) as f: | |
| config = yaml.safe_load(f) | |
| except FileNotFoundError: | |
| config = {} | |
| # ββ Configure image_tool at startup (if enabled) βββββββββββββββββββββββββββββ | |
| _img_cfg = config.get("image_analysis", {}) | |
| if _img_cfg.get("enabled", True): | |
| try: | |
| from environments.trace_env.tools.image_tool import configure as _cfg_img | |
| _cfg_img(_img_cfg) | |
| logger.info("[APP] image_tool configured successfully") | |
| except Exception as _e: | |
| logger.warning(f"[APP] image_tool configuration skipped: {_e}") | |
| # ββ One env instance per server (single-session demo mode) βββββββββββββββββββ | |
| env = TraceEnv(config) | |
| app = FastAPI( | |
| title="Trace β OpenEnv Environment", | |
| description="Federated digital-life RL environment for long-horizon planning.", | |
| version="1.0.0", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ Request/response models ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ResetRequest(BaseModel): | |
| instruction: str | |
| difficulty: str = "easy" | |
| available_sources: list[str] = ["gmail", "image"] | |
| ground_truth: dict = {} | |
| class StepResponse(BaseModel): | |
| observation: dict | |
| reward: float | |
| done: bool | |
| info: dict | |
| # ββ Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def health(): | |
| img_cfg = config.get("image_analysis", {}) | |
| return { | |
| "status": "ok", | |
| "env": "TraceEnv", | |
| "version": "1.0.0", | |
| "capabilities": { | |
| "image_analysis": img_cfg.get("enabled", True), | |
| "image_model": img_cfg.get("model_id", "meta-llama/Llama-4-Scout-17B-16E-Instruct"), | |
| "gmail_attachments": img_cfg.get("analyse_gmail_attachments", True), | |
| }, | |
| } | |
| def reset(req: ResetRequest): | |
| """Start a new episode.""" | |
| task = req.dict() | |
| obs = env.reset(task) | |
| return obs.dict() | |
| async def step(action: TraceAction): | |
| """Execute one agent action (Async to prevent blocking).""" | |
| try: | |
| import anyio | |
| obs, reward, done, info = await anyio.to_thread.run_sync(env.step, action) | |
| except AssertionError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| return StepResponse( | |
| observation=obs.dict(), | |
| reward=reward, | |
| done=done, | |
| info=info, | |
| ) | |
| def state(): | |
| """Return full episode state for debugging.""" | |
| s = env.state() | |
| if s is None: | |
| raise HTTPException(status_code=400, detail="No active episode. Call /reset first.") | |
| return { | |
| "episode_id": s.episode_id, | |
| "steps": s.steps, | |
| "plan": s.plan, | |
| "retrieved_count": len(s.retrieved_data), | |
| "verified": s.verified, | |
| "done": s.done, | |
| } | |
| def observation_prompt(): | |
| """Return current observation as a ready-to-use LLM prompt string.""" | |
| s = env.state() | |
| if s is None: | |
| raise HTTPException(status_code=400, detail="Call /reset first.") | |
| obs = env._build_obs("(current state)") | |
| return {"prompt": obs.to_prompt()} | |
| # ββ Image Analysis Endpoint ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ImageAnalyseRequest(BaseModel): | |
| """ | |
| Request body for POST /analyse_image. | |
| source_type: "url" β http/https URL or data: URL | |
| "path" β absolute local file path on the server | |
| "base64" β raw base64-encoded image bytes | |
| data: The image reference (URL string, path string, or base64 string). | |
| question: Optional specific question to ask about the image. | |
| If omitted, runs full OCR + entity extraction. | |
| """ | |
| source_type: Optional[str] = None # auto-detected if None | |
| data: str # URL / path / base64 | |
| question: Optional[str] = None | |
| def analyse_image(req: ImageAnalyseRequest): | |
| """ | |
| Analyse an image using LLaMA 4 Scout (HuggingFace Inference API). | |
| Returns extracted text, entities (amounts, dates, vendors, items), | |
| and a natural language summary of the image content. | |
| Requires HF_TOKEN environment variable set to your HuggingFace token. | |
| Example request: | |
| { | |
| "data": "https://example.com/receipt.jpg", | |
| "question": "What is the total amount on this receipt?" | |
| } | |
| """ | |
| img_cfg = config.get("image_analysis", {}) | |
| if not img_cfg.get("enabled", True): | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Image analysis is disabled. Set image_analysis.enabled: true in env_config.yaml." | |
| ) | |
| try: | |
| from environments.trace_env.tools.image_tool import analyse_image as _analyse | |
| result = _analyse( | |
| source=req.data, | |
| question=req.question, | |
| source_type=req.source_type, | |
| ) | |
| # Also inject result into the active world model if episode is running | |
| if env.state() is not None: | |
| env.world_model.inject_image_analysis(result) | |
| logger.info(f"[APP] Image analysis injected into world model: {result['id']}") | |
| if result.get("error"): | |
| raise HTTPException( | |
| status_code=422, | |
| detail=f"Image analysis failed: {result['error']}" | |
| ) | |
| return result | |
| except HTTPException: | |
| raise | |
| except EnvironmentError as e: | |
| raise HTTPException( | |
| status_code=401, | |
| detail=str(e) + " Set HF_TOKEN environment variable." | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Internal error: {e}") | |
| def analyse_gmail_attachment(message_id: str, filename: Optional[str] = None): | |
| """ | |
| Download and analyse image attachments from a specific Gmail message. | |
| Args: | |
| message_id: Gmail message ID (from /step RETRIEVE gmail results). | |
| filename: Optional β only analyse the attachment matching this filename. | |
| Returns: | |
| List of image analysis results for all image attachments in the message. | |
| """ | |
| img_cfg = config.get("image_analysis", {}) | |
| if not img_cfg.get("enabled", True): | |
| raise HTTPException(status_code=503, detail="Image analysis is disabled.") | |
| try: | |
| from environments.trace_env.tools.gmail_tool import fetch_gmail_attachments | |
| attachments = fetch_gmail_attachments(message_id, analyse_images=True) | |
| results = [] | |
| for att in attachments: | |
| if filename and att["filename"] != filename: | |
| continue | |
| if att.get("image_analysis"): | |
| # Inject into world model | |
| if env.state() is not None: | |
| env.world_model.inject_image_analysis(att["image_analysis"]) | |
| results.append(att["image_analysis"]) | |
| if not results: | |
| return {"message": "No image attachments found or analysed.", "attachments_found": len(attachments)} | |
| return {"analyses": results, "count": len(results)} | |
| except EnvironmentError as e: | |
| raise HTTPException(status_code=401, detail=str(e)) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Internal error: {e}") | |
| # ββ Document Extraction Endpoint βββββββββββββββββββββββββββββββββββββββββββββ | |
| async def extract_document_endpoint( | |
| file: UploadFile = File(...), | |
| analyse_images: bool = Form(True) | |
| ): | |
| """ | |
| Extract text and embedded images from a document (PDF, DOCX, PPTX). | |
| """ | |
| try: | |
| from environments.trace_env.tools.doc_tool import extract_document as _extract | |
| file_bytes = await file.read() | |
| result = _extract( | |
| file_bytes=file_bytes, | |
| filename=file.filename, | |
| mime_type=file.content_type or "", | |
| analyse_images=analyse_images | |
| ) | |
| # Inject result into the active world model if episode is running | |
| if env.state() is not None: | |
| # We can use the same image analysis injection for any images found | |
| for img_analysis in result.get("image_analyses", []): | |
| env.world_model.inject_image_analysis(img_analysis) | |
| # Since world model might need document text too, we could add a method | |
| # but for now we just log it. | |
| logger.info(f"[APP] Document extracted: {result['id']}") | |
| if result.get("error"): | |
| raise HTTPException( | |
| status_code=422, | |
| detail=f"Document extraction failed: {result['error']}" | |
| ) | |
| return result | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Internal error: {e}") | |