Toollar va JSON plan
Tool — bu shartnoma
Uchinchi qism Tezning engine i haqida. Birinchi navbatda — toollar, chunki ular model bilan engine orasidagi shartnoma. Model faqat toollar tilida gapiradi. Engine faqat shu tilni tushunadi.
2-bobda o'n besh toolning ro'yxatini ko'rdik. Endi bitta tool ichiga kiramiz: u qanday yozilgan, nega aynan shunday, va plan bir nechta toolni qanday bog'laydi.
Bitta toolning anatomiyasi
Hamma toollar bitta faylda — prompt/tools.json. Undan filter ni olamiz:
import json asboblar = {a["name"]: a for a in json.load(open(TEZ / "prompt/tools.json", encoding="utf-8"))} print(json.dumps(asboblar["filter"], ensure_ascii=False, indent=1))
{
"name": "filter",
"description": "Keep only rows where a column matches a condition. in and not_in take a values list; is_empty and not_empty take no value",
"args": {
"column": {
"ref": "column"
},
"op": {
"enum": [
"=",
"!=",
">",
">=",
"<",
"<=",
"contains",
"in",
"not_in",
"is_empty",
"not_empty"
]
},
"value": {
"type": [
"string",
"number"
],
"optional": true
},
"values": {
"type": "array",
"items": {
"type": [
"string",
"number"
]
},
"minItems": 1,
"optional": true
}
},
"variants": [
{
"op": {
"enum": [
"=",
"!="
]
},
"value": {
"ref": "column_value",
"type": [
"string",
"number"
]
},
"values": null
},
{
"op": {
"enum": [
">",
">=",
"<",
"<=",
"contains"
]
},
"value": {
"type": [
"string",
"number"
]
},
"values": null
},
{
"op": {
"enum": [
"in",
"not_in"
]
},
"value": null,
"values": {
"type": "array",
"items": {
"ref": "column_value",
"type": [
"string",
"number"
]
},
"minItems": 1
}
},
{
"op": {
"enum": [
"is_empty",
"not_empty"
]
},
"value": null,
"values": null
}
]
}
To'rt qism:
namevadescription— model o'qiydigan nom va tavsif. Tavsif ingliz tilida yozilgan, foydalanuvchi esa istalgan tilda yozadi. (Tavsifni boshqa tilda yozib solishtirmaganmiz.)args— argumentlar. Ba'zilari oddiy tur (integer,boolean), ba'zilari havola:{"ref": "column"}— "bu yerga jadvalning ustun nomi keladi". Havolaning aniq qiymatlari har bir jadval uchun alohida qo'yiladi (3-bobdagi schema).optional— ixtiyoriy argument. Bu kichik so'z 9-bobda katta muammoga aylanadi.variants— argumentlar orasidagi qoidalar. Bular haqida alohida gaplashamiz.
Model nimani ko'radi
Model JSON ni ko'rmaydi. build.py har bir toolni bitta qatorga aylantiradi va promptga
qo'yadi:
from prompt import build print(build.tool_lines([asboblar["filter"], asboblar["top_n"], asboblar["fix_numbers"]]))
- filter(column: column, op: =|!=|>|>=|<|<=|contains|in|not_in|is_empty|not_empty, value: string/number?, values: array?): Keep only rows where a column matches a condition. in and not_in take a values list; is_empty and not_empty take no value
- top_n(by: column, n: integer, descending: boolean, group: {columns: columns, agg: sum|count|avg|min|max}?): Keep the top or bottom N rows by a column. With group, first aggregate the column per group, then rank the groups. Use group when the user ranks people, branches or other groups rather than single rows
- fix_numbers(column: column): Convert a text column with numbers written as text into real numbers
Qisqa, funksiya chaqiruviga o'xshash yozuv: nom, argumentlar va turlari, ? — ixtiyoriy belgisi,
keyin tavsif. Bu qatorlar fayldan avtomatik yasaladi — tool o'zgarsa, prompt ham o'zgaradi.
Qo'lda yozilgan, unutilib eskirib qoladigan ikkinchi nusxa yo'q.
Nega plan — ro'yxat
Spike 2 ning birinchi versiyasida model bitta tool chaqirardi. Keyin ruscha so'rov keldi: "Покажи топ 10 клиентов по сумме" — summasi bo'yicha eng katta 10 ta mijoz.
Bitta tool bilan buni yozib bo'lmaydi. Avval mijozlar bo'yicha jamlash kerak, keyin saralash va o'nta olish. To'rtala model ham bu yerda xato qildi — chunki to'g'ri javobni yozish imkonsiz edi. 7-bobdagi "olib tashla" kabi, bu ham model xatosi emas, shakl xatosi edi.
Shu kuni javob shakli o'zgardi (qarorlar jurnalida D-14):
{"steps": [{"tool": "...", "args": {...}}, ...]}
Har bir qadam oldingi qadamning natijasi ustida ishlaydi. Qadamlar soni — 1 dan
max_steps gacha, va bu son sozlamada turadi, kodda emas:
sozlama = json.load(open(TEZ / "prompt/settings.json", encoding="utf-8")) print(f"max_steps = {sozlama['max_steps']}")
max_steps = 4
Nega chegara? Uzun plan — xato ehtimoli ko'p plan. Tahlilchining odatiy savoli bir-uch qadamga sig'adi. To'rtdan uzun plan kerak bo'lsa — bu odatda ikkita alohida savol.
Guruh: ikkalasi yoki hech biri
"Eng katta 10 ta mijoz" ni ikki qadam bilan yozish mumkin: group_and_total, keyin top_n. Lekin
bu juda tez-tez uchraydigan so'rov — shuning uchun top_n ning o'zida ixtiyoriy group
argumenti bor. U oddiy argument emas, ob'ekt: ichida columns (nima bo'yicha guruhlash) va
agg (qanday jamlash) — ikkalasi ham majburiy.
Nega ob'ekt? Chunki "guruhla, lekin qanday jamlashni aytma" degan yarim buyruq ma'nosiz. Agar
group_columns va group_agg alohida ixtiyoriy argumentlar bo'lsa, model ulardan bittasini
yozib, ikkinchisini unutishi mumkin edi. Ob'ekt ichida ikkalasi ham majburiy bo'lgani uchun
grammar faqat ikki holatga ruxsat beradi: ikkalasi ham bor yoki ikkalasi ham yo'q.
Ikki yo'lni sintetik kredit jadvalida solishtiramiz:
from accuracy.run import open_data, BASE from accuracy.execute import run_plan from accuracy.compare import same_result sozlamalar = json.load(open(TEZ / "accuracy/settings.json", encoding="utf-8")) con = open_data(str(DATA / "loans.csv")) bitta = run_plan(con, BASE, [ {"tool": "top_n", "args": {"by": "qoldiq_uzs", "n": 3, "descending": True, "group": {"columns": ["viloyat"], "agg": "sum"}}}, ], sozlamalar) ikki = run_plan(con, BASE, [ {"tool": "group_and_total", "args": {"group_by": ["viloyat"], "value_column": "qoldiq_uzs", "agg": "sum"}}, {"tool": "top_n", "args": {"by": "qoldiq_uzs", "n": 3, "descending": True}}, ], sozlamalar) guruhsiz = run_plan(con, BASE, [ {"tool": "top_n", "args": {"by": "qoldiq_uzs", "n": 3, "descending": True}}, ], sozlamalar) print("bitta qadam (group bilan):", bitta["rows"]) print("ikki qadam bilan bir xilmi:", same_result(bitta, ikki, sozlamalar)) print(f"group siz: {len(guruhsiz['rows'])} qator, {len(guruhsiz['columns'])} ustun — eng katta uchta kredit, viloyatlar emas")
bitta qadam (group bilan): [('Toshkent sh.', 1460145964265.9033), ('Namangan', 1442975847712.3447), ('Buxoro', 1441073780449.128)]
ikki qadam bilan bir xilmi: True
group siz: 3 qator, 22 ustun — eng katta uchta kredit, viloyatlar emas
group bilan — uchta viloyat va ularning jami qoldig'i. Ikki qadamli plan bilan aynan bir xil.
group siz esa — butunlay boshqa savolning javobi: eng katta uchta kredit. Tool tavsifida
shuning uchun bitta jumla bor: "odamlar, filiallar yoki boshqa guruhlarni reytinglaganda group
ishlat".
Variantlar: argumentlar orasidagi qoidalar
Ba'zi argumentlar bir-biriga bog'liq. filter da: = amali bitta value talab qiladi, in
amali — values ro'yxatini, is_empty esa hech qanday qiymat talab qilmaydi. Bu qoidalar
variants da yozilgan:
for variant in asboblar["filter"]["variants"]: amallar = variant["op"]["enum"] qiymat = "yo'q" if variant["value"] is None else ("jadvaldagi qiymatlardan biri" if variant["value"].get("ref") == "column_value" else "erkin") royxat = "yo'q" if variant["values"] is None else "ro'yxat" print(f"{', '.join(amallar):28s} value: {qiymat:30s} values: {royxat}")
=, != value: jadvaldagi qiymatlardan biri values: yo'q >, >=, <, <=, contains value: erkin values: yo'q in, not_in value: yo'q values: ro'yxat is_empty, not_empty value: yo'q values: yo'q
Har bir variant — alohida, qat'iy shakl. null — "bu variantda bu argument umuman yo'q". Natijada
model is_empty ga qiymat yoza olmaydi, = ni qiymatsiz qoldira olmaydi.
top_n da ham variantlar bor, va ular nozikroq qoidani ushlaydi:
for variant in asboblar["top_n"]["variants"]: guruh = variant["group"] if guruh is None: print("group yo'q: by — istalgan ustun") else: by = variant.get("by", {}).get("ref", "column") print(f"group, agg = {'|'.join(guruh['properties']['agg']['enum']):12s} by — {by}")
group yo'q: by — istalgan ustun group, agg = sum|avg by — number_column group, agg = count|min|max by — column
Jamlash sum yoki avg bo'lsa, by — faqat sonli ustun: matnni qo'shib bo'lmaydi.
count, min, max esa matn ustunida ham ma'noli. group_and_total da ham xuddi shunday
variant bor. 7-bobda 0.8B model aynan shu xatoni qilgan edi — group_and_total bilan matn
ustunini jamlashga urindi (sum(VARCHAR)) va plan yiqildi. Endi grammar bunday planni yozishga
yo'l qo'ymaydi.
Har bir qadam — yangi jadval
Plan qanday bajariladi? Engine ning yuragi — run_plan funksiyasi, bor-yo'g'i yigirma qator:
import inspect from accuracy import execute print(inspect.getsource(execute.run_plan))
def run_plan(con, base, steps, settings, keep_as=None):
current = base
artifacts = []
created = []
try:
for index, step in enumerate(steps, 1):
tool = TOOLS.get(step["tool"])
if tool is None:
raise ToolError(f"unknown tool {step['tool']}")
target = f"{STEP_PREFIX}{index}"
con.execute(f"DROP TABLE IF EXISTS {q(target)}")
artifacts = [*artifacts, *tool(con, current, target, step["args"], settings)]
created.append(target)
current = target
names, rows = _fetch(con, current)
if keep_as:
con.execute(f"CREATE OR REPLACE TABLE {q(keep_as)} AS SELECT * FROM {q(current)}")
return {"columns": names, "rows": rows, "artifacts": artifacts}
finally:
for table in created:
con.execute(f"DROP TABLE IF EXISTS {q(table)}")
Har bir qadam yangi jadval yaratadi (_step_1, _step_2, ...) va keyingi qadam shu jadvaldan
o'qiydi. Asl jadval hech qachon o'zgarmaydi. Oxirida oraliq jadvallar o'chiriladi — xato bo'lsa
ham (finally). Agar tool nomi noma'lum bo'lsa, plan darhol to'xtaydi.
Bu "hech narsani ustidan yozma" tamoyili kelajakdagi "bekor qilish" tugmasining asosi: har bir holat alohida jadval bo'lsa, orqaga qaytish — shunchaki oldingi jadvalga qaytish.
Davomli suhbat
Tahlilchi bitta savol bilan to'xtamaydi: "Faqat dollardagi kreditlar." Keyin: "Endi viloyatlar bo'yicha jamla." Ikkinchi so'rov birinchisining natijasi ustida.
Lekin ba'zan u yangi narsa boshlaydi: "Eng katta uchta kreditni ko'rsat" — bu dollar filtri
ustida emas, butun jadval ustida. Model buni farqlashi kerak. Suhbatda har bir javob boshida
"on" maydoni turadi: "result" — joriy natija ustida davom et, "table" — butun jadvaldan
qaytadan boshla:
from accuracy.execute import effective_steps birinchi = {"steps": [{"tool": "filter", "args": {"column": "valyuta", "op": "=", "value": "USD"}}]} davom = {"on": "result", "steps": [{"tool": "group_and_total", "args": {"group_by": ["viloyat"], "value_column": "qoldiq_uzs", "agg": "sum"}}]} yangi = {"on": "table", "steps": [{"tool": "top_n", "args": {"by": "qoldiq_uzs", "n": 3, "descending": True}}]} for nom, javob in (("on: result", davom), ("on: table", yangi)): qadamlar = effective_steps([birinchi], javob) print(f"{nom:11s} -> bajariladi: {' -> '.join(q['tool'] for q in qadamlar)}")
on: result -> bajariladi: filter -> group_and_total on: table -> bajariladi: top_n
"result" da engine oldingi qadamlarni ham qayta bajaradi: filtr, keyin jamlash. "table" da
eski qadamlar tashlab yuboriladi.
Suhbatda model yana bir narsani bilishi kerak: joriy natijada qaysi ustunlar bor va u allaqachon jamlanganmi. Buning uchun ba'zi toollarda qo'shimcha belgilar bor:
for nom, a in asboblar.items(): belgilar = {k: v for k, v in a.items() if k in ("groups", "makes_numbers", "changes_values")} if belgilar: print(f"{nom:16s} {belgilar}")
group_and_total {'groups': ['group_by']}
period_compare {'groups': ['period', 'group_by']}
top_n {'groups': ['group.columns']}
fix_dates {'changes_values': ['column']}
fix_numbers {'makes_numbers': ['column'], 'changes_values': ['column']}
unify_spelling {'changes_values': ['column']}
fill_empty {'changes_values': ['column']}
groups— bu tool natijani shu ustunlar bo'yicha jamlaydi. Keyingi so'rovda promptga "joriy natija allaqachon ... bo'yicha jamlangan" degan eslatma qo'shiladi.makes_numbers—fix_numbersdan keyin ustun son bo'ladi va sonli ustun talab qiladigan toollarda ham tanlanadigan bo'ladi.changes_values— tozalashdan keyin ustundagi qiymatlar o'zgaradi, shuning uchun eski "jadvaldagi qiymatlar ro'yxati" grammar dan olib tashlanadi.
Ya'ni grammar ham suhbat bilan birga o'zgaradi: har bir yangi so'rovda u joriy natijaga mos qilib qayta yasaladi.
Lug'at va misollar
Toollardan tashqari promptda yana ikki narsa bor. Lug'at — biznes so'zlarini ustunlarga bog'laydi:
kontekst = build.load_context(TEZ / "prompt/domains/bank_loans") for soz, manosi in list(kontekst["glossary"].items())[:5]: print(f"{soz:45s} -> {manosi}") print() print(f"misollar soni: {len(kontekst['examples'])}") print(json.dumps(kontekst["examples"][0], ensure_ascii=False))
kredit / кредит / qarz / loan -> one row of the table
kredit summasi / сумма кредита / miqdor -> loan amount, prefer column kredit_summasi_uzs for totals
qoldiq / остаток / balance -> outstanding balance, column qoldiq_uzs
qarzdor / mijoz / клиент / заемщик / borrower -> borrower, columns qarzdor_id and qarzdor_ismi
kechikish / просрочка / overdue -> column kechikish_kuni > 0, or holat = Muddati o'tgan
misollar soni: 3
{"request": "Kredit xodimlari bo'yicha qoldiqlar yig'indisi", "answer": {"steps": [{"tool": "group_and_total", "args": {"group_by": ["kredit_xodimi"], "value_column": "qoldiq_uzs", "agg": "sum"}}]}}
Bank xodimi "qoldiq" deydi, rus tilida "остаток", ingliz tilida "balance" — lekin ustun nomi
qoldiq_uzs. Lug'at shu ko'prikni quradi. U har bir jadval turi uchun alohida fayl
(prompt/domains/.../glossary.json), chunki "summa" kreditlar jadvalida bitta ustun, kassa
jadvalida boshqa.
Misollar — bir nechta "so'rov → to'g'ri javob" juftligi. Promptda ularning oldida qat'iy ogohlantirish bor: "bu faqat namuna, suhbatning qismi emas". Bu ogohlantirish bejiz emas: 7-bobda promptdagi bitta misol test savollariga juda yaqin chiqqani uchun almashtirilgan edi.
Testlar
Engine va schema yasovchi — bank raqamlari o'tadigan kod. Ular test bilan himoyalangan. Testlarni kitob yig'ilayotgan paytda ishga tushiramiz:
cd /mnt/c/Users/Admin/Desktop/Offline/spikes/spike2_3_model PYTHONDONTWRITEBYTECODE=1 "$OLDPWD/.venv/bin/python" -m pytest accuracy/test_engine.py prompt/test_build.py -q -p no:cacheprovider 2>&1 | tail -3
........................................................................ [ 85%] ............................................................. [100%] 421 passed in 19.47s
Testlarning nomlari o'zi ko'p narsani aytadi: test_remove_rows_is_the_opposite_of_filter,
test_sum_on_text_column_fails, test_unknown_tool_fails_and_cleans_up,
test_missing_column_after_grouping_fails, test_follow_up_continues_on_the_current_result. Bu
bobdagi har bir qoida — o'chirish filtrning teskarisi, matnni jamlab bo'lmaydi, noma'lum tool
to'xtaydi, suhbat davom etadi — test bilan qotirilgan.
Xatolar jurnalida bitta qoida bor: "har bir yangi tool shakli bo'sh natija bilan test qilinadi". U training ma'lumotini tekshiruvchi koddagi xatodan chiqqan: bo'sh jadval ustidagi jami bitta qator qaytarardi va tekshiruv nolga bo'lib, yomon planlarni o'tkazib yuborgan edi (jurnalda 9-raqam).
Xulosa
- Toollar — model va engine orasidagi shartnoma, bitta faylda (
tools.json). Promptdagi tavsif ham, grammar ham shu fayldan avtomatik yasaladi. - Javob — qadamlar ro'yxati (1 dan 4 gacha) yoki savol. Ro'yxat "eng katta 10 ta mijoz" kabi so'rovlarni yozish mumkin bo'lishi uchun kiritildi.
- Bog'liq argumentlar bitta ob'ektga yig'iladi (
top_nninggroupi): ikkalasi ham bor yoki hech biri. - Variantlar argumentlar orasidagi qoidalarni ushlaydi:
is_emptyga qiymat yo'q, matn ustunini jamlab bo'lmaydi. - Har bir qadam yangi jadval yaratadi; asl jadval o'zgarmaydi. Suhbatda
"on"maydoni davom etish yoki qaytadan boshlashni bildiradi.
Keyingi bob — schema ning grammar ga aylanishi va undagi eng qimmat tuzoq: ixtiyoriy kalitlar.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.