jade_port / app.py
Madras1's picture
Upload 41 files
5b99f07 verified
# app.py - VERSÃO COMPLETA COM VOZ (BASE64) E VISÃO
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()
# Instantiate Heavy Agent. It uses environment variables.
jade_heavy_agent = JadeHeavyAgent()
# Instantiate WebDev Agent
webdev_agent = WebDevAgent()
# Analyst Agent (lazy loaded)
analyst_agent = None
# Web Sandbox (lazy loaded)
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()
# COOP/COEP Middleware for WebContainers support
class COOPCOEPMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
# Only add headers for webdev routes (required for WebContainers)
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"],
)
# Mount generated directory for static files (PDFs, Images)
os.makedirs("backend/generated", exist_ok=True)
app.mount("/generated", StaticFiles(directory="backend/generated"), name="generated")
# Mount frontend static files (only if exists - not in HF Space)
if os.path.isdir("frontend"):
app.mount("/static", StaticFiles(directory="frontend"), name="static")
# Dicionário global para armazenar sessões de usuários
# Structure: user_sessions[user_id] = { "jade": [...], "scholar": [...], "heavy": [...] }
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)
# ========== AUTHENTICATION ENDPOINTS ==========
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
# Exchange code for token
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
# Get user info
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
# Create JWT
jwt_token = auth.create_jwt_token(user)
# Log for debug
final_url = f"{FRONTEND_URL}/apps.html"
logging.info(f"✅ OAuth success! Redirecting to: {final_url[:100]}...")
# Redirect to frontend with token (use 302 Found)
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", "scholar", "heavy"
jade_model: str = "high" # "high" (GLM 4.7), "minimax" (MiniMax M2.1), or "flash" (Cerebras)
web_search: bool = False # Toggle para busca web na J.A.D.E.
@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": []
}
# Ensure sub-keys exist
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:
# Use Pixtral Vision API for image analysis
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 = "" # Reasoning nativo dos modelos TEE
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"]
# Heavy agent is async
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 # Passa web search para Heavy Mode
)
user_sessions[user_id]["heavy"] = updated_history
else:
# Default to J.A.D.E.
current_history = user_sessions[user_id]["jade"]
# Jade agent is now async - retorna (resposta, reasoning, audio, history)
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 # 'high' (GLM 4.7), 'minimax' (MiniMax M2.1), or 'flash' (Cerebras)
)
user_sessions[user_id]["jade"] = updated_history
# Audio Logic
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')
# Remove only if temp file
if "backend/generated" not in audio_path:
os.remove(audio_path)
return {
"success": True,
"bot_response": bot_response_text,
"reasoning_content": reasoning_content, # Reasoning nativo do modelo TEE
"audio_base64": audio_base64
}
except Exception as e:
print(f"Erro crítico no endpoint /chat: {e}")
return {"success": False, "error": str(e)}
# WebDev Vibe Coder endpoint
class WebDevRequest(BaseModel):
prompt: str
existing_code: str | None = None
mode: str = "html" # "html", "react", or "react_project"
error_message: str | None = None # For agentic error fixing
model: str | None = None # Model to use (e.g., "kimi-k2", "claude-sonnet")
@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)}
# WebDev Page Route (for WebContainers headers)
@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"}
# E2B Project Build endpoint
class WebProjectRequest(BaseModel):
files: dict # {filepath: content}
dependencies: dict = {} # {package: version}
prompt: str = "" # Original prompt for auto-fix context
@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."
}
# Auto-fix callback: use WebDevAgent to fix build errors
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)}
# SSE Streaming Project Build endpoint
@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
# Auto-fix callback
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
# Send initial event
yield f"data: {json.dumps({'stage': 'starting', 'message': '🚀 Iniciando build...'})}\n\n"
# Run build — await directly (simpler and reliable)
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
)
# Stream collected progress events
for event in progress_events:
yield f"data: {json.dumps(event)}\n\n"
# Send final result
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" # Disable nginx buffering
}
)
# HuggingFace Space Deploy endpoint
class HFDeployRequest(BaseModel):
token: str
files: dict # {"index.html": "...", "style.css": "..."}
space_name: str
username: str | None = None # If not provided, will be fetched from token
@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)
# Get username from token if not provided
username = request.username or api.whoami()["name"]
repo_id = f"{username}/{request.space_name}"
# Delete existing Space if it exists (to clean old files)
try:
api.delete_repo(repo_id=repo_id, repo_type="space")
print(f"Deleted existing Space: {repo_id}")
except Exception:
pass # Space doesn't exist, that's fine
# Create a fresh static Space
api.create_repo(
repo_id=repo_id,
repo_type="space",
space_sdk="static",
exist_ok=False # Should not exist since we just deleted it
)
# Upload each file
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)
}
# Data Analysis endpoints
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)}
# ========== SCHOLAR NOTEBOOK ==========
from typing import Optional, List
class ScholarIngestRequest(BaseModel):
"""Request para ingestão de fontes no Scholar."""
source_type: str # "pdf", "url", "topic", "youtube"
content: str # Base64 para PDF, URL para url/youtube, texto para topic
user_id: str = "default"
notebook_id: Optional[str] = None # Se None, cria novo
class ScholarGenerateRequest(BaseModel):
"""Request para gerar outputs (resumo, podcast, etc)."""
output_type: str # "summary", "podcast", "quiz", "flashcards", "mindmap"
source_ids: List[str] # IDs das fontes a usar
user_id: str = "default"
class ScholarChatRequest(BaseModel):
"""Request para chat contextual sobre fontes."""
message: str
source_ids: List[str] # Contexto das fontes
user_id: str = "default"
history: List[dict] = []
# In-memory storage for sources (will be replaced by SQLite/IndexedDB)
scholar_sources = {} # user_id -> {source_id: {type, name, content, vectorized}}
@app.post("/scholar/ingest")
async def scholar_ingest(request: ScholarIngestRequest):
"""Processa e indexa uma nova fonte (PDF, URL, Tópico, YouTube)."""
try:
import uuid
# Get or create user storage
user_id = request.user_id
if user_id not in scholar_sources:
scholar_sources[user_id] = {}
# Get the scholar agent state for this user
u = scholar_agent.get_or_create_state(user_id)
# Process based on source type
source_id = str(uuid.uuid4())[:8]
source_name = ""
extracted_content = ""
if request.source_type == "pdf":
# Decode base64 PDF and extract text
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":
# Use deep research
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."}
# Index in vector memory
u["memory"].ingest(extracted_content)
# Store source metadata
scholar_sources[user_id][source_id] = {
"id": source_id,
"type": request.source_type,
"name": source_name,
"content": extracted_content[:500] + "...", # Preview only
"full_content": extracted_content,
"vectorized": True
}
# Persist to R2 Storage
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 memory is empty, try loading from R2
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]
# Also delete from R2
r2_storage.delete_source(user_id, source_id)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
# POST alternative for delete (proxy only supports GET/POST)
@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)
# Combine content from selected sources
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)
# Encode audio to base64
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)
# Retrieve relevant context from vector memory
context = u["memory"].retrieve(request.message, k=3)
# Build messages with context
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})
# Use LLM to respond
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)}
# Scholar page route
@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"}
# ========== CHAT ANYTHING ==========
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)}
# ========== IMAGE GENERATION ==========
class ImageGenRequest(BaseModel):
prompt: str
model: str = "z-turbo" # z-turbo, qwen, qwen-edit
negative_prompt: str = ""
guidance_scale: float = 7.5
width: int = 1024
height: int = 1024
images_base64: list[str] | None = None # For qwen-edit mode (1-3 images)
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()}
# ========== VIDEO GENERATION ==========
class VideoGenRequest(BaseModel):
prompt: str
mode: str = "t2v" # t2v (text-to-video), i2v (image-to-video)
model: str = "ltx-2" # ltx-2 or wan-2.2
image_base64: str | None = None # For i2v mode
duration: int = 5 # Seconds (3-15)
resolution: str = "480p" # 480p, 720p, 1080p
aspect_ratio: str = "16:9" # 16:9, 9:16, 1:1
camera_motion: str = "none" # dolly-in, zoom-out, pan-left, etc.
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()}
# ========== OCR ==========
class OCRRequest(BaseModel):
file_base64: str # Base64 encoded file (image or PDF)
file_type: str = "image" # "image" or "pdf"
@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)}
# ========== CODE SANDBOX (E2B) ==========
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
}
# ==================== CODEJADE ENDPOINTS ====================
class CodeJadeChatRequest(BaseModel):
message: str
context: str = "" # CAT context opcional
repo: str = None # Current repo name
file: str = None # Current file path
file_content: str = None # Content of file open in editor
class CodeJadeContextRequest(BaseModel):
context: str # Markdown context
@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"}
# SECURITY: Set user context before ANY operation
if code_jade_agent.github:
code_jade_agent.github.set_user(user_id)
# Set repo context if provided
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}")
# Build intelligent context (File + AST + RAG)
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:
# Fallback to provided 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."""
# SECURITY: Extract user for per-user repo status
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, # Only returns THIS user's repo
"authenticated": user_id is not None
}
# ========== DIRECT FILE ENDPOINTS (JWT Auth Required) ==========
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)
# Build tree structure
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)