|
|
| import os |
| import base64 |
| import io |
| import json |
| import asyncio |
| from urllib.parse import urlsplit |
| from fastapi import FastAPI, Header, Request |
| from fastapi.middleware.cors import CORSMiddleware
|
| from fastapi.staticfiles import StaticFiles
|
| from fastapi.responses import FileResponse, StreamingResponse, JSONResponse |
| from starlette.middleware.base import BaseHTTPMiddleware
|
| from pydantic import BaseModel
|
| from PIL import Image
|
| from jade.core import JadeAgent
|
| from jade.scholar import ScholarAgent
|
| from jade.heavy_mode import JadeHeavyAgent
|
| from jade.webdev import WebDevAgent
|
| from jade.analyst import get_analyst
|
| from jade.anything import chat_anything
|
| from jade.imagegen import get_imagegen_agent
|
| from jade.videogen import get_videogen_agent
|
| from jade.vision import get_vision_handler, get_ocr_handler
|
| from jade.storage import r2_storage
|
| from jade.sandbox import code_sandbox
|
| from jade.web_sandbox import WebSandbox
|
| from jade.code_agent import code_jade_agent
|
| from jade import auth
|
| from fastapi.responses import RedirectResponse
|
|
|
| print("Iniciando a J.A.D.E. com FastAPI...")
|
| jade_agent = JadeAgent()
|
| scholar_agent = ScholarAgent()
|
|
|
| jade_heavy_agent = JadeHeavyAgent()
|
|
|
| webdev_agent = WebDevAgent()
|
|
|
| analyst_agent = None
|
|
|
| web_sandbox = None
|
|
|
| print("J.A.D.E. pronta para receber requisições.")
|
|
|
| app = FastAPI(title="J.A.D.E. API") |
|
|
|
|
| def _parse_allowed_origins() -> list[str]: |
| def normalize_origin(value: str) -> str: |
| parsed = urlsplit(value.strip()) |
| if parsed.scheme and parsed.netloc: |
| return f"{parsed.scheme}://{parsed.netloc}" |
| return value.strip().rstrip("/") |
|
|
| configured = os.getenv("ALLOWED_ORIGINS", "") |
| if configured.strip(): |
| return [normalize_origin(origin) for origin in configured.split(",") if origin.strip()] |
|
|
| defaults = [ |
| os.getenv("FRONTEND_URL", "https://gabrielyukio2205-lgtm.github.io/github.io"), |
| "http://localhost:3000", |
| "http://127.0.0.1:3000", |
| "http://localhost:5500", |
| "http://127.0.0.1:5500", |
| "http://localhost:7860", |
| "http://127.0.0.1:7860", |
| ] |
| origins = [] |
| for origin in defaults: |
| normalized = normalize_origin(origin) |
| if normalized and normalized not in origins: |
| origins.append(normalized) |
| return origins |
|
|
|
|
| FRONTEND_URL = os.getenv("FRONTEND_URL", "https://gabrielyukio2205-lgtm.github.io/github.io").rstrip("/") |
| BACKEND_URL = os.getenv("BACKEND_URL", "https://madras1-jade-port.hf.space").rstrip("/") |
| ALLOWED_ORIGINS = _parse_allowed_origins() |
|
|
|
|
| class COOPCOEPMiddleware(BaseHTTPMiddleware):
|
| async def dispatch(self, request, call_next):
|
| response = await call_next(request)
|
|
|
| if request.url.path.startswith("/webdev") or request.url.path.startswith("/static"):
|
| response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
|
| response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
| return response
|
|
|
| app.add_middleware(COOPCOEPMiddleware) |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=ALLOWED_ORIGINS, |
| allow_credentials=True, |
| allow_methods=["GET", "POST", "DELETE", "OPTIONS"], |
| allow_headers=["Authorization", "Content-Type", "X-CSRF-Token"], |
| ) |
|
|
|
|
| os.makedirs("backend/generated", exist_ok=True)
|
| app.mount("/generated", StaticFiles(directory="backend/generated"), name="generated")
|
|
|
|
|
| if os.path.isdir("frontend"):
|
| app.mount("/static", StaticFiles(directory="frontend"), name="static")
|
|
|
| |
| |
| user_sessions = {} |
| user_session_meta = {} |
| MAX_IN_MEMORY_SESSIONS = int(os.getenv("MAX_IN_MEMORY_SESSIONS", "200")) |
| SESSION_TTL_SECONDS = int(os.getenv("USER_SESSION_TTL_SECONDS", str(24 * 60 * 60))) |
|
|
|
|
| def _touch_user_session(user_id: str) -> None: |
| user_session_meta[user_id] = asyncio.get_event_loop().time() |
|
|
|
|
| def _prune_user_sessions() -> None: |
| now = asyncio.get_event_loop().time() |
|
|
| expired_user_ids = [ |
| user_id for user_id, last_seen in user_session_meta.items() |
| if now - last_seen > SESSION_TTL_SECONDS |
| ] |
| for user_id in expired_user_ids: |
| user_sessions.pop(user_id, None) |
| user_session_meta.pop(user_id, None) |
|
|
| if len(user_session_meta) <= MAX_IN_MEMORY_SESSIONS: |
| return |
|
|
| overflow = len(user_session_meta) - MAX_IN_MEMORY_SESSIONS |
| oldest_users = sorted(user_session_meta.items(), key=lambda item: item[1])[:overflow] |
| for user_id, _ in oldest_users: |
| user_sessions.pop(user_id, None) |
| user_session_meta.pop(user_id, None) |
|
|
| |
|
|
| def _resolve_user(request: Request, authorization: str = None): |
| return auth.resolve_request_auth( |
| authorization=authorization, |
| session_token=request.cookies.get(auth.SESSION_COOKIE_NAME), |
| ) |
|
|
|
|
| def _csrf_failed(request: Request, auth_method: str | None, csrf_token: str | None) -> bool: |
| if auth_method != "cookie": |
| return False |
|
|
| user = auth.get_user_from_request(session_token=request.cookies.get(auth.SESSION_COOKIE_NAME)) |
| return not auth.validate_csrf_token(user, csrf_token) |
|
|
| @app.get("/auth/status")
|
| async def auth_status():
|
| """Check if GitHub OAuth is configured."""
|
| return {"configured": auth.is_auth_configured()}
|
|
|
| @app.get("/auth/login") |
| async def auth_login(): |
| """Redirect to GitHub OAuth login.""" |
| if not auth.is_auth_configured(): |
| return {"error": "GitHub OAuth not configured"} |
| |
| redirect_uri = f"{BACKEND_URL}/auth/callback" |
| state = auth.create_oauth_state() |
| login_url = auth.get_login_url(redirect_uri, state=state) |
| response = RedirectResponse(url=login_url, status_code=302) |
| auth.set_oauth_state_cookie(response, state, BACKEND_URL) |
| return response |
|
|
| @app.get("/auth/callback") |
| async def auth_callback(request: Request, code: str = None, error: str = None, state: str = None): |
| """Handle GitHub OAuth callback."""
|
| import logging
|
|
|
| if error: |
| logging.error(f"OAuth error: {error}") |
| response = RedirectResponse(url=f"{FRONTEND_URL}/login.html?error={error}", status_code=302) |
| auth.clear_oauth_state_cookie(response, BACKEND_URL) |
| return response |
| |
| if not code: |
| logging.error("No code received") |
| response = RedirectResponse(url=f"{FRONTEND_URL}/login.html?error=no_code", status_code=302) |
| auth.clear_oauth_state_cookie(response, BACKEND_URL) |
| return response |
|
|
| expected_state = request.cookies.get(auth.OAUTH_STATE_COOKIE_NAME) |
| if not state or not expected_state or state != expected_state: |
| logging.error("OAuth state validation failed") |
| response = RedirectResponse(url=f"{FRONTEND_URL}/login.html?error=invalid_state", status_code=302) |
| auth.clear_oauth_state_cookie(response, BACKEND_URL) |
| return response |
|
|
|
|
| redirect_uri = f"{BACKEND_URL}/auth/callback"
|
| access_token = await auth.exchange_code_for_token(code, redirect_uri) |
| |
| if not access_token: |
| logging.error("Token exchange failed") |
| response = RedirectResponse(url=f"{FRONTEND_URL}/login.html?error=token_exchange_failed", status_code=302) |
| auth.clear_oauth_state_cookie(response, BACKEND_URL) |
| return response |
|
|
|
|
| user = await auth.get_github_user(access_token) |
| |
| if not user: |
| logging.error("Failed to get user") |
| response = RedirectResponse(url=f"{FRONTEND_URL}/login.html?error=user_fetch_failed", status_code=302) |
| auth.clear_oauth_state_cookie(response, BACKEND_URL) |
| return response |
|
|
|
|
| jwt_token = auth.create_jwt_token(user)
|
|
|
|
|
| final_url = f"{FRONTEND_URL}/apps.html" |
| logging.info(f"✅ OAuth success! Redirecting to: {final_url[:100]}...")
|
|
|
|
|
| response = RedirectResponse(url=f"{FRONTEND_URL}/apps.html", status_code=302) |
| auth.set_session_cookie(response, jwt_token, FRONTEND_URL, BACKEND_URL) |
| auth.clear_oauth_state_cookie(response, BACKEND_URL) |
| logging.info("OAuth success! Secure session cookie issued.") |
| return response |
|
|
| @app.get("/auth/me") |
| async def auth_me(request: Request, authorization: str = Header(default=None)): |
| """Get current user info from JWT header or secure cookie.""" |
| user, auth_method = _resolve_user(request, authorization) |
| |
| if not user: |
| return {"authenticated": False, "user": None} |
| |
| return { |
| "authenticated": True, |
| "auth_method": auth_method, |
| "csrf_token": user.get("csrf") if auth_method == "cookie" else None, |
| "user": { |
| "id": user.get("sub"), |
| "login": user.get("login"), |
| "name": user.get("name"), |
| "avatar": user.get("avatar"), |
| } |
| } |
|
|
| @app.post("/auth/logout") |
| async def auth_logout(request: Request, authorization: str = Header(default=None), x_csrf_token: str = Header(default=None, alias="X-CSRF-Token")): |
| """Clear the current auth session.""" |
| _, auth_method = _resolve_user(request, authorization) |
| if _csrf_failed(request, auth_method, x_csrf_token): |
| return JSONResponse({"success": False, "error": "CSRF validation failed"}, status_code=403) |
|
|
| response = JSONResponse({"success": True}) |
| auth.clear_session_cookie(response, FRONTEND_URL, BACKEND_URL) |
| auth.clear_oauth_state_cookie(response, BACKEND_URL) |
| return response |
|
|
| @app.get("/login") |
| async def login_page(): |
| """Serve login page."""
|
| return FileResponse("frontend/login.html")
|
|
|
| class UserRequest(BaseModel):
|
| user_input: str
|
| image_base64: str | None = None
|
| user_id: str | None = None
|
| agent_type: str = "jade"
|
| jade_model: str = "high"
|
| web_search: bool = False
|
|
|
| @app.post("/chat")
|
| async def handle_chat(request: UserRequest):
|
| try: |
| user_id = request.user_id if request.user_id else "default_user" |
| agent_type = request.agent_type.lower() |
| _prune_user_sessions() |
| _touch_user_session(user_id) |
| |
| if user_id not in user_sessions: |
| print(f"Nova sessão criada para: {user_id}")
|
| user_sessions[user_id] = {
|
| "jade": [jade_agent.system_prompt],
|
| "scholar": [],
|
| "heavy": []
|
| }
|
|
|
|
|
| if "jade" not in user_sessions[user_id]: user_sessions[user_id]["jade"] = [jade_agent.system_prompt]
|
| if "scholar" not in user_sessions[user_id]: user_sessions[user_id]["scholar"] = []
|
| if "heavy" not in user_sessions[user_id]: user_sessions[user_id]["heavy"] = []
|
|
|
| vision_context = None
|
| if request.image_base64:
|
| try:
|
|
|
| vision_handler = get_vision_handler()
|
| vision_context = await vision_handler.analyze_image(request.image_base64)
|
| except Exception as img_e:
|
| print(f"Erro ao processar imagem com Pixtral: {img_e}")
|
| vision_context = "Houve um erro ao analisar a imagem."
|
|
|
| final_user_input = request.user_input if request.user_input else "Descreva a imagem em detalhes."
|
|
|
| bot_response_text = ""
|
| reasoning_content = ""
|
| audio_path = None
|
|
|
| if agent_type == "scholar":
|
| current_history = user_sessions[user_id]["scholar"]
|
| bot_response_text, audio_path, updated_history = scholar_agent.respond(
|
| history=current_history,
|
| user_input=final_user_input,
|
| user_id=user_id,
|
| vision_context=vision_context
|
| )
|
| user_sessions[user_id]["scholar"] = updated_history
|
|
|
| elif agent_type == "heavy":
|
| current_history = user_sessions[user_id]["heavy"]
|
|
|
| bot_response_text, audio_path, updated_history = await jade_heavy_agent.respond(
|
| history=current_history,
|
| user_input=final_user_input,
|
| user_id=user_id,
|
| vision_context=vision_context,
|
| web_search=request.web_search
|
| )
|
| user_sessions[user_id]["heavy"] = updated_history
|
|
|
| else:
|
|
|
| current_history = user_sessions[user_id]["jade"]
|
|
|
| bot_response_text, reasoning_content, audio_path, updated_history = await jade_agent.respond(
|
| history=current_history,
|
| user_input=final_user_input,
|
| user_id=user_id,
|
| vision_context=vision_context,
|
| web_search=request.web_search,
|
| jade_model=request.jade_model
|
| )
|
| user_sessions[user_id]["jade"] = updated_history
|
|
|
|
|
| audio_base64 = None
|
| if audio_path and os.path.exists(audio_path):
|
| print(f"Codificando arquivo de áudio: {audio_path}")
|
| with open(audio_path, "rb") as audio_file:
|
| audio_bytes = audio_file.read()
|
| audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
|
|
|
|
|
| if "backend/generated" not in audio_path:
|
| os.remove(audio_path)
|
|
|
| return {
|
| "success": True,
|
| "bot_response": bot_response_text,
|
| "reasoning_content": reasoning_content,
|
| "audio_base64": audio_base64
|
| }
|
| except Exception as e:
|
| print(f"Erro crítico no endpoint /chat: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| class WebDevRequest(BaseModel):
|
| prompt: str
|
| existing_code: str | None = None
|
| mode: str = "html"
|
| error_message: str | None = None
|
| model: str | None = None
|
|
|
| @app.post("/webdev/generate")
|
| async def handle_webdev(request: WebDevRequest):
|
| try:
|
| result = webdev_agent.generate(
|
| prompt=request.prompt,
|
| refine_code=request.existing_code,
|
| mode=request.mode,
|
| error_message=request.error_message,
|
| model=request.model
|
| )
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /webdev: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| @app.get("/webdev")
|
| async def webdev_page():
|
| """Serve webdev.html with COOP/COEP headers for WebContainers."""
|
| if os.path.exists("frontend/webdev.html"):
|
| return FileResponse("frontend/webdev.html")
|
| return {"error": "WebDev frontend not available in this deployment"}
|
|
|
|
|
| class WebProjectRequest(BaseModel):
|
| files: dict
|
| dependencies: dict = {}
|
| prompt: str = ""
|
|
|
| @app.post("/webdev/project")
|
| async def handle_webdev_project(request: WebProjectRequest):
|
| """Build project in E2B sandbox and return preview URL."""
|
| global web_sandbox
|
| try:
|
| if web_sandbox is None:
|
| web_sandbox = WebSandbox()
|
|
|
| if not web_sandbox.is_available():
|
| return {
|
| "success": False,
|
| "error": "E2B not configured. Set E2B_API_KEY."
|
| }
|
|
|
|
|
| async def auto_fix(error_msg: str, current_files: dict) -> dict:
|
| import json
|
| fix_result = webdev_agent.generate(
|
| prompt=request.prompt,
|
| refine_code=json.dumps({"files": current_files, "dependencies": request.dependencies}),
|
| mode="react_project",
|
| error_message=error_msg
|
| )
|
| if fix_result.get("success") and fix_result.get("files"):
|
| return fix_result["files"]
|
| return current_files
|
|
|
| result = await web_sandbox.create_and_build(
|
| files=request.files,
|
| dependencies=request.dependencies,
|
| auto_fix_callback=auto_fix,
|
| max_fix_attempts=2
|
| )
|
| return result
|
|
|
| except Exception as e:
|
| print(f"Erro no endpoint /webdev/project: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| @app.post("/webdev/project/stream")
|
| async def handle_webdev_project_stream(request: WebProjectRequest):
|
| """Build project in E2B sandbox with streaming progress updates (SSE)."""
|
|
|
| async def event_generator():
|
| global web_sandbox
|
| progress_events = []
|
|
|
| async def progress_callback(stage: str, message: str):
|
| event = {"stage": stage, "message": message}
|
| progress_events.append(event)
|
|
|
| try:
|
| if web_sandbox is None:
|
| web_sandbox = WebSandbox()
|
|
|
| if not web_sandbox.is_available():
|
| yield f"data: {json.dumps({'stage': 'error', 'message': 'E2B not configured. Set E2B_API_KEY.'})}\n\n"
|
| return
|
|
|
|
|
| async def auto_fix(error_msg: str, current_files: dict) -> dict:
|
| import json
|
| fix_result = webdev_agent.generate(
|
| prompt=request.prompt,
|
| refine_code=json.dumps({"files": current_files, "dependencies": request.dependencies}),
|
| mode="react_project",
|
| error_message=error_msg
|
| )
|
| if fix_result.get("success") and fix_result.get("files"):
|
| return fix_result["files"]
|
| return current_files
|
|
|
|
|
| yield f"data: {json.dumps({'stage': 'starting', 'message': '🚀 Iniciando build...'})}\n\n"
|
|
|
|
|
| result = await web_sandbox.create_and_build(
|
| files=request.files,
|
| dependencies=request.dependencies,
|
| auto_fix_callback=auto_fix,
|
| max_fix_attempts=2,
|
| progress_callback=progress_callback
|
| )
|
|
|
|
|
| for event in progress_events:
|
| yield f"data: {json.dumps(event)}\n\n"
|
|
|
|
|
| yield f"data: {json.dumps({'stage': 'result', **result})}\n\n"
|
|
|
| except Exception as e:
|
| print(f"Erro no endpoint /webdev/project/stream: {e}")
|
| yield f"data: {json.dumps({'stage': 'error', 'message': str(e)})}\n\n"
|
|
|
| return StreamingResponse(
|
| event_generator(),
|
| media_type="text/event-stream",
|
| headers={
|
| "Cache-Control": "no-cache",
|
| "Connection": "keep-alive",
|
| "X-Accel-Buffering": "no"
|
| }
|
| )
|
|
|
|
|
| class HFDeployRequest(BaseModel):
|
| token: str
|
| files: dict
|
| space_name: str
|
| username: str | None = None
|
|
|
| @app.post("/webdev/deploy")
|
| async def deploy_to_hf(request: HFDeployRequest):
|
| """Deploy generated code to a HuggingFace Static Space."""
|
| try:
|
| from huggingface_hub import HfApi
|
|
|
| api = HfApi(token=request.token)
|
|
|
|
|
| username = request.username or api.whoami()["name"]
|
| repo_id = f"{username}/{request.space_name}"
|
|
|
|
|
| try:
|
| api.delete_repo(repo_id=repo_id, repo_type="space")
|
| print(f"Deleted existing Space: {repo_id}")
|
| except Exception:
|
| pass
|
|
|
|
|
| api.create_repo(
|
| repo_id=repo_id,
|
| repo_type="space",
|
| space_sdk="static",
|
| exist_ok=False
|
| )
|
|
|
|
|
| for filename, content in request.files.items():
|
| api.upload_file(
|
| path_or_fileobj=content.encode('utf-8'),
|
| path_in_repo=filename,
|
| repo_id=repo_id,
|
| repo_type="space"
|
| )
|
|
|
| space_url = f"https://huggingface.co/spaces/{repo_id}"
|
|
|
| return {
|
| "success": True,
|
| "url": space_url,
|
| "repo_id": repo_id
|
| }
|
|
|
| except Exception as e:
|
| print(f"Erro no deploy HF: {e}")
|
| return {
|
| "success": False,
|
| "error": str(e)
|
| }
|
|
|
|
|
| class AnalyzeRequest(BaseModel):
|
| csv_data: str
|
| user_prompt: str = ""
|
| target_col: str | None = None
|
| model_type: str | None = None
|
| only_eda: bool = False
|
|
|
| @app.post("/analyze")
|
| async def handle_analyze(request: AnalyzeRequest):
|
| """Análise de dados: EDA + Treino de modelos."""
|
| global analyst_agent
|
| try:
|
| if analyst_agent is None:
|
| analyst_agent = get_analyst()
|
|
|
| result = analyst_agent.analyze(
|
| csv_data=request.csv_data,
|
| user_prompt=request.user_prompt,
|
| target_col=request.target_col,
|
| model_type=request.model_type,
|
| only_eda=request.only_eda
|
| )
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /analyze: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.post("/analyze/eda")
|
| async def handle_eda(request: AnalyzeRequest):
|
| """Apenas EDA, sem treino."""
|
| global analyst_agent
|
| try:
|
| if analyst_agent is None:
|
| analyst_agent = get_analyst()
|
|
|
| result = analyst_agent.quick_eda(request.csv_data)
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /analyze/eda: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| from typing import Optional, List
|
|
|
| class ScholarIngestRequest(BaseModel):
|
| """Request para ingestão de fontes no Scholar."""
|
| source_type: str
|
| content: str
|
| user_id: str = "default"
|
| notebook_id: Optional[str] = None
|
|
|
| class ScholarGenerateRequest(BaseModel):
|
| """Request para gerar outputs (resumo, podcast, etc)."""
|
| output_type: str
|
| source_ids: List[str]
|
| user_id: str = "default"
|
|
|
| class ScholarChatRequest(BaseModel):
|
| """Request para chat contextual sobre fontes."""
|
| message: str
|
| source_ids: List[str]
|
| user_id: str = "default"
|
| history: List[dict] = []
|
|
|
|
|
| scholar_sources = {}
|
|
|
| @app.post("/scholar/ingest")
|
| async def scholar_ingest(request: ScholarIngestRequest):
|
| """Processa e indexa uma nova fonte (PDF, URL, Tópico, YouTube)."""
|
| try:
|
| import uuid
|
|
|
|
|
| user_id = request.user_id
|
| if user_id not in scholar_sources:
|
| scholar_sources[user_id] = {}
|
|
|
|
|
| u = scholar_agent.get_or_create_state(user_id)
|
|
|
|
|
| source_id = str(uuid.uuid4())[:8]
|
| source_name = ""
|
| extracted_content = ""
|
|
|
| if request.source_type == "pdf":
|
|
|
| import tempfile
|
| pdf_bytes = base64.b64decode(request.content)
|
| with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
| tmp.write(pdf_bytes)
|
| tmp_path = tmp.name
|
|
|
| from jade.scholar import ToolBox
|
| extracted_content = ToolBox.read_pdf(tmp_path)
|
| source_name = f"PDF_{source_id}"
|
| os.remove(tmp_path)
|
|
|
| elif request.source_type == "url":
|
| from jade.scholar import ToolBox
|
| extracted_content = ToolBox.scrape_web(request.content)
|
| source_name = request.content[:50]
|
|
|
| elif request.source_type == "youtube":
|
| from jade.scholar import ToolBox
|
| extracted_content = ToolBox.get_youtube_transcript(request.content)
|
| source_name = f"YouTube_{source_id}"
|
|
|
| elif request.source_type == "topic":
|
|
|
| extracted_content = u["researcher"].deep_research(request.content)
|
| source_name = request.content[:50]
|
|
|
| if not extracted_content or len(extracted_content) < 50:
|
| return {"success": False, "error": "Não foi possível extrair conteúdo suficiente da fonte."}
|
|
|
|
|
| u["memory"].ingest(extracted_content)
|
|
|
|
|
| scholar_sources[user_id][source_id] = {
|
| "id": source_id,
|
| "type": request.source_type,
|
| "name": source_name,
|
| "content": extracted_content[:500] + "...",
|
| "full_content": extracted_content,
|
| "vectorized": True
|
| }
|
|
|
|
|
| r2_storage.save_source(user_id, source_id, {
|
| "id": source_id,
|
| "type": request.source_type,
|
| "name": source_name,
|
| "content": extracted_content,
|
| "created_at": __import__('datetime').datetime.now().isoformat()
|
| })
|
|
|
| return {
|
| "success": True,
|
| "source_id": source_id,
|
| "source_name": source_name,
|
| "preview": extracted_content[:300],
|
| "char_count": len(extracted_content)
|
| }
|
|
|
| except Exception as e:
|
| print(f"Erro no endpoint /scholar/ingest: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.get("/scholar/sources")
|
| async def scholar_list_sources(user_id: str = "default"):
|
| """Lista todas as fontes do usuário."""
|
| try:
|
| sources = scholar_sources.get(user_id, {})
|
|
|
|
|
| if not sources and r2_storage.enabled:
|
| r2_sources = r2_storage.list_sources(user_id)
|
| for src in r2_sources:
|
| if user_id not in scholar_sources:
|
| scholar_sources[user_id] = {}
|
| scholar_sources[user_id][src["id"]] = {
|
| "id": src["id"],
|
| "type": src["type"],
|
| "name": src["name"],
|
| "content": src.get("content", "")[:500] + "...",
|
| "full_content": src.get("content", ""),
|
| "vectorized": True
|
| }
|
| sources = scholar_sources.get(user_id, {})
|
|
|
| return {
|
| "success": True,
|
| "sources": [
|
| {
|
| "id": s["id"],
|
| "type": s["type"],
|
| "name": s["name"],
|
| "preview": s["content"]
|
| }
|
| for s in sources.values()
|
| ]
|
| }
|
| except Exception as e:
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.delete("/scholar/source/{source_id}")
|
| async def scholar_delete_source(source_id: str, user_id: str = "default"):
|
| """Remove uma fonte."""
|
| try:
|
| if user_id in scholar_sources and source_id in scholar_sources[user_id]:
|
| del scholar_sources[user_id][source_id]
|
|
|
|
|
| r2_storage.delete_source(user_id, source_id)
|
|
|
| return {"success": True}
|
| except Exception as e:
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| @app.post("/scholar/source/{source_id}/delete")
|
| async def scholar_delete_source_post(source_id: str, user_id: str = "default"):
|
| """Remove uma fonte (POST fallback for proxy)."""
|
| return await scholar_delete_source(source_id, user_id)
|
|
|
| @app.post("/scholar/generate")
|
| async def scholar_generate(request: ScholarGenerateRequest):
|
| """Gera output baseado nas fontes selecionadas."""
|
| try:
|
| user_id = request.user_id
|
| u = scholar_agent.get_or_create_state(user_id)
|
|
|
|
|
| combined_content = ""
|
| for source_id in request.source_ids:
|
| if user_id in scholar_sources and source_id in scholar_sources[user_id]:
|
| combined_content += scholar_sources[user_id][source_id].get("full_content", "") + "\n\n"
|
|
|
| if not combined_content:
|
| return {"success": False, "error": "Nenhuma fonte válida selecionada"}
|
|
|
| result = {"success": True, "type": request.output_type}
|
|
|
| if request.output_type == "summary":
|
| summary = u["professor"].summarize(combined_content)
|
| result["content"] = summary
|
|
|
| elif request.output_type == "podcast":
|
| script = u["scriptwriter"].create_script(combined_content, mode="lecture")
|
| from jade.scholar import ToolBox
|
| import uuid
|
| filename = f"podcast_{uuid.uuid4().hex[:8]}.mp3"
|
| audio_path = ToolBox.generate_audio_mix(script, filename)
|
|
|
|
|
| if audio_path and os.path.exists(audio_path):
|
| with open(audio_path, "rb") as f:
|
| audio_base64 = base64.b64encode(f.read()).decode('utf-8')
|
| result["audio_base64"] = audio_base64
|
| result["script"] = script
|
| else:
|
| return {"success": False, "error": "Falha ao gerar áudio"}
|
|
|
| elif request.output_type == "quiz":
|
| quiz = u["examiner"].generate_quiz(combined_content)
|
| result["questions"] = quiz
|
|
|
| elif request.output_type == "flashcards":
|
| cards = u["flashcarder"].create_deck(combined_content)
|
| if cards:
|
| from jade.scholar import ToolBox
|
| apkg_path = ToolBox.generate_anki_deck(cards)
|
| if apkg_path and os.path.exists(apkg_path):
|
| with open(apkg_path, "rb") as f:
|
| result["file_base64"] = base64.b64encode(f.read()).decode('utf-8')
|
| result["cards"] = cards
|
| result["filename"] = os.path.basename(apkg_path)
|
| else:
|
| return {"success": False, "error": "Falha ao gerar flashcards"}
|
|
|
| elif request.output_type == "mindmap":
|
| from jade.scholar import ToolBox
|
| import uuid
|
| dot_code = u["visualizer"].create_mindmap(combined_content)
|
| filename = f"mindmap_{uuid.uuid4().hex[:8]}"
|
| image_path = ToolBox.generate_mindmap_image(dot_code, filename)
|
|
|
| if image_path and os.path.exists(image_path):
|
| with open(image_path, "rb") as f:
|
| result["image_base64"] = base64.b64encode(f.read()).decode('utf-8')
|
| result["dot_code"] = dot_code
|
| else:
|
| return {"success": False, "error": "Falha ao gerar mapa mental"}
|
|
|
| return result
|
|
|
| except Exception as e:
|
| print(f"Erro no endpoint /scholar/generate: {e}")
|
| import traceback
|
| traceback.print_exc()
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.post("/scholar/chat")
|
| async def scholar_chat(request: ScholarChatRequest):
|
| """Chat contextual usando RAG sobre as fontes selecionadas."""
|
| try:
|
| user_id = request.user_id
|
| u = scholar_agent.get_or_create_state(user_id)
|
|
|
|
|
| context = u["memory"].retrieve(request.message, k=3)
|
|
|
|
|
| messages = request.history.copy()
|
| if context:
|
| messages.append({
|
| "role": "system",
|
| "content": f"Use o seguinte contexto das fontes do usuário para responder:\n\n{context}"
|
| })
|
| messages.append({"role": "user", "content": request.message})
|
|
|
|
|
| response = u["llm"].chat(messages)
|
|
|
| return {
|
| "success": True,
|
| "response": response,
|
| "context_used": context[:500] if context else None
|
| }
|
|
|
| except Exception as e:
|
| print(f"Erro no endpoint /scholar/chat: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| @app.get("/scholar")
|
| async def scholar_page():
|
| """Serve scholar.html page."""
|
| if os.path.exists("frontend/scholar.html"):
|
| return FileResponse("frontend/scholar.html")
|
| return {"error": "Scholar frontend not available yet"}
|
|
|
|
|
| class AnythingRequest(BaseModel):
|
| provider: str
|
| model: str
|
| messages: list
|
| temperature: float = 0.7
|
| max_tokens: int = 4096
|
|
|
| @app.post("/anything")
|
| async def handle_anything(request: AnythingRequest):
|
| """Multi-provider chat endpoint."""
|
| try:
|
| result = await chat_anything.chat(
|
| provider=request.provider,
|
| model=request.model,
|
| messages=request.messages,
|
| temperature=request.temperature,
|
| max_tokens=request.max_tokens
|
| )
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /anything: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| class ImageGenRequest(BaseModel):
|
| prompt: str
|
| model: str = "z-turbo"
|
| negative_prompt: str = ""
|
| guidance_scale: float = 7.5
|
| width: int = 1024
|
| height: int = 1024
|
| images_base64: list[str] | None = None
|
| seed: int | None = None
|
|
|
| @app.post("/imagegen/generate")
|
| async def handle_imagegen(request: ImageGenRequest):
|
| """Generate or edit images using Chutes.ai models."""
|
| try:
|
| agent = get_imagegen_agent()
|
| result = await agent.generate(
|
| prompt=request.prompt,
|
| model=request.model,
|
| negative_prompt=request.negative_prompt,
|
| guidance_scale=request.guidance_scale,
|
| width=request.width,
|
| height=request.height,
|
| images_base64=request.images_base64,
|
| seed=request.seed
|
| )
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /imagegen: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.get("/imagegen/models")
|
| async def get_imagegen_models():
|
| """Get available image generation models."""
|
| agent = get_imagegen_agent()
|
| return {"success": True, "models": agent.get_models()}
|
|
|
|
|
| class VideoGenRequest(BaseModel):
|
| prompt: str
|
| mode: str = "t2v"
|
| model: str = "ltx-2"
|
| image_base64: str | None = None
|
| duration: int = 5
|
| resolution: str = "480p"
|
| aspect_ratio: str = "16:9"
|
| camera_motion: str = "none"
|
| guidance_scale: float = 3.0
|
| seed: int | None = None
|
|
|
| @app.post("/videogen/generate")
|
| async def handle_videogen(request: VideoGenRequest):
|
| """Generate videos using LTX-2 or Wan 2.2 on Chutes.ai."""
|
| try:
|
| agent = get_videogen_agent()
|
| result = await agent.generate(
|
| prompt=request.prompt,
|
| mode=request.mode,
|
| model=request.model,
|
| image_base64=request.image_base64,
|
| duration=request.duration,
|
| resolution=request.resolution,
|
| aspect_ratio=request.aspect_ratio,
|
| camera_motion=request.camera_motion,
|
| guidance_scale=request.guidance_scale,
|
| seed=request.seed
|
| )
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /videogen: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.get("/videogen/options")
|
| async def get_videogen_options():
|
| """Get available video generation options."""
|
| agent = get_videogen_agent()
|
| return {"success": True, "options": agent.get_options()}
|
|
|
|
|
| class OCRRequest(BaseModel):
|
| file_base64: str
|
| file_type: str = "image"
|
|
|
| @app.post("/ocr")
|
| async def handle_ocr(request: OCRRequest):
|
| """Extract text from documents using Mistral OCR API."""
|
| try:
|
| ocr_handler = get_ocr_handler()
|
| result = await ocr_handler.extract_text(
|
| file_base64=request.file_base64,
|
| file_type=request.file_type
|
| )
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /ocr: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
|
|
| class SandboxRequest(BaseModel):
|
| code: str
|
| timeout: int = 30
|
|
|
| @app.post("/sandbox/execute")
|
| async def handle_sandbox_execute(request: SandboxRequest):
|
| """Execute Python code in E2B sandbox."""
|
| try:
|
| if not code_sandbox.is_available():
|
| return {
|
| "success": False,
|
| "error": "E2B Sandbox not configured. Set E2B_API_KEY environment variable."
|
| }
|
|
|
| result = await code_sandbox.execute_code(
|
| code=request.code,
|
| timeout=request.timeout
|
| )
|
| return result
|
| except Exception as e:
|
| print(f"Erro no endpoint /sandbox: {e}")
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.get("/sandbox/status")
|
| async def get_sandbox_status():
|
| """Check if E2B sandbox is available."""
|
| return {
|
| "available": code_sandbox.is_available(),
|
| "provider": "E2B" if code_sandbox.is_available() else None
|
| }
|
|
|
|
|
|
|
| class CodeJadeChatRequest(BaseModel):
|
| message: str
|
| context: str = ""
|
| repo: str = None
|
| file: str = None
|
| file_content: str = None
|
|
|
| class CodeJadeContextRequest(BaseModel):
|
| context: str
|
|
|
| @app.post("/codejade/chat")
|
| async def handle_codejade_chat(payload: CodeJadeChatRequest, request: Request, authorization: str = Header(default=None), x_csrf_token: str = Header(default=None, alias="X-CSRF-Token")): |
| """Chat with CodeJade agent. Requires authentication."""
|
| try:
|
| user, auth_method = _resolve_user(request, authorization) |
| if _csrf_failed(request, auth_method, x_csrf_token): |
| return JSONResponse({"response": "Validação CSRF falhou.", "tools_used": [], "mode": "error"}, status_code=403) |
|
|
| user_id = user.get("login") or user.get("sub") if user else None |
|
|
| if not user_id:
|
| return {"response": "⚠️ Por favor faça login para usar o CodeJade.", "tools_used": [], "mode": "error"}
|
|
|
|
|
| if code_jade_agent.github:
|
| code_jade_agent.github.set_user(user_id)
|
|
|
|
|
| if payload.repo and code_jade_agent.github: |
| code_jade_agent.github.current_repo = payload.repo |
| print(f"Repo context set for {user_id}: {payload.repo}") |
|
|
|
|
| if payload.file_content or payload.file: |
| smart_context = code_jade_agent.build_context(
|
| message=payload.message, |
| current_file=payload.file, |
| file_content=payload.file_content, |
| user_id=user_id |
| )
|
| if smart_context:
|
| code_jade_agent.set_context(smart_context, user_id=user_id) |
| print(f"🧠 [{user_id}] Built smart context: {len(smart_context)} chars")
|
| elif payload.context: |
|
|
| code_jade_agent.set_context(payload.context, user_id=user_id) |
|
|
| result = await code_jade_agent.chat(payload.message, user_id=user_id) |
| return result
|
| except Exception as e:
|
| print(f"Erro no CodeJade: {e}")
|
| return {"response": f"❌ Erro: {str(e)}", "tools_used": [], "mode": "error"}
|
|
|
| @app.post("/codejade/context") |
| async def set_codejade_context(payload: CodeJadeContextRequest, request: Request, authorization: str = Header(default=None), x_csrf_token: str = Header(default=None, alias="X-CSRF-Token")): |
| """Set CAT context for CodeJade. Requires authentication.""" |
| try: |
| user, auth_method = _resolve_user(request, authorization) |
| if _csrf_failed(request, auth_method, x_csrf_token): |
| return JSONResponse({"success": False, "error": "CSRF validation failed"}, status_code=403) |
|
|
| user_id = user.get("login") or user.get("sub") if user else None |
| if not user_id: |
| return JSONResponse({"success": False, "error": "Unauthorized"}, status_code=401) |
|
|
| code_jade_agent.set_context(payload.context, user_id=user_id) |
| return {"success": True, "message": f"Contexto definido ({len(payload.context)} chars)"} |
| except Exception as e: |
| return {"success": False, "error": str(e)} |
|
|
| @app.get("/codejade/status") |
| async def get_codejade_status(request: Request, authorization: str = Header(default=None)): |
| """Get CodeJade agent status. Requires authentication for repo info."""
|
|
|
| user, _ = _resolve_user(request, authorization) |
| user_id = None |
| current_repo = None |
| |
| if user: |
| user_id = user.get("login") or user.get("sub") |
| if user_id and code_jade_agent.github: |
| code_jade_agent.github.set_user(user_id) |
| current_repo = code_jade_agent.github.current_repo |
|
|
| return {
|
| "available": True,
|
| "sandbox": code_sandbox.is_available(),
|
| "r2": r2_storage.enabled,
|
| "has_context": code_jade_agent.has_context(user_id) if user_id else False, |
| "history_length": code_jade_agent.get_history_length(user_id) if user_id else 0, |
| "current_repo": current_repo,
|
| "authenticated": user_id is not None
|
| }
|
|
|
|
|
|
|
| def get_user_id_from_auth(request: Request, authorization: str = None) -> str: |
| """Extract user_id from JWT token or return None if invalid."""
|
| user, _ = _resolve_user(request, authorization) |
| if not user: |
| print("❌ AUTH: No authorization header")
|
| return None
|
|
|
| print("AUTH: Request authenticated") |
| user_id = user.get("login") or user.get("sub") |
| print(f"✅ AUTH: User authenticated as {user_id}")
|
| return user_id
|
|
|
| @app.get("/codejade/repos")
|
| async def list_codejade_repos(request: Request, authorization: str = Header(default=None)): |
| """List user's cloned repos from R2. Requires auth."""
|
| user_id = get_user_id_from_auth(request, authorization) |
| if not user_id:
|
| return {"success": False, "error": "Unauthorized. Login required.", "repos": []}
|
|
|
| try:
|
| repos = r2_storage.list_user_repos(user_id)
|
| return {"success": True, "repos": repos}
|
| except Exception as e:
|
| return {"success": False, "error": str(e), "repos": []}
|
|
|
| @app.get("/codejade/files/{repo_name}")
|
| async def list_codejade_files(repo_name: str, request: Request, path: str = "", authorization: str = Header(default=None)): |
| """List files in a repo from R2. Requires auth."""
|
| user_id = get_user_id_from_auth(request, authorization) |
| if not user_id:
|
| return {"success": False, "error": "Unauthorized", "files": []}
|
|
|
| try:
|
| files = r2_storage.list_repo_files(user_id, repo_name, path)
|
|
|
|
|
| tree = {}
|
| for f in files:
|
| rel_path = f["path"]
|
| if path:
|
| rel_path = rel_path[len(path):].lstrip("/")
|
|
|
| parts = rel_path.split("/")
|
| if parts[0] and parts[0] != "_metadata.json":
|
| name = parts[0]
|
| is_dir = len(parts) > 1
|
| tree[name] = {"name": name, "type": "dir" if is_dir else "file"}
|
|
|
| return {"success": True, "files": list(tree.values())}
|
| except Exception as e:
|
| return {"success": False, "error": str(e), "files": []}
|
|
|
| @app.get("/codejade/file/{repo_name}/{file_path:path}")
|
| async def get_codejade_file(repo_name: str, file_path: str, request: Request, authorization: str = Header(default=None)): |
| """Get file content from R2. Requires auth."""
|
| user_id = get_user_id_from_auth(request, authorization) |
| if not user_id:
|
| return {"success": False, "error": "Unauthorized"}
|
|
|
| try:
|
| content = r2_storage.get_repo_file(user_id, repo_name, file_path)
|
| if content is None:
|
| return {"success": False, "error": "File not found"}
|
| return {"success": True, "content": content, "path": file_path}
|
| except Exception as e:
|
| return {"success": False, "error": str(e)}
|
|
|
| class CodeJadeSaveFileRequest(BaseModel):
|
| content: str
|
|
|
| @app.post("/codejade/file/{repo_name}/{file_path:path}")
|
| async def save_codejade_file(repo_name: str, file_path: str, payload: CodeJadeSaveFileRequest, request: Request, authorization: str = Header(default=None), x_csrf_token: str = Header(default=None, alias="X-CSRF-Token")): |
| """Save file to R2. Requires auth.""" |
| _, auth_method = _resolve_user(request, authorization) |
| if _csrf_failed(request, auth_method, x_csrf_token): |
| return JSONResponse({"success": False, "error": "CSRF validation failed"}, status_code=403) |
|
|
| user_id = get_user_id_from_auth(request, authorization) |
| if not user_id:
|
| return {"success": False, "error": "Unauthorized"}
|
|
|
| try:
|
| success = r2_storage.save_repo_file(user_id, repo_name, file_path, payload.content) |
| return {"success": success}
|
| except Exception as e:
|
| return {"success": False, "error": str(e)}
|
|
|
| class CodeJadeCloneRequest(BaseModel):
|
| repo_url: str
|
|
|
| @app.post("/codejade/clone")
|
| async def clone_codejade_repo(payload: CodeJadeCloneRequest, request: Request, authorization: str = Header(default=None), x_csrf_token: str = Header(default=None, alias="X-CSRF-Token")): |
| """Clone a repo directly. Requires auth.""" |
| _, auth_method = _resolve_user(request, authorization) |
| if _csrf_failed(request, auth_method, x_csrf_token): |
| return JSONResponse({"success": False, "error": "CSRF validation failed"}, status_code=403) |
|
|
| user_id = get_user_id_from_auth(request, authorization) |
| if not user_id:
|
| return {"success": False, "error": "Unauthorized. Login required."}
|
|
|
| try:
|
| if code_jade_agent.github: |
| code_jade_agent.github.set_user(user_id) |
| result = await code_jade_agent.github.clone(payload.repo_url) |
| return result |
| return {"success": False, "error": "GitHub tools not available"}
|
| except Exception as e:
|
| return {"success": False, "error": str(e)}
|
|
|
| @app.get("/")
|
| def root():
|
| return {"message": "Servidor J.A.D.E. com FastAPI está online."}
|
|
|
| if __name__ == "__main__":
|
| import uvicorn
|
| port = int(os.environ.get("PORT", 7860))
|
| print(f"Iniciando o servidor Uvicorn em http://0.0.0.0:{port}")
|
| uvicorn.run(app, host="0.0.0.0", port=port)
|
|
|