from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import requests import zipfile import io import os from typing import Dict, List app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # MEMÓRIA TEMPORÁRIA DO PROJETO projeto_atual = { "url": "", "arquivos": {}, # {"lib/main.dart": "conteúdo..."} "estrutura": [] } # Cliente de IA - MODELO LEVE QUE FUNCIONA EM CPU GRATUITA from huggingface_hub import InferenceClient client = InferenceClient() class LinkGitHub(BaseModel): url: str class Mensagem(BaseModel): mensagem: str class EditarArquivo(BaseModel): caminho: str novo_conteudo: str def baixar_github(url: str) -> tuple[Dict[str, str], List[str]]: """Baixa TODOS os arquivos de um repo GitHub público""" # Extrai owner/repo url = url.rstrip('/') if url.endswith('.git'): url = url[:-4] parts = url.replace('https://github.com/', '').split('/') if len(parts) < 2: raise ValueError("URL inválida") owner, repo = parts[0], parts[1] # Tenta main primeiro, depois master for branch in ['main', 'master']: api_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1" resp = requests.get(api_url) if resp.status_code == 200: break else: raise ValueError("Repositório não encontrado ou privado") tree = resp.json().get('tree', []) arquivos = {} estrutura = [] for item in tree: path = item['path'] estrutura.append(path) # Ignora node_modules, .git, build, etc if any(x in path for x in ['node_modules', '.git/', 'build/', '.gradle', '.idea']): continue if item['type'] == 'blob': # Baixa arquivo raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}" try: file_resp = requests.get(raw_url, timeout=5) if file_resp.status_code == 200: # Tenta decodificar como texto try: arquivos[path] = file_resp.content.decode('utf-8') except: # Binário - ignora pass except: pass return arquivos, estrutura @app.post("/carregar-projeto") async def carregar_projeto(data: LinkGitHub): """Carrega projeto do GitHub""" try: arquivos, estrutura = baixar_github(data.url) projeto_atual['url'] = data.url projeto_atual['arquivos'] = arquivos projeto_atual['estrutura'] = estrutura return { "status": "sucesso", "total_arquivos": len(arquivos), "estrutura": estrutura } except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.get("/estrutura") def get_estrutura(): """Retorna estrutura de pastas/arquivos""" return {"estrutura": projeto_atual['estrutura']} @app.get("/arquivo/{caminho:path}") def get_arquivo(caminho: str): """Pega conteúdo de um arquivo""" if caminho in projeto_atual['arquivos']: return {"conteudo": projeto_atual['arquivos'][caminho]} raise HTTPException(status_code=404, detail="Arquivo não encontrado") @app.post("/editar-arquivo") def editar_arquivo(data: EditarArquivo): """Salva edição de arquivo""" projeto_atual['arquivos'][data.caminho] = data.novo_conteudo return {"status": "salvo"} @app.post("/chat") async def chat(data: Mensagem): """Chat com IA que conhece o projeto""" if not projeto_atual['arquivos']: return {"resposta": "Carregue um projeto primeiro!"} # Monta contexto compacto com arquivos principais contexto = f"PROJETO: {projeto_atual['url']}\n\n" contexto += f"ESTRUTURA:\n{chr(10).join(projeto_atual['estrutura'][:30])}\n\n" contexto += "ARQUIVOS PRINCIPAIS:\n\n" # Adiciona apenas trechos dos arquivos mais relevantes count = 0 for caminho, conteudo in projeto_atual['arquivos'].items(): if caminho.endswith(('.dart', '.yaml', '.json')): if count < 8: contexto += f"--- {caminho} ---\n{conteudo[:1500]}\n\n" count += 1 # Limita tamanho total if len(contexto) > 12000: contexto = contexto[:12000] prompt = f"""{contexto} PERGUNTA: {data.mensagem} Responda de forma clara sobre o código Flutter/Dart.""" try: # Usa modelo pequeno e rápido que REALMENTE funciona em CPU gratuita resposta = client.text_generation( prompt, model="bigcode/starcoder2-3b", max_new_tokens=600, temperature=0.5, ) return {"resposta": resposta} except Exception as e: # Fallback para modelo ainda menor try: resposta = client.text_generation( prompt, model="Salesforce/codegen-350M-mono", max_new_tokens=400, ) return {"resposta": resposta} except: return {"resposta": f"IA indisponível no momento. Erro: {str(e)}"} @app.get("/baixar-projeto") def baixar_projeto(): """Gera ZIP do projeto modificado""" if not projeto_atual['arquivos']: raise HTTPException(status_code=400, detail="Nenhum projeto carregado") # Cria ZIP em memória zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: for caminho, conteudo in projeto_atual['arquivos'].items(): zip_file.writestr(caminho, conteudo) zip_buffer.seek(0) # Salva temporariamente with open('/tmp/projeto.zip', 'wb') as f: f.write(zip_buffer.getvalue()) return FileResponse( '/tmp/projeto.zip', media_type='application/zip', filename='projeto_modificado.zip' ) @app.get("/") def root(): return {"status": "IA Code Online", "projeto_carregado": bool(projeto_atual['arquivos'])} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)