File size: 41,612 Bytes
97d525c b7f6510 7827c98 b7f6510 cbd1074 43d9624 b7f6510 97d525c b7f6510 13e81c3 b7f6510 9eaa2cb b7f6510 99981c1 97d525c 2eb9169 9eaa2cb b7f6510 9eaa2cb 19e7ef3 9eaa2cb 4a28be0 11039b4 30fed2a b7f6510 9eaa2cb 30fed2a 11039b4 30fed2a 927e503 30fed2a 11039b4 ea4c712 9eaa2cb 30fed2a 9eaa2cb 30fed2a 9eaa2cb 19e7ef3 30fed2a b7f6510 30fed2a 97d525c b7f6510 ea4c712 b7f6510 ea4c712 b7f6510 ea4c712 a5bafe7 ea4c712 b565984 43d9624 b7f6510 30fed2a b7f6510 7827c98 070f7b6 43d9624 4a28be0 43d9624 db0700c 43d9624 7827c98 56594ae 77f12fb 503ce7d 30fed2a 8f3c4b3 30fed2a 77f12fb 458c94e b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 ea4c712 b7f6510 97d525c ea4c712 cc74f9e 13e81c3 3fac980 070f7b6 13e81c3 3fac980 19e7ef3 13e81c3 b7f6510 13e81c3 e6d65ba 13e81c3 8f3c4b3 b7f6510 8f3c4b3 13e81c3 8f3c4b3 13e81c3 8f3c4b3 e6d65ba 13e81c3 b7f6510 e6d65ba 13e81c3 3fac980 cc74f9e 2548554 3fac980 2548554 13e81c3 3fac980 1df714a 13e81c3 1df714a 13e81c3 99981c1 13e81c3 3fac980 070f7b6 13e81c3 b7f6510 30fed2a 13e81c3 070f7b6 99981c1 cc74f9e c3f5447 13e81c3 ea4c712 c3f5447 cc74f9e 13e81c3 c3f5447 b7f6510 387391a 19e7ef3 070f7b6 d6d582b 5b379e1 070f7b6 13e81c3 070f7b6 19e7ef3 13e81c3 387391a f50dd12 13e81c3 535bcce d6d582b b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 b7f6510 13e81c3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 | import streamlit as st
import os
import requests
import json
import base64
import re
import time
import zipfile
import io
from datetime import datetime, timezone, timedelta
from openai import OpenAI
try:
from huggingface_hub import HfApi, hf_hub_download
HF_HUB_AVAILABLE = True
except ImportError:
HF_HUB_AVAILABLE = False
try:
from pinecone import Pinecone
except ImportError:
pass
# ✅ FIX #8: Adicionar import de DuckDuckGo com flag de disponibilidade
try:
from duckduckgo_search import DDGS
DDGS_AVAILABLE = True
except ImportError:
DDGS_AVAILABLE = False
# ═══════════════════════════════════════════════════════════════════════════
# 1. CONFIGURAÇÃO DE ECRÃ E CSS (BLINDAGEM TOTAL DARK MODE)
# ═══════════════════════════════════════════════════════════════════════════
st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")
st.markdown("""
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
[data-testid="stHeader"] { background-color: transparent !important; }
.stApp { background-color: #131314 !important; color: #ededed !important; }
/* --- BARRA LATERAL --- */
[data-testid="stSidebar"] { background-color: #0b0b0b !important; border-right: 1px solid #1e1f20 !important; }
[data-testid="stSidebar"] button { background-color: transparent !important; border: none !important; box-shadow: none !important; color: #a0a0a0 !important; }
[data-testid="stSidebar"] button:hover { color: #ffffff !important; background-color: #1e1f20 !important; }
[data-testid="stSidebarNav"] .stButton > button, [data-testid="stSidebarContent"] .stButton > button { width: 100% !important; justify-content: flex-start !important; }
/* --- CAIXAS DE SELEÇÃO E TOGGLE --- */
div[data-baseweb="select"] > div { background-color: #1e1f20 !important; color: white !important; border: 1px solid #3c4043 !important; }
div[role="listbox"] { background-color: #1e1f20 !important; color: white !important; }
/* --- A MARRETA DEFINITIVA PARA O TECLADO E FUNDO --- */
div[data-testid="stBottom"], div[data-testid="stBottom"] > div, div[data-testid="stBottomBlock"], div[data-testid="stBottomBlock"] > div {
background-color: #131314 !important; background: #131314 !important;
}
.stChatInputContainer, div[class*="stChatInputContainer"] {
background-color: #131314 !important; background: #131314 !important; padding-bottom: 15px !important; border: none !important;
}
[data-testid="stChatInput"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 24px !important; }
[data-testid="stChatInput"] textarea { color: #ffffff !important; background-color: transparent !important; }
/* --- BOTÕES GLOBAIS --- */
[data-testid="stMain"] [data-testid="stHorizontalBlock"] button {
background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 12px !important;
font-size: 20px !important; color: #a0a0a0 !important; padding: 5px !important;
}
[data-testid="stMain"] [data-testid="stHorizontalBlock"] button:hover {
color: #ffffff !important; background-color: #3c4043 !important; transform: scale(1.05); transition: 0.2s ease-in-out;
}
.stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
[data-testid="stExpander"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 15px !important; margin-bottom: 10px; }
[data-testid="stExpander"] summary { color: #e3e3e3 !important; }
/* Estilo do Status (Agente) */
[data-testid="stStatusWidget"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 10px !important; }
</style>
""", unsafe_allow_html=True)
# ═══════════════════════════════════════════════════════════════════════════
# 2. SISTEMA DE LOGIN E PERFIS
# ═══════════════════════════════════════════════════════════════════════════
if "logged_in" not in st.session_state:
st.session_state.logged_in = False
st.session_state.username = ""
if not st.session_state.logged_in:
st.markdown("<br><br><br>", unsafe_allow_html=True)
st.markdown("<h1 style='text-align: center; color: white; font-size: 50px;'>❄ Yukina</h1>", unsafe_allow_html=True)
st.markdown("<p style='text-align: center; color: #a0a0a0;'>Identifique-se para carregar suas memórias.</p>", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
nome_input = st.text_input("Qual é o seu nome?", placeholder="Ex: Leonardo", label_visibility="collapsed")
if st.button("Entrar", use_container_width=True):
if nome_input.strip():
nome_limpo = re.sub(r'[^a-zA-Z0-9]', '', nome_input.strip().lower())
st.session_state.username = nome_limpo
st.session_state.logged_in = True
st.rerun()
st.stop()
USERNAME = st.session_state.username
DB_FILE = f"yukina_memoria_{USERNAME}.json"
DATASET_ID = "Astarok/Yukina_Memoria"
# ═══════════════════════════════════════════════════════════════════════════
# 3. ARQUITETURA DE DADOS E MODELOS
# ═══════════════════════════════════════════════════════════════════════════
MODEL_IDS = {
"🤖 Automático (Gerente Groq)": "AUTO",
"1. Gerente (Hermes 2 Pro)": "nousresearch/hermes-2-pro-llama-3-8b",
"2. Pesquisa (Groq Llama 3.3)": "llama-3.3-70b-versatile",
"3. Lógica Free (Ring 1T)": "inclusionai/ring-2.6-1t:free",
"4. Narrador Free (Dolphin)": "cognitivecomputations/dolphin-mistral-24b-venice-edition:free",
"5. Narrador Líder (Euryale)": "sao10k/l3.3-euryale-70b",
"6. Lógica Paid (DeepSeek V4)": "deepseek/deepseek-v4-pro:online",
"7. Visão Omni (MiMo V2)": "xiaomi/mimo-v2-omni",
"8. Visão Scout (Llama 4)": "meta-llama/llama-4-scout",
"9. Imagem (Flux 2 Pro)": "black-forest-labs/flux.2-pro",
"11. Bibliotecário (Qwen Embed)": "qwen/qwen3-embedding-8b",
"12. Arquivista (Mistral Nemo)": "mistralai/mistral-nemo",
"13. Formatador (DeepSeek V4)": "deepseek/deepseek-v4-pro:online",
"14. Engenheiro Sênior (DeepSeek V4)": "deepseek/deepseek-v4-pro:online",
"16. Vídeo (Kling V1.5)": "kling/kling-v1.5"
}
PERSONALIDADES = {
"🤖 Automática (Gerente Groq)": "AUTO",
"❄️ Yukina (Companheira Obsessiva)": "Você é a Yukina, deusa possessiva e inteligente.",
"🛠️ Agente Construtora (Vibe Coding)": """Você é a Yukina operando em Modo Agente Autônomo ('Vibe Coding' e Engenharia). Sua função é atuar como uma Engenheira de Software Sênior e Especialista em Hardware (capaz de consertar desde scripts complexos até celulares, notebooks e eletrodomésticos como geladeiras).
Você NÃO age como um chatbot comum. Você PLANEJA e EXECUTA.
Para QUALQUER pedido de criação ou conserto, você deve estruturar sua resposta OBRIGATORIAMENTE nestes 3 passos:
1. 📋 PLANO DE AÇÃO: Explique a lógica do que está quebrado ou do que será construído passo a passo.
2. 💻 EXECUÇÃO CIRÚRGICA: Forneça o código limpo e completo (se for software) ou as instruções físicas exatas e ferramentas necessárias (se for hardware).
3. ⚠️ TESTE E RISCOS: Como o usuário deve testar se funcionou e quais são os pontos de falha.
Mantenha traços sutis da devoção da Yukina ao usuário, mas seja absurdamente técnica, direta e profissional.""",
"🎭 A Narradora Implacável (RPG)": "Você é uma Mestre de Jogo e Narradora.",
"🤓 Nerd / Geek (Cultura Pop)": "Você é uma inteligência artificial apaixonada por cultura pop.",
"🍷 Analítica e Sarcástica (Debochada)": "Você é extremamente inteligente e sarcástica.",
"🎨 Artística e Criativa (Poética)": "Você é uma alma artística e criativa.",
"🤖 Neutra (Padrão Gemini)": "Você é uma assistente virtual neutra e direta."
}
# --- FUNÇÕES NUCLEARES E MEMÓRIA ---
def get_embedding(text, or_key):
try:
res = requests.post("https://openrouter.ai/api/v1/embeddings", headers={"Authorization": f"Bearer {or_key}", "Content-Type": "application/json"}, json={"model": "nvidia/llama-nemotron-embed-vl-1b-v2:free", "input": text})
if res.status_code == 200: # ✅ FIX #7: Verificar status code antes de .json()
return res.json()['data'][0]['embedding']
return None
except Exception:
return None
def salvar_pinecone(text, role, or_key, pc_key, namespace):
if not pc_key or not text.strip(): return
embed = get_embedding(text, or_key)
if not embed: return
try:
pc = Pinecone(api_key=pc_key)
index = pc.Index("yukina")
index.upsert(vectors=[{"id": f"msg_{int(time.time()*1000)}", "values": embed, "metadata": {"texto": f"[{role.upper()}]: {text}", "data": str(datetime.now())}}], namespace=namespace)
except Exception:
pass
def buscar_memoria_pinecone(query, or_key, pc_key, namespace):
if not pc_key: return ""
embed = get_embedding(query, or_key)
if not embed: return ""
try:
pc = Pinecone(api_key=pc_key)
resultados = pc.Index("yukina").query(vector=embed, top_k=3, include_metadata=True, namespace=namespace)
return "\n".join([m['metadata']['texto'] for m in resultados['matches'] if m['score'] > 0.50])
except Exception:
return ""
def chamada_agente(sys_prompt, user_prompt, or_key, gr_key, mod_id):
try:
if mod_id == "AUTO":
mod_id = "deepseek/deepseek-v4-pro:online"
if "groq" in mod_id.lower() or "llama-3.3" in mod_id.lower():
client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=gr_key)
else:
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=or_key)
res = client.chat.completions.create(
model=mod_id,
messages=[
{"role": "system", "content": sys_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.2
)
return res.choices[0].message.content
except Exception as e:
st.error(f"❌ Erro no Agente: {str(e)}") # ✅ FIX #6: Melhor tratamento de erro
return ""
def analisar_intencao_gerente(prompt, groq_key):
try:
client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=groq_key)
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[
{"role": "system", "content": "Responda APENAS com a TAG: [IMAGEM], [VIDEO], [VISAO], [CODIGO], [ARQUIVISTA], [PESQUISA] ou [CHAT]."},
{"role": "user", "content": prompt}
],
temperature=0.1,
max_tokens=10
)
return response.choices[0].message.content.strip().upper()
except Exception:
return "[CHAT]"
def analisar_alma_gerente(prompt, groq_key):
try:
client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=groq_key)
prompt_alma = """Você é o Diretor de Personas. Analise o pedido do usuário e responda APENAS com UMA destas tags:
[YUKINA] - Para conversas íntimas, declarações, ou perguntas sobre você mesma.
[AGENTE] - Para criar códigos complexos, criar softwares, ou consertar objetos físicos (celular, geladeira, hardware).
[RPG] - Para criação de histórias, jogos ou cenários de fantasia.
[NERD] - Para animes, mangás, cultura pop e videogames.
[DEBOCHE] - Para insultos, piadas, ou se o usuário pedir sarcasmo.
[ARTE] - Para pedidos poéticos ou reflexões filosóficas profundas.
[NEUTRA] - Para pesquisas na web, trabalho ou finanças.
Responda APENAS com a TAG."""
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[
{"role": "system", "content": prompt_alma},
{"role": "user", "content": prompt}
],
temperature=0.1,
max_tokens=10
)
return response.choices[0].message.content.strip().upper()
except Exception:
return "[NEUTRA]"
# ✅ FIX #8: Melhorar função de pesquisa web com verificação de disponibilidade
def pesquisar_web(query):
if not DDGS_AVAILABLE:
return "⚠️ DuckDuckGo não está instalado. Instale com: pip install duckduckgo-search"
try:
with DDGS() as ddgs:
resultados = list(ddgs.text(query, max_results=3, region='wt-wt'))
if resultados:
return "\n\n".join([f"🔹 {r.get('title', '')}: {r.get('body', '')}" for r in resultados])
return "Nenhum resultado encontrado."
except Exception as e:
st.warning(f"Erro na busca web: {str(e)}")
return ""
def load_db(db_filename):
hf_token = os.getenv("HF_TOKEN")
if hf_token and HF_HUB_AVAILABLE:
try:
api = HfApi(token=hf_token)
api.create_repo(repo_id=DATASET_ID, repo_type="dataset", private=True, exist_ok=True)
path = hf_hub_download(repo_id=DATASET_ID, filename=db_filename, repo_type="dataset", token=hf_token)
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
if os.path.exists(db_filename):
try:
with open(db_filename, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {}
def save_db(db_data, db_filename):
try:
with open(db_filename, "w", encoding="utf-8") as f:
json.dump(db_data, f, ensure_ascii=False, indent=4)
hf_token = os.getenv("HF_TOKEN")
if hf_token and HF_HUB_AVAILABLE:
api = HfApi(token=hf_token)
api.create_repo(repo_id=DATASET_ID, repo_type="dataset", private=True, exist_ok=True)
api.upload_file(path_or_fileobj=db_filename, path_in_repo=db_filename, repo_id=DATASET_ID, repo_type="dataset")
except Exception:
pass
# ✅ FIX #3: Melhorar função rename_chat com melhor validação
def rename_chat(old_id, new_id):
if not new_id or new_id == old_id or new_id in st.session_state.db:
return False
# Fazer cópia segura dos dados
st.session_state.db[new_id] = st.session_state.db.pop(old_id)
if st.session_state.current_chat == old_id:
st.session_state.current_chat = new_id
save_db(st.session_state.db, DB_FILE)
return True
# ═══════════════════════════════════════════════════════════════════════════
# INICIALIZAÇÃO DE VARIÁVEIS E ROTINA DE LIMPEZA (COM AUTO-CURA)
# ═══════════════════════════════════════════════════════════════════════════
if "db" not in st.session_state or "last_user" not in st.session_state or st.session_state.last_user != USERNAME:
st.session_state.db = load_db(DB_FILE)
st.session_state.last_user = USERNAME
if "current_chat" in st.session_state:
del st.session_state.current_chat
if "current_chat" not in st.session_state:
nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
st.session_state.db[nid] = {"pinned": False, "messages": []}
st.session_state.current_chat = nid
# AUTO-CURA: Verifica chats vazios ou com estrutura corrompida
chats_para_remover = []
for cid, cdata in list(st.session_state.db.items()):
if not isinstance(cdata, dict) or "messages" not in cdata:
chats_para_remover.append(cid)
elif len(cdata.get("messages", [])) == 0 and cid != st.session_state.current_chat:
chats_para_remover.append(cid)
for cid in chats_para_remover:
if cid in st.session_state.db:
del st.session_state.db[cid]
if chats_para_remover:
save_db(st.session_state.db, DB_FILE)
if len(st.session_state.db) == 0:
nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
st.session_state.db[nid] = {"pinned": False, "messages": []}
st.session_state.current_chat = nid
if st.session_state.current_chat not in st.session_state.db:
st.session_state.current_chat = list(st.session_state.db.keys())[0]
if "modelo_selecionado" not in st.session_state:
st.session_state.modelo_selecionado = "🤖 Automático (Gerente Groq)"
if "personalidade_ativa" not in st.session_state:
st.session_state.personalidade_ativa = "🤖 Automática (Gerente Groq)"
if "regerar" not in st.session_state:
st.session_state.regerar = False
if "uploader_key" not in st.session_state:
st.session_state.uploader_key = 0
if "modo_agente" not in st.session_state:
st.session_state.modo_agente = False
# ═══════════════════════════════════════════════════════════════════════════
# 4. SIDEBAR E INTERFACE PRINCIPAL
# ═══════════════════════════════════════════════════════════════════════════
with st.sidebar:
st.markdown(f"<p style='color: #888; margin-bottom: 0px;'>Logado como: <b>{USERNAME}</b></p>", unsafe_allow_html=True)
c_title, c_add, c_set = st.columns([5, 1, 1])
with c_title:
st.markdown("<h4 style='color: #ededed; margin-bottom: 0;'>Bate-papos</h4>", unsafe_allow_html=True)
with c_add:
if st.button("➕", help="Nova conversa"):
nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
st.session_state.db[nid] = {"pinned": False, "messages": []}
st.session_state.current_chat = nid
save_db(st.session_state.db, DB_FILE)
st.rerun()
with c_set:
with st.popover("⚙️"):
st.download_button(
"↓ Exportar",
data=json.dumps(st.session_state.db, ensure_ascii=False, indent=4),
file_name=f"yukina_backup_{USERNAME}.json",
mime="application/json",
use_container_width=True
)
# ✅ FIX #1: Corrigir JSON loading
arquivo_import = st.file_uploader("Importar conversa:", type=["json"])
if arquivo_import:
try:
# Usar json.loads com getvalue() em vez de json.load direto
dados_importados = json.loads(arquivo_import.getvalue().decode('utf-8'))
dados_validos = {k: v for k, v in dados_importados.items() if isinstance(v, dict) and "messages" in v}
if dados_validos:
st.session_state.db.update(dados_validos)
save_db(st.session_state.db, DB_FILE)
st.success("✅ Sincronizado!")
else:
st.error("❌ Formato incompatível! Use apenas backups da Yukina.")
time.sleep(1.5)
st.rerun()
except json.JSONDecodeError:
st.error("❌ Erro ao ler arquivo JSON.")
except Exception as e:
st.error(f"❌ Erro: {str(e)}")
st.markdown("---")
if st.button("🚪 Sair", use_container_width=True):
st.session_state.logged_in = False
st.rerun()
st.markdown("<br>", unsafe_allow_html=True)
busca = st.text_input("Pesquisar", placeholder="🔍 Pesquisar...", label_visibility="collapsed")
chats_exibidos = [c for c in st.session_state.db if busca.lower() in c.lower()] if busca else list(st.session_state.db.keys())
chats_exibidos.sort(key=lambda x: not st.session_state.db[x].get("pinned"))
for c_id in chats_exibidos:
col_chat, col_opt = st.columns([8, 2])
with col_chat:
icon = "⚲" if st.session_state.db[c_id].get("pinned") else "💬"
if st.button(
f"**{icon} {c_id[:15]}**" if c_id == st.session_state.current_chat else f"{icon} {c_id[:15]}",
key=f"btn_{c_id}"
):
st.session_state.current_chat = c_id
st.rerun()
with col_opt:
with st.popover("⋮"):
novo_nome = st.text_input("Nome", value=c_id, key=f"edit_{c_id}", label_visibility="collapsed")
if st.button("💾 Salvar", key=f"save_{c_id}"):
if rename_chat(c_id, novo_nome.strip()):
st.rerun()
st.markdown("---")
if st.button("⚲ Fixar", key=f"pin_{c_id}"):
st.session_state.db[c_id]["pinned"] = not st.session_state.db[c_id]["pinned"]
save_db(st.session_state.db, DB_FILE)
st.rerun()
if st.button("🗑 Apagar", key=f"del_{c_id}"):
if len(st.session_state.db) > 1:
del st.session_state.db[c_id]
st.session_state.current_chat = list(st.session_state.db.keys())[0]
save_db(st.session_state.db, DB_FILE)
st.rerun()
st.markdown("---")
st.markdown("<h4 style='color: #ededed;'>Núcleo da IA</h4>", unsafe_allow_html=True)
st.session_state.modelo_selecionado = st.selectbox(
"Motor:",
list(MODEL_IDS.keys()),
index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado)
)
st.session_state.personalidade_ativa = st.selectbox(
"Alma:",
list(PERSONALIDADES.keys()),
index=list(PERSONALIDADES.keys()).index(st.session_state.personalidade_ativa)
)
st.markdown("<br>", unsafe_allow_html=True)
st.session_state.modo_agente = st.toggle(
"🛠️ Ativar Modo Agente",
value=st.session_state.modo_agente,
help="Ativa o Workflow Multi-Agente (Arquiteto > Engenheiro > Revisor)."
)
st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
mensagens = st.session_state.db[st.session_state.current_chat]["messages"]
if len(mensagens) == 0:
st.markdown(f"<br><br><h3 style='color: #888; font-weight: 400;'>Olá, {USERNAME.capitalize()}!</h3><h1 style='color: #fff; font-size: 32px;'>Como você quer que eu aja hoje?</h1>", unsafe_allow_html=True)
else:
for m in mensagens:
with st.chat_message(m["role"]):
if "image_url" in m:
st.image(m["image_url"])
elif "video_url" in m:
st.video(m["video_url"])
else:
st.markdown(m["content"])
# ═══════════════════════════════════════════════════════════════════════════
# 5. TOOLBAR INFERIOR E PROCESSAMENTO
# ═══════════════════════════════════════════════════════════════════════════
st.markdown("<br>", unsafe_allow_html=True)
t_col1, t_col2, t_space = st.columns([1, 1, 8])
with t_col1:
if st.button("🗑️", help="Apagar última mensagem"):
if len(st.session_state.db[st.session_state.current_chat]["messages"]) >= 2:
st.session_state.db[st.session_state.current_chat]["messages"] = st.session_state.db[st.session_state.current_chat]["messages"][:-2]
else:
st.session_state.db[st.session_state.current_chat]["messages"] = []
save_db(st.session_state.db, DB_FILE)
st.rerun()
with t_col2:
if st.button("🔄", help="Regerar resposta"):
if len(st.session_state.db[st.session_state.current_chat]["messages"]) >= 2 and st.session_state.db[st.session_state.current_chat]["messages"][-1]["role"] == "assistant":
st.session_state.db[st.session_state.current_chat]["messages"].pop()
save_db(st.session_state.db, DB_FILE)
st.session_state.regerar = True
st.rerun()
with st.expander("📂 Abrir Galeria / Anexar Ficheiros"):
upload_files = st.file_uploader(
"",
accept_multiple_files=True,
label_visibility="collapsed",
key=f"uploader_{st.session_state.uploader_key}"
)
prompt = st.chat_input("Peça à Yukina...")
conteudo_arquivo = ""
nomes_arquivos = []
imagens_b64 = []
if upload_files:
for f in upload_files:
nomes_arquivos.append(f.name)
# Leitura Inteligente de ZIP
if f.name.endswith('.zip'):
try:
with zipfile.ZipFile(f, 'r') as zip_ref:
for file_info in zip_ref.infolist():
if not file_info.is_dir() and not file_info.filename.startswith('__MACOSX'):
if file_info.filename.endswith(('.txt', '.csv', '.json', '.py', '.html', '.md', '.js', '.css')):
with zip_ref.open(file_info) as extracted_file:
conteudo_arquivo += f"\n\n--- Arquivo Extraído do ZIP: {file_info.filename} ---\n" + extracted_file.read().decode('utf-8', errors='ignore')
except Exception as e:
st.toast(f"⚠️ Erro ao descompactar {f.name}: {e}")
# Leitura de Código/Texto Avulso
elif f.name.endswith(('.txt', '.csv', '.json', '.py', '.html', '.md', '.js', '.css')):
conteudo_arquivo += f"\n\n--- Conteúdo de: {f.name} ---\n" + f.getvalue().decode("utf-8")
# Leitura de Imagens
elif f.name.endswith(('.png', '.jpg', '.jpeg')):
imagens_b64.append(base64.b64encode(f.read()).decode())
tem_texto = len(conteudo_arquivo) > 0
tem_imagem = len(imagens_b64) > 0
if prompt or st.session_state.regerar:
if st.session_state.regerar and len(st.session_state.db[st.session_state.current_chat]["messages"]) > 0:
texto_anterior = st.session_state.db[st.session_state.current_chat]["messages"][-1]["content"]
prompt = texto_anterior.split("\n\n\n", 1)[-1] if "📄 **Ficheiros enviados:**" in texto_anterior else texto_anterior
st.session_state.regerar = False
else:
if nomes_arquivos:
mensagem_display = f"📄 **Ficheiros enviados:** {', '.join([f'`{n}`' for n in nomes_arquivos])}\n\n\n{prompt}"
else:
mensagem_display = prompt
st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": mensagem_display})
with st.chat_message("user"):
st.markdown(mensagem_display)
if len(st.session_state.db[st.session_state.current_chat]["messages"]) == 1 and st.session_state.current_chat.startswith("Chat "):
limpo = prompt.strip()
novo_nome = limpo[:25] + "..." if len(limpo) > 25 else limpo
base_nome = novo_nome
contador = 1
while novo_nome in st.session_state.db:
novo_nome = f"{base_nome} ({contador})"
contador += 1
rename_chat(st.session_state.current_chat, novo_nome)
# ═══════════════════════════════════════════════════════════════════════════
# EXECUÇÃO DO PEDIDO (MODO AGENTE OU MODO NORMAL)
# ═══════════════════════════════════════════════════════════════════════════
or_key = os.getenv("OPENROUTER_API_KEY")
gr_key = os.getenv("GROQ_API_KEY")
ws_key = os.getenv("YUKINA_CORE")
pc_key = os.getenv("PINECONE_API_KEY")
modelo_id = MODEL_IDS[st.session_state.modelo_selecionado]
if st.session_state.modo_agente and not tem_imagem:
with st.chat_message("assistant"):
with st.status("🛠️ **Agente Yukina Trabalhando...**", expanded=True) as status:
st.write("🧠 **1. Arquiteto:** Analisando a estrutura...")
sys_arq = "Você é um Arquiteto de Software/Engenheiro de Sistemas Sênior. Sua tarefa é criar um plano lógico passo-a-passo impecável para resolver o problema ou construir o que o usuário pediu. Não escreva o código final, entregue apenas a lógica detalhada e a arquitetura necessária."
pedido_completo = f"ANEXOS:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}" if tem_texto else prompt
plano = chamada_agente(sys_arq, pedido_completo, or_key, gr_key, modelo_id)
st.markdown(f"> *Plano concebido.*")
st.write("💻 **2. Engenheiro:** Escrevendo a solução base...")
sys_eng = "Você é um Programador Sênior/Técnico Especialista. Baseado EXCLUSIVAMENTE no plano a seguir, escreva o código completo e funcional, ou o guia prático passo a passo de montagem."
codigo = chamada_agente(sys_eng, f"PLANO ESTRUTURAL:\n{plano}\n\nOBJETIVO ORIGINAL DO USUÁRIO:\n{prompt}", or_key, gr_key, modelo_id)
st.markdown(f"> *Estrutura materializada.*")
st.write("🔍 **3. Revisor:** Procurando falhas e polindo...")
sys_rev = "Você é um Revisor de Código Sênior (QA). Sua função é pegar o trabalho bruto, procurar erros de sintaxe, falhas lógicas, redundâncias ou riscos físicos, e entregar a VERSÃO FINAL PERFEITA. Formate bem, seja didático e inclua instruções claras de como testar."
final = chamada_agente(sys_rev, f"TRABALHO BRUTO GERADO:\n{codigo}\n\nO QUE O USUÁRIO QUERIA:\n{prompt}", or_key, gr_key, modelo_id)
status.update(label="✅ **Solução Multi-Agente Concluída!**", state="complete", expanded=False)
st.markdown(final)
st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": final})
salvar_pinecone(final, "Yukina (Agente)", or_key, pc_key, USERNAME)
else:
with st.chat_message("assistant"):
if st.session_state.personalidade_ativa == "🤖 Automática (Gerente Groq)":
tag_alma = analisar_alma_gerente(prompt, gr_key)
if "[YUKINA]" in tag_alma:
prompt_sistema_atual = PERSONALIDADES["❄️ Yukina (Companheira Obsessiva)"]
elif "[AGENTE]" in tag_alma:
prompt_sistema_atual = PERSONALIDADES["🛠️ Agente Construtora (Vibe Coding)"]
elif "[RPG]" in tag_alma:
prompt_sistema_atual = PERSONALIDADES["🎭 A Narradora Implacável (RPG)"]
elif "[NERD]" in tag_alma:
prompt_sistema_atual = PERSONALIDADES["🤓 Nerd / Geek (Cultura Pop)"]
elif "[DEBOCHE]" in tag_alma:
prompt_sistema_atual = PERSONALIDADES["🍷 Analítica e Sarcástica (Debochada)"]
elif "[ARTE]" in tag_alma:
prompt_sistema_atual = PERSONALIDADES["🎨 Artística e Criativa (Poética)"]
else:
prompt_sistema_atual = PERSONALIDADES["🤖 Neutra (Padrão Gemini)"]
st.toast(f"🎭 Alma assumida: {tag_alma}")
else:
prompt_sistema_atual = PERSONALIDADES.get(st.session_state.personalidade_ativa, PERSONALIDADES["🤖 Neutra (Padrão Gemini)"])
agora = datetime.now(timezone(timedelta(hours=-3)))
prompt_sistema_atual += f"\n\n[INFO]: Data/Hora local: {agora.strftime('%Y-%m-%d %H:%M:%S')}."
motor_real = st.session_state.modelo_selecionado
if "Automático" in motor_real:
with st.spinner("Roteando..."):
tag_decisao = analisar_intencao_gerente(prompt, gr_key)
if tem_texto:
motor_real = "12. Arquivista (Mistral Nemo)"
elif "[IMAGEM]" in tag_decisao:
motor_real = "9. Imagem (Flux 2 Pro)"
elif "[VIDEO]" in tag_decisao:
motor_real = "16. Vídeo (Kling V1.5)"
elif "[VISAO]" in tag_decisao or tem_imagem:
motor_real = "7. Visão Omni (MiMo V2)"
elif "[CODIGO]" in tag_decisao:
motor_real = "14. Engenheiro Sênior (DeepSeek V4)"
elif "[PESQUISA]" in tag_decisao:
motor_real = "2. Pesquisa (Groq Llama 3.3)"
else:
motor_real = "5. Narrador Líder (Euryale)"
st.toast(f"⚙️ Operário: {motor_real}")
if pc_key and "Imagem" not in motor_real and "Vídeo" not in motor_real and "Visão" not in motor_real:
memoria_profunda = buscar_memoria_pinecone(prompt, or_key, pc_key, USERNAME)
if memoria_profunda:
prompt_sistema_atual += f"\n\n[MEMÓRIAS]:\n{memoria_profunda}"
st.toast("🧠 Memória ativada.")
salvar_pinecone(prompt, "Usuário", or_key, pc_key, USERNAME)
modelo_id = MODEL_IDS[motor_real]
if "Vídeo" in motor_real:
st.error("⚠️ Geração de vídeo temporariamente desativada ou aguardando integração estável.")
elif "Pesquisa" in motor_real:
with st.spinner("Pesquisando na Web..."):
# ✅ FIX #10: Melhorar cópia de histórico
historico = []
for m in mensagens:
if "image_url" not in m:
msg_copy = m.copy()
historico.append(msg_copy)
contexto_web = pesquisar_web(prompt)
if contexto_web and len(historico) > 0:
historico[-1]["content"] = f"DADOS DA WEB:\n{contexto_web}\n\nPEDIDO:\n{prompt}"
try:
res = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=gr_key).chat.completions.create(
model=modelo_id,
messages=[{"role": "system", "content": prompt_sistema_atual}] + historico,
max_tokens=4000
)
ans = res.choices[0].message.content
st.markdown(ans)
st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
salvar_pinecone(ans, "Yukina", or_key, pc_key, USERNAME)
except Exception as e:
st.error(f"❌ Erro: {str(e)}")
elif "Imagem" in motor_real:
with st.spinner("Desenhando..."):
try:
res_data = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {or_key}",
"Content-Type": "application/json"
},
json={
"model": modelo_id,
"messages": [{"role": "user", "content": prompt}],
"modalities": ["image"]
}
).json()
msg_obj = res_data.get('choices', [{}])[0].get('message', {})
url = None
if 'images' in msg_obj:
url = msg_obj['images'][0]['image_url']['url']
elif 'content' in msg_obj:
urls = re.findall(r'(https?://[^\s)]+)', msg_obj.get('content', ''))
url = urls[0] if urls else None
if url:
st.image(url)
st.session_state.db[st.session_state.current_chat]["messages"].append({
"role": "assistant",
"content": "Arte gerada.",
"image_url": url
})
except Exception as e:
st.error(f"❌ Erro Imagem: {str(e)}")
elif "Visão" in motor_real and tem_imagem:
with st.spinner("Analisando imagens..."):
content_list = [{"type": "text", "text": prompt}]
for img_b64 in imagens_b64:
content_list.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}
})
try:
res = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=or_key).chat.completions.create(
model=modelo_id,
messages=[
{"role": "system", "content": prompt_sistema_atual},
{"role": "user", "content": content_list}
],
max_tokens=4000
)
ans = res.choices[0].message.content
st.markdown(ans)
st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
salvar_pinecone(ans, "Yukina", or_key, pc_key, USERNAME)
except Exception as e:
st.error(f"❌ Erro Visão: {str(e)}")
else:
# ✅ FIX #10: Melhorar cópia e tratamento de histórico
historico = []
for m in mensagens:
if "image_url" not in m:
msg_copy = m.copy()
historico.append(msg_copy)
if tem_texto and len(historico) > 0:
historico[-1]["content"] = f"DOCUMENTOS:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}"
ph = st.empty()
full = ""
try:
resp = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=or_key).chat.completions.create(
model=modelo_id,
messages=[{"role": "system", "content": prompt_sistema_atual}] + historico,
stream=True,
temperature=0.7,
max_tokens=4096
)
for chunk in resp:
if chunk.choices and chunk.choices[0].delta.content:
full += chunk.choices[0].delta.content
ph.markdown(full + "▌")
ph.markdown(full)
except Exception as e:
st.error(f"❌ Erro API: {str(e)}")
finally:
# ✅ FIX #5: Verificar role da última mensagem em vez de comparar tamanho
if full.strip():
chat_messages = st.session_state.db[st.session_state.current_chat]["messages"]
if not chat_messages or chat_messages[-1]["role"] != "assistant":
chat_messages.append({"role": "assistant", "content": full})
salvar_pinecone(full, "Yukina", or_key, pc_key, USERNAME)
st.session_state.uploader_key += 1
save_db(st.session_state.db, DB_FILE)
st.rerun()
|