ricsrdocasro's picture
Update app.py
80fbd75 verified
Raw
History Blame Contribute Delete
13.3 kB
import os
import sys
import json
import logging
import random
import hashlib
import re
import chromadb
import pysqlite3
from datetime import datetime
# =============================================================================
# 1. HACK DO SQLITE
# =============================================================================
sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
from huggingface_hub import snapshot_download
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
from openai import OpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
# =============================================================================
# LOGGING & CONFIG
# =============================================================================
def log_force(msg):
print(f"👉 {msg}", flush=True)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="FoddaciTron RAG (Router + Classic Prompt)")
# --- MODELOS ---
MODEL_MAIN = "deepseek-ai/DeepSeek-V3.2"
MODEL_ROUTER = "deepseek-ai/DeepSeek-V3.2"
MODEL_EMBEDDING = "intfloat/multilingual-e5-small"
DATASET_REPO_ID = "ricsrdocasro/FoddaciBrainWiki"
# --- RATE LIMIT ---
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
MAX_INPUT_CHARS = 500 # <--- DEFINA AQUI O LIMITE (500 é um bom tamanho)
@app.exception_handler(RateLimitExceeded)
async def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={"detail": "CALMA AÍ, VICIADO! 🛑 Muita mensagem. Vai tocar grama um pouco."}
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
SILICON_KEY = os.environ.get("SILICONFLOW_API_KEY")
client = OpenAI(api_key=SILICON_KEY, base_url="https://api.siliconflow.com/v1")
# =============================================================================
# 💰 CONTROLE FINANCEIRO & PRIVACIDADE
# =============================================================================
DAILY_QUOTA = 10
USAGE_FILE = "user_tracker.json"
def get_client_ip_hash(request: Request):
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
real_ip = forwarded.split(",")[0]
else:
real_ip = request.client.host
return hashlib.sha256(real_ip.encode()).hexdigest()
def check_budget_limit(request: Request):
user_hash = get_client_ip_hash(request)
today = datetime.now().strftime("%Y-%m-%d")
data = {"date": today, "usage": {}}
if os.path.exists(USAGE_FILE):
try:
with open(USAGE_FILE, "r") as f:
loaded = json.load(f)
if loaded.get("date") == today:
data = loaded
except: pass
user_usage = data["usage"].get(user_hash, 0)
if user_usage >= DAILY_QUOTA:
return False
data["usage"][user_hash] = user_usage + 1
try:
with open(USAGE_FILE, "w") as f:
json.dump(data, f)
except Exception as e:
log_force(f"⚠️ Erro tracker: {e}")
return True
def get_user_usage_count(request: Request):
user_hash = get_client_ip_hash(request)
today = datetime.now().strftime("%Y-%m-%d")
if os.path.exists(USAGE_FILE):
try:
with open(USAGE_FILE, "r") as f:
data = json.load(f)
if data.get("date") == today:
return data["usage"].get(user_hash, 0)
except: pass
return 0
# =============================================================================
# MOTOR PRINCIPAL (FODDACI ENGINE - DUAL BRAIN)
# =============================================================================
class FoddaciEngine:
def __init__(self):
self.wiki_db = None
self.episodes_db = None
self.style_bank = []
self.path_wiki = "foddaci_wiki"
self.path_episodes = "foddaci_db_episodes"
self.style_filename = "style_bank.json"
# CAMINHO LOCAL (Da sua pasta no repositório)
self.local_style_path = "foddaci_db_v2/style_bank.json"
def load_resources(self):
log_force("🚀 INICIANDO SISTEMA...")
# 1. DOWNLOAD DOS BANCOS (EXTERNO)
try:
log_force(f"📥 Baixando DBs de: {DATASET_REPO_ID}...")
snapshot_download(
repo_id=DATASET_REPO_ID,
repo_type="dataset",
local_dir=".",
resume_download=True,
allow_patterns=["foddaci_wiki/*", "foddaci_db_episodes/*"]
)
log_force("✅ Download dos bancos concluído.")
except Exception as e:
log_force(f"❌ Erro Download DB: {e}")
# 2. CARREGA ESTILO (DA PASTA LOCAL 'foddaci_db_v2')
if os.path.exists(self.local_style_path):
try:
with open(self.local_style_path, "r", encoding="utf-8") as f:
data = json.load(f)
self.style_bank = data.get("comments", []) if isinstance(data, dict) else data
log_force(f"🤬 Estilo carregado de '{self.local_style_path}': {len(self.style_bank)} frases.")
except Exception as e:
log_force(f"⚠️ Erro ao ler style_bank: {e}")
else:
log_force(f"⚠️ AVISO: '{self.local_style_path}' não encontrado. Verifique se a pasta subiu no git.")
# 3. EMBEDDINGS (E5)
try:
embeddings = HuggingFaceEmbeddings(
model_name=MODEL_EMBEDDING,
encode_kwargs={'normalize_embeddings': True}
)
except Exception as e:
log_force(f"❌ Erro Embeddings: {e}")
return
# 4. CONECTA WIKI
if os.path.exists(self.path_wiki):
try:
self.wiki_db = Chroma(
persist_directory=self.path_wiki,
embedding_function=embeddings,
collection_name="foddaci_wiki"
)
log_force("📘 Wiki ON.")
except: pass
else:
log_force("⚠️ Banco Wiki não encontrado após download.")
# 5. CONECTA EPISÓDIOS
if os.path.exists(self.path_episodes):
try:
self.episodes_db = Chroma(
persist_directory=self.path_episodes,
embedding_function=embeddings,
collection_name="foddaci_episodes"
)
log_force("📺 Episódios ON.")
except: pass
else:
log_force("⚠️ Banco Episódios não encontrado após download.")
def get_rag_data(self, query: str):
formatted_query = f"query: {query}"
log_force(f"❓ Buscando: '{formatted_query}'")
docs_found = []
# Busca Wiki
if self.wiki_db:
try:
results = self.wiki_db.similarity_search_with_score(formatted_query, k=5)
for doc, score in results:
if score < 1.45:
clean = doc.page_content.replace("passage: ", "")
docs_found.append(f"[FONTE: WIKI/BONDA] {clean}")
except: pass
# Busca Episódios
if self.episodes_db:
try:
results = self.episodes_db.similarity_search_with_score(formatted_query, k=5)
for doc, score in results:
if score < 1.45:
origem = doc.metadata.get("arquivo", "Episódio Desconhecido")
clean = doc.page_content.replace("passage: ", "")
docs_found.append(f"[FONTE: VÍDEO '{origem}'] {clean}")
except: pass
context = "\n\n".join(docs_found) if docs_found else ""
style = ""
if self.style_bank:
style = "\n".join([f"- {s}" for s in random.sample(self.style_bank, min(5, len(self.style_bank)))])
return context, style
engine = FoddaciEngine()
@app.on_event("startup")
async def startup():
engine.load_resources()
# --- INPUT MODEL ---
class ChatMessage(BaseModel):
role: str
content: str
class UserRequest(BaseModel):
message: str
history: List[ChatMessage] = []
# --- ROTAS ---
@app.get("/")
@app.head("/")
def home():
return {"status": "Online", "mode": "Router R1 + Prompt Clássico"}
@app.get("/usage")
def get_usage(request: Request):
current_count = get_user_usage_count(request)
return {
"count": current_count,
"limit": DAILY_QUOTA,
"status": "blocked" if current_count >= DAILY_QUOTA else "open"
}
@app.post("/chat")
@limiter.limit("10/minute")
async def chat_handler(req: UserRequest, request: Request):
# 1. COTA
if not check_budget_limit(request):
raise HTTPException(status_code=429, detail="COTA DIÁRIA ATINGIDA.")
if len(req.message) > MAX_INPUT_CHARS:
raise HTTPException(
status_code=400,
detail=f"TEXTO MUITO LONGO! ({len(req.message)}/{MAX_INPUT_CHARS}). Resuma essa bíblia aí, não sou leitor de TCC."
)
user_msg = req.message
search_query = None
# 2. R1 ROUTER (Decide SE busca e O QUE busca)
log_force("🤔 R1 Analisando...")
try:
history_txt = "\n".join([f"{m.role}: {m.content}" for m in req.history[-3:]])
prompt = f"""
Você é um assistente especialista no canal Jogandofoddaci.
DEFINIÇÕES: "Bonda" = Série de Álbuns Musicais (Lore).
Histórico: {history_txt}
Usuário: "{user_msg}"
Tarefa:
1. Se for conversa fiada, retorne "NO_SEARCH".
2. Se for dúvida, Reescreva a pergunta para Wiki (Substitua pronomes).
Retorne APENAS a pergunta reescrita.
"""
resp = client.chat.completions.create(
model=MODEL_ROUTER,
messages=[{"role": "user", "content": prompt}],
max_tokens=150,
temperature=0.3
)
raw_r1 = resp.choices[0].message.content
clean_decision = re.sub(r'<think>.*?</think>', '', raw_r1, flags=re.DOTALL).strip().replace('"', '')
log_force(f"🧭 Router: {clean_decision}")
if "NO_SEARCH" not in clean_decision and len(clean_decision) > 2:
search_query = clean_decision
except Exception as e:
log_force(f"⚠️ Erro Router: {e}")
search_query = user_msg
# 3. BUSCA (Se necessário)
context = ""
style_examples = ""
if search_query:
context, style_examples = engine.get_rag_data(search_query)
else:
log_force("⏩ Modo Chat (Sem busca)")
if engine.style_bank:
style_examples = "\n".join([f"- {s}" for s in random.sample(engine.style_bank, 3)])
# 4. GERAÇÃO (PROMPT CLÁSSICO RESTAURADO)
# Aqui entra exatamente o prompt que você pediu
system_instruction = f"""
Você é o FODDACITRON, mascote do canal Jogandofoddaci.
### HIERARQUIA DE VERDADE:
1. **BASE DE DADOS (RAG):** É a fonte principal.
2. **HISTÓRICO:** Se não estiver no banco, use o histórico.
### SUAS DIRETRIZES:
- Se a informação não existir, xingue o usuário, mas não invente dados.
- PERSONALIDADE: Tóxico, ofensivo e engraçado.
- ESTILO: Integre ofensas na explicação naturalmente.
### REGRAS DE FORMATAÇÃO (CRÍTICO PARA O FRONTEND):
1. **USE MARKDOWN À VONTADE:** O frontend suporta e fica bonito.
- Use **NEGRITO** (`**palavra**`) para destacar nomes, xingamentos ou ênfases.
- Use `#` para Títulos/Gritos se quiser impactar.
### GLOSSÁRIO (Vocabulário - Não copie frases inteiras):
{style_examples}
### BASE DE DADOS - SUA MEMÓRIA (Fatos para: "{search_query if search_query else user_msg}"):
{context if context else "Nada no RAG. Verifique o Histórico."}
"""
msgs = [{"role": "system", "content": system_instruction}]
for m in req.history[-5:]:
msgs.append({"role": m.role, "content": m.content})
msgs.append({"role": "user", "content": user_msg})
return StreamingResponse(stream_deepseek(msgs), media_type="text/plain")
async def stream_deepseek(messages_list):
try:
stream = client.chat.completions.create(
model=MODEL_MAIN,
messages=messages_list,
stream=True,
temperature=0.7,
max_tokens=1500
)
for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
except Exception as e:
yield f"Erro API: {e}"