Asosiy qismga o'tish

Toollar va JSON plan

8-bob 12 kod bloki 2601 so'z

Bu bobdagi kod shu fayllar bilan ishlaydio'z kompyuteringizda ishga tushiring

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:

python4 qator
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))
Chiqish
{
 "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:

  • name va description — 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:

python3 qator
from prompt import build

print(build.tool_lines([asboblar["filter"], asboblar["top_n"], asboblar["fix_numbers"]]))
Chiqish
- 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):

json1 qator
{"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:

python2 qator
sozlama = json.load(open(TEZ / "prompt/settings.json", encoding="utf-8"))
print(f"max_steps = {sozlama['max_steps']}")
Chiqish
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:

python22 qator
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")
Chiqish
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:

python5 qator
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}")
Chiqish
=, !=                        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:

python7 qator
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}")
Chiqish
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:

python4 qator
import inspect
from accuracy import execute

print(inspect.getsource(execute.run_plan))
Chiqish
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:

python9 qator
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)}")
Chiqish
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:

python4 qator
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}")
Chiqish
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_numbers dan 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:

python6 qator
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))
Chiqish
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:

bash2 qator
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
Chiqish (qaytish kodi 0, 21671 ms)
........................................................................ [ 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_n ning group i): ikkalasi ham bor yoki hech biri.
  • Variantlar argumentlar orasidagi qoidalarni ushlaydi: is_empty ga 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.

Telegramda yozish Narxlar