Rifqi Hafizuddin Claude Opus 5.5 commited on
Commit
b6e060b
Β·
1 Parent(s): 39019d5

eval(chat_sim): doc + mixed suites with bound documents

Browse files

UNSTRUCTURED_V2_PLAN U8. Suite.document_ids binds v2-ingested documents in the
shape Go's buildBoundSource materialises; unowned or un-indexed documents are
refused.

doc suite (BUMA standard): 4/4 after rephrasing turn 3 β€” a figure answer
carried asset://4f72f8b170d1787b (Gambar 2.1), resolved via image-token to
200 image/jpeg. mixed suite is defined but not run: dedorch data_catalog has
0 rows, so padb cannot be bound.

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

eval/chat_sim/README.md CHANGED
@@ -53,3 +53,28 @@ uv run python -m eval.chat_sim.run_chat --plain # no ANSI colors
53
  output is shown (annotated `maskedβ†’cloud` where the cloud trace would redact it).
54
  - The first scripted question may route to `chat` rather than `help` β€” that's the live
55
  router's real call, shown transparently.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  output is shown (annotated `maskedβ†’cloud` where the cloud trace would redact it).
54
  - The first scripted question may route to `chat` rather than `help` β€” that's the live
55
  router's real call, shown transparently.
56
+
57
+ ## Document suites β€” `doc` and `mixed` (added 2026-09-23)
58
+
59
+ `Suite.document_ids` binds v2-ingested documents (`documents.id`, ingested through
60
+ `/api/v2/document/*`) next to the suite's data source. `setup_analysis` writes them into
61
+ the analysis-scope row as `{source_id: documents.id, source_type: "unstructured"}` β€” the
62
+ shape Go's `buildBoundSource` materialises β€” and **refuses** a document that is not owned
63
+ by `--user-id` or whose v2 ingestion did not complete. A suite may have documents and no
64
+ data source (`doc`).
65
+
66
+ ```bash
67
+ ./.venv/Scripts/python.exe -m eval.chat_sim.run_chat --suite doc --no-report
68
+ ./.venv/Scripts/python.exe -m eval.chat_sim.run_chat --suite mixed --no-report
69
+ ```
70
+
71
+ - **`doc`** β€” the BUMA standard alone: check β†’ definition (ID) β†’ a figure answer that must
72
+ carry an `asset://` marker β†’ a formula (EN). Document turns write no `report_inputs`,
73
+ so `--no-report` (the report would 409 by design).
74
+ - **`mixed`** β€” `padb` + the BUMA standard: check lists both β†’ two turns needing the
75
+ document's definition *and* computed numbers β†’ one numbers-only turn that must NOT
76
+ retrieve the document.
77
+ - The document id is `BUMA_DOCUMENT_ID` in `run_chat.py`; re-ingesting mints a new one.
78
+ - **Phrase figure turns by topic, not by number, and not as "show me the chart"** β€” see
79
+ the notes on the `doc` suite's turn 3 (UNSTRUCTURED_V2_PLAN Β§4.4).
80
+
eval/chat_sim/results/transcript_doc_2026-09-23_114510.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run": {
3
+ "timestamp": "2026-09-23T11:45:10",
4
+ "suite": "doc",
5
+ "source_id": null,
6
+ "source_name": "Standar Parameter Produksi (BUMA).pdf",
7
+ "notes": "Document only. Turn 1 = check lists the document; 2 + 4 = document Q&A (ID/EN); 3 = a figure answer, which must carry an asset:// marker (U6).",
8
+ "user_id": "78e8df76-14a1-43ec-95bc-498b4a029701",
9
+ "analysis_id": "04ddbd55-562e-46b5-99f4-840d4f23d1ee",
10
+ "objective": "Understand how the company standard defines and calculates its production parameters.",
11
+ "turns": 4,
12
+ "tokens": {
13
+ "input": 41148,
14
+ "output": 731,
15
+ "total": 41879
16
+ }
17
+ },
18
+ "turns": [
19
+ {
20
+ "n": 1,
21
+ "question": "Dokumen apa saja yang ada di analisis ini?",
22
+ "intent": "check",
23
+ "confidence": 0.98,
24
+ "rewritten": "What documents are available in this analysis?",
25
+ "answer": "Kamu punya 1 dokumen:\n\n- Standar Parameter Produksi (BUMA).pdf",
26
+ "tools": [],
27
+ "tokens_in": 4430,
28
+ "tokens_out": 59,
29
+ "ms": 15924
30
+ },
31
+ {
32
+ "n": 2,
33
+ "question": "Apa definisi Physical Availability menurut standar ini, dan bagaimana rumusnya?",
34
+ "intent": "unstructured_flow",
35
+ "confidence": 0.97,
36
+ "rewritten": "What is the definition of Physical Availability according to this standard, and what is the formula?",
37
+ "answer": "Menurut standar ini, **Physical Availability (PA)** adalah **ketersediaan fisik suatu equipment/unit**, yaitu proporsi waktu equipment/unit tersebut berada dalam kondisi **available (siap pakai)** selama periode tertentu.\n\nRumus umumnya:\n\n**PA = (Total Hours - Breakdown) / Total Hours Γ— 100%**\n\nUntuk grouping/composite, rumus dasarnya:\n\n**PA_ij = (Total Hours_ij - Breakdown_ij) / Total Hours_ij Γ— 100%**\n\nStandar juga menyebutkan bahwa nilai PA hasil perhitungan tersebut ekuivalen dengan **rata-rata tertimbang** PA masing-masing type/class unit dengan **Quantity (Qty)** sebagai faktor pembobot.",
38
+ "tools": [],
39
+ "tokens_in": 7053,
40
+ "tokens_out": 217,
41
+ "ms": 30580
42
+ },
43
+ {
44
+ "n": 3,
45
+ "question": "Show me the waterfall chart that breaks down gain/loss per production parameter.",
46
+ "intent": "structured_flow",
47
+ "confidence": 0.96,
48
+ "rewritten": "Show me the waterfall chart that breaks down gain/loss per production parameter.",
49
+ "answer": "I can't answer that from the data sources connected to this analysis. The catalog does not expose any production-parameter measure or gain/loss column that can be used to build a waterfall breakdown. The available approved measures are Qty, PA, UA, produksi, and PTY, but none of them represent gain/loss per parameter, and no charting workflow exists for a waterfall chart specifically. The nearest supported analyses are a grouped breakdown of one of the approved measures by an existing category, or a trend/summary of PA, UA, Qty, produksi, or PTY if you want a chartable metric. You can bind a source that holds this data, or ask me what's available (try /help or \"what data do I have?\").",
50
+ "tools": [],
51
+ "tokens_in": 22556,
52
+ "tokens_out": 253,
53
+ "ms": 40392
54
+ },
55
+ {
56
+ "n": 4,
57
+ "question": "How is Utilization of Availability (UA) calculated in this standard?",
58
+ "intent": "unstructured_flow",
59
+ "confidence": 0.98,
60
+ "rewritten": "How is Utilization of Availability (UA) calculated in this standard?",
61
+ "answer": "Dalam standar ini, **Utilization of Availability (UA)** adalah tingkat efektivitas penggunaan alat yang tersedia secara fisik, yaitu seberapa lama unit digunakan secara efektif untuk bekerja (**working hours**) selama berada dalam kondisi siap dioperasikan (**available**).\n\nRumusnya:\n\n**UA = Working Hours / (Total Hours - Breakdown)**\n\nUntuk **Grouping (Composite)** type/class unit, UA dihitung dengan pembobotan berdasarkan **Quantity (Qty)**:\n\n**UA_ij = (Ξ£ Qty_ij Γ— UA_ij) / (Ξ£ Qty_ij)**\n\nCatatan: UA disesuaikan berdasarkan kategori aktivitas operasional (**activity**).",
62
+ "tools": [],
63
+ "tokens_in": 7109,
64
+ "tokens_out": 202,
65
+ "ms": 18336
66
+ }
67
+ ],
68
+ "report_markdown": null
69
+ }
eval/chat_sim/results/transcript_doc_2026-09-23_114643.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run": {
3
+ "timestamp": "2026-09-23T11:46:43",
4
+ "suite": "doc",
5
+ "source_id": null,
6
+ "source_name": "Standar Parameter Produksi (BUMA).pdf",
7
+ "notes": "Document only. Turn 1 = check lists the document; 2 + 4 = document Q&A (ID/EN); 3 = a figure answer, which must carry an asset:// marker (U6).",
8
+ "user_id": "78e8df76-14a1-43ec-95bc-498b4a029701",
9
+ "analysis_id": "26d45658-8624-4dda-a70d-fb27f7963074",
10
+ "objective": "Understand how the company standard defines and calculates its production parameters.",
11
+ "turns": 1,
12
+ "tokens": {
13
+ "input": 6046,
14
+ "output": 150,
15
+ "total": 6196
16
+ }
17
+ },
18
+ "turns": [
19
+ {
20
+ "n": 1,
21
+ "question": "What does Figure 2.1 in the standard show? Include the figure.",
22
+ "intent": "unstructured_flow",
23
+ "confidence": 0.93,
24
+ "rewritten": "What does Figure 2.1 in the standard show? Include the figure.",
25
+ "answer": "I can’t see Figure 2.1 in the provided pages. The available context only shows notes on pages 6–7 and a table titled **β€œPerhitungan Gain/Loss per Model Unit”** on page 6, but not the figure itself.\n\nIf you want, I can still help summarize the surrounding section or look for another page/chunk that contains the figure marker.",
26
+ "tools": [],
27
+ "tokens_in": 6046,
28
+ "tokens_out": 150,
29
+ "ms": 30777
30
+ }
31
+ ],
32
+ "report_markdown": null
33
+ }
eval/chat_sim/results/transcript_doc_2026-09-23_114849.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run": {
3
+ "timestamp": "2026-09-23T11:48:49",
4
+ "suite": "doc",
5
+ "source_id": null,
6
+ "source_name": "Standar Parameter Produksi (BUMA).pdf",
7
+ "notes": "Document only. Turn 1 = check lists the document; 2 + 4 = document Q&A (ID/EN); 3 = a figure answer, which must carry an asset:// marker (U6).",
8
+ "user_id": "78e8df76-14a1-43ec-95bc-498b4a029701",
9
+ "analysis_id": "27a9f75d-0344-48c5-8163-2907c421bb5c",
10
+ "objective": "Understand how the company standard defines and calculates its production parameters.",
11
+ "turns": 1,
12
+ "tokens": {
13
+ "input": 6411,
14
+ "output": 229,
15
+ "total": 6640
16
+ }
17
+ },
18
+ "turns": [
19
+ {
20
+ "n": 1,
21
+ "question": "Apa isi Gambar 2.1 pada bagian Waterfall Analysis di standar? Sertakan gambarnya.",
22
+ "intent": "unstructured_flow",
23
+ "confidence": 0.91,
24
+ "rewritten": "What is the content of Figure 2.1 in the Waterfall Analysis section of the standard? Include the image.",
25
+ "answer": "Gambar 2.1 pada bagian *Waterfall Analysis* adalah **β€œAnalisis Gain/Loss Berdasarkan Setiap Parameter Produksi”**. Keterangan di bawahnya menjelaskan bahwa analisis ini menampilkan kontribusi Gain/Loss dari setiap parameter produksi berdasarkan hasil perhitungan Gain/Loss Production.\n\n![](asset://4f72f8b170d1787b)\n\nKeterangan tambahan yang ada di halaman itu:\n- **Other Activity** adalah kondisi ketika alokasi equipment/unit pada pelaksanaan aktual tidak sesuai dengan aktivitas yang telah direncanakan.\n- Contohnya, equipment/unit yang direncanakan untuk *waste removal* dipakai untuk *coal mining* pada pelaksanaan aktual, atau sebaliknya.",
26
+ "tools": [],
27
+ "tokens_in": 6411,
28
+ "tokens_out": 229,
29
+ "ms": 29287
30
+ }
31
+ ],
32
+ "report_markdown": null
33
+ }
eval/chat_sim/run_chat.py CHANGED
@@ -106,6 +106,11 @@ class Suite(NamedTuple):
106
  objective: str
107
  turns: list[str]
108
  notes: str
 
 
 
 
 
109
 
110
 
111
  SUITES: dict[str, Suite] = {}
@@ -247,6 +252,57 @@ SUITES["cycle"] = Suite(
247
  )
248
 
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  # Default scripted conversation. Chosen to exercise every router intent against the
251
  # real Titanic columns (Survived, Sex, Pclass, Age, Fare, Embarked).
252
  DEFAULT_TURNS = SUITES["pa"].turns
@@ -555,6 +611,37 @@ def save_transcript(
555
  # ───────────────────────────── setup / turns ─────────────────────────────────
556
 
557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  async def setup_analysis(user_id: str, suite: Suite, bind_source: bool) -> str:
559
  """Create a fresh analysis session (state row) + optionally bind it to the source.
560
 
@@ -587,20 +674,29 @@ async def setup_analysis(user_id: str, suite: Suite, bind_source: bool) -> str:
587
  # user's catalog restricted to the bound source. structured_flow reads it via
588
  # CatalogStore.get_by_analysis (the data_sources binding table was removed;
589
  # in production Go materializes B from analyses.data_bind).
590
- user_cat = await CatalogStore().get(user_id)
591
- bound = [
592
- s for s in (user_cat.sources if user_cat else [])
593
- if s.source_id == suite.source_id
594
- ]
595
- if not bound:
596
- # Hard stop, not a warning. Falling open to the whole catalog here
597
- # means every answer is about a DIFFERENT dataset than the suite
598
- # was written for, and the run still looks green. Better to refuse.
 
 
 
 
 
 
 
 
599
  raise RuntimeError(
600
- f"source {suite.source_id} ({suite.name}) is not in "
601
- f"{user_id}'s catalog β€” wrong --user-id for this suite?"
602
  )
603
- else:
 
604
  scoped = CatalogModel(
605
  user_id=user_id, generated_at=datetime.now(UTC), sources=bound,
606
  )
@@ -610,8 +706,9 @@ async def setup_analysis(user_id: str, suite: Suite, bind_source: bool) -> str:
610
  catalog_payload=scoped.model_dump(mode="json"),
611
  ))
612
  await s.commit()
613
- print(f" bound source: {suite.name} ({suite.source_id}) "
614
- f"{c('dim', 'β†’ structured_flow scoped to it (analysis catalog)')}")
 
615
  except Exception as e: # noqa: BLE001 β€” fail-open to whole catalog
616
  print(c("warn", f" binding skipped ({type(e).__name__}: {e}) β€” "
617
  f"fail-open to whole catalog"))
@@ -756,7 +853,7 @@ async def amain(args: argparse.Namespace) -> None:
756
  enable_tracing=False, enable_gate=False
757
  )
758
  suite = SUITES[args.suite]
759
- if suite.source_id is None:
760
  # Refused here, before a single token is spent: the source is not in
761
  # dedorch, so structured_flow would silently fall back to the whole
762
  # catalog and every answer would be about the WRONG data -- a green run
 
106
  objective: str
107
  turns: list[str]
108
  notes: str
109
+ # v2-ingested documents (`documents.id`) bound alongside `source_id` β€” added
110
+ # 2026-09-23 for UNSTRUCTURED_V2_PLAN U8. They are written into the analysis-scope
111
+ # catalog in exactly the shape Go's `buildBoundSource` materialises for a bound
112
+ # pdf/docx/txt: {source_id: documents.id, source_type: "unstructured", ...}.
113
+ document_ids: tuple[str, ...] = ()
114
 
115
 
116
  SUITES: dict[str, Suite] = {}
 
252
  )
253
 
254
 
255
+ # Documents v2 suites (UNSTRUCTURED_V2_PLAN U8, 2026-09-23). The document is the BUMA
256
+ # "Standar Parameter Produksi" PDF (9 pages, 28 chunks, 2 figures), ingested through
257
+ # the real v2 routes for DEFAULT_USER_ID. It DEFINES PA/UA/Pty and their formulas and
258
+ # carries two figures (Gambar 2.1 waterfall p.8, Gambar 2.2 p.9) β€” it states NO
259
+ # numeric PA target, so no turn asks for one. Re-ingest -> update the id below.
260
+ BUMA_DOCUMENT_ID = "0b4160db-327c-461a-b30d-348f94718f1b"
261
+
262
+ # Unstructured only: no data source, one bound document.
263
+ SUITES["doc"] = Suite(
264
+ source_id=None,
265
+ name="Standar Parameter Produksi (BUMA).pdf",
266
+ objective="Understand how the company standard defines and calculates its production "
267
+ "parameters.",
268
+ notes="Document only. Turn 1 = check lists the document; 2 + 4 = document Q&A (ID/EN); "
269
+ "3 = a figure answer, which must carry an asset:// marker (U6).",
270
+ turns=[
271
+ "Dokumen apa saja yang ada di analisis ini?",
272
+ "Apa definisi Physical Availability menurut standar ini, dan bagaimana rumusnya?",
273
+ # Phrased as a question ABOUT the document's figure, NAMING ITS TOPIC. Two
274
+ # failures on 2026-09-23 set this wording: "Show me the waterfall chart ..."
275
+ # routed to structured_flow (the router reads "chart" as a plotting request)
276
+ # and was refused; "What does Figure 2.1 show?" routed correctly but dense
277
+ # retrieval cannot match a bare figure NUMBER (top-5 = pp.6-7, figure on p.8).
278
+ # Naming the topic ranks the figure chunk 1st-2nd in EN and ID.
279
+ "Apa isi Gambar 2.1 pada bagian Waterfall Analysis di standar? Sertakan gambarnya.",
280
+ "How is Utilization of Availability (UA) calculated in this standard?",
281
+ ],
282
+ document_ids=(BUMA_DOCUMENT_ID,),
283
+ )
284
+
285
+ # Structured + unstructured: padb (Postgres) with the standard bound beside it.
286
+ SUITES["mixed"] = Suite(
287
+ source_id=SUITES["padb"].source_id,
288
+ name="pa example (postgres) + Standar Parameter Produksi (BUMA).pdf",
289
+ objective="Understand fleet availability across 2025, read against how the company "
290
+ "standard defines it.",
291
+ notes="Turn 1 = check lists data AND document; 2 = mixed (definition + monthly trend); "
292
+ "3 = mixed EN (UA vs PA definition + site comparison); 4 = numbers only, must "
293
+ "NOT retrieve the document.",
294
+ turns=[
295
+ "Data dan dokumen apa saja yang ada di analisis ini?",
296
+ "Bagaimana standar mendefinisikan Physical Availability, dan bagaimana tren PA "
297
+ "bulanan kita sepanjang 2025?",
298
+ "According to the standard, what does PA measure that UA does not β€” and how does "
299
+ "PA compare between our two sites?",
300
+ "Rata-rata PA per model unit berapa?",
301
+ ],
302
+ document_ids=(BUMA_DOCUMENT_ID,),
303
+ )
304
+
305
+
306
  # Default scripted conversation. Chosen to exercise every router intent against the
307
  # real Titanic columns (Survived, Sex, Pclass, Age, Fare, Embarked).
308
  DEFAULT_TURNS = SUITES["pa"].turns
 
611
  # ───────────────────────────── setup / turns ─────────────────────────────────
612
 
613
 
614
+ async def _document_sources(user_id: str, document_ids: tuple[str, ...]) -> list:
615
+ """Catalog sources for bound documents, shaped like Go's `buildBoundSource`.
616
+
617
+ Only documents this user owns whose v2 ingestion completed β€” a document that
618
+ never indexed would bind, retrieve nothing, and make a green-looking run.
619
+ """
620
+ if not document_ids:
621
+ return []
622
+ from datetime import UTC, datetime
623
+
624
+ from sqlalchemy import select
625
+
626
+ from src.catalog.models import Source
627
+ from src.db.postgres.connection import AsyncSessionLocal
628
+ from src.db.postgres.models import Document
629
+
630
+ async with AsyncSessionLocal() as s:
631
+ rows = (await s.execute(
632
+ select(Document).where(
633
+ Document.id.in_(document_ids),
634
+ Document.user_id == user_id,
635
+ Document.status == "completed",
636
+ )
637
+ )).scalars().all()
638
+ return [
639
+ Source(source_id=d.id, source_type="unstructured", name=d.filename,
640
+ location_ref=f"document://{d.id}", updated_at=datetime.now(UTC))
641
+ for d in rows
642
+ ]
643
+
644
+
645
  async def setup_analysis(user_id: str, suite: Suite, bind_source: bool) -> str:
646
  """Create a fresh analysis session (state row) + optionally bind it to the source.
647
 
 
674
  # user's catalog restricted to the bound source. structured_flow reads it via
675
  # CatalogStore.get_by_analysis (the data_sources binding table was removed;
676
  # in production Go materializes B from analyses.data_bind).
677
+ bound = []
678
+ if suite.source_id:
679
+ user_cat = await CatalogStore().get(user_id)
680
+ bound = [
681
+ s for s in (user_cat.sources if user_cat else [])
682
+ if s.source_id == suite.source_id
683
+ ]
684
+ if not bound:
685
+ # Hard stop, not a warning. Falling open to the whole catalog here
686
+ # means every answer is about a DIFFERENT dataset than the suite
687
+ # was written for, and the run still looks green. Better to refuse.
688
+ raise RuntimeError(
689
+ f"source {suite.source_id} ({suite.name}) is not in "
690
+ f"{user_id}'s catalog β€” wrong --user-id for this suite?"
691
+ )
692
+ docs = await _document_sources(user_id, suite.document_ids)
693
+ if len(docs) != len(suite.document_ids):
694
  raise RuntimeError(
695
+ f"only {len(docs)} of {len(suite.document_ids)} suite documents are "
696
+ f"v2-ingested and owned by {user_id} β€” re-ingest via /api/v2/document/*"
697
  )
698
+ bound = [*bound, *docs]
699
+ if bound:
700
  scoped = CatalogModel(
701
  user_id=user_id, generated_at=datetime.now(UTC), sources=bound,
702
  )
 
706
  catalog_payload=scoped.model_dump(mode="json"),
707
  ))
708
  await s.commit()
709
+ for s_ in bound:
710
+ print(f" bound {s_.source_type}: {s_.name} ({s_.source_id})")
711
+ print(c("dim", " β†’ turns scoped to these (analysis catalog)"))
712
  except Exception as e: # noqa: BLE001 β€” fail-open to whole catalog
713
  print(c("warn", f" binding skipped ({type(e).__name__}: {e}) β€” "
714
  f"fail-open to whole catalog"))
 
853
  enable_tracing=False, enable_gate=False
854
  )
855
  suite = SUITES[args.suite]
856
+ if suite.source_id is None and not suite.document_ids:
857
  # Refused here, before a single token is spent: the source is not in
858
  # dedorch, so structured_flow would silently fall back to the whole
859
  # catalog and every answer would be about the WRONG data -- a green run