| |
| """ |
| chat.py — CLI chatbot powered by Necyklopedie knowledge via RAG. |
| """ |
|
|
| import logging |
| import os |
| import warnings |
| import readline |
|
|
| |
| 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 dotenv import load_dotenv |
| from retrieve import retrieve_chunks |
| from embedder import Embedder, get_backend |
| from providers import MODELS, stream_chat, log_reliability |
|
|
| load_dotenv(override=True) |
|
|
| DB_PATH = "db/chroma" |
| COLLECTION_NAME = "necyklopedie" |
| TOP_K = 10 |
| MAX_HISTORY = 10 |
|
|
|
|
| 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) |
|
|
| 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 |
|
|
| |
| 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 |
|
|
| |
| chunks = retrieve_chunks(user_input, embedder, collection, TOP_K) |
|
|
| |
| system_msg = build_context_prompt(chunks) |
| messages = [{"role": "system", "content": system_msg}] |
| messages.extend(history[-MAX_HISTORY:]) |
| messages.append({"role": "user", "content": user_input}) |
|
|
| |
| 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() |
|
|