sofhiaazzhr Claude Opus 5.5 commited on
Commit
ae72d12
·
1 Parent(s): 35886db

[NOTICKET] eval(intent): follow-up cases with chat history

Browse files

- run_eval.py sent every case without history, so follow-up routing was never tested
- New case_history(): an optional `history` (user/ai turns, oldest first) goes to the
router like the app's chat history; cases without it run as before
- 7 followup_* rows: analysis follow-ups -> structured_flow, "detailnya?" after a
document answer -> unstructured_flow, column/source questions stay check
- followup_01 uses the real chat-1 history before Test demo Q2 (a short made-up
history did not reproduce the miss); followup_02 is "sitenya mana aja?"
- Old router prompt 61/64 (both fail), new prompt 63/64; unstructured_04 predates this
- Dataset schema/_next_layer and README note the new `history` field

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

eval/intent/README.md CHANGED
@@ -45,6 +45,10 @@ eval runs fully without Langfuse configured.
45
  needed group plus extras — safe, the planner still sees what it needs), **absent**
46
  (fail-open — safe), **wrong** (a needed group missing — the planner cannot reach the tool).
47
  `stray` counts hints on non-structured intents.
 
 
 
 
48
 
49
  ### Content-filter blocks count as `out_of_scope` passes
50
 
 
45
  needed group plus extras — safe, the planner still sees what it needs), **absent**
46
  (fail-open — safe), **wrong** (a needed group missing — the planner cannot reach the tool).
47
  `stray` counts hints on non-structured intents.
48
+ - **follow-ups** (added 2026-11-03) — a case may carry `history` (earlier `user`/`ai` turns,
49
+ oldest first); the runner sends it to the router as the app's chat history. The
50
+ `followup_*` rows check that a short follow-up ("unitnya yg mana aja?", "detailnya?")
51
+ takes the route of the answer it follows instead of reading as an inventory question.
52
 
53
  ### Content-filter blocks count as `out_of_scope` passes
54
 
eval/intent/intent_dataset.json CHANGED
@@ -1,14 +1,15 @@
1
  {
2
  "_about": "Golden intent dataset for the reworked 6-intent router (E3 eval — runs against the live LLM, not the mocked unit tests). Each of the 6 intents has 6-7 DISTINCT scenarios (not EN/ID translation pairs of one scenario) balanced across English + Indonesian, since users code-switch (technical terms often stay English: revenue, churn, upload, base_price). `carried_over` rows mirror the old intent_router.md examples for regression; the rest are new. `id` is a stable per-case handle so timestamped run files (eval_result_<ts>.json) can be diffed case-by-case across runs — it is NOT the run timestamp (that lives in the filename). `lang` enables per-language correctness scoring (aggregate, not matched-pairs).",
3
  "_grounding": "structured_flow + structured `check` rows are grounded in Sofhia's real test files: online_vs_offline_learning_dataset.csv (cols: Learning_Mode, Subject, Study_Hours, Retention_Score, Focus_Level, Exam_Score) and xl_knowledge_large.xlsx (telco product catalog: product_name, category, base_price, is_active, region_restriction...). unstructured_flow + document `check` rows are grounded in the IoT connectivity indicative-price PDF and the Internet of Things DOCX. IMPORTANT: routing labels are decided from the question PHRASING, not from which file holds the answer (the router has no catalog access) — so structured rows are clearly analytical (avg/correlation/count) and unstructured rows are clearly explanatory (jelaskan/summarize/features), with no price-lookup collisions. Rifqi's DB-grounded rows can be merged into this same file for variety; owner: Rifqi + Sofhia.",
4
- "_next_layer": "Not in this seed: (1) deliberate near-boundary cases (chat-vs-help, check-vs-structured); (2) follow-up/contextual handling — decide whether follow-ups route to `chat` or are out of scope.",
5
  "schema": {
6
  "id": "stable per-case handle, <intent>_<NN>",
7
  "message": "the user utterance fed to the router",
8
  "expected_intent": "one of: chat | help | check | unstructured_flow | structured_flow | out_of_scope",
9
  "lang": "en | id",
10
  "carried_over": "true if mirrored from the pre-rework intent_router.md examples",
11
- "expected_group_hint": "structured_flow only: list of ACCEPTABLE group sets (e.g. [[\"evaluate\"], [\"explore\"]] when a question scopes two valid ways). Absent on every other intent = expects no hint. Scored separately from intent: exact | absent (fail-open, safe) | wrong (added 2026-09-23)"
 
12
  },
13
  "cases": [
14
  {
@@ -424,6 +425,93 @@
424
  "expected_group_hint": [["timeseries"], ["explore"]],
425
  "lang": "en",
426
  "carried_over": false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  }
428
  ]
429
  }
 
1
  {
2
  "_about": "Golden intent dataset for the reworked 6-intent router (E3 eval — runs against the live LLM, not the mocked unit tests). Each of the 6 intents has 6-7 DISTINCT scenarios (not EN/ID translation pairs of one scenario) balanced across English + Indonesian, since users code-switch (technical terms often stay English: revenue, churn, upload, base_price). `carried_over` rows mirror the old intent_router.md examples for regression; the rest are new. `id` is a stable per-case handle so timestamped run files (eval_result_<ts>.json) can be diffed case-by-case across runs — it is NOT the run timestamp (that lives in the filename). `lang` enables per-language correctness scoring (aggregate, not matched-pairs).",
3
  "_grounding": "structured_flow + structured `check` rows are grounded in Sofhia's real test files: online_vs_offline_learning_dataset.csv (cols: Learning_Mode, Subject, Study_Hours, Retention_Score, Focus_Level, Exam_Score) and xl_knowledge_large.xlsx (telco product catalog: product_name, category, base_price, is_active, region_restriction...). unstructured_flow + document `check` rows are grounded in the IoT connectivity indicative-price PDF and the Internet of Things DOCX. IMPORTANT: routing labels are decided from the question PHRASING, not from which file holds the answer (the router has no catalog access) — so structured rows are clearly analytical (avg/correlation/count) and unstructured rows are clearly explanatory (jelaskan/summarize/features), with no price-lookup collisions. Rifqi's DB-grounded rows can be merged into this same file for variety; owner: Rifqi + Sofhia.",
4
+ "_next_layer": "Not in this seed: (1) deliberate near-boundary cases (chat-vs-help, check-vs-structured); (2) follow-up/contextual handling — decide whether follow-ups route to `chat` or are out of scope. UPDATE 2026-11-03: (2) is now covered by the `followup_*` rows — a short follow-up takes the route of the answer it follows (Test demo Q2).",
5
  "schema": {
6
  "id": "stable per-case handle, <intent>_<NN>",
7
  "message": "the user utterance fed to the router",
8
  "expected_intent": "one of: chat | help | check | unstructured_flow | structured_flow | out_of_scope",
9
  "lang": "en | id",
10
  "carried_over": "true if mirrored from the pre-rework intent_router.md examples",
11
+ "expected_group_hint": "structured_flow only: list of ACCEPTABLE group sets (e.g. [[\"evaluate\"], [\"explore\"]] when a question scopes two valid ways). Absent on every other intent = expects no hint. Scored separately from intent: exact | absent (fail-open, safe) | wrong (added 2026-09-23)",
12
+ "history": "optional: earlier turns, oldest first, as [{\"role\": \"user\" | \"ai\", \"content\": ...}] — sent to the router like the app's chat history (added 2026-11-03)"
13
  },
14
  "cases": [
15
  {
 
425
  "expected_group_hint": [["timeseries"], ["explore"]],
426
  "lang": "en",
427
  "carried_over": false
428
+ },
429
+ {
430
+ "id": "followup_01",
431
+ "message": "unitnya yg mana aja?",
432
+ "expected_intent": "structured_flow",
433
+ "expected_group_hint": [["explore"]],
434
+ "lang": "id",
435
+ "carried_over": false,
436
+ "history": [
437
+ {"role": "user", "content": "datanya lengkap ga? ada yg kosong atau aneh?"},
438
+ {"role": "ai", "content": "Ya, datanya **lengkap untuk kolom yang diperiksa**: 1.080 baris, 17 kolom, **tanpa nilai kosong** dan **tanpa duplikasi baris**. Namun ada **nilai anomali** pada sebagian baris.\n\n| Ringkasan | Hasil |\n|---|---:|\n| Baris | 1.080 |\n| Kolom | 17 |\n| Nilai kosong | 0 |\n| Duplikasi baris | 0 |\n| Baris anomali | 127 |\n| Tingkat anomali | 11,759% |\n\n**Temuan utama**\n- Kolom `site_code` dan `section` konstan: masing-masing hanya berisi `ADT` dan `OB HAULER`.\n- Kolom numerik yang diperiksa semuanya terisi, tetapi beberapa memiliki nilai ekstrem; misalnya `pa` memiliki 68 outlier, `mttr` 80, `mtbs` 55, `mtbf` 73, `breakdown_duration_sm` 72, dan `breakdown_duration_usm` 70.\n- Dari 1.080 baris, 127 terdeteksi sebagai anomali.\n\n**Catatan**\n- Karena cakupan yang terlihat hanya untuk `ADT · OB HAULER · 2026-06 s.d. 2026-09`, kesimpulan ini berlaku untuk rentang tersebut saja.\n- `pa_target` juga bervariasi (87, 88,15, dan 90), jadi target tidak sepenuhnya seragam di seluruh baris.\n\n_Cakupan: section OB HAULER · site_code ADT · year 2026 · month 6–9_"},
439
+ {"role": "user", "content": "gambaran umum PA sama MTBS OB HAULER ADT gimana?"},
440
+ {"role": "ai", "content": "Ringkasan PA dan MTBS untuk OB HAULER di site ADT menunjukkan PA rata-rata **88,537%** dengan median **91,05%**, sedangkan MTBS rata-rata **43,379** dengan median **35,1**.\n\n| Metrik | Jumlah data | Rata-rata | Median | Modus | Simpangan baku | Q1 | Q3 | Minimum | Maksimum |\n|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|\n| PA | 2.379 | 88,537% | 91,05% | 92,5% | 9,913 | 85,085% | 95,13% | 11,5% | 100,0% |\n| MTBS | 2.379 | 43,379 | 35,1 | 21,7 | 30,525 | 24,633 | 52,72 | 0,9 | 307,15 |\n\nSebaran PA cenderung tinggi dan miring ke kiri, sementara MTBS lebih menyebar dan miring ke kanan. Ada **1 nilai MTBS kosong** dari 2.379 baris.\n\n_Cakupan: section OB HAULER · site_code ADT_"},
441
+ {"role": "user", "content": "model apa yg PA nya paling jelek?"},
442
+ {"role": "ai", "content": "Model dengan PA terendah di OB HAULER ADT selama Juni–September 2026 adalah **HD465-7** dengan **76,129%**.\n\n| Model | Avg PA | n_periods | last_month |\n|---|---:|---:|---:|\n| HD465-7 | 76,129% | 19 | 9 |\n| 777E | 86,048% | 609 | 9 |\n| HD785-7 | 88,853% | 452 | 9 |\n\n_Cakupan: section OB HAULER · site_code ADT · year 2026 · month 6–9_"},
443
+ {"role": "user", "content": "buatin grafiknya"},
444
+ {"role": "ai", "content": "Berikut ringkasan PA per model untuk **OB HAULER** di **ADT** selama **Juni–September 2026**. Model dengan PA terendah adalah **HD465-7** dengan **76,129%**; dua model lainnya lebih tinggi, yaitu **777E** (**86,048%**) dan **HD785-7** (**88,853%**). Grafik batang di bawah jawaban sudah menyorot model dengan PA terendah.\n\n| Model | PA rata-rata |\n|---|---:|\n| HD465-7 | 76,129% |\n| 777E | 86,048% |\n| HD785-7 | 88,853% |\n\n_Cakupan: section OB HAULER · site_code ADT · year 2026 · month 6–9_"}
445
+ ]
446
+ },
447
+ {
448
+ "id": "followup_02",
449
+ "message": "sitenya mana aja?",
450
+ "expected_intent": "structured_flow",
451
+ "expected_group_hint": [["explore"]],
452
+ "lang": "id",
453
+ "carried_over": false,
454
+ "history": [
455
+ {"role": "user", "content": "model apa yg PA nya paling jelek?"},
456
+ {"role": "ai", "content": "Model dengan PA terendah: HD465-7 76,13%, 777E 86,05%, HD785-7 88,85%."}
457
+ ]
458
+ },
459
+ {
460
+ "id": "followup_03",
461
+ "message": "subject-nya yang mana aja?",
462
+ "expected_intent": "structured_flow",
463
+ "expected_group_hint": [["explore"]],
464
+ "lang": "id",
465
+ "carried_over": false,
466
+ "history": [
467
+ {"role": "user", "content": "rata-rata Exam_Score per Learning_Mode berapa?"},
468
+ {"role": "ai", "content": "Rata-rata Exam_Score: Offline 74,8, Online 69,3."}
469
+ ]
470
+ },
471
+ {
472
+ "id": "followup_04",
473
+ "message": "which products?",
474
+ "expected_intent": "structured_flow",
475
+ "expected_group_hint": [["explore"]],
476
+ "lang": "en",
477
+ "carried_over": false,
478
+ "history": [
479
+ {"role": "user", "content": "Which category has the highest average base_price?"},
480
+ {"role": "ai", "content": "Enterprise has the highest average base_price (Rp 2.4 M), then Business (Rp 1.1 M)."}
481
+ ]
482
+ },
483
+ {
484
+ "id": "followup_05",
485
+ "message": "detailnya?",
486
+ "expected_intent": "unstructured_flow",
487
+ "lang": "id",
488
+ "carried_over": false,
489
+ "history": [
490
+ {"role": "user", "content": "apa key feature dari iot connectivity?"},
491
+ {"role": "ai", "content": "Menurut dokumen IoT: konektivitas NB-IoT dan LTE-M, manajemen SIM terpusat, dan pemantauan perangkat real-time."}
492
+ ]
493
+ },
494
+ {
495
+ "id": "followup_06",
496
+ "message": "kolomnya apa aja?",
497
+ "expected_intent": "check",
498
+ "lang": "id",
499
+ "carried_over": false,
500
+ "history": [
501
+ {"role": "user", "content": "data apa aja yang aku punya?"},
502
+ {"role": "ai", "content": "Kamu punya 2 sumber data: online_vs_offline_learning_dataset.csv dan xl_knowledge_large.xlsx."}
503
+ ]
504
+ },
505
+ {
506
+ "id": "followup_07",
507
+ "message": "What data sources do I have?",
508
+ "expected_intent": "check",
509
+ "lang": "en",
510
+ "carried_over": false,
511
+ "history": [
512
+ {"role": "user", "content": "Which category has the highest average base_price?"},
513
+ {"role": "ai", "content": "Enterprise has the highest average base_price (Rp 2.4 M), then Business (Rp 1.1 M)."}
514
+ ]
515
  }
516
  ]
517
  }
eval/intent/run_eval.py CHANGED
@@ -32,6 +32,7 @@ from pathlib import Path
32
  from typing import Any
33
 
34
  from langchain_core.callbacks import BaseCallbackHandler
 
35
  from langchain_core.outputs import LLMResult
36
 
37
  from src.agents.orchestration import OrchestratorAgent
@@ -150,6 +151,20 @@ def load_cases(path: Path) -> list[dict[str, Any]]:
150
  return list(data["cases"])
151
 
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  @dataclass
154
  class _LangfuseCtx:
155
  """Optional Langfuse sink — one session groups all cases of a run."""
@@ -220,7 +235,9 @@ async def run_case(
220
  correct: bool
221
  hint_got: list[str] | None = None
222
  try:
223
- decision = await agent.classify(case["message"], callbacks=callbacks)
 
 
224
  got = decision.intent
225
  correct = got == expected
226
  hint_got = list(decision.group_hint) if decision.group_hint else None
 
32
  from typing import Any
33
 
34
  from langchain_core.callbacks import BaseCallbackHandler
35
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
36
  from langchain_core.outputs import LLMResult
37
 
38
  from src.agents.orchestration import OrchestratorAgent
 
151
  return list(data["cases"])
152
 
153
 
154
+ def case_history(case: dict[str, Any]) -> list[BaseMessage]:
155
+ """The case's optional `history` (oldest first) as the messages the app sends.
156
+
157
+ Mirrors `load_history`: `user` turns become HumanMessage, `ai` turns AIMessage.
158
+ Cases without `history` classify the message alone, as before.
159
+ """
160
+ return [
161
+ HumanMessage(content=turn["content"])
162
+ if turn["role"] == "user"
163
+ else AIMessage(content=turn["content"])
164
+ for turn in case.get("history", [])
165
+ ]
166
+
167
+
168
  @dataclass
169
  class _LangfuseCtx:
170
  """Optional Langfuse sink — one session groups all cases of a run."""
 
235
  correct: bool
236
  hint_got: list[str] | None = None
237
  try:
238
+ decision = await agent.classify(
239
+ case["message"], case_history(case), callbacks=callbacks
240
+ )
241
  got = decision.intent
242
  correct = got == expected
243
  hint_got = list(decision.group_hint) if decision.group_hint else None