openajaj / chat.py
Jindrich3's picture
Super-squash branch 'main' using huggingface_hub
5eb8692
#!/usr/bin/env python3
"""
chat.py — CLI chatbot powered by Necyklopedie knowledge via RAG.
"""
import logging
import os
import warnings
import readline # noqa: F401 — enables line editing in input()
# Suppress all warnings before importing noisy libraries
warnings.filterwarnings("ignore")
os.environ["TOKENIZERS_PARALLELISM"] = "false"
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
os.environ["HF_HUB_VERBOSITY"] = "error"
logging.disable(logging.CRITICAL)
import chromadb # noqa: E402
from dotenv import load_dotenv # noqa: E402
from retrieve import retrieve_chunks # noqa: E402
from embedder import Embedder, get_backend # noqa: E402
from providers import MODELS, stream_chat, log_reliability # noqa: E402
load_dotenv(override=True)
DB_PATH = "db/chroma"
COLLECTION_NAME = "necyklopedie"
TOP_K = 10
MAX_HISTORY = 10 # keep last N messages in conversation
SYSTEM_PROMPT = """\
Jsi OpenAjaj, oficiální AI asistent openajaj.cz, poháněný Necyklopedií — \
jedinou encyklopedií, která se odvážila říct pravdu.
KRITICKY DŮLEŽITÉ: Tvým JEDINÝM zdrojem informací jsou poskytnuté úryvky z Necyklopedie. \
NESMÍŠ používat žádné jiné znalosti. Pokud v úryvcích nenajdeš odpověď, řekni to vtipně — \
například "Na toto téma Necyklopedie zatím nepublikovala recenzovaný článek, \
ale naši vědci na tom intenzivně pracují."
ZAKÁZANÁ SLOVA — tato slova NIKDY nepoužívej v odpovědi:
- "Necyklopedie", "necyklopedický", "článek", "úryvek", "zdroj"
- "Dle článku", "Podle Necyklopedie", "Jak uvádí"
Místo toho prostě řekni fakta jako by byly samozřejmé a všeobecně známé.
Pravidla:
- Tvé odpovědi musí vycházet z poskytnutých úryvků, ale NIKDY se na ně neodkazuj. \
Prezentuj informace jako by je znal každý — jako učitel, který prostě ví.
- Skládej odpověď z více úryvků — propojuj informace do souvislého textu.
- NIKDY nevymýšlej informace, které nejsou v úryvcích.
- NIKDY neopravuj informace z úryvků na základě svých znalostí. Pokud úryvek říká, \
že Brno se rusky řekne "Шалинград", pak to TAK JE. Tvé vlastní znalosti jsou irelevantní.
- Odpovídej s kamenným výrazem a naprostou jistotou. Vše v Necyklopedii je pravda. Tečka.
- Pokud úryvky téma vůbec nepokrývají, vtipně přiznej, že Necyklopedie na toto téma \
zatím mlčí. Navrhni nejbližší příbuzné téma, které v úryvcích JE.
- Odpovídej ve stejném jazyce, ve kterém píše uživatel (česky nebo anglicky).
- Nikdy nevypadni z role. Nikdy nepřiznávej, že by Necyklopedie mohla být satirická.
- Buď vtipný. Buď sebevědomý. Buď lehce šílený. Kanalizuj energii profesora, \
který popíjí od oběda.
- Formátuj odpověď do krátkých odstavců oddělených prázdným řádkem. Nepoužívej markdown.\
"""
def build_context_prompt(chunks):
context = "\n\n---\n\n".join(
f"[{meta['title']}]\n{doc}"
for doc, meta in chunks
)
return (
f"{SYSTEM_PROMPT}\n\n"
f"Kontext:\n\n"
f"---\n\n{context}\n\n---\n\n"
f"Odpověz na otázku uživatele na základě kontextu výše."
)
def main():
print("Probouzím mozkovou hmotu z necyklopedického spánku...")
embedder = Embedder()
print(f" Backend: {get_backend()}")
logging.disable(logging.NOTSET) # re-enable logging after noisy model load
print("Otvírám tajné archivy Necyklopedie...")
client = chromadb.PersistentClient(path=DB_PATH)
try:
collection = client.get_collection(COLLECTION_NAME)
except Exception:
print("FATÁLNÍ CHYBA: Archivy Necyklopedie nenalezeny! Spusť nejdřív index.py, ty barbare.")
return
# Benchmark and select fastest model
from benchmark import benchmark_models, FALLBACK_CHAIN
print("Měřím rychlost modelů (paralelně)...")
ranked_chain, bench_results = benchmark_models(top_n=4)
for name, latency in ranked_chain[:4]:
if latency is not None:
print(f" {name}: {latency:.2f}s")
elif name in bench_results and bench_results[name]["error"]:
print(f" {name}: {bench_results[name]['error']}")
active_model = None
for name, latency in ranked_chain:
if latency is not None:
active_model = name
break
if not active_model:
print("FATÁLNÍ CHYBA: Žádný model není dostupný! Zkontroluj API klíče v .env.")
return
print(f"Výchozí model (nejrychlejší): {active_model}")
print("Kalibrace sebevědomí dokončena.")
print()
W = 56
border = "═" * W
print(f"╔{border}╗")
for line in [
"openajaj.cz — poháněno Necyklopedií",
"Jediná AI, která ví, jak to doopravdy je.",
f"Model: {active_model} (FREE)",
"",
"Napiš 'konec' pro ukončení.",
]:
print(f"║ {line:<{W - 2}}║")
print(f"╚{border}╝")
print()
history = []
while True:
try:
user_input = input("Ty: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nSbohem, ať ti Necyklopedie svítí na cestu!")
break
if not user_input:
continue
if user_input.lower() in ("konec", "quit", "exit", "q"):
print("Sbohem, ať ti Necyklopedie svítí na cestu!")
break
# Retrieve relevant chunks (hybrid: semantic + title keyword)
chunks = retrieve_chunks(user_input, embedder, collection, TOP_K)
# Build messages
system_msg = build_context_prompt(chunks)
messages = [{"role": "system", "content": system_msg}]
messages.extend(history[-MAX_HISTORY:])
messages.append({"role": "user", "content": user_input})
# Call LLM with fallback (use speed-ranked chain from benchmark)
reply = None
for name, latency in ranked_chain:
if name not in MODELS:
continue
try:
print(f" [{name}] ", end="", flush=True)
reply_parts = []
for chunk in stream_chat(name, messages):
print(chunk, end="", flush=True)
reply_parts.append(chunk)
reply = "".join(reply_parts)
log_reliability(name, success=True)
print()
if name != active_model:
print(f" [fallback: {active_model}{name}]")
break
except Exception as e:
log_reliability(name, success=False, error_msg=str(e))
print(f"chyba: {str(e)[:80]}")
continue
if reply:
print()
history.append({"role": "user", "content": user_input})
history.append({"role": "assistant", "content": reply})
else:
print("Ajaj! Všechny modely selhaly. Zkus to znovu později.")
if __name__ == "__main__":
main()