Ilova: engine, kalit va pipe
Uch qism
29-bobgacha Tez faqat benchmark ichida yashardi: savollar fayldan o'qilardi, javoblar faylga yozilardi. Tahlilchi esa savolni oynaga yozadi. Bu bob — ilovaning birinchi qurilgan qismi haqida. Ilova uch qismdan iborat:
oyna (Tauri) fayl tashlash, oldindan ko'rish, savol, javob va tushuntirish | pipe: stdin/stdout orqali JSON qatorlar engine (Python) fayl o'qish, tavsif, prompt, validator, darvoza, DuckDB, tushuntirish | HTTP, faqat 127.0.0.1, tasodifiy port va kalit llama-server + tez8 faqat JSON plan yozadi
Pastki ikki qism qurildi va sinaldi. Oyna — hali yo'q. Engine kodi loyihaning app/ papkasida:
for fayl in sorted((TEZ / "app").glob("*.py")): turi = "sinov" if fayl.name.startswith("test_") or fayl.name == "fake_llama.py" else "kod" print(f"{fayl.name:16s} {turi:6s} {len(fayl.read_text(encoding='utf-8').splitlines()):4d} qator")
endpoints.py kod 24 qator engine.py kod 77 qator fake_llama.py sinov 41 qator model.py kod 62 qator parity.py kod 66 qator pipe.py kod 85 qator table.py kod 36 qator test_model.py sinov 81 qator test_pipe.py sinov 74 qator test_session.py sinov 94 qator test_table.py sinov 49 qator
Kod kam, chunki engine deyarli hech narsani yangidan yozmaydi.
Engine benchmark yo'lidan yuradi
Bu kitobdagi har bir raqam benchmark kodi bilan o'lchangan. Agar ilova savolni boshqa yo'l bilan qayta ishlasa — boshqa tavsif, boshqa prompt, boshqa darvoza — o'sha raqamlar ilovaga tegishli bo'lmaydi. Shuning uchun engine benchmark funksiyalarining o'zini chaqiradi:
import ast for nom in ("table.py", "engine.py", "model.py"): for tugun in ast.parse((TEZ / "app" / nom).read_text(encoding="utf-8")).body: if isinstance(tugun, ast.ImportFrom) and tugun.module.split(".")[0] in ("accuracy", "prompt", "runtime", "training"): print(f"{nom:10s} <- {tugun.module}: {', '.join(n.name for n in tugun.names)}")
table.py <- accuracy.load_excel: clean_sheet, read_sheet, write_csv table.py <- accuracy.run: BASE, open_data table.py <- accuracy.tools: ROW_ID, q table.py <- training.make_table: describe_table engine.py <- accuracy.execute: effective_steps, run_plan engine.py <- accuracy.run: BASE, guard, prompt_history engine.py <- accuracy.tools: ROW_ID, ToolError, q engine.py <- accuracy.validate: confidence engine.py <- prompt: build engine.py <- runtime.client: parse_answer engine.py <- training.describe: explain, load_texts model.py <- runtime: server model.py <- runtime.client: condition, payload model.py <- runtime.launch: launch
Fayl 11-bobdagi loader dan o'tadi, tavsifni o'sha tavsifchi yozadi (29-bobdagi 600 belgi qoidasi bilan), prompt
benchmark dagi kod bilan quriladi, guard — benchmark dagi validator va darvozaning o'zi.
Bu da'voni tekshirish oson: ilova oltita real jadvalni ochsin, va uning tavsifi benchmark dagi tavsif bilan solishtirilsin. To'rtta jadval — CSV fayl. Hududlar jadvali va valyuta kurslari esa — asl Excel fayllar, 11-bobda ishlatilganlarning o'zi; ular loader dan o'tadi, sarlavha ustidagi matnlar bilan:
import json import shutil from app.table import open_table def oqi(nisbiy): return json.loads((TEZ / nisbiy).read_text(encoding="utf-8")) ilova = oqi("app/config.json") qoidalar, sozlama, profil = oqi("accuracy/load.json"), oqi("accuracy/settings.json"), oqi("training/settings.json") manbalar = {"real_banks": "/mnt/d/Tez_spikes/data/real_banks.csv", "real_credit_clients": "/mnt/d/Tez_spikes/data/real_credit_clients.csv", "real_industry": "/mnt/d/Tez_spikes/data/real_industry.csv", "real_retail": "/mnt/d/Tez_spikes/data/real_retail.csv", "real_regions": DATA / "regions_eng.xlsx", "real_fx_rates": DATA / "0009_eng.xlsx"} for toplam, manba in manbalar.items(): kutilgan = oqi(f"prompt/domains/{toplam}/table.json") nusxa = ISH / f"{kutilgan['name']}{Path(manba).suffix}" shutil.copyfile(manba, nusxa) baza, jadval = open_table(nusxa, ilova, qoidalar, sozlama, profil) print(f"{toplam:20s} {Path(manba).suffix:5s} {jadval['rows']:7,d} qator tavsif bir xil: {jadval == kutilgan}") baza.close()
real_banks .csv 36 qator tavsif bir xil: True real_credit_clients .csv 30,000 qator tavsif bir xil: True real_industry .csv 9 qator tavsif bir xil: True real_retail .csv 541,909 qator tavsif bir xil: True real_regions .xlsx 14 qator tavsif bir xil: True real_fx_rates .xlsx 75 qator tavsif bir xil: True
Oltitasi ham bir xil. Excel fayl uchun loader tozalagan jadval vaqtinchalik CSV ga yoziladi, DuckDB uni yuklaydi, va vaqtinchalik papka darhol o'chiriladi — benchmark ham Excel ni aynan shu yo'l bilan o'qigan.
Model serveri: xesh, port, kalit
Engine model serverini o'zi ishga tushiradi va o'zi to'xtatadi. Ishga tushirish kodi:
manba = (TEZ / "app/model.py").read_text(encoding="utf-8") for tugun in ast.walk(ast.parse(manba)): if isinstance(tugun, ast.FunctionDef) and tugun.name == "__init__": print(ast.get_source_segment(manba, tugun))
def __init__(self, config, log_path):
path = Path(config["model_dir"]) / config["model"]["file"]
if file_hash(path) != config["model"]["sha256"]:
raise ModelError(f"{path} does not match the expected SHA-256")
if sys.platform not in config["binaries"]:
raise ModelError(f"no llama-server configured for {sys.platform}")
key = secrets.token_hex(config["key_bytes"])
runtime = {**config["runtime"], "binaries": {"cpu": config["binaries"][sys.platform]},
"env": {**config["runtime"]["env"], config["key_env"]: key}}
self.config = {"runtimes": {RUNTIME: runtime}, "default_runtime": RUNTIME, "model_dir": config["model_dir"],
"port": free_port(), "context": config["context"], "timeout": config["timeout"],
"max_tokens": config["max_tokens"]}
self.headers = {"Authorization": f"Bearer {key}"}
self.process, self.runtime = launch(self.config, config["model"], condition(f"cpu:{config['threads']}"), log_path, self.headers)
Tartib muhim. Avval model faylining xeshi tekshiriladi (28-bob) — mos kelmasa, server umuman ishga
tushmaydi. Keyin bo'sh port tanlanadi va har safar yangi tasodifiy kalit yaratiladi. Kalit server jarayoniga
muhit o'zgaruvchisi orqali beriladi, buyruq qatori orqali emas. Engine kalitni faqat o'z so'rovlarida yuboradi.
Kalitsiz so'rovlarga server nima deyishi 27-bobda o'lchandi: faqat /health javob beradi.
Pipe
Oyna engine bilan qanday gaplashadi? Loyiha planida (04-architecture.md) bu yerda FastAPI va WebSocket
turardi. Lekin bu ikkinchi mahalliy port degani — va 27-bobdagi teshik ikkinchi marta ochilishi mumkin edi.
Shuning uchun boshqa yo'l tanlandi: oyna engine ni o'zining bola jarayoni sifatida ishga tushiradi va u bilan
pipe (quvur) orqali — jarayonning standart kirishi va chiqishi orqali — gaplashadi. Har bir so'rov — bitta
JSON qator, har bir javob — bitta JSON qator. Tarmoq porti yo'q, demak brauzer ham, boshqa dastur ham
engine ga ulana olmaydi.
Protokolni ko'rish uchun engine ga model o'rniga oldindan yozilgan javob beramiz — bu yerda model
ishlamaydi, faqat engine ning pipe qismi ishlaydi. Fayl — kitobdagi sintetik kreditlar jadvali (datasets/loans.csv):
import io from app.pipe import Pipe, serve class YozilganModel: def __init__(self, javob): self.javob = javob def body(self, context, schema, text, history): return {} def chat(self, body): return {"choices": [{"message": {"content": json.dumps(self.javob)}, "logprobs": {"content": [{"logprob": 0.0}]}}]} plan = {"steps": [{"tool": "group_and_total", "args": {"group_by": ["viloyat"], "value_column": "qoldiq_uzs", "agg": "sum"}}]} shutil.copyfile(DATA / "loans.csv", ISH / "kreditlar.csv") sorovlar = [{"id": 1, "op": "open", "path": str(ISH / "kreditlar.csv")}, {"id": 2, "op": "ask", "text": "viloyatlar bo'yicha qoldiq"}, {"id": 3, "op": "ask_everything"}] chiqish = io.StringIO() serve([json.dumps(s) for s in sorovlar], chiqish, Pipe(YozilganModel(plan), ilova, lambda: sozlama["today"])) for qator in chiqish.getvalue().splitlines(): javob = json.loads(qator) if "rows" in javob: javob = {**javob, "columns": javob["columns"][:4], "rows": [r[:4] for r in javob["rows"][:2]]} print(json.dumps({k: v for k, v in javob.items() if k not in ("output",)}, ensure_ascii=False)[:420], "\n")
{"id": 1, "ok": true, "table": "kreditlar", "columns": ["kredit_raqami", "berilgan_sana", "filial", "viloyat"], "rows": [[500001, "20/11/2024", "Buxoro", "Buxoro"], [500002, "04.04.2025", "Toshkent", "Andijon"]], "total_rows": 20000}
{"id": 2, "ok": true, "kind": "result", "text": "1. Har bir viloyat uchun qoldiq_uzs bo'yicha jami hisoblanadi.", "held": null, "columns": ["viloyat", "qoldiq_uzs"], "rows": [["Qashqadaryo", 1416469087152.8354], ["Namangan", 1442975847712.3447]], "total_rows": 6, "artifacts": [], "confidence": 1.0}
{"id": 3, "ok": false, "error": "unknown op 'ask_everything'"}
Birinchi javob — fayl ochildi: jadval nomi, ustunlar, birinchi qatorlar va jami qatorlar soni (oyna buni oldindan ko'rish uchun ko'rsatadi). Ikkinchisi — natija: plan bajarilgan jadval va tahlilchi tilidagi tushuntirish (29-bob). Uchinchisi — noma'lum buyruq: engine yiqilmaydi, xato bilan javob beradi.
Pipe ning yana bir foydasi bor. Oyna yopilsa — yoki kutilmaganda o'lsa — engine ning standart kirishi yopiladi. Engine buni ko'radi va model serverini to'xtatadi: kompyuterda deyarli 5 GB xotira (26-bob) egallagan yetim server qolmaydi. Bu va boshqa hamma holatlar sinovlarda tekshiriladi — haqiqiy llama-server o'rniga uni taqlid qiladigan kichik server bilan:
import os import subprocess import sys sinov = subprocess.run([sys.executable, "-B", "-m", "pytest", "-q", "-p", "no:cacheprovider", "app"], cwd=TEZ, capture_output=True, text=True, env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}) print(sinov.stdout.strip().splitlines()[-1]) for fayl in sorted((TEZ / "app").glob("test_*.py")): nomlar = [t.name for t in ast.parse(fayl.read_text(encoding="utf-8")).body if isinstance(t, ast.FunctionDef) and t.name.startswith("test_")] print(f"\n{fayl.name}:") for nom in nomlar: print(f" {nom}")
20 passed in 7.92s test_model.py: test_file_hash_matches_hashlib test_wrong_hash_refuses_to_start test_unknown_platform_refuses_to_start test_server_needs_the_key_and_stops_on_close test_each_start_gets_its_own_key test_pipe.py: test_open_then_ask test_ask_before_open_is_an_error test_bad_requests_are_answered_not_fatal test_quit_stops_reading test_engine_process_stops_the_model_when_the_window_goes_away test_session.py: test_plan_becomes_a_result_with_an_explanation test_low_confidence_asks_and_is_not_remembered test_validator_problem_asks test_model_question_is_passed_on test_unusable_output_fails_politely test_follow_up_sees_the_earlier_turn_and_restart_forgets_it test_table.py: test_csv_is_described_like_the_benchmark_tables test_excel_keeps_titles_and_leaves_no_copy test_other_files_are_refused
Bitta chegara: agar engine ning o'zi majburan o'ldirilsa (masalan, vazifalar menejeridan), u serverni to'xtatishga ulgurmaydi. Buning uchun Windows da operatsion tizimning o'zi bola jarayonlarni birga yopadigan mexanizm kerak — bu o'rnatuvchi bilan birga qilinadi.
Windows da: o'sha javoblarmi
Engine benchmark kodini chaqiradi — lekin bu "javoblar bir xil" degani emasmi? Buni ham o'lchaymiz. Windows da, tahlilchi ishlatadigan llama.cpp build ida, haqiqiy tez8 bilan, ikkinchi yangi to'plamning (29-bob) besh jadvaldagi hamma savollari engine orqali o'tdi, va har bir chiqish o'sha build dagi benchmark javobi bilan solishtirildi. Hududlar jadvali bu o'tishga kirmadi: o'tish CSV nusxalar bilan qilingan, hududlar CSV sida esa Excel dagi sarlavhalar yo'q. Natija saqlangan:
natija = (TEZ / "results/app_parity_win.txt").read_text(encoding="utf-8").splitlines() for qator in natija: if not qator.startswith(" #"): print(qator)
hash seconds 1.8 start seconds (hash + load) 3.8 /props without key: HTTP 401 /slots without key: HTTP 401 chat without key: HTTP 401 real_banks: description identical True real_credit_clients: description identical True real_fx_rates: description identical True real_industry: description identical True real_retail: description identical True questions 72, identical output 71, same gate/validator decision 72 confidence gap median 0.00e+00 max 3.03e-03, median seconds 2.50 real_retail #11: held None -> None server still running: False
Xesh 1.8 soniya, server ishga tushishi bilan birga 3.8 soniya. Kalitsiz uchta so'rovning uchalasi rad etildi. Tavsiflar beshala jadvalda bir xil. 72 savoldan 71 tasida model aynan o'sha chiqishni berdi, va 72 tasining hammasida darvoza va validator bir xil qaror qildi. Farq qilgan bitta savol:
qism = "\n".join(natija).split("real_retail:")[1] ilova_javobi = next(q for q in qism.splitlines() if q.startswith(" #11 ")) savol = next(s for s in oqi("accuracy/suites/real_retail/fresh2.json") if s["id"] == 11) benchmark = next(j for j in map(json.loads, open(TEZ / "results/win8f2_cpu_real_retail.jsonl", encoding="utf-8")) if j["id"] == 11) print("savol: ", savol["text"]) print("kutilgan: ", json.dumps(savol["expect"][0], ensure_ascii=False)) print("benchmark: ", benchmark["output"], "->", benchmark["reason"]) print("ilova: ", ilova_javobi.strip())
savol: line chart of quantity by date
kutilgan: {"steps": [{"tool": "make_chart", "args": {"chart": "line", "x": "InvoiceDate", "y": "Quantity", "agg": "sum"}}]}
benchmark: {"steps":[{"tool":"make_chart","args":{"chart":"line","x":"InvoiceDate","y":"Quantity","agg":"none"}}]} -> different result
ilova: #11 result: "1. Har bir InvoiceDate uchun Quantity bo'yicha jami ko'rsatilgan chiziqli diagramma chizil"
Benchmark da model kunlar bo'yicha xom nuqtalarni chizgan (agg: none) — noto'g'ri. Engine orqali u
yig'indini tanladi — kutilgan javob. Bu safar farq foydali tomonga chiqdi, lekin buni yutuq deb hisoblamaymiz:
sababi tekshirilmagan (ehtimol, prompt keshining holati ikki o'tishda boshqacha bo'lgan), va keyingi safar u
teskari tomonga ham chiqishi mumkin. Bu — 29-bobdagi Windows va WSL farqi bilan bir turdagi shovqin.
Bankka: internetsiz papka
Bankda dastur internetdan hech narsa yuklab olmasligi kerak — na o'rnatishda, na ishlashda. Ko'p bank kompyuterlari tarmoqdan umuman uzilgan bo'lishi ham mumkin. Shuning uchun Tez bitta papka sifatida yetkaziladi (USB yoki ichki tarmoq): o'rnatuvchi, model fayli va o'rnatish skripti. Skript modelni foydalanuvchi papkasiga ko'chiradi va xeshini tekshiradi. Bu va oldingi qarorlar loyihaning qarorlar jurnalida:
for qator in (TEZ.parent.parent / "10-decisions-and-open-questions.md").read_text(encoding="utf-8").splitlines(): if any(qator.startswith(f"| D-{n} |") for n in (18, 19, 20, 21)): katak = [k.strip() for k in qator.strip("|").split("|")] print(f"{katak[0]} ({katak[1]}): {katak[2]}\n")
D-18 (2026-11-03): Desktop window (Tauri) talks to the Python engine over stdin/stdout JSON lines, not FastAPI + WebSocket D-19 (2026-11-03): Explanations and clarify messages follow one language chosen in settings, no per-question detection D-20 (2026-11-03): Model file lives in the user folder (%LOCALAPPDATA%\Tez\models, macOS Application Support, Linux ~/.local/share/tez), checked by SHA-256 D-21 (2026-11-03): Tez ships as one offline folder (USB stick or network share): the app installer, the GGUF and one install script that copies the model and checks its hash. Every runtime is inside: Python engine with DuckDB and calamine, llama-server, and the WebView2 runtime. Nothing is downloaded at install or at run time
Model o'rnatuvchining ichida emas, yonida. Bizning bilishimizcha, keng tarqalgan o'rnatuvchi formatlari taxminan 2 GB lik chegaraga ega, model fayli esa ikki yarim gigabaytdan katta (28-bob). Bu chegara hali tekshirilmagan: u o'rnatuvchi qurilganda o'lchanadi, va agar model ichiga sig'sa — papka bitta faylga qisqaradi.
Bank xavfsizlik bo'limi, ehtimol, yana ikki narsani so'raydi: dasturning raqamli imzosi (imzosiz .exe
fayllarni ko'p bank sozlamalari bloklaydi) va ichidagi hamma qismlarning ro'yxati — versiyalari, litsenziyalari
va xeshlari bilan. Ikkalasi ham hali yo'q.
Hali yo'q
| Qism | Holat |
|---|---|
| Engine: fayl ochish, savol, natija, tushuntirish | Bor, sinalgan |
Model serveri: xesh, tasodifiy port va kalit, --offline |
Bor, Windows da o'lchangan |
| Pipe: oyna yopilsa server to'xtaydi | Bor, sinalgan |
| Oyna (Tauri): fayl tashlash, oldindan ko'rish, javob | Yo'q — keyingi qadam |
| Engine majburan o'ldirilsa ham serverni yopish | Yo'q — o'rnatuvchi bilan |
| Internetsiz o'rnatuvchi papkasi va skript | Yo'q — shakli tanlangan |
| Tarmoq to'liq yopiq holdagi sinov | Yo'q |
| Haqiqiy brauzerdan kalit sinovi | Yo'q |
| Tushuntirish tili | Sozlamada bitta til; savol tilini aniqlash yo'q |
Xulosa
- Ilovaning engine qismi qurildi: u benchmark funksiyalarining o'zini chaqiradi — loader, tavsifchi, prompt, validator va darvoza.
- Oltita real jadvalda — to'rtta CSV va ikkita asl Excel fayl — ilova yozgan tavsif benchmark dagisi bilan bir xil.
- Model serveri har ishga tushishda xesh bilan tekshiriladi, tasodifiy port va yangi kalit oladi; kalitsiz faqat
/healthjavob beradi. - Oyna engine bilan pipe orqali gaplashadi — ikkinchi port yo'q; oyna yopilsa, model serveri ham to'xtaydi.
- Windows da 72 ko'rilmagan savoldan 71 tasida engine benchmark bilan aynan bir xil chiqish berdi, darvoza va validator qarorlari 72 tasida ham bir xil.
- Bankka Tez internetsiz papka bo'lib boradi. Oyna, o'rnatuvchi va tarmoqsiz sinov — keyingi qadamlar.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.