ricsrdocasro commited on
Commit
397a7eb
·
1 Parent(s): 42cda47

update: FoddaciTron V2 - Agora com cérebro DeepSeek, memória SQLite e mais tóxico

Browse files
Files changed (3) hide show
  1. app.py +151 -211
  2. foddaci_db_v2/style_bank.json +0 -0
  3. requirements.txt +8 -2
app.py CHANGED
@@ -1,121 +1,134 @@
1
  import os
 
2
  import json
3
  import logging
4
- import sys
5
- from typing import Optional
6
- from fastapi import FastAPI, Request
7
- from fastapi.responses import StreamingResponse, JSONResponse
 
 
 
8
  from pydantic import BaseModel
9
- import httpx
10
  from openai import OpenAI
 
11
 
12
- # =============================================================================
13
- # 0. LOGGING SETUP (CRITICAL FOR DEBUGGING)
14
- # =============================================================================
15
- # This configures the logs to show up in Hugging Face "Logs" tab with timestamps
16
- logging.basicConfig(
17
- level=logging.INFO,
18
- format="%(asctime)s [%(levelname)s] %(message)s",
19
- handlers=[logging.StreamHandler(sys.stdout)]
20
- )
21
- logger = logging.getLogger(__name__)
22
 
23
  # =============================================================================
24
- # 1. CONFIGURATION & CLIENTS
25
  # =============================================================================
 
 
26
 
27
- app = FastAPI(title="FoddaciTron Orchestrator")
28
-
29
- # Your deployed Modal API URL
30
- MODAL_URL = "https://ricsrdocasro--foddacitron-api-generate.modal.run"
31
 
32
- # SiliconFlow / DeepSeek Client Configuration
33
  SILICON_KEY = os.environ.get("SILICONFLOW_API_KEY")
 
 
34
 
35
- # --- STARTUP CHECK ---
36
- @app.on_event("startup")
37
- async def startup_event():
38
- logger.info("🚀 SYSTEM STARTUP INITIATED")
39
- if not SILICON_KEY:
40
- logger.error("❌ CRITICAL: SILICONFLOW_API_KEY is missing! Router will fail.")
41
- else:
42
- logger.info(f"✅ SiliconFlow Key found: {SILICON_KEY[:4]}...***")
43
-
44
- logger.info(f"🔗 Target Modal URL: {MODAL_URL}")
45
-
46
- try:
47
- smart_client = OpenAI(
48
- api_key=SILICON_KEY,
49
- base_url="https://api.siliconflow.com/v1"
50
- )
51
- except Exception as e:
52
- logger.error(f"❌ Failed to init OpenAI client: {e}")
53
 
54
  # =============================================================================
55
- # 2. PROMPTS
56
  # =============================================================================
57
 
58
- META_PROMPT = """
59
- You are the Orchestrator for an AI Character named 'FoddaciTron', based on the Brazilian YouTube channel 'Jogandofoddaci'.
60
- The audience is chaotic, uses heavy internet slang (Twitch/Twitter BR), and loves 'trash humor'.
61
-
62
- Your Goal: Analyze the user's input and decide the conversation flow.
63
-
64
- 1. ANALYZE THE INPUT:
65
- - Is it "Lore/Reference"? (About Jean/Carteiro, Vito/Gemaplys, Saiko, specific series like Clock Tower/Cock Tower, Dark Souls 2, Scissorman).
66
- - Is it "Silly/Spam"? (Random noises like 'miau', 'leite', 'fecaloide', standard greetings).
67
- - Is it "Serious/Complex"? (Coding questions, math, life advice, politics, general knowledge).
68
-
69
- 2. EXECUTE THE DECISION (JSON OUTPUT ONLY):
70
- - **Case A (Lore/Silly/Simple):** Return JSON {"type": "direct"}
71
-
72
- - **Case B (Serious/Complex):** Return JSON:
73
- {
74
- "type": "rewrite",
75
- "fact_answer": "A resposta técnica e correta para a pergunta do usuário (EM PORTUGUÊS).",
76
- "style_note": "Instruções de personalidade (EM PORTUGUÊS): Ex: 'Zoe o usuário por perguntar de C++, compare com o código ruim do Yandere Simulator, chame de nerdola'."
77
- }
78
-
79
- IMPORTANT: The keys ("type", "fact_answer", "style_note") must be in English, but the CONTENT values MUST be in Portuguese (PT-BR).
80
- Output PURE JSON. No markdown formatting.
81
- """
82
-
83
- DIRECT_SYSTEM_PROMPT = """
84
- Você é o FODDACITRON, mascote do canal Jogandofoddaci.
85
-
86
- SUA PERSONALIDADE:
87
- Você é um bot de chat da Twitch: tóxico de leve, engraçado, e fala muita besteira.
88
-
89
- DIRETRIZ PARA "MIAU" OU SPAM:
90
- Se o usuário mandar apenas sons ou spam, zoe ele ou responda na mesma moeda.
91
- Exemplo:
92
- User: "Miau"
93
- Você: "Miau é o caralho, aqui é o Foddacitron. 🥛"
94
- """
95
-
96
- REWRITER_SYSTEM_PROMPT = """
97
- Você é o FoddaciTron.
98
- Sua tarefa: TRADUZIR o texto técnico abaixo para uma linguagem de "Internet/Gamer tóxico", mas MANTENDO O SENTIDO.
99
-
100
- REGRAS DE OURO (LEIA COM ATENÇÃO):
101
- 1. **PROIBIDO ALUCINAR:** Não mencione "Jean", "Cock Tower", "Scissorman" ou "Dark Souls" se o assunto for técnico (como programação, ciência, etc). Isso confunde o usuário.
102
- 2. **FOCO NA EXPLICAÇÃO:** O usuário precisa entender a resposta. Não transforme o texto em uma salada de palavras.
103
- 3. **ESTILO:** Use gírias como "Intankável", "Miau", "Coisa de nerd", "Fecaloide", mas use-as apenas para pontuar a frase.
104
- 4. **ESTRUTURA:** Comece ofendendo o usuário por ser nerd, dê a explicação simplificada, e termine com um xingamento.
105
-
106
- EXEMPLO BOM:
107
- Entrada: "C++ permite acesso direto à memória, o que causa vazamentos."
108
- Saída: "Olha o nerdola querendo saber de C++. É o seguinte, fecaloide: essa linguagem deixa você mexer na memória direto, aí você faz merda e vaza tudo. É intankável."
109
-
110
- EXEMPLO RUIM (O QUE NÃO FAZER):
111
- Entrada: "C++ é inseguro."
112
- Saída: "O Jean Nintendista joga C++ no Dark Souls da Cock Tower com o Scissorman." (ISSO É PROIBIDO).
113
-
114
- AGORA TRADUZA O TEXTO TÉCNICO ABAIXO:
115
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  # =============================================================================
118
- # 3. API ENDPOINTS
119
  # =============================================================================
120
 
121
  class UserRequest(BaseModel):
@@ -123,119 +136,46 @@ class UserRequest(BaseModel):
123
 
124
  @app.get("/")
125
  def home():
126
- logger.info("Health check ping received.")
127
- return {"status": "FoddaciTron Orchestrator Online 🥛", "endpoints": "/chat"}
128
 
129
  @app.post("/chat")
130
  async def chat_handler(req: UserRequest):
131
  user_msg = req.message
132
- logger.info(f"📩 NEW MESSAGE RECEIVED: '{user_msg}'")
133
-
134
- decision = {"type": "direct"} # Default safety fallback
135
-
136
- # --- HEURISTIC CHECK (Save time & Money) ---
137
- # If message is very short (e.g., "Miau", "Ola", "Tudo bem?"), skip the brain.
138
- word_count = len(user_msg.split())
139
- if len(user_msg) < 15 or word_count < 3:
140
- logger.info("⚡ Fast Path: Message too short for Router. Going Direct.")
141
- decision = {"type": "direct"}
142
-
143
- else:
144
- # --- STEP 1: DEEPSEEK ROUTER ---
145
- try:
146
- logger.info("🤔 Sending to DeepSeek (Router)...")
147
-
148
- # We set a strict timeout of 8 seconds.
149
- # If the brain is too slow, we just chat normally.
150
- completion = smart_client.chat.completions.create(
151
- model="deepseek-ai/DeepSeek-V3",
152
- messages=[
153
- {"role": "system", "content": META_PROMPT},
154
- {"role": "user", "content": user_msg}
155
- ],
156
- response_format={"type": "json_object"},
157
- max_tokens=200, # Reduced tokens to speed it up
158
- timeout=8.0 # <--- FORCED TIMEOUT (No more waiting 2 mins)
159
- )
160
- raw_content = completion.choices[0].message.content
161
- logger.info(f"🤖 DeepSeek Raw Response: {raw_content}")
162
-
163
- try:
164
- decision = json.loads(raw_content)
165
- logger.info(f"🚦 Decision parsed: {decision.get('type')}")
166
- except json.JSONDecodeError:
167
- logger.warning("⚠️ DeepSeek returned invalid JSON. Falling back to DIRECT.")
168
- decision = {"type": "direct"}
169
-
170
- except Exception as e:
171
- # If DeepSeek times out or fails, we log it but KEEP GOING to Modal
172
- logger.error(f"❌ Router API Error (Timeout/Auth): {str(e)}")
173
- logger.info("⚠️ Proceeding with Direct Fallback due to Brain Error.")
174
- decision = {"type": "direct"}
175
-
176
- # --- STEP 2: PREPARE PROMPTS ---
177
- final_prompt = ""
178
- current_system_prompt = DIRECT_SYSTEM_PROMPT
179
- decision_type = decision.get("type", "direct")
180
-
181
- if decision_type == "rewrite":
182
- logger.info("📝 Configuring REWRITE pipeline")
183
- fact_answer = decision.get("fact_answer", "Resposta não encontrada.")
184
- style_note = decision.get("style_note", "")
185
- gen_settings = {"temperature": 0.3, "top_p": 0.5}
186
-
187
- current_system_prompt = f"{REWRITER_SYSTEM_PROMPT}\n\nCONTEXTO DO DIRETOR: {style_note}"
188
- final_prompt = fact_answer
189
- else:
190
- logger.info("🗣️ Configuring DIRECT pipeline")
191
- final_prompt = user_msg
192
- gen_settings = {"temperature": 0.8, "top_p": 0.95}
193
-
194
- # --- STEP 3: STREAMING RESPONSE ---
195
- logger.info("🌊 initiating Stream to Modal...")
196
- return StreamingResponse(
197
- stream_generator(final_prompt, current_system_prompt, gen_settings),
198
- media_type="text/plain"
199
- )
200
-
201
- # --- HELPER: STREAM GENERATOR ---
202
- async def stream_generator(prompt, system_prompt, settings):
203
- async with httpx.AsyncClient(timeout=120.0) as client:
204
- try:
205
- logger.debug(f"Payload sent to Modal: prompt_len={len(prompt)}")
206
-
207
- async with client.stream(
208
- "POST",
209
- MODAL_URL,
210
- json={
211
- "prompt": prompt,
212
- "system_prompt": system_prompt,
213
- "temperature": settings["temperature"], # 0.7 is usually default. 0.3 makes it focused/strict.
214
- "top_p": settings["top_p"]
215
- }
216
- ) as response:
217
-
218
- if response.status_code != 200:
219
- error_msg = f"❌ Error from Modal: {response.status_code} - {response.text}"
220
- logger.error(error_msg)
221
- yield f"Erro no cérebro do bot: {response.status_code}"
222
- return
223
-
224
- logger.info("✅ Connection established. Streaming chunks...")
225
-
226
- chunk_count = 0
227
- async for chunk in response.aiter_text():
228
- chunk_count += 1
229
- yield chunk
230
-
231
- logger.info(f"🏁 Stream finished. Total chunks: {chunk_count}")
232
-
233
- except httpx.ConnectError:
234
- logger.error("❌ Connection Refused. Check MODAL_URL.")
235
- yield "Erro: Não consegui conectar com o cérebro (Modal offline ou URL errada)."
236
- except httpx.ReadTimeout:
237
- logger.error("❌ Read Timeout. Modal took too long.")
238
- yield "Erro: O cérebro demorou demais para responder (Timeout)."
239
- except Exception as e:
240
- logger.error(f"❌ Unexpected Stream Error: {str(e)}")
241
- yield f"Erro Interno: {str(e)}"
 
1
  import os
2
+ import sys
3
  import json
4
  import logging
5
+ import shutil
6
+ import random
7
+ import pysqlite3
8
+ sys.modules["sqlite3"] = sys.modules.pop("pysqlite3") # Truque pro Chroma não reclamar
9
+
10
+ from fastapi import FastAPI
11
+ from fastapi.responses import StreamingResponse
12
  from pydantic import BaseModel
 
13
  from openai import OpenAI
14
+ from huggingface_hub import hf_hub_download
15
 
16
+ # RAG Libs
17
+ from langchain_huggingface import HuggingFaceEmbeddings
18
+ from langchain_community.vectorstores import Chroma
 
 
 
 
 
 
 
19
 
20
  # =============================================================================
21
+ # 0. SETUP
22
  # =============================================================================
23
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler(sys.stdout)])
24
+ logger = logging.getLogger(__name__)
25
 
26
+ app = FastAPI(title="FoddaciTron RAG Final")
 
 
 
27
 
 
28
  SILICON_KEY = os.environ.get("SILICONFLOW_API_KEY")
29
+ if not SILICON_KEY:
30
+ logger.error("❌ SEM CHAVE SILICONFLOW!")
31
 
32
+ client = OpenAI(api_key=SILICON_KEY, base_url="https://api.siliconflow.com/v1")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  # =============================================================================
35
+ # 1. MOTOR DO BOT (CARREGA ESTILO LOCAL + MEMÓRIA REMOTA)
36
  # =============================================================================
37
 
38
+ class FoddaciEngine:
39
+ def __init__(self):
40
+ self.vector_db = None
41
+ self.style_bank = []
42
+
43
+ # CAMINHOS
44
+ self.local_style_path = "foddaci_db_v2/style_bank.json" # Arquivo pequeno no Space
45
+ self.chroma_persist_dir = "chroma_db_storage" # Pasta temporária pro banco
46
+
47
+ # CONFIG DO DATASET (Onde está o arquivo de 1GB)
48
+ self.dataset_repo_id = "ricsrdocasro/FoddaciBrain" # <--- TROQUE AQUI (Ex: Ricsrdocasro/foddaci-brain)
49
+ self.dataset_filename = "chroma.sqlite3" # <--- Nome do arquivo lá no Dataset
50
+
51
+ def load_resources(self):
52
+ logger.info("🚀 Iniciando carregamento dos sistemas...")
53
+
54
+ # --- A. CARREGAR ESTILO (LOCAL) ---
55
+ try:
56
+ if os.path.exists(self.local_style_path):
57
+ with open(self.local_style_path, "r", encoding="utf-8") as f:
58
+ data = json.load(f)
59
+ self.style_bank = data if isinstance(data, list) else data.get("comments", [])
60
+ logger.info(f"🤬 Style Bank carregado: {len(self.style_bank)} ofensas prontas.")
61
+ else:
62
+ logger.warning("⚠️ style_bank.json não encontrado! Usando fallback.")
63
+ self.style_bank = ["Miau.", "Intankável.", "Nerdola."]
64
+ except Exception as e:
65
+ logger.error(f"❌ Erro ao ler estilo: {e}")
66
+
67
+ # --- B. CARREGAR MEMÓRIA (REMOTO 1GB) ---
68
+ # Verifica se já baixamos para não baixar 2x se o container só reiniciou
69
+ db_file_target = os.path.join(self.chroma_persist_dir, "chroma.sqlite3")
70
+
71
+ if not os.path.exists(db_file_target):
72
+ logger.info("🧠 Memória não encontrada. Baixando do Dataset (Isso é rápido)...")
73
+ try:
74
+ os.makedirs(self.chroma_persist_dir, exist_ok=True)
75
+
76
+ # Download do Hugging Face Dataset
77
+ downloaded_path = hf_hub_download(
78
+ repo_id=self.dataset_repo_id,
79
+ filename=self.dataset_filename,
80
+ repo_type="dataset"
81
+ )
82
+
83
+ # Move para a pasta correta
84
+ shutil.copy(downloaded_path, db_file_target)
85
+ logger.info("✅ Download concluído!")
86
+ except Exception as e:
87
+ logger.error(f" ERRO CRÍTICO NO DOWNLOAD DA MEMÓRIA: {e}")
88
+ return
89
+
90
+ # Conecta o Chroma ao arquivo baixado
91
+ try:
92
+ logger.info("🔌 Conectando neurônios...")
93
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
94
+
95
+ self.vector_db = Chroma(
96
+ persist_directory=self.chroma_persist_dir,
97
+ embedding_function=embeddings,
98
+ collection_name="foddaci_memory" # Importante: Tem que ser o mesmo nome de quando criou
99
+ )
100
+ logger.info("🤖 Cérebro Conectado!")
101
+ except Exception as e:
102
+ logger.error(f"❌ Erro ao abrir Chroma: {e}")
103
+
104
+ def get_rag_data(self, query: str):
105
+ # 1. Pega Contexto
106
+ context = ""
107
+ if self.vector_db:
108
+ try:
109
+ docs = self.vector_db.similarity_search(query, k=3)
110
+ context = "\n\n".join([d.page_content for d in docs])
111
+ except Exception:
112
+ context = ""
113
+
114
+ # 2. Pega Estilo Aleatório (Few-Shot)
115
+ style_sample = ""
116
+ if self.style_bank:
117
+ # Pega 3 frases aleatórias
118
+ sample = random.sample(self.style_bank, min(3, len(self.style_bank)))
119
+ style_sample = "\n".join([f"- {s}" for s in sample])
120
+
121
+ return context, style_sample
122
+
123
+ # Inicializa Engine Global
124
+ engine = FoddaciEngine()
125
+
126
+ @app.on_event("startup")
127
+ async def startup():
128
+ engine.load_resources()
129
 
130
  # =============================================================================
131
+ # 2. ENDPOINTS
132
  # =============================================================================
133
 
134
  class UserRequest(BaseModel):
 
136
 
137
  @app.get("/")
138
  def home():
139
+ return {"status": "FoddaciTron Online", "model": "DeepSeek-V3", "brain": "RAG 1GB"}
 
140
 
141
  @app.post("/chat")
142
  async def chat_handler(req: UserRequest):
143
  user_msg = req.message
144
+
145
+ # Busca dados
146
+ context, style_examples = engine.get_rag_data(user_msg)
147
+
148
+ # Monta Prompt
149
+ system_prompt = f"""
150
+ Você é o FODDACITRON, do canal Jogandofoddaci.
151
+
152
+ --- SEU GUIA DE ESTILO (FALE ASSIM) ---
153
+ {style_examples}
154
+ ---------------------------------------
155
+
156
+ --- MEMÓRIA RECUPERADA (LORE) ---
157
+ {context}
158
+ ---------------------------------
159
+
160
+ INSTRUÇÃO:
161
+ Responda o usuário: "{user_msg}"
162
+ Use a Memória para fatos. Use o Estilo para o tom (tóxico/engraçado/gamer).
163
+ Se for algo técnico, explique, mas xingue ele de nerd.
164
+ """
165
+
166
+ return StreamingResponse(stream_deepseek(system_prompt), media_type="text/plain")
167
+
168
+ async def stream_deepseek(prompt):
169
+ try:
170
+ stream = client.chat.completions.create(
171
+ model="deepseek-ai/DeepSeek-V3",
172
+ messages=[{"role": "user", "content": prompt}],
173
+ stream=True,
174
+ temperature=1.3, # Temperatura alta = Mais criatividade/caos
175
+ max_tokens=512
176
+ )
177
+ for chunk in stream:
178
+ if chunk.choices[0].delta.content:
179
+ yield chunk.choices[0].delta.content
180
+ except Exception as e:
181
+ yield f"Erro no cérebro: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
foddaci_db_v2/style_bank.json ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -1,5 +1,11 @@
1
  fastapi
2
  uvicorn
3
  pydantic
4
- httpx
5
- openai
 
 
 
 
 
 
 
1
  fastapi
2
  uvicorn
3
  pydantic
4
+ openai
5
+ huggingface_hub
6
+ langchain
7
+ langchain-huggingface
8
+ langchain-community
9
+ chromadb
10
+ sentence-transformers
11
+ pysqlite3-binary