Yukina / app.py
Astarok's picture
Update app.py
13e81c3 verified
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()