#!/usr/bin/env python3 """ chat.py — CLI chatbot powered by Necyklopedie knowledge via RAG. """ import logging import os import readline # noqa: F401 — enables line editing in input() import warnings # 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 from chromadb.errors import NotFoundError from dotenv import load_dotenv from embedder import Embedder, get_backend from providers import MODELS, log_reliability, stream_chat from retrieve import retrieve_chunks 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 NotFoundError: 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 print("Měřím rychlost modelů (paralelně)...") _active, 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, _live_titles, _live_attempted = 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: # noqa: BLE001 - any provider error falls through to the next model 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()