/feat knowledge management
#20
by rhbt6767 - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- .gitignore +5 -1
- DEV_PLAN.md +50 -0
- KNOWLEDGE_PIPELINE_CALIBRATION.md +257 -0
- KNOWLEDGE_PIPELINE_TODO.md +210 -0
- eval/knowledge/README.md +84 -0
- eval/knowledge/knowledge_gold.yaml +312 -0
- eval/knowledge/results/baseline_prototype_2026-08-13_145132.json +157 -0
- eval/knowledge/results/v2_parser_comparison_2026-08-19_152842.json +59 -0
- eval/knowledge/results/v2_term_filter_2026-08-19_144318.json +53 -0
- eval/knowledge/score.py +227 -0
- eval/planner/results/planner_result_2026-07-23_134947.json +662 -0
- eval/planner/results/planner_result_2026-07-23_141931.json +1818 -0
- eval/planner/results/planner_result_2026-07-23_152650.json +2123 -0
- eval/readiness/results/readiness_result_2026-06-23_094622.json +284 -0
- eval/readiness/results/readiness_result_2026-07-02_083335.json +250 -0
- eval/readiness/results/readiness_result_2026-07-02_093621.json +250 -0
- knowledge_pipeline_context.md +299 -0
- pyproject.toml +22 -0
- src/config/settings.py +7 -0
- src/knowledge_extraction/__init__.py +62 -0
- src/knowledge_extraction/adapter.py +136 -0
- src/knowledge_extraction/cli.py +251 -0
- src/knowledge_extraction/cluster/__init__.py +4 -0
- src/knowledge_extraction/cluster/cluster.py +111 -0
- src/knowledge_extraction/cluster/normalize.py +77 -0
- src/knowledge_extraction/config/__init__.py +0 -0
- src/knowledge_extraction/config/cues.yaml +51 -0
- src/knowledge_extraction/config/labels.yaml +44 -0
- src/knowledge_extraction/diff/__init__.py +3 -0
- src/knowledge_extraction/diff/glossary_diff.py +33 -0
- src/knowledge_extraction/extract/__init__.py +27 -0
- src/knowledge_extraction/extract/base.py +67 -0
- src/knowledge_extraction/extract/branches.py +235 -0
- src/knowledge_extraction/extract/client.py +221 -0
- src/knowledge_extraction/extract/prompts/formula.txt +28 -0
- src/knowledge_extraction/extract/prompts/glossary.txt +68 -0
- src/knowledge_extraction/extract/prompts/rule.txt +39 -0
- src/knowledge_extraction/extract/prompts/summary.txt +21 -0
- src/knowledge_extraction/extract/schemas.py +85 -0
- src/knowledge_extraction/filters/__init__.py +12 -0
- src/knowledge_extraction/filters/cue_filter.py +76 -0
- src/knowledge_extraction/filters/legend_filter.py +98 -0
- src/knowledge_extraction/filters/span_filter.py +169 -0
- src/knowledge_extraction/models.py +273 -0
- src/knowledge_extraction/queue/__init__.py +3 -0
- src/knowledge_extraction/queue/review_queue.py +65 -0
- src/knowledge_extraction/rank/__init__.py +3 -0
- src/knowledge_extraction/rank/evidence.py +168 -0
- src/knowledge_extraction/service.py +231 -0
- src/knowledge_extraction/settings.py +93 -0
.gitignore
CHANGED
|
@@ -54,4 +54,8 @@ docs/specs/tabular_parquet_contract.md
|
|
| 54 |
docs/specs/tabular_parquet.md
|
| 55 |
|
| 56 |
# Personal / local working docs (not for the shared repo) β archived out of root
|
| 57 |
-
docs/_archive/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
docs/specs/tabular_parquet.md
|
| 55 |
|
| 56 |
# Personal / local working docs (not for the shared repo) β archived out of root
|
| 57 |
+
docs/_archive/
|
| 58 |
+
# Knowledge pipeline output β data, not code (parse cache + run artifacts)
|
| 59 |
+
data/knowledge_cache/
|
| 60 |
+
data/knowledge_runs/
|
| 61 |
+
data/knowledge_docs/
|
DEV_PLAN.md
CHANGED
|
@@ -273,6 +273,51 @@ by design** until #43 (Go-forwarded identity). Not a gap to re-raise β it is a
|
|
| 273 |
CORS was left at `["*"]` on purpose (tightening it needs the FE origin as config, which we chose
|
| 274 |
not to set for now).
|
| 275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
## 5. Critical path & sequencing
|
| 277 |
|
| 278 |
- **Critical path:** ~~#22 (send Harry the `report_inputs` schema)~~ **β
resolved 2026-07-22** β now **#32** (`message_traceability` + `message_charts` DDL to Harry) and **#31** (non-convergent migration set). HF deploy (#13) for the playground. (#4 β
, #21 β
; Harry's #3 no longer blocks us β Python is getattr-tolerant.)
|
|
@@ -297,3 +342,8 @@ not to set for now).
|
|
| 297 |
- **W3 re-open timing** (deferred `analyze_*` activation) β Rifqi (deferred at the 2026-07-13 approval).
|
| 298 |
- **INV-6 relaxation for S1b targeted repair** β team, only after S1a `repair_candidate` telemetry
|
| 299 |
shows a meaningful hit-rate (SPINE_V2_PLAN Β§6).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
CORS was left at `["*"]` on purpose (tightening it needs the FE origin as config, which we chose
|
| 274 |
not to set for now).
|
| 275 |
|
| 276 |
+
## 0.8. Knowledge pipeline β test integration (planning, 2026-08-19)
|
| 277 |
+
|
| 278 |
+
**Nothing has landed in this repo.** This section exists so the in-flight work is tracked in the
|
| 279 |
+
usual place; no code, dependency, table or endpoint has been added here yet. Do not read any of it
|
| 280 |
+
into REPO_STATUS.
|
| 281 |
+
|
| 282 |
+
Context and intent: [knowledge_pipeline_context.md](knowledge_pipeline_context.md). Task breakdown,
|
| 283 |
+
owners and verified statuses: [KNOWLEDGE_PIPELINE_TODO.md](KNOWLEDGE_PIPELINE_TODO.md).
|
| 284 |
+
|
| 285 |
+
**Shape.** A six-stage pipeline (parse β free CPU/regex filter β one LLM call per *term cluster* β
|
| 286 |
+
deterministic span validation β diff vs. the active glossary β frequency-sorted expert review) that
|
| 287 |
+
turns client documents into reviewable knowledge entries, ultimately served over MCP. It splits into
|
| 288 |
+
two independently runnable halves β **parsing (Sofhia, MinerU)** and **knowledge extraction
|
| 289 |
+
(Rifqi)** β meeting only at a **persisted, versioned parsed-document artifact**. Extraction never
|
| 290 |
+
takes a file path; that constraint is the whole point of the split.
|
| 291 |
+
|
| 292 |
+
**Where it stands (audited 2026-08-19).** A working **prototype** (`kex`, ~2,763 LOC, 21 passing
|
| 293 |
+
tests, 8-command CLI, four experiments against real Azure calls) exists outside version control at
|
| 294 |
+
`Documents/Dataeyond_catalog/knowledge-pipeline/`. It is **not the product and will not be ported** β
|
| 295 |
+
it answered the design questions; extraction v2 gets rebuilt in this repo. What carries forward is
|
| 296 |
+
the *evidence* (gold sets, experiment results) and the *tuned constants*, not the code.
|
| 297 |
+
Measured on the BUMA standard (9 pages): GLiNER Indonesian recall **0.854** (kill line 0.70, PASS β
|
| 298 |
+
this closes the context doc's headline open risk), clustering compression **2.56Γ** (kill 2.0, PASS),
|
| 299 |
+
nano schema-fill precision **0.75** (kill 0.80, **FAIL**), span-validation rejection **1.00** with
|
| 300 |
+
0.00 false rejections (kill 0.90, PASS). Cost **$0.0069** for the document.
|
| 301 |
+
|
| 302 |
+
| # | Task | Owner | Status | Note |
|
| 303 |
+
|---|---|---|---|---|
|
| 304 |
+
| P1 | Freeze the `kex` prototype (read-only snapshot) | Rifqi | β¬ | No `.git`. ~2,763 LOC + both gold sets + all four experiment results on one machine. It is an archive, **not** a codebase to develop β v2 is a rebuild here, not a port |
|
| 305 |
+
| P2 | Harvest the prototype's tuned constants | Rifqi | β¬ | Every number that took runs to find, with its reason, **before** v2 code: rapidfuzz 92, fuzzy off below 5 chars (`PA`/`UA` collide), K=3, the evidence weight table, the 1024-token prefix pad (below it, caching does not engage β worth the measured 54% hit rate), label + cue sets, the 4 prompts |
|
| 306 |
+
| P3 | Migrate the evidence into `eval/knowledge/` | Rifqi | β¬ | Gold sets (41 terms + 15 rules) + scorer + experiment runners, following the existing `eval/{intent,help,readiness,planner}/` convention. **Before any v2 pipeline code** β without a standing baseline, "improved" is unfalsifiable |
|
| 307 |
+
| S1 | Agree the parsed-artifact schema | Sofhia + Rifqi | π | Sofhia's draft `contracts.py` (2026-08-19) adopted as the base. Open: document-level envelope (no home for `version`), the section-structure claim, `source_wording` |
|
| 308 |
+
| S5 | Keep the seam off the file path | Sofhia + Rifqi | β¬ | The prototype's boundary was `Parser.parse(path) -> list[Page]` β the shape the plan forbids. Settle **before** the MinerU parser is built against it |
|
| 309 |
+
| X* | Build extraction v2 in this repo | Rifqi | β¬ | 17 stages, `src/query/`-shaped. Design is validated by the prototype; **none of it exists here yet** β see the two-column table in the checklist |
|
| 310 |
+
| D2 | Artifact + candidate-entry persistence | Rifqi β Harry | β¬ | Parsed artifacts, candidate entries, glossary versions + approval audit trail. Go owns dedorch DDL β one consolidated Harry-ready handoff, never DDL from Python (Β§2.2) |
|
| 311 |
+
| R1 | Fix the literal-source-wording violation | Rifqi | β¬ | A locked decision is violated: the BUMA heading reads "Physical **of** Availability (PA)"; the extracted entry carries "Physical Availability". The discrepancy is meant to reach the expert |
|
| 312 |
+
| D3 | Provision a mini deployment | Rifqi | β | Only nano is provisioned, so all four branches route there β including `rule` and `summary`, whose failure mode is least detectable. Blocks the nano-vs-mini A/B |
|
| 313 |
+
| D6 | Expert review of the gold set | Rifqi β Mas Beta | β | The 41-term / 15-rule gold set is self-bootstrapped and provisional. Now the baseline v2 is measured against |
|
| 314 |
+
| D5 | v2 module name | Rifqi | β¬ | `src/knowledge/` is taken by the existing OCRβpgvector service. Pick a non-colliding name before the first commit |
|
| 315 |
+
|
| 316 |
+
**Not decided, do not assume:** the v2 module name (D5); whether the admin ingest surface is HTTP
|
| 317 |
+
at all (the offline script path is the honest first milestone); and GLiNER + LangExtract ensembling
|
| 318 |
+
stays βΈοΈ flagged as scope creep. **`kex` is not being ported** οΏ½οΏ½ it is a prototype that answered the
|
| 319 |
+
design questions; v2 is built here against this repo's patterns.
|
| 320 |
+
|
| 321 |
## 5. Critical path & sequencing
|
| 322 |
|
| 323 |
- **Critical path:** ~~#22 (send Harry the `report_inputs` schema)~~ **β
resolved 2026-07-22** β now **#32** (`message_traceability` + `message_charts` DDL to Harry) and **#31** (non-convergent migration set). HF deploy (#13) for the playground. (#4 β
, #21 β
; Harry's #3 no longer blocks us β Python is getattr-tolerant.)
|
|
|
|
| 342 |
- **W3 re-open timing** (deferred `analyze_*` activation) β Rifqi (deferred at the 2026-07-13 approval).
|
| 343 |
- **INV-6 relaxation for S1b targeted repair** β team, only after S1a `repair_candidate` telemetry
|
| 344 |
shows a meaningful hit-rate (SPINE_V2_PLAN Β§6).
|
| 345 |
+
- **Knowledge pipeline β v2 module name** (Β§0.8 D5): `src/knowledge/` is already the OCRβpgvector
|
| 346 |
+
service, so extraction v2 needs its own module name before the first commit.
|
| 347 |
+
- **Knowledge pipeline β admin ingest surface** (Β§0.8 D4): whether it is HTTP at all, and if so its
|
| 348 |
+
auth posture. A write surface triggered by an admin is a different risk class from the current
|
| 349 |
+
unauthenticated read-only chat surface (Β§0.7) β raise it, do not inherit the posture by default.
|
KNOWLEDGE_PIPELINE_CALIBRATION.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Knowledge Pipeline β Calibration Reference
|
| 2 |
+
|
| 3 |
+
**Harvested from the `kex` prototype, 2026-08-19.** Every number below cost a run, a bug, or a
|
| 4 |
+
measurement to find, and **none of it is in the spec** β it lived only as constants and comments in
|
| 5 |
+
a codebase that is not being ported.
|
| 6 |
+
|
| 7 |
+
This file exists because clean rewrites drop accidental knowledge and then rediscover it as
|
| 8 |
+
production defects. When building extraction v2, take the value **and** the reason. If you change
|
| 9 |
+
one, change it deliberately and record why.
|
| 10 |
+
|
| 11 |
+
Companions: [KNOWLEDGE_PIPELINE_TODO.md](KNOWLEDGE_PIPELINE_TODO.md) (plan) Β·
|
| 12 |
+
[eval/knowledge/](eval/knowledge/) (gold set, scorer, frozen baseline) Β·
|
| 13 |
+
[knowledge_pipeline_context.md](knowledge_pipeline_context.md) (intent).
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 1. Term filter β GLiNER
|
| 18 |
+
|
| 19 |
+
| Constant | Value | Why this value |
|
| 20 |
+
|---|---|---|
|
| 21 |
+
| Model | `urchade/gliner_multi-v2.1` | CPU, ~400 MB. Cheap enough to run unconditionally on every chunk, which is what lets us reject a standalone relevance gate |
|
| 22 |
+
| Score threshold | **0.25** | Not 0.35. The sweep measured recall **0.854 @ 0.25** vs **0.658 @ 0.35** on the `broad` label set. Precision falls (0.41 vs 0.50) and that is accepted: the filter is meant to be over-inclusive, clustering and evidence ranking absorb the noise, and **a term the filter never proposes can never be recovered downstream** |
|
| 23 |
+
| Label variant | **`broad`** | Two earlier variants both missed the same class β mining activities and materials (coal mining, land clearing, waste removal, joint survey). `broad` adds `mining activity`, `material or commodity`, `data source or system`, `work category`, `analysis method` to cover it |
|
| 24 |
+
| Window size | **130 words**, overlap **30** | Not 220. GLiNER truncates past ~384 of its own tokens and **warns rather than failing** β silently dropping the tail of every long chunk. Indonesian technical prose subword-tokenises at roughly **2.5Γ**, so a 220-word window still tripped the cap. Chunks are fed as overlapping word windows with offsets remapped |
|
| 25 |
+
| Span token cap | **12** (GLiNER's own) | Compound terms ("UA plan grouping (composite) actual") exceed it. Candidates that hit the cap are **flagged, not silently truncated**, so a recall shortfall can be attributed to the cap |
|
| 26 |
+
|
| 27 |
+
**Label sets** (`labels.yaml`) β three variants were tried and A/B'd without touching code. Keep
|
| 28 |
+
that property: label phrasing is the main recall lever and GLiNER is very sensitive to it. The
|
| 29 |
+
Indonesian-phrasing variant (`istilah produksi tambang`, β¦) did **not** win; `broad` did.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 2. Clustering
|
| 34 |
+
|
| 35 |
+
| Constant | Value | Why this value |
|
| 36 |
+
|---|---|---|
|
| 37 |
+
| Match order | exact (normalised) β abbreviationβexpansion β fuzzy | Cheapest and most certain first |
|
| 38 |
+
| Fuzzy threshold | **92** (`rapidfuzz.token_set_ratio`) | Conservative on purpose |
|
| 39 |
+
| **Fuzzy disabled below 5 characters** | `min(len(a), len(b)) < 5` β skip | **The one to not lose.** `PA` vs `UA` scores high on `token_set_ratio`. Below 5 chars only exact matching is allowed |
|
| 40 |
+
| Canonical surface | shortest variant β₯ 2 chars | That is how a reader looks a term up β `PA`, not `Physical Availability (PA) untuk β¦` |
|
| 41 |
+
|
| 42 |
+
**Over-merging is worse than under-merging, and the asymmetry is not close.** An under-merge costs
|
| 43 |
+
one extra LLM call and one extra review row. A wrong merge silently destroys a distinct term and
|
| 44 |
+
**the expert never sees it** β there is no downstream mechanism that recovers it.
|
| 45 |
+
|
| 46 |
+
**Legend extraction must run before clustering.** Without the abbreviation index, `PA` and
|
| 47 |
+
`Physical Availability` cluster as two unrelated terms.
|
| 48 |
+
|
| 49 |
+
**Stop surfaces** β dropped as whole surface forms only (never as substrings, so no term is lost):
|
| 50 |
+
`unit Β· type Β· class Β· equipment Β· equipment unit Β· parameter Β· activity Β· data Β· nilai Β· proses Β·
|
| 51 |
+
hasil Β· total`.
|
| 52 |
+
|
| 53 |
+
**Normalisation** (clustering only, never for span checking): NFKC β casefold β `-`/`_` β space β
|
| 54 |
+
strip `.β'` β non-word to space β collapse whitespace β strip ` ()/`.
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## 3. Evidence ranking
|
| 59 |
+
|
| 60 |
+
Six signals plus one penalty. The full ranked list is retained, not just the top K β escalation
|
| 61 |
+
needs the tail.
|
| 62 |
+
|
| 63 |
+
| Signal | Weight |
|
| 64 |
+
|---|---|
|
| 65 |
+
| Definitional cue within 100 chars of the mention | **+5.0** |
|
| 66 |
+
| Term appears in the chunk heading | **+4.0** |
|
| 67 |
+
| Mention sits inside a legend block | **+3.5** |
|
| 68 |
+
| Chunk contains a formula | **+2.0** |
|
| 69 |
+
| Mention is bold/italic | **+1.5** |
|
| 70 |
+
| First occurrence of the cluster | **+1.0** |
|
| 71 |
+
| Chunk is tabular | **β3.0** |
|
| 72 |
+
|
| 73 |
+
| Constant | Value | Why |
|
| 74 |
+
|---|---|---|
|
| 75 |
+
| `evidence_k` | **3** | Chunks fed per extraction round |
|
| 76 |
+
| Cue proximity | **100 chars** | Distance from mention to a definitional cue |
|
| 77 |
+
|
| 78 |
+
**Heading matching is word-boundary, never substring.** `PA` is a substring of `Parameter`, `pada`
|
| 79 |
+
and `composite` β substring matching handed the +4.0 heading bonus to almost every Indonesian
|
| 80 |
+
heading and pushed real definition sections *below* formula tables.
|
| 81 |
+
|
| 82 |
+
**Why this filter is legitimate when the relevance gate was rejected:** no term is dropped (only its
|
| 83 |
+
*evidence* is narrowed), every mention stays in provenance so a bad top-3 is visible, and the
|
| 84 |
+
escalation loop self-corrects. The rejected gate had none of those three properties. Preserve all
|
| 85 |
+
three if ranking is ever revisited.
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## 4. Chunking
|
| 90 |
+
|
| 91 |
+
| Constant | Value | Why |
|
| 92 |
+
|---|---|---|
|
| 93 |
+
| Max chunk tokens | **1500** | With paragraph-boundary splitting; sentence boundaries as fallback for a single over-budget paragraph |
|
| 94 |
+
| Max heading length | **90 chars** | Longer lines are sentences or formula rows, not headings |
|
| 95 |
+
| Boilerplate frequency | **β₯ 0.6 of pages** | A line repeating on 60%+ of pages is a running header/footer. Detected by frequency with **digits normalised to `#`** so page counters collapse β never by a hardcoded document-specific string |
|
| 96 |
+
| Tabular detection | short lines > 0.6 **and** numeric lines > 0.4 | Feeds the β3.0 ranking penalty |
|
| 97 |
+
|
| 98 |
+
**Three things the real document forced, all of which corrupt every downstream number if missed:**
|
| 99 |
+
|
| 100 |
+
1. **Running headers/footers** β every page repeats the title block and "Confidential".
|
| 101 |
+
2. **Breadcrumb headings** β pages re-print `2. PENJELASAN PARAMETER / 2.1. Production Parameter / β¦`
|
| 102 |
+
at the top. A naive splitter re-opens the section and shatters it. Rule used: *a heading that is
|
| 103 |
+
the current section or an ancestor of it, already seen, is a breadcrumb* β the section continues
|
| 104 |
+
and `page_end` extends.
|
| 105 |
+
3. **Colon-continuation** β legend rows arrive as `MOHH` then `: Machine on Hand Hours` on the next
|
| 106 |
+
line. Rejoined before the legend filter sees them.
|
| 107 |
+
|
| 108 |
+
Points 1 and 2 apply to MinerU output too: its `title` blocks repeat the same breadcrumbs on pages
|
| 109 |
+
2β8 of the standard.
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## 5. Extraction (the paid stage)
|
| 114 |
+
|
| 115 |
+
| Constant | Value | Why |
|
| 116 |
+
|---|---|---|
|
| 117 |
+
| Temperature | **0.0** | And still not deterministic β see below |
|
| 118 |
+
| Seed | 7 | |
|
| 119 |
+
| **Cache minimum** | **1024 prompt tokens** | OpenAI-family prompt caching **does not engage at all** below this. The glossary prefix is padded past it *on purpose*. Measured hit rate **54%** β 125,184 of 155,313 prompt tokens |
|
| 120 |
+
| Structured output | `json_schema`, fallback `json_object` + validate-retry | Which mode was used is recorded in the run manifest |
|
| 121 |
+
| API version | `2024-12-01-preview` | `json_schema` needs β₯ `2024-08-01-preview` |
|
| 122 |
+
|
| 123 |
+
**Prompts live in files, not code, and the fixed prefix must stay byte-identical across calls** β
|
| 124 |
+
any drift and caching stops engaging, silently, at ~10Γ the input cost.
|
| 125 |
+
|
| 126 |
+
**`temperature=0` is not determinism.** Consecutive runs of the identical pipeline scored 0.75 and
|
| 127 |
+
0.625 on schema fill. Never report a single run as a measurement on a small base.
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
## 6. Validation
|
| 132 |
+
|
| 133 |
+
| Constant | Value | Why |
|
| 134 |
+
|---|---|---|
|
| 135 |
+
| Span normalisation | **whitespace only** (+ NFKC) | Not case, not punctuation, not diacritics. Every additional normalisation is a hole a fabrication fits through |
|
| 136 |
+
| Escalation rounds | **max 2** | Then keep the entry flagged `no_definition_found` and pass it to review anyway β a term we found but could not define is still useful; dropping it hides a known unknown |
|
| 137 |
+
| Conflict overlap threshold | **0.4** token overlap | Token overlap, not embeddings: cheaper, needs no model, and **explainable to the reviewer who has to act on it** |
|
| 138 |
+
| Duplicate overlap threshold | **0.8** | Above it, a differing definition is a duplicate; below, a conflict |
|
| 139 |
+
|
| 140 |
+
**Guarded fields** (span-checked; failure sets the field to `None` and logs it):
|
| 141 |
+
|
| 142 |
+
| Branch | Fields |
|
| 143 |
+
|---|---|
|
| 144 |
+
| glossary | `definition`, `full_name`, `formula_latex`, `interpretation` |
|
| 145 |
+
| rule | `statement`, `condition`, `consequence` |
|
| 146 |
+
| formula | `formula_latex` |
|
| 147 |
+
| summary | *(none β generation cannot be span-checked at all)* |
|
| 148 |
+
|
| 149 |
+
**If the provenance span itself is not verbatim, every guarded field on the entry is rejected** β
|
| 150 |
+
the entry's only evidence link is broken, so nothing on it can be trusted.
|
| 151 |
+
|
| 152 |
+
**Never repair a failed span.** A repaired span is an unfalsifiable claim, which is exactly what the
|
| 153 |
+
control exists to prevent.
|
| 154 |
+
|
| 155 |
+
**Conflict detection never picks a winner.** The expert decides.
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## 7. Negative findings β do not re-derive these
|
| 160 |
+
|
| 161 |
+
| Finding | Detail |
|
| 162 |
+
|---|---|
|
| 163 |
+
| **nano is not sufficient for schema fill** | 0.75 precision against a 0.80 kill line. Decision 2026-08-19: **stay on nano anyway** for now; the `__54n` env quad is provisioned. Revisit if quality blocks the demo |
|
| 164 |
+
| **Escalation is unreachable on small documents** | 54 of 66 clusters had exactly **1** evidence chunk against K=3, so `rounds_available()` correctly returned 0 and the loop never fired. `escalated = 0` is not a bug. It stays unverified until a larger corpus runs |
|
| 165 |
+
| **Conflict detection had nothing to find** | 0 conflicts on a single internally-consistent standard. Untested against real disagreement |
|
| 166 |
+
| **85% abstention** | 56 of 66 entries returned no definition. Correct behaviour, but it means the review queue is mostly *"term found, no definition in document"*. Whether that is useful or noise is a review-experience question for Mas Beta |
|
| 167 |
+
| **Diff only ever ran the empty-baseline path** | The prototype diffed against the file it then overwrote, so every entry came back `new`. v2 needs a real active-version pointer |
|
| 168 |
+
| **Literal source wording was normalised away** | The standard's heading reads *"Physical **of** Availability (PA)"*; the entry carried `full_name: "Physical Availability"`. The gold set records the literal form correctly, so **gold was right and the pipeline was wrong**. Mechanism: the heading is a separate field from the chunk text, and the chunk text does not contain the phrase, so the literal wording never reached the model. Fix: feed the heading verbatim + a span-checked `source_wording` field |
|
| 169 |
+
| **A `glob('*.pdf')` picked the wrong document once** | Scored the textbook against the standard's gold set β recall 0.05. Document selection must be explicit and error on ambiguity |
|
| 170 |
+
| **torch wheels install corrupt** | `torchgen` missing, version reports `None`. Fix is delete `site-packages/torch*` and reinstall. Needs **torch β₯ 2.6**. Documented so nobody debugs it as a GLiNER problem |
|
| 171 |
+
|
| 172 |
+
---
|
| 173 |
+
|
| 174 |
+
## 8. What was measured, so v2 can be compared
|
| 175 |
+
|
| 176 |
+
Frozen in `eval/knowledge/results/baseline_prototype_2026-08-13_145132.json`.
|
| 177 |
+
|
| 178 |
+
Funnel on the 9-page standard: **9 pages β 13 chunks β 169 mentions** (195 before noise filtering)
|
| 179 |
+
**β 66 clusters β 83 LLM calls β 66 entries β 66 review-queue items.**
|
| 180 |
+
|
| 181 |
+
E1 recall **0.854** Β· E2 compression **2.56Γ** Β· E3 schema-fill precision **0.75** (FAIL) Β·
|
| 182 |
+
E4 fabrication rejection **1.00** with **0.00** false rejections. Cost **$0.0069**, 242 s wall
|
| 183 |
+
clock, 3 fields rejected by span check.
|
| 184 |
+
|
| 185 |
+
**When quoting E2 externally:** the context doc's **6.2Γ** is a 1,000-page projection; **2.56Γ** is
|
| 186 |
+
the 9-page measurement. Compression grows as terms repeat across a corpus. Both are true β use the
|
| 187 |
+
right one for the audience.
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## 9. Deliberate deviations from the prototype (v2)
|
| 192 |
+
|
| 193 |
+
Changes made while rebuilding, with the reason. Each one alters behaviour, so
|
| 194 |
+
each is listed rather than folded silently into the tables above.
|
| 195 |
+
|
| 196 |
+
| # | Change | Why | Measured? |
|
| 197 |
+
|---|---|---|---|
|
| 198 |
+
| **V1** | **A heading that names the term counts as a mention at offset 0** for cue-proximity scoring | Indonesian standards are written *"2.1.3 Physical of Availability (PA)"* / *"Adalah ketersediaan fisikβ¦"* β the heading names the term and the body opens with the definitional cue **without repeating it**. The prototype therefore awarded no cue bonus on the definition section itself. Measured on the BUMA standard: PA's own definition section (Β§2.1.3) scored 7.5 and ranked **third**, below a computation section (Β§2.2.4, 8.5) that merely mentions PA often. After the fix, all five terms rank their own definition section first β PAβ2.1.3, UAβ2.1.4, Qtyβ2.1.2, Ptyβ2.1.5, MOHHβ2.1.1 | Structurally verified on the 9-page standard. **Not yet scored** β needs the paid stage to show up in E3 |
|
| 199 |
+
| **V2** | **`first_occurrence` follows document order**, and ranking ties break on document order | The prototype awarded the +1.0 to whichever chunk a `set` iteration yielded first, making the bonus arbitrary and the ranking unreproducible across runs. "First occurrence" should mean the term's first appearance in the document | Determinism locked by test; verified stable across `PYTHONHASHSEED` 0/1/7/42/99 |
|
| 200 |
+
| **V3** | **`source_wording` field added** to the glossary entry | The control for the literal-source-wording defect (Β§7). Not yet populated β needs the seam to carry the heading verbatim and the extraction prompt to fill it | No |
|
| 201 |
+
|
| 202 |
+
**Both V1 and V2 change which chunks reach the paid stage**, so they can move E3
|
| 203 |
+
when the extraction branch lands. Re-score against
|
| 204 |
+
`eval/knowledge/results/baseline_prototype_2026-08-13_145132.json` at that point
|
| 205 |
+
and record the delta rather than assuming the improvement carries.
|
| 206 |
+
|
| 207 |
+
**V1 and the R1 literal-wording defect share a root cause:** the heading is a
|
| 208 |
+
separate field from the chunk text, so anything that reasons over "the term and
|
| 209 |
+
its definition together" has to be told to look at both.
|
| 210 |
+
|
| 211 |
+
| **V4** | **`source_wording` is taken from the section heading, not from the model** | Asked to quote the source wording, the model returned "Physical Availability (PA)" β a genuine verbatim quote from the page-1 intro, but not the Β§2.1.3 heading "Physical **of** Availability (PA)". Both occur in the document; only the heading form reveals the disagreement. Taking it deterministically means it cannot be normalised away | Live-verified on the pilot: the entry now carries both forms and the queue flags the mismatch |
|
| 212 |
+
| **V5** | **`evidence_text` includes chunk headings** | The heading is part of the source document and is often the only place a term is formally named. Excluding it rejected a correct verbatim quotation of the document's own section title (2 rejections on a 3-entry pilot, both spurious) | Live-verified: 0 rejections after the change |
|
| 213 |
+
| **V6** | **`full_name` and `source_wording` are span-checked against the source themselves** | Both claim to be literal transcriptions. The provenance span passing does not prove the transcription is faithful, and a normalised "full name" is exactly the silent correction this pipeline exists to surface | Unit-tested |
|
| 214 |
+
|
| 215 |
+
**Cache-floor finding (measured on the real prompts):** only the **glossary**
|
| 216 |
+
prefix clears the 1024-token floor at **1,401 tokens**. `rule` (731), `formula`
|
| 217 |
+
(459) and `summary` (288) are **not cacheable** and cache nothing today. Glossary
|
| 218 |
+
is also the branch with by far the most calls (66 of 83 on the reference
|
| 219 |
+
document), so the padding is on the branch that matters β but padding the other
|
| 220 |
+
three is free money if their call counts ever grow.
|
| 221 |
+
|
| 222 |
+
---
|
| 223 |
+
|
| 224 |
+
## 10. Parser comparison β MinerU vs PyMuPDF (2026-08-19)
|
| 225 |
+
|
| 226 |
+
First run of the extraction half on **MinerU-parsed input**, same document, same
|
| 227 |
+
gold set, same pipeline code. Result:
|
| 228 |
+
`eval/knowledge/results/v2_parser_comparison_2026-08-19_152842.json`.
|
| 229 |
+
|
| 230 |
+
| Input | E1 recall | Terms found |
|
| 231 |
+
|---|---|---|
|
| 232 |
+
| PyMuPDF (what the frozen baseline used) | **0.8537** | 35 / 41 |
|
| 233 |
+
| MinerU, raw markup | **0.7561** | 31 / 41 |
|
| 234 |
+
| MinerU, formulas + tables rendered to prose | **0.8293** | 34 / 41 |
|
| 235 |
+
|
| 236 |
+
**The span model reads prose, not markup.** MinerU's structure is an asset for
|
| 237 |
+
the formula branch and a liability for the filter unless it is rendered first:
|
| 238 |
+
|
| 239 |
+
- Terms wrapped in LaTeX are invisible. `\text {Total Hours} - \text {Breakdown}`
|
| 240 |
+
yields no mentions for either term, though both are gold terms and both are
|
| 241 |
+
found in the plain-text parse. Unwrapping `\text{}`/`\mathrm{}`/`\frac{}{}`
|
| 242 |
+
and collapsing MinerU's character-spaced output recovers them.
|
| 243 |
+
- Raw `<table>` HTML in the chunk text is noise the model has to read past. Eight
|
| 244 |
+
of fourteen chunks carried it before rendering.
|
| 245 |
+
|
| 246 |
+
**The residual ~2.4 points are chunking, not content.** MinerU marks non-section
|
| 247 |
+
labels β `Keterangan:`, `Keterangan grafik:` β as `text_level` titles. Opening a
|
| 248 |
+
new section at each one splits a legend away from the figure it explains, and
|
| 249 |
+
`Other Activity` / `Uncontrollable` are then missed **despite being present as
|
| 250 |
+
prose in a chunk the filter processed**. Only **numbered** headings should open a
|
| 251 |
+
section, which is consistent with Β§9's finding that MinerU flattens every heading
|
| 252 |
+
to `level: 2` β numbering is the only reliable section signal it gives us.
|
| 253 |
+
|
| 254 |
+
**Caveat on attribution:** the MinerU artifacts here were built by a scratch
|
| 255 |
+
converter, because section-aware chunking belongs to the parsing half. These are
|
| 256 |
+
findings about what that chunker must handle, not a measurement of MinerU
|
| 257 |
+
itself.
|
KNOWLEDGE_PIPELINE_TODO.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Knowledge Pipeline β Task Checklist
|
| 2 |
+
|
| 3 |
+
**Date:** 2026-08-19 Β· **Owner of this doc:** Rifqi Β· **Shared with:** Sofhia
|
| 4 |
+
**Intent + rationale:** [knowledge_pipeline_context.md](knowledge_pipeline_context.md)
|
| 5 |
+
**Tracked in:** [DEV_PLAN.md](DEV_PLAN.md) Β§0.8
|
| 6 |
+
**Target repo:** `Agentic-Service-Data-Eyond-Catalog` (Python agentic service)
|
| 7 |
+
|
| 8 |
+
**Status legend:** β¬ not started Β· π in progress Β· β
done Β· β blocked Β· π built but unverified Β· βΈοΈ deferred
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## How to read this
|
| 13 |
+
|
| 14 |
+
The pipeline splits into two independently runnable halves that meet at **one persisted, versioned
|
| 15 |
+
parsed-document artifact**: **parsing** (Sofhia, MinerU) and **knowledge extraction** (Rifqi).
|
| 16 |
+
Extraction never takes a file path β that constraint is the point of the split, because it keeps the
|
| 17 |
+
parser swappable and lets extraction be re-run cheaply on an already-parsed corpus.
|
| 18 |
+
|
| 19 |
+
**The extraction half has a working prototype (`kex`) that is not the product.** It lives outside
|
| 20 |
+
this repo, is not under version control, and will **not** be ported. It answered the design
|
| 21 |
+
questions; v2 gets rebuilt here against this repo's patterns. That distinction drives the whole
|
| 22 |
+
structure below β Β§2 disposes of the prototype, Β§5 builds v2, and Β§5 deliberately separates *design
|
| 23 |
+
validated* from *implemented here*, because the two are very different amounts of work.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## 1. What the prototype already proved
|
| 28 |
+
|
| 29 |
+
`kex` β ~2,763 LOC, 21 passing tests, 8-command CLI, four experiments against real Azure calls.
|
| 30 |
+
Measured on the BUMA standard `STD_2026_006_MNO` (9 pages, majority Bahasa Indonesia), run
|
| 31 |
+
`20260813-145132`.
|
| 32 |
+
|
| 33 |
+
**Funnel:** 9 pages β 13 chunks β 169 mentions (195 raw) β 66 clusters β 66 entries β 66 queue items.
|
| 34 |
+
|
| 35 |
+
| Experiment | Question | Result | Kill line | Verdict |
|
| 36 |
+
|---|---|---|---|---|
|
| 37 |
+
| **E1** | GLiNER recall on Bahasa Indonesia technical prose | **0.854** | 0.70 | **PASS** |
|
| 38 |
+
| **E2** | Does clustering cut expert review burden? | **2.56Γ** (169 β 66) | 2.0Γ | **PASS** |
|
| 39 |
+
| **E3** | Is nano sufficient for schema fill? | **0.75** | 0.80 | **FAIL** |
|
| 40 |
+
| **E4** | Does verbatim-span validation catch fabrication? | **1.00** (false-rejection 0.00) | 0.90 | **PASS** |
|
| 41 |
+
|
| 42 |
+
Also measured: **$0.0069** for the document Β· 155,313 prompt tokens of which **125,184 cached**
|
| 43 |
+
(54% hit rate) Β· 242 s wall clock Β· 3 fields rejected by span check Β· 56 of 66 entries returned no
|
| 44 |
+
definition Β· 0 escalations Β· 0 definition conflicts.
|
| 45 |
+
|
| 46 |
+
**Two results that settle open questions:**
|
| 47 |
+
|
| 48 |
+
- **E1 closes the Indonesian risk.** The context doc names GLiNER's unverified Indonesian
|
| 49 |
+
performance as the headline unknown. It is now measured, on the exact document named as the test
|
| 50 |
+
case, comfortably above the kill line.
|
| 51 |
+
- **E3 half-settles the model tier.** nano **fails** at 0.75 against a 0.80 line. Caveats: the
|
| 52 |
+
scoreable base is 8 entries, and nano is not deterministic at `temperature=0` (consecutive
|
| 53 |
+
identical runs gave 0.75 and 0.625). No mini deployment exists, so the intended nano-vs-mini A/B
|
| 54 |
+
has never run and all four branches currently route to nano.
|
| 55 |
+
|
| 56 |
+
**Caveat when quoting E2.** The context doc's **6.2Γ** is a 1,000-page projection; **2.56Γ** is the
|
| 57 |
+
9-page measurement. Compression grows as terms repeat across a corpus. Both are true β quote the
|
| 58 |
+
right one to the right audience.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## 2. Prototype disposition β do this before building v2
|
| 63 |
+
|
| 64 |
+
`kex` is three different assets with three different fates. Treating it as one thing is how you
|
| 65 |
+
either port scaffolding you didn't want or discard measurements you can't recreate.
|
| 66 |
+
|
| 67 |
+
| # | Task | Owner | Status | Description |
|
| 68 |
+
|---|---|---|---|---|
|
| 69 |
+
| **P1** | Freeze the prototype | Rifqi | β
| **Resolved 2026-08-19: kept as-is, in place, unversioned β Rifqi's call.** The machine-failure risk is accepted knowingly. It is materially reduced anyway now that P2 and P3 have lifted the two things that could not be recreated (the calibration knowledge and the gold set + baseline) into this repo |
|
| 70 |
+
| **P2** | Harvest the tuned constants | Rifqi | β
| **Done 2026-08-19 β [KNOWLEDGE_PIPELINE_CALIBRATION.md](KNOWLEDGE_PIPELINE_CALIBRATION.md).** 8 sections, every value with its reason: GLiNER threshold 0.25 (recall 0.854 vs 0.658 @ 0.35) and the 130-word window (Indonesian subword-tokenises ~2.5Γ, so 220 still tripped GLiNER's 384 cap); rapidfuzz 92 with fuzzy disabled below 5 chars (`PA`/`UA`); the six-signal weight table + word-boundary heading matching; chunking's 1500-token cap, 0.6 boilerplate frequency and the three real-document traps; the 1024-token cache floor; span/escalation/conflict thresholds and the guarded-field table |
|
| 71 |
+
| **P3** | Migrate the evidence into `eval/knowledge/` | Rifqi | β
| **Done 2026-08-19.** `eval/knowledge/` now carries `knowledge_gold.yaml` (41 terms + 15 rules, verbatim), `score.py` (ported; only the prototype's `sys.path` bootstrap removed), `README.md`, and the frozen `results/baseline_prototype_2026-08-13_145132.json`. **Migration verified faithful:** the ported scorer re-derives the baseline exactly from the prototype's artifacts β E1 recall 0.8537, E3 precision 0.75, scoreable base 8, abstentions 56, all MATCH. `ruff` clean; `import main` OK. No `run_eval.py` yet β it lands with the first v2 stage that produces scoreable output |
|
| 72 |
+
| **P4** | Record the negative findings | Rifqi | β
| **Done 2026-08-19 β calibration doc Β§7.** Eight entries: nano insufficient for schema fill; escalation unreachable on small documents; conflict detection had nothing to find; 85% abstention; diff only ever ran the empty-baseline path; literal source wording normalised away (gold was right, pipeline wrong); a `glob('*.pdf')` once scored the wrong document (recall 0.05); torch wheels install corrupt and need β₯ 2.6 |
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 3. The seam β shared with Sofhia
|
| 77 |
+
|
| 78 |
+
**Status update 2026-08-19 (Sofhia).** The contract has landed in
|
| 79 |
+
`src/knowledge_parsing/contracts.py` and every open item raised against the draft is closed except
|
| 80 |
+
the two that need a joint decision (S6a formatting, S6b page indexing). Rows below updated
|
| 81 |
+
accordingly; the extraction-side rows (S7, S8) are left as Rifqi has them.
|
| 82 |
+
|
| 83 |
+
**Original note β under active discussion.** Sofhia circulated a draft `contracts.py` on 2026-08-19,
|
| 84 |
+
derived from real MinerU output rather than designed on paper, and it is adopted as the base. Three
|
| 85 |
+
of its choices improve on the prototype: a `kind` discriminator (text/table/chart/equation),
|
| 86 |
+
`source_items` index back-references into MinerU's `content_list.json`, and an explicit
|
| 87 |
+
verbatim-text warning that independently identifies the same failure mode as the span-check control
|
| 88 |
+
β including that it fails *silently to null* and looks like a bad LLM.
|
| 89 |
+
|
| 90 |
+
| # | Task | Owner | Status | Description |
|
| 91 |
+
|---|---|---|---|---|
|
| 92 |
+
| **S1** | Agree the parsed-artifact schema | Sofhia + Rifqi | π | **Adopted as the base and landed** in `src/knowledge_parsing/contracts.py` (pydantic, per S6e). Everything raised against the draft is closed except the two joint decisions in S6 (a: formatting signal, b: page indexing). |
|
| 93 |
+
| **S2** | Add a document-level envelope | Sofhia + Rifqi | β
| **Done 2026-08-19.** `ParsedDocument{doc_id, source_path, content_hash, version, n_pages, parser_name, parser_version, chunks}` as proposed, plus three fields worth having: **`parser_backend`** read back from MinerU's `_middle.json` so the artifact records what *actually* ran rather than what was configured; `parser_config` (fingerprint of the settings that affect output); and `raw_output_dir`, pointing at the untouched MinerU output kept in the parse cache. |
|
| 94 |
+
| **S3** | Correct the section-structure claim | Rifqi β Sofhia | β
| **Corrected 2026-08-19.** Checked against MinerU's source: `pipeline` and `vlm` run *identical* `text_level` logic and both emit `bbox`, so heading availability is document-dependent, not backend-dependent β the original claim came from sampling the handbook, which has no numbered headings. The normalizer now **reads** the hierarchy into a `heading_path` breadcrumb instead of deriving it, with numbering as fallback. Fields stay Optional. |
|
| 95 |
+
| **S4** | Add `source_wording` while the contract is open | Rifqi β Sofhia | β
| **Done 2026-08-19.** `source_wording` added to `TermRecord`, and `heading` is carried verbatim β so "Physical **of** Availability" survives instead of being silently normalised (R1). |
|
| 96 |
+
| **S5** | Keep the seam off the file path | Sofhia + Rifqi | π | **Satisfied on the parsing side.** `ParsedDocument` never carries a file handle; `source_path` is provenance only, and extraction receives the artifact. Left open for Rifqi to confirm from his side β the prototype's `Parser.parse(path)` boundary is the shape this must not reproduce. |
|
| 97 |
+
| **S6** | Minor contract items (deferred to review) | Rifqi β Sofhia | π | **Four of six closed 2026-08-19; two need a joint call.** **(b) page indexing β OPEN:** the artifact uses `page_idx`/`page_idxs`, **0-based**, no conversion anywhere, so it always matches the raw MinerU output kept beside it; converting to 1-based is the UI's job, done once at display. Say if you want a 1-based `page_no` added instead, since this number reaches Mas Beta. **(a) formatting signal β OPEN, but the premise was wrong:** `text_format` is emitted by *both* backends and is not a formatting signal at all β it is the constant `'latex'` on equation blocks. Checked further: MinerU carries **no emphasis information anywhere** (zero `**` in its markdown, no style/font/span keys in `middle.json`). So the signal has no source in MinerU; it would need a separate pass reading the PDF directly (PyMuPDF exposes per-span bold flags). Recommend dropping it from the ranker for now β one of six signals, and adding a PDF pass buys nothing measured yet. Your ranker, your call. **(c) chunk size β done:** ~1500-token cap, matching calibration Β§4; splits at item boundaries so text stays verbatim. **(d) `chapter` lossy β done:** now `chapters: list[str]`, one per page the chunk spans. **(e) pydantic β done.** **(f) `TermRecord`** β left to you; the declaration in `contracts.py` is a placeholder, not a claim. |
|
| 98 |
+
| **S7** | Commit fixture artifacts | Rifqi | β¬ | Both source PDFs exist (BUMA standard + Open Pit textbook). Promote a parsed artifact of each into the target repo as committed fixtures, so the extraction half is testable **with MinerU not installed at all** β the seam doing its job |
|
| 99 |
+
| **S8** | Write the seam rule into the repo docs | Rifqi | β¬ | Extraction never takes a file path; neither half imports the other's internals; parsing stays swappable behind the artifact. The prototype drifted from this, so the rule needs to outlive our memory of it |
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## 4. Parsing pipeline β Sofhia
|
| 104 |
+
|
| 105 |
+
MinerU replaces the current text+table-only extraction: it adds formula, image, chart and diagram
|
| 106 |
+
detection with type labels. Presented to the execs 13 Aug as the prerequisite for Scenario 2.
|
| 107 |
+
|
| 108 |
+
| # | Task | Owner | Status | Description |
|
| 109 |
+
|---|---|---|---|---|
|
| 110 |
+
| **B1** | MinerU cost justification (written) | Sofhia | β¬ | Owed for the budget proposal, from the 13 Aug exec review |
|
| 111 |
+
| **B2** | MinerU install + run on the sample docs | Sofhia | β
| Complete 9-page parse of the BUMA standard delivered 2026-08-19: **118 items** (90 text, 14 table, 11 equation, 3 chart), pages 0β8. Includes the rule-of-thumb grounding content (Other Activity, Controllable/Uncontrollable, Standby), so the fixture exercises all four extraction branches. Supersedes an earlier 5-page run |
|
| 112 |
+
| **B3** | Evaluate MinerU quality vs. the current path | Sofhia | β¬ | Compare on tables, the p.2 legend block, formulas and figures β against the PyMuPDF baseline the prototype used, which has real numbers rather than a guess |
|
| 113 |
+
| **B4** | Indonesian check on **parsing** | Sofhia | π | Distinct from E1, which measured GLiNER, not the parser. Partly answered 2026-08-19: MinerU has **no `id` language code** β Indonesian documents use `ch`, whose model covers Latin script (`config.periksa()` now rejects a wrong code before models load). `lang` only applies to the `pipeline` backend and only when OCR actually runs, so it is moot under `vlm`. Text/layout **quality** on Bahasa Indonesia still to be judged against B3 |
|
| 114 |
+
| **B5** | Section-aware chunking | Sofhia | π | **Built 2026-08-19** in `normalize.py`. Splits on MinerU's `text_level` hierarchy into a `heading_path` breadcrumb, with numbering as fallback. Three real-document traps handled, two of them from Β§4 of the calibration doc: repeated breadcrumbs no longer reopen a section, **only numbered headings** may open one (so `Keterangan:` keeps a legend with its figure), and a ~1500-token cap guards documents with no detected headings. **Acceptance test still open** β needs the Open Pit textbook (S7) to confirm the NSR definition survives pp. 92β94 intact |
|
| 115 |
+
| **B6** | Emit the agreed artifact | Sofhia | β
| **Done 2026-08-19** (`5840d201`). `src/knowledge_parsing/` writes `ParsedDocument`, pydantic, not MinerU's native shape. `parse.py` keeps a content-addressed cache of the untouched MinerU output beside it, and `parser_backend` is read back from `_middle.json` so the artifact records what actually ran |
|
| 116 |
+
| **B7** | Fallback path behind the same artifact | Sofhia | β¬ | Tesseract / Azure Document Intelligence emits the *same* artifact, so extraction never knows which parser ran |
|
| 117 |
+
| **B8** | Keep the parsing subsystem additive + gated | Sofhia | β
| **Done 2026-08-19.** `src/knowledge/` is untouched; `src/knowledge_parsing/` sits beside it. MinerU is an **optional extra** (`knowledge-parsing`), not a main dependency, and is imported inside `parse.py` at call time β verified that importing the package leaves `mineru` and `torch` out of `sys.modules`, so the deployed Space neither builds nor ships them |
|
| 118 |
+
|
| 119 |
+
**Findings from the real MinerU output that affect B5/B6:**
|
| 120 |
+
|
| 121 |
+
- **Heading depth is flattened** β all 29 `title` blocks carry `"level": 2`, including `1. TUJUAN
|
| 122 |
+
PARAMETER` and `2.1.3. Physical of Availability (PA)`. Hierarchy must come from the numbering
|
| 123 |
+
text, never from `level`. The heading *text* is supplied (see S3).
|
| 124 |
+
- **Equation LaTeX is character-spaced** β `P r o d u c t i o n = M O H H \times Q t y \times P A
|
| 125 |
+
\times U A \times P t y`. Naive matching will never find `MOHH`. Needs normalising before the
|
| 126 |
+
formula/legend branch, and before span validation can locate anything inside a formula.
|
| 127 |
+
- **Breadcrumb headings repeat per page** β `2. PENJELASAN PARAMETER` / `2.1. Production Parameter`
|
| 128 |
+
re-appear as `title` blocks on pages 2β8. They must not be treated as new sections.
|
| 129 |
+
- **Charts carry captions only** β all 3 chart items have `content: ""`, with the caption in
|
| 130 |
+
`chart_caption`. A chart chunk contributes a caption plus an image reference and nothing else.
|
| 131 |
+
Chart detection buys Scenario 2, not glossary coverage.
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
## 5. Extraction pipeline v2 β Rifqi, built in this repo
|
| 136 |
+
|
| 137 |
+
**Two separate columns, deliberately.** *Design* = validated by the prototype, with a number behind
|
| 138 |
+
it. *Build* = exists in this repo. The gap between them is the remaining engineering effort, and
|
| 139 |
+
collapsing the two is how a rebuild gets under-estimated.
|
| 140 |
+
|
| 141 |
+
**Module: `src/knowledge_extraction/`** (decided 2026-08-19 β `src/knowledge/` is the existing
|
| 142 |
+
OCRβpgvector service and stays untouched). Offline runner: `scripts/run_knowledge_extraction.py`.
|
| 143 |
+
Structure follows `src/query/`: typed models β deterministic stages β thin entry point.
|
| 144 |
+
|
| 145 |
+
**The seam is absorbed by one file.** `adapter.py` is the only module that knows the artifact's
|
| 146 |
+
shape; every stage depends on the internal `models.Chunk` subset instead. Verified against **both**
|
| 147 |
+
live shapes β Sofhia's bare-list draft (`contoh-chunk.json`, `kind` discriminator) and the
|
| 148 |
+
envelope shape proposed in Β§3 β so S1 can settle either way without touching a stage.
|
| 149 |
+
|
| 150 |
+
| # | Stage | Design | Build | Note |
|
| 151 |
+
|---|---|---|---|---|
|
| 152 |
+
| **X1** | Candidate-entry schema | β
| β
| `models.py`. All three invariants encoded: content fields Optional, `SubdomainEnum`, `Provenance.span` mandatory. Adds `source_wording` for R1. Pydantic v2 |
|
| 153 |
+
| **X2** | Offline runner over a fixture | β
| β
| `scripts/run_knowledge_extraction.py`. Takes an artifact, **never a PDF**; each stage writes its own JSON. Runs green on the 9-page BUMA chunks and on Sofhia's draft |
|
| 154 |
+
| **X3** | GLiNER span filter | β
| β
| `filters/span_filter.py`. Runs on all chunks unconditionally; overlapping 130-word windows with character offsets remapped; span-cap hits flagged not truncated; never-throw. **Scored: E1 recall 0.8537 β exactly the frozen baseline**, 35/41 gold terms, 66 clusters (baseline 66), compression 2.47Γ (baseline 2.56Γ). Result: `eval/knowledge/results/v2_term_filter_2026-08-19_144318.json` |
|
| 155 |
+
| **X4** | Indonesian performance | β
**0.854** | β
| Re-measured on v2: **0.8537, zero regression** against the prototype |
|
| 156 |
+
| **X5** | Discourse-cue filter β rule-of-thumb | β
| β
| `filters/cue_filter.py`, cues in `config/cues.yaml`. 9 rule candidates on the BUMA standard. Also supplies `definitional_hits` for the ranker's +5.0 signal |
|
| 157 |
+
| **X6** | Legend-block filter β formula vars | β
| β
| `filters/legend_filter.py`. Extracts the p.2 legend block **exactly** β MOHH Β· Qty Β· PA Β· UA Β· Pty, 5/5 β and rejects equation rows. LaTeX normalisation (Β§4) still pending for MinerU input |
|
| 158 |
+
| **X7** | Section pass β summary units | β
| β¬ | The quiet cost risk: few calls but ~ΒΌ of all input tokens, because summarisation cannot be filtered β it needs whole documents |
|
| 159 |
+
| **X8** | Normalise + cluster mentions | β
**2.56Γ** | β
| `cluster/`. Constants carried from P2 with their reasons. Locked by tests: PA/UA never merge; abbreviationβexpansion merges only with legend pairs; noise surfaces dropped as whole forms only |
|
| 160 |
+
| **X9** | Evidence ranking β top-K | β
| β
| `rank/evidence.py`, six signals + tabular penalty. **Verified on the real document: every term's top-ranked chunk is its own definition section** β PAβ2.1.3, UAβ2.1.4, Qtyβ2.1.2, Ptyβ2.1.5. Full ranked list retained; word-boundary heading matching locked by test |
|
| 161 |
+
| **X10** | LLM extraction call | β
| β
| `extract/` β client (nano `__54n`, probed structured output, API-sourced cached tokens), 4 branches, prompts as files. **Live pilot: 3 calls, 10.1s, 7,168 of 8,029 prompt tokens cached (89%)**, correct ID definitions + tags, 0 span rejections. `--dry-run` and `--limit` for cost control; `--mock` runs the whole pipeline with no spend |
|
| 162 |
+
| **X11** | Verbatim-span validation | β
**1.00** | β
| `validate/span_check.py`. Whitespace-only normalisation; unlocatable provenance rejects every guarded field; never repairs. **Extended:** `full_name`/`source_wording` are checked against the source themselves, so a silent normalisation is caught rather than stored |
|
| 163 |
+
| **X12** | Null-definition escalation | π | β
| `validate/escalate.py` + the glossary loop. Still **unexercised on this document** β most clusters have too little evidence to escalate to, which is a property of a 9-page corpus, not a defect |
|
| 164 |
+
| **X13** | Conflict detection | π | β
| `validate/conflict.py`, token overlap (explainable to the reviewer, unlike embeddings); never picks a winner. Unit-tested both ways; still **0 conflicts on real data** β one consistent standard gives it nothing to find. Works only because clustering puts all evidence in one call |
|
| 165 |
+
| **X14** | Diff vs. active glossary version | π | β
| `diff/glossary_diff.py`. The baseline is passed in explicitly (`--active-glossary`) rather than read from wherever the last run wrote, so the duplicate and conflicting paths are reachable β all three verified by test |
|
| 166 |
+
| **X15** | Frequency-sorted review queue | β
| β
| `queue/review_queue.py`. Each row carries page, section and the verbatim span so review is quote-vs-page. Gained a review reason for **wording discrepancies**, which is how the literal-wording decision reaches the expert |
|
| 167 |
+
| **X16** | Bulk vs. incremental ingest | β¬ | β¬ | Bulk setup ingest (corpus-frequency statistics as a candidate booster) and incremental single-file add (no corpus context; diff against the active version). Neither exists |
|
| 168 |
+
| **X17** | Persistence | β¬ | π | Stage artifacts are written as JSON by the CLI, which is enough for the offline path. Tables still needed β D2 |
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## 6. Decisions needed
|
| 173 |
+
|
| 174 |
+
| # | Decision | Owner | Status | Description |
|
| 175 |
+
|---|---|---|---|---|
|
| 176 |
+
| **D1** | New dependencies | Rifqi | β
| **All approved and added 2026-08-19:** `rapidfuzz>=3.14.5`, `gliner>=0.2.13` (resolved 0.2.28), `torch>=2.6` (resolved 2.11.0+cpu). Install verified clean β `import torchgen` OK, so not the corrupt-wheel failure the calibration doc warns about. `PyYAML` was already present transitively |
|
| 177 |
+
| **D2** | New tables (DDL handoff) | Rifqi β Harry | β¬ | Parsed artifacts, candidate entries, glossary versions + approval audit trail. Go owns the dedorch schema β Python never executes DDL. One consolidated Harry-ready handoff beats three |
|
| 178 |
+
| **D3** | Model tier per branch | Rifqi | β
| **Decided 2026-08-19: stay on nano for all four branches.** The `.env` carries the quad `azureai__{api_key,endpoint__url,deployment__name,api__version}__54n`, mirroring the existing `__54m` scheme. Accepted knowingly: nano measured 0.75 against a 0.80 line, and `rule`/`summary` run on the tier whose failure mode is least detectable. Revisit if extraction quality blocks the demo. **`src/config/settings.py` does not expose the `__54n` quad yet** β four `Field(alias=β¦)` entries, first build step |
|
| 179 |
+
| **D4** | Admin ingest surface | Rifqi | β¬ | Whether it is HTTP at all, and if so its auth posture. A write surface triggered by an admin is a different risk class from the current unauthenticated read-only chat surface β raise it, don't inherit the posture by default. Only after the offline path works |
|
| 180 |
+
| **D5** | v2 module name | Rifqi | β
| **`src/knowledge_extraction/`** (2026-08-19). Offline runner in `scripts/`, per the repo's existing precedent |
|
| 181 |
+
| **D6** | Expert review of the gold set | Rifqi β Mas Beta | βΈοΈ | **Deferred 2026-08-19: provisional gold is good enough for now** so the rebuild is not blocked on expert time. Standing caveat: E1 and E3 both move when it is reviewed, and E3's scoreable base is 8 entries, so each adjudication shifts it ~12 points. Every E3 failure is listed verbatim in the frozen baseline for when he does review it |
|
| 182 |
+
| **D7** | GLiNER + LangExtract ensembling | β | βΈοΈ | Proposed, not decided; flagged as scope creep. Cheap in API terms but adds a reconciliation step between two candidate sets that may disagree. Not in scope for the test integration |
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
## 7. Defects and risks to carry into v2
|
| 187 |
+
|
| 188 |
+
| # | Finding | Severity | Detail |
|
| 189 |
+
|---|---|---|---|
|
| 190 |
+
| **R1** | ~~Literal source wording is normalised away~~ | **FIXED** | **Resolved 2026-08-19 in v2, live-verified.** Three parts: `source_wording` on the entry, taken **deterministically from the document's own section heading** rather than left to the model (which quoted the normalised page-1 form instead); `evidence_text` now includes headings, so quoting a section title is not wrongly rejected; and the review queue gained a reason for the mismatch. On the live pilot PA carries `full_name="Physical Availability"` **and** `source_wording="Physical of Availability (PA)"`, and ranks as *"source wording differs from the expanded name β confirm which is correct"* β the discrepancy reaches the expert instead of being silently corrected |
|
| 191 |
+
| **R2** | **Prototype is not under version control** | **High** | Addressed by P1 |
|
| 192 |
+
| **R3** | **85% abstention** | Medium | 56 of 66 entries carry no definition. Correct behaviour β for a term the document never defines, null *is* the right answer β but it means the review queue is mostly "term found, no definition in document". Whether that is useful to Mas Beta or noise is a **review-experience question to put to him**, and review experience is explicitly where engineering time is worth spending |
|
| 193 |
+
| **R4** | **Everything generalises from one 9-page document** | Medium | Single document, single language, single company. E2's compression, E3's scoreable base and the escalation path are all constrained by it |
|
| 194 |
+
| **R5** | **Rule and summary run on the wrong tier** | Medium | Both route to nano because it is the only provisioned deployment. Same blocker as D3 |
|
| 195 |
+
| **R6** | **Rewrites drop accidental knowledge** | Medium | Addressed by P2. The `PA`/`UA` fuzzy-collision guard and the 1024-token caching threshold are the known examples; there will be others |
|
| 196 |
+
|
| 197 |
+
---
|
| 198 |
+
|
| 199 |
+
## 8. Settled β do not reopen
|
| 200 |
+
|
| 201 |
+
1. The LLM call unit is the **term cluster**. Justification is review-burden reduction, **not** tokens (~$0.45 saved is trivial).
|
| 202 |
+
2. **Conflict detection depends on the clustering** β two contradictory definitions can only be spotted because all evidence for a term arrives in one call.
|
| 203 |
+
3. **No standalone relevance gate.** A pre-filter's false negatives drop content before extraction sees it, and dropped content never reaches review to be caught.
|
| 204 |
+
4. **Evidence ranking is allowed** even though it is also a filter β no term is dropped, all mentions stay in provenance, and the escalation loop self-corrects. Preserve those three properties if it is ever revisited.
|
| 205 |
+
5. **`provenance.span` is mandatory and verbatim**, never repaired. The primary anti-hallucination control, and what makes expert review finishable: the reviewer checks a quote against a page, not a claim against their memory.
|
| 206 |
+
6. **All content fields Optional.** Abstention is correct behaviour, not an error.
|
| 207 |
+
7. **Frequency-sorted review queue** β validate the ~40 terms carrying the document first, let the long tail wait.
|
| 208 |
+
8. **API cost is negligible, latency irrelevant** (measured $0.0069 for 9 pages). This runs a few times a year, admin-triggered, not continuous and not per-user. Optimise the review experience, never tokens. Prefer boring, inspectable stages.
|
| 209 |
+
9. **The pipeline records the literal source wording** and surfaces discrepancies rather than normalising them (currently violated β R1).
|
| 210 |
+
10. **This is a test integration, not a productionisation.** Both halves are new, gated subsystems alongside the existing document/catalog pipelines β not a rewrite of them.
|
eval/knowledge/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# eval/knowledge β knowledge-extraction eval harness
|
| 2 |
+
|
| 3 |
+
Scores the knowledge-extraction pipeline against a gold set built from
|
| 4 |
+
**BUMA `STD/2026/006/MNO Rev.0.0` β Production Parameter & ECA** (9 pages, majority Bahasa
|
| 5 |
+
Indonesia).
|
| 6 |
+
|
| 7 |
+
Migrated from the `kex` prototype on 2026-08-19. **The prototype itself is not being ported** β it
|
| 8 |
+
answered the design questions and stays where it is. This directory carries the parts that survive
|
| 9 |
+
it: the gold set, the scorer, and a frozen baseline. Plan: [KNOWLEDGE_PIPELINE_TODO.md](../../KNOWLEDGE_PIPELINE_TODO.md),
|
| 10 |
+
tuned constants: [KNOWLEDGE_PIPELINE_CALIBRATION.md](../../KNOWLEDGE_PIPELINE_CALIBRATION.md).
|
| 11 |
+
|
| 12 |
+
## Why this landed before any v2 pipeline code
|
| 13 |
+
|
| 14 |
+
Without a standing baseline, "new and improved" is unfalsifiable β and one of the four experiments
|
| 15 |
+
already **failed** (E3, schema fill). Quality is what this work gets judged on, so the yardstick
|
| 16 |
+
ships first and every v2 stage is measured from its first commit rather than retroactively.
|
| 17 |
+
|
| 18 |
+
## Contents
|
| 19 |
+
|
| 20 |
+
| Path | What it is |
|
| 21 |
+
|---|---|
|
| 22 |
+
| `knowledge_gold.yaml` | 41 terms + 15 rules. **Provisional** β bootstrapped by reading the source PDF, *not* expert-reviewed |
|
| 23 |
+
| `score.py` | Precision / recall / F1 per branch. Pipeline-independent: takes plain lists of surfaces and entry dicts, so it scores the prototype's artifacts and v2 alike |
|
| 24 |
+
| `results/baseline_prototype_2026-08-13_145132.json` | **Frozen.** The prototype's measured numbers. Never regenerate β add new timestamped files beside it |
|
| 25 |
+
|
| 26 |
+
No `run_eval.py` yet: there is no v2 pipeline to run. It lands with the first stage that produces
|
| 27 |
+
scoreable output, following the house convention (module mode, timestamped results, never
|
| 28 |
+
overwritten) used by `eval/{intent,help,readiness,planner}/`.
|
| 29 |
+
|
| 30 |
+
## The baseline to beat
|
| 31 |
+
|
| 32 |
+
Measured by the prototype on the document above, run `20260813-145132`, deployment `gpt-5.4-nano`:
|
| 33 |
+
|
| 34 |
+
| Experiment | Question | Result | Kill line | Verdict |
|
| 35 |
+
|---|---|---|---|---|
|
| 36 |
+
| **E1** | GLiNER recall on Bahasa Indonesia technical prose | **0.854** | 0.70 | **PASS** |
|
| 37 |
+
| **E2** | Does clustering cut expert review burden? | **2.56Γ** (169 β 66) | 2.0Γ | **PASS** |
|
| 38 |
+
| **E3** | Is nano sufficient for schema fill? | **0.75** | 0.80 | **FAIL** |
|
| 39 |
+
| **E4** | Does verbatim-span validation catch fabrication? | **1.00** (false-rejection 0.00) | 0.90 | **PASS** |
|
| 40 |
+
|
| 41 |
+
Funnel: 9 pages β 13 chunks β 169 mentions β 66 clusters β 83 LLM calls β 66 entries β 66 queue
|
| 42 |
+
items. Cost $0.0069; 125,184 of 155,313 prompt tokens served from cache.
|
| 43 |
+
|
| 44 |
+
## Two things the scorer refuses to do
|
| 45 |
+
|
| 46 |
+
**It never conflates term-filter recall with extraction precision.** They are different failure
|
| 47 |
+
modes with different fixes β recall is fixed at the filter stage (GLiNER labels), precision at the
|
| 48 |
+
extraction stage (model tier, prompt). E1 is the recall number specifically.
|
| 49 |
+
|
| 50 |
+
**It never counts abstention as an error.** For a term the document does not define, `null` is the
|
| 51 |
+
correct answer. Scoring is restricted to the *scoreable* subset: entries that produced a definition,
|
| 52 |
+
whose term is in the gold set, and whose gold record carries `definition_contains` to check against.
|
| 53 |
+
Counting the rest as errors would measure gold coverage while claiming to measure model accuracy.
|
| 54 |
+
Coverage is reported separately in `Score.as_dict()`.
|
| 55 |
+
|
| 56 |
+
## Reading E3 carefully
|
| 57 |
+
|
| 58 |
+
The scoreable base is **8 entries**, so each adjudication moves the number ~12 points. nano is also
|
| 59 |
+
not deterministic at `temperature=0` β consecutive runs of the identical pipeline scored 0.75 and
|
| 60 |
+
0.625. Treat E3 as a signal, not a measurement, until the gold set is reviewed.
|
| 61 |
+
|
| 62 |
+
Every failure is listed verbatim under `experiments.E3.scoreable_basis.failures` in the baseline
|
| 63 |
+
file, for Mas Beta to adjudicate. Some are genuine extraction errors; others are cases where the
|
| 64 |
+
document carries two valid definitions and the provisional gold names only one. **Gold was not
|
| 65 |
+
edited after seeing output**, so the figure is a lower bound.
|
| 66 |
+
|
| 67 |
+
## Gold-set conventions (keep these when extending it)
|
| 68 |
+
|
| 69 |
+
- `term` is the string a reader would look up.
|
| 70 |
+
- `full_name` is the **literal source wording**, never normalised. The standard writes *"Physical of
|
| 71 |
+
Availability (PA)"* in the heading and *"Physical Availability"* in the legend β both are recorded
|
| 72 |
+
as variants. This is deliberate: the pipeline is required to surface that discrepancy to the
|
| 73 |
+
expert rather than silently correct it.
|
| 74 |
+
- `definition_contains` are substrings that **must** appear in a correct definition. Substring
|
| 75 |
+
matching, not exact, so a correct-but-differently-worded extraction is not scored as a miss.
|
| 76 |
+
- The gold set is **partial by design** β the expert is the labelling bottleneck, so scoring reports
|
| 77 |
+
coverage rather than blocking on a complete file.
|
| 78 |
+
|
| 79 |
+
## Status of the gold set
|
| 80 |
+
|
| 81 |
+
`status: provisional`, `labelled_by: claude-bootstrap`, `labelled_on: 2026-08-13`. It has **not**
|
| 82 |
+
been reviewed by Mas Beta. Every score derived from it carries that caveat, and both E1 and E3 will
|
| 83 |
+
move when it is reviewed. Accepted as good enough for now (2026-08-19, Rifqi) so the rebuild is not
|
| 84 |
+
blocked on expert time.
|
eval/knowledge/knowledge_gold.yaml
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PROVISIONAL gold set β bootstrapped by reading the source PDF, NOT yet expert-reviewed.
|
| 2 |
+
# Reviewer: Mas Beta. Until reviewed, every score derived from this file carries that caveat.
|
| 3 |
+
#
|
| 4 |
+
# Labelling rules used:
|
| 5 |
+
# - `term` is the string a reader would look up.
|
| 6 |
+
# - `full_name` is the LITERAL source wording, not a normalised or corrected form.
|
| 7 |
+
# (e.g. the STD writes "Physical of Availability (PA)" in the heading and
|
| 8 |
+
# "Physical Availability" in the legend β both are recorded as variants.)
|
| 9 |
+
# - `definition_contains` are substrings that MUST appear in a correct definition.
|
| 10 |
+
# Substring matching, not exact match, so a correct-but-differently-worded
|
| 11 |
+
# extraction is not scored as a miss (spec Β§5).
|
| 12 |
+
|
| 13 |
+
doc_id: STD_2026_006_MNO
|
| 14 |
+
status: provisional
|
| 15 |
+
labelled_by: claude-bootstrap
|
| 16 |
+
labelled_on: 2026-08-13
|
| 17 |
+
source_pages: 9
|
| 18 |
+
|
| 19 |
+
terms:
|
| 20 |
+
- term: Production
|
| 21 |
+
full_name: Production
|
| 22 |
+
variants: ["Production", "produksi"]
|
| 23 |
+
definition_contains: ["nilai tambah", "sumber daya"]
|
| 24 |
+
formula_present: true
|
| 25 |
+
page: 2
|
| 26 |
+
- term: MOHH
|
| 27 |
+
full_name: Machine on Hand Hours
|
| 28 |
+
variants: ["MOHH"]
|
| 29 |
+
definition_contains: ["Machine on Hand Hours"]
|
| 30 |
+
formula_present: false
|
| 31 |
+
page: 2
|
| 32 |
+
- term: Qty
|
| 33 |
+
full_name: Quantity (Qty)
|
| 34 |
+
variants: ["Qty", "Quantity", "quantity"]
|
| 35 |
+
definition_contains: ["jumlah equipment", "periode"]
|
| 36 |
+
formula_present: true
|
| 37 |
+
page: 2
|
| 38 |
+
- term: PA
|
| 39 |
+
full_name: Physical of Availability (PA)
|
| 40 |
+
variants: ["PA", "Physical Availability", "Physical of Availability"]
|
| 41 |
+
definition_contains: ["ketersediaan fisik", "available"]
|
| 42 |
+
formula_present: true
|
| 43 |
+
page: 4
|
| 44 |
+
- term: UA
|
| 45 |
+
full_name: Utilization of Availability (UA)
|
| 46 |
+
variants: ["UA", "Utilization of Availability"]
|
| 47 |
+
definition_contains: ["efektifitas penggunaan", "working hours"]
|
| 48 |
+
formula_present: true
|
| 49 |
+
page: 4
|
| 50 |
+
- term: Pty
|
| 51 |
+
full_name: Productivity (Pty)
|
| 52 |
+
variants: ["Pty", "PTY", "Productivity", "Produktivitas"]
|
| 53 |
+
definition_contains: ["efektifitas penggunaan sumber daya", "output"]
|
| 54 |
+
formula_present: true
|
| 55 |
+
page: 5
|
| 56 |
+
- term: ECA
|
| 57 |
+
full_name: Equipment Capacity Analysis (ECA)
|
| 58 |
+
variants: ["ECA", "Equipment Capacity Analysis"]
|
| 59 |
+
definition_contains: ["ketidaktercapaian", "kapasitas produksi"]
|
| 60 |
+
formula_present: false
|
| 61 |
+
page: 6
|
| 62 |
+
- term: INPR
|
| 63 |
+
full_name: in operation
|
| 64 |
+
variants: ["INPR", "in operation", "INPR Hours"]
|
| 65 |
+
definition_contains: ["in operation"]
|
| 66 |
+
formula_present: false
|
| 67 |
+
page: 2
|
| 68 |
+
- term: Total Hours
|
| 69 |
+
full_name: Total Hours
|
| 70 |
+
variants: ["Total Hours", "total jam"]
|
| 71 |
+
definition_contains: ["total jam"]
|
| 72 |
+
formula_present: true
|
| 73 |
+
page: 2
|
| 74 |
+
- term: Breakdown
|
| 75 |
+
full_name: Breakdown
|
| 76 |
+
variants: ["Breakdown", "breakdown"]
|
| 77 |
+
definition_contains: []
|
| 78 |
+
formula_present: true
|
| 79 |
+
page: 4
|
| 80 |
+
- term: Working Hours
|
| 81 |
+
full_name: Working Hours
|
| 82 |
+
variants: ["Working Hours", "working hours"]
|
| 83 |
+
definition_contains: ["waktu kerja"]
|
| 84 |
+
formula_present: true
|
| 85 |
+
page: 4
|
| 86 |
+
- term: EWH
|
| 87 |
+
full_name: Effective Working Hours (EWH)
|
| 88 |
+
variants: ["EWH", "Effective Working Hours", "productive working hours"]
|
| 89 |
+
definition_contains: ["waktu kerja efektif"]
|
| 90 |
+
formula_present: false
|
| 91 |
+
page: 5
|
| 92 |
+
- term: Land clearing
|
| 93 |
+
full_name: land clearing
|
| 94 |
+
variants: ["land clearing"]
|
| 95 |
+
definition_contains: []
|
| 96 |
+
formula_present: false
|
| 97 |
+
page: 2
|
| 98 |
+
- term: Overburden removal
|
| 99 |
+
full_name: pengupasan lapisan penutup (overburden removal)
|
| 100 |
+
variants: ["overburden removal", "pengupasan lapisan penutup"]
|
| 101 |
+
definition_contains: []
|
| 102 |
+
formula_present: false
|
| 103 |
+
page: 2
|
| 104 |
+
- term: Coal mining
|
| 105 |
+
full_name: penambangan batubara (coal mining)
|
| 106 |
+
variants: ["coal mining", "Coal Mining", "CM", "penambangan batubara"]
|
| 107 |
+
definition_contains: []
|
| 108 |
+
formula_present: false
|
| 109 |
+
page: 2
|
| 110 |
+
- term: Coal hauling
|
| 111 |
+
full_name: pengangkutan batubara (coal hauling/coal transporting)
|
| 112 |
+
variants: ["coal hauling", "coal transporting", "Coal Transport", "CT"]
|
| 113 |
+
definition_contains: []
|
| 114 |
+
formula_present: false
|
| 115 |
+
page: 2
|
| 116 |
+
- term: Coal barging
|
| 117 |
+
full_name: coal barging atau ship loading
|
| 118 |
+
variants: ["coal barging", "ship loading", "pengapalan"]
|
| 119 |
+
definition_contains: []
|
| 120 |
+
formula_present: false
|
| 121 |
+
page: 2
|
| 122 |
+
- term: Waste Removal
|
| 123 |
+
full_name: Waste Removal (WR)
|
| 124 |
+
variants: ["Waste Removal", "WR", "waste removal"]
|
| 125 |
+
definition_contains: []
|
| 126 |
+
formula_present: false
|
| 127 |
+
page: 3
|
| 128 |
+
- term: General Work
|
| 129 |
+
full_name: General Work
|
| 130 |
+
variants: ["General Work"]
|
| 131 |
+
definition_contains: []
|
| 132 |
+
formula_present: false
|
| 133 |
+
page: 3
|
| 134 |
+
- term: Rental
|
| 135 |
+
full_name: Rental
|
| 136 |
+
variants: ["Rental"]
|
| 137 |
+
definition_contains: []
|
| 138 |
+
formula_present: false
|
| 139 |
+
page: 3
|
| 140 |
+
- term: Grouping (Composite)
|
| 141 |
+
full_name: Grouping (Composite)
|
| 142 |
+
variants: ["Grouping (Composite)", "grouping (composite)", "Composite"]
|
| 143 |
+
definition_contains: ["type", "class"]
|
| 144 |
+
formula_present: true
|
| 145 |
+
page: 3
|
| 146 |
+
- term: EX2500
|
| 147 |
+
full_name: EX2500
|
| 148 |
+
variants: ["EX2500"]
|
| 149 |
+
definition_contains: []
|
| 150 |
+
formula_present: false
|
| 151 |
+
page: 3
|
| 152 |
+
- term: Time Performance MCD Application
|
| 153 |
+
full_name: raw data Time Performance MCD Application
|
| 154 |
+
variants: ["Time Performance MCD Application", "MCD Application", "MCD"]
|
| 155 |
+
definition_contains: []
|
| 156 |
+
formula_present: false
|
| 157 |
+
page: 3
|
| 158 |
+
- term: Joint survey
|
| 159 |
+
full_name: joint survey
|
| 160 |
+
variants: ["joint survey"]
|
| 161 |
+
definition_contains: []
|
| 162 |
+
formula_present: false
|
| 163 |
+
page: 5
|
| 164 |
+
- term: Truck count
|
| 165 |
+
full_name: truck count
|
| 166 |
+
variants: ["truck count"]
|
| 167 |
+
definition_contains: []
|
| 168 |
+
formula_present: false
|
| 169 |
+
page: 5
|
| 170 |
+
- term: Weight average
|
| 171 |
+
full_name: rata-rata tertimbang (weight average)
|
| 172 |
+
variants: ["weight average", "rata-rata tertimbang", "pembobotan"]
|
| 173 |
+
definition_contains: []
|
| 174 |
+
formula_present: false
|
| 175 |
+
page: 4
|
| 176 |
+
- term: Gain/Loss
|
| 177 |
+
full_name: Gain/Loss
|
| 178 |
+
variants: ["Gain/Loss", "Gain / Loss"]
|
| 179 |
+
definition_contains: ["plan", "actual"]
|
| 180 |
+
formula_present: true
|
| 181 |
+
page: 6
|
| 182 |
+
- term: Plan
|
| 183 |
+
full_name: rencana (plan)
|
| 184 |
+
variants: ["plan", "Plan", "rencana", "P(M)", "P(G)"]
|
| 185 |
+
definition_contains: []
|
| 186 |
+
formula_present: false
|
| 187 |
+
page: 6
|
| 188 |
+
- term: Actual
|
| 189 |
+
full_name: realisasi (actual)
|
| 190 |
+
variants: ["actual", "Actual", "realisasi", "A(M)", "A(G)"]
|
| 191 |
+
definition_contains: []
|
| 192 |
+
formula_present: false
|
| 193 |
+
page: 6
|
| 194 |
+
- term: Model Unit
|
| 195 |
+
full_name: Model Unit
|
| 196 |
+
variants: ["Model Unit", "model unit", "(M)", "Model"]
|
| 197 |
+
definition_contains: []
|
| 198 |
+
formula_present: true
|
| 199 |
+
page: 6
|
| 200 |
+
- term: Standby
|
| 201 |
+
full_name: Delay/Standby
|
| 202 |
+
variants: ["Standby", "standby", "Delay/Standby", "Gap Standby"]
|
| 203 |
+
definition_contains: []
|
| 204 |
+
formula_present: true
|
| 205 |
+
page: 6
|
| 206 |
+
- term: BCM
|
| 207 |
+
full_name: BCM
|
| 208 |
+
variants: ["BCM"]
|
| 209 |
+
definition_contains: []
|
| 210 |
+
formula_present: false
|
| 211 |
+
page: 6
|
| 212 |
+
- term: Waterfall Analysis
|
| 213 |
+
full_name: Waterfall Analysis
|
| 214 |
+
variants: ["Waterfall Analysis", "grafik Waterfall"]
|
| 215 |
+
definition_contains: ["visualisasi", "Gain/Loss"]
|
| 216 |
+
formula_present: false
|
| 217 |
+
page: 8
|
| 218 |
+
- term: Other Activity
|
| 219 |
+
full_name: Other Activity
|
| 220 |
+
variants: ["Other Activity"]
|
| 221 |
+
definition_contains: ["tidak sesuai", "direncanakan"]
|
| 222 |
+
formula_present: false
|
| 223 |
+
page: 8
|
| 224 |
+
- term: Loader
|
| 225 |
+
full_name: loader
|
| 226 |
+
variants: ["loader", "PA Loader"]
|
| 227 |
+
definition_contains: []
|
| 228 |
+
formula_present: false
|
| 229 |
+
page: 8
|
| 230 |
+
- term: Hauler
|
| 231 |
+
full_name: hauler
|
| 232 |
+
variants: ["hauler"]
|
| 233 |
+
definition_contains: []
|
| 234 |
+
formula_present: false
|
| 235 |
+
page: 8
|
| 236 |
+
- term: Hierarki Level Analysis
|
| 237 |
+
full_name: Hierarki Level Analyisis
|
| 238 |
+
variants: ["Hierarki Level Analyisis", "Hierarki Level Analysis"]
|
| 239 |
+
definition_contains: ["level"]
|
| 240 |
+
formula_present: false
|
| 241 |
+
page: 9
|
| 242 |
+
- term: Uncontrollable
|
| 243 |
+
full_name: Uncontrollable
|
| 244 |
+
variants: ["Uncontrollable", "uncontrollable"]
|
| 245 |
+
definition_contains: ["level 1"]
|
| 246 |
+
formula_present: false
|
| 247 |
+
page: 9
|
| 248 |
+
- term: Controllable
|
| 249 |
+
full_name: controllable
|
| 250 |
+
variants: ["controllable", "UA Controllable"]
|
| 251 |
+
definition_contains: ["level 1"]
|
| 252 |
+
formula_present: false
|
| 253 |
+
page: 9
|
| 254 |
+
- term: Fleet management
|
| 255 |
+
full_name: fleet management
|
| 256 |
+
variants: ["fleet management"]
|
| 257 |
+
definition_contains: []
|
| 258 |
+
formula_present: false
|
| 259 |
+
page: 9
|
| 260 |
+
- term: Mineplan
|
| 261 |
+
full_name: mineplan
|
| 262 |
+
variants: ["mineplan"]
|
| 263 |
+
definition_contains: []
|
| 264 |
+
formula_present: false
|
| 265 |
+
page: 9
|
| 266 |
+
|
| 267 |
+
rules:
|
| 268 |
+
- rule_id: QTY_PER_ACTIVITY
|
| 269 |
+
statement_contains: ["Qty", "kategori aktivitas", "terpisah"]
|
| 270 |
+
page: 3
|
| 271 |
+
- rule_id: QTY_GROUPING_SUM
|
| 272 |
+
statement_contains: ["Qty Grouping", "menjumlahkan"]
|
| 273 |
+
page: 3
|
| 274 |
+
- rule_id: ACTIVITY_CLASSIFICATION_SOURCE
|
| 275 |
+
statement_contains: ["status equipment", "Time Performance MCD"]
|
| 276 |
+
page: 3
|
| 277 |
+
- rule_id: PA_COMPOSITE_WEIGHTED
|
| 278 |
+
statement_contains: ["weight average", "Quantity", "pembobot"]
|
| 279 |
+
page: 4
|
| 280 |
+
- rule_id: UA_BY_ACTIVITY
|
| 281 |
+
statement_contains: ["UA", "kategori aktivitas"]
|
| 282 |
+
page: 4
|
| 283 |
+
- rule_id: PTY_PRODUCTION_SOURCE
|
| 284 |
+
statement_contains: ["joint survey", "truck count"]
|
| 285 |
+
page: 5
|
| 286 |
+
- rule_id: PTY_WORKING_HOURS_DEFINITION
|
| 287 |
+
statement_contains: ["Effective Working Hours", "waktu kerja efektif"]
|
| 288 |
+
page: 5
|
| 289 |
+
- rule_id: PTY_COMPOSITE_WEIGHTED
|
| 290 |
+
statement_contains: ["Pty", "pembobotan", "Quantity"]
|
| 291 |
+
page: 5
|
| 292 |
+
- rule_id: GAINLOSS_QTY_ZERO_PLAN
|
| 293 |
+
statement_contains: ["QtyP(M) = 0", "QtyA(M) > 0"]
|
| 294 |
+
page: 6
|
| 295 |
+
- rule_id: GAINLOSS_COMPOSITE_SUM
|
| 296 |
+
statement_contains: ["grouping", "penjumlahan", "model unit"]
|
| 297 |
+
page: 6
|
| 298 |
+
- rule_id: GAINLOSS_UNITS
|
| 299 |
+
statement_contains: ["BCM", "ton", "jam"]
|
| 300 |
+
page: 6
|
| 301 |
+
- rule_id: QTY_ACTUAL_ZERO_PLAN_ALT
|
| 302 |
+
statement_contains: ["QtyA", "QtyP", "0"]
|
| 303 |
+
page: 7
|
| 304 |
+
- rule_id: QTY_PHYSICAL_REPORTING
|
| 305 |
+
statement_contains: ["fisik unit", "INPR"]
|
| 306 |
+
page: 7
|
| 307 |
+
- rule_id: ECA_OTHER_ACTIVITY
|
| 308 |
+
statement_contains: ["alokasi", "tidak sesuai", "direncanakan"]
|
| 309 |
+
page: 8
|
| 310 |
+
- rule_id: WATERFALL_TWO_METHODS
|
| 311 |
+
statement_contains: ["Waterfall", "dua metode"]
|
| 312 |
+
page: 8
|
eval/knowledge/results/baseline_prototype_2026-08-13_145132.json
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_note": "FROZEN BASELINE β measured by the `kex` prototype, not by this repo. Extraction v2 must match or beat these numbers on the same document and the same gold set. Copied from the prototype's eval/out/metrics.json on 2026-08-19; never regenerate this file, add a new timestamped one.",
|
| 3 |
+
"_source": {
|
| 4 |
+
"repo": "kex prototype (outside version control)",
|
| 5 |
+
"file": "eval/out/metrics.json",
|
| 6 |
+
"copied_on": "2026-08-19"
|
| 7 |
+
},
|
| 8 |
+
"provenance": {
|
| 9 |
+
"run_id": "20260813-145132",
|
| 10 |
+
"config_hash": "89545a78c702",
|
| 11 |
+
"created_at": "2026-08-13T14:51:32",
|
| 12 |
+
"doc_id": "STD_2026_006_MNO",
|
| 13 |
+
"client_mode": "azure",
|
| 14 |
+
"deployments": {
|
| 15 |
+
"glossary": "gpt-5.4-nano",
|
| 16 |
+
"rule": "gpt-5.4-nano",
|
| 17 |
+
"formula": "gpt-5.4-nano",
|
| 18 |
+
"summary": "gpt-5.4-nano"
|
| 19 |
+
},
|
| 20 |
+
"api_version": "2024-12-01-preview",
|
| 21 |
+
"gold_status": "provisional β not yet expert-reviewed"
|
| 22 |
+
},
|
| 23 |
+
"funnel": {
|
| 24 |
+
"status": "measured",
|
| 25 |
+
"stages": [
|
| 26 |
+
{
|
| 27 |
+
"stage": "pages",
|
| 28 |
+
"count": 9
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"stage": "chunks",
|
| 32 |
+
"count": 13
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"stage": "mentions",
|
| 36 |
+
"count": 169
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"stage": "clusters",
|
| 40 |
+
"count": 66
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"stage": "llm_calls",
|
| 44 |
+
"count": 83
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"stage": "glossary_entries",
|
| 48 |
+
"count": 66
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"stage": "review_queue",
|
| 52 |
+
"count": 66
|
| 53 |
+
}
|
| 54 |
+
],
|
| 55 |
+
"raw_mentions_before_noise_filter": 195
|
| 56 |
+
},
|
| 57 |
+
"experiments": {
|
| 58 |
+
"E1": {
|
| 59 |
+
"question": "GLiNER recall on Bahasa Indonesia technical prose",
|
| 60 |
+
"status": "measured",
|
| 61 |
+
"value": 0.8537,
|
| 62 |
+
"kill_line": 0.7,
|
| 63 |
+
"verdict": "PASS",
|
| 64 |
+
"detail": "broad @ threshold 0.25",
|
| 65 |
+
"n_gold": 41
|
| 66 |
+
},
|
| 67 |
+
"E2": {
|
| 68 |
+
"question": "Does clustering cut expert review burden?",
|
| 69 |
+
"status": "measured",
|
| 70 |
+
"value": 2.561,
|
| 71 |
+
"kill_line": 2.0,
|
| 72 |
+
"verdict": "PASS",
|
| 73 |
+
"detail": "169 mentions -> 66 clusters",
|
| 74 |
+
"llm_calls_avoided": 103
|
| 75 |
+
},
|
| 76 |
+
"E3": {
|
| 77 |
+
"question": "Is nano sufficient for schema fill?",
|
| 78 |
+
"status": "measured_nano_only",
|
| 79 |
+
"value": 0.75,
|
| 80 |
+
"kill_line": 0.8,
|
| 81 |
+
"verdict": "FAIL",
|
| 82 |
+
"detail": "nano only β no mini deployment provisioned, so the spec's nano-vs-mini A/B could not be run",
|
| 83 |
+
"glossary_precision": 0.75,
|
| 84 |
+
"glossary_recall": 0.3333,
|
| 85 |
+
"glossary_f1": 0.4615,
|
| 86 |
+
"rule_precision": 0.5714,
|
| 87 |
+
"rule_recall": 0.2667,
|
| 88 |
+
"n_gold_terms": 18,
|
| 89 |
+
"scoreable_basis": {
|
| 90 |
+
"entries_total": 66,
|
| 91 |
+
"entries_with_definition": 10,
|
| 92 |
+
"abstained_null_definition": 56,
|
| 93 |
+
"scoreable": 8,
|
| 94 |
+
"unscoreable_term_not_in_gold": 2,
|
| 95 |
+
"unscoreable_gold_has_no_criteria": 0,
|
| 96 |
+
"gold_terms_with_criteria": 18,
|
| 97 |
+
"failures": [
|
| 98 |
+
{
|
| 99 |
+
"term": "produksi",
|
| 100 |
+
"gold_requires": [
|
| 101 |
+
"nilai tambah",
|
| 102 |
+
"sumber daya"
|
| 103 |
+
],
|
| 104 |
+
"extracted": "Dalam konteks industri pertambangan, produksi merupakan seluruh rangkaian kegiatan operasional penambangan yang dimulai dari land clearing, pengupasan lapisan penutup (overburden removal), penambangan batubara (coal mining), pengangkutan ba"
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"term": "Production",
|
| 108 |
+
"gold_requires": [
|
| 109 |
+
"nilai tambah",
|
| 110 |
+
"sumber daya"
|
| 111 |
+
],
|
| 112 |
+
"extracted": "Dalam konteks industri pertambangan, produksi merupakan seluruh rangkaian kegiatan operasional penambangan yang dimulai dari land clearing, pengupasan lapisan penutup (overburden removal), penambangan batubara (coal mining), pengangkutan ba"
|
| 113 |
+
}
|
| 114 |
+
]
|
| 115 |
+
},
|
| 116 |
+
"abstention_note": "entries where nano returned null are NOT counted as errors β for a term the document never defines, null is the correct answer",
|
| 117 |
+
"adjudication_pending": "The scoreable base is small, so each failure moves the number by ~12 points. Every failure is listed verbatim under scoreable_basis.failures for Mas Beta to adjudicate: some are genuine extraction errors, others are cases where the document carries two valid definitions and the provisional gold names only one. Gold was NOT edited after seeing output, so this figure is a lower bound.",
|
| 118 |
+
"stability_warning": "nano is not fully deterministic at temperature=0: consecutive runs of the identical pipeline produced precision 0.75 and 0.625. On a base this small, treat E3 as a signal, not a measurement."
|
| 119 |
+
},
|
| 120 |
+
"E4": {
|
| 121 |
+
"question": "Does verbatim-span validation catch fabrication?",
|
| 122 |
+
"status": "measured",
|
| 123 |
+
"value": 1.0,
|
| 124 |
+
"kill_line": 0.9,
|
| 125 |
+
"verdict": "PASS",
|
| 126 |
+
"false_rejection_rate": 0.0,
|
| 127 |
+
"detail": "span_check is deterministic; measures the control itself"
|
| 128 |
+
}
|
| 129 |
+
},
|
| 130 |
+
"quality": {
|
| 131 |
+
"status": "measured",
|
| 132 |
+
"glossary_entries": 66,
|
| 133 |
+
"rule_entries": 7,
|
| 134 |
+
"formula_entries": 7,
|
| 135 |
+
"fields_rejected_by_span_check": 3,
|
| 136 |
+
"no_definition_found": 56,
|
| 137 |
+
"escalated": 0,
|
| 138 |
+
"definition_conflicts": 0
|
| 139 |
+
},
|
| 140 |
+
"efficiency": {
|
| 141 |
+
"status": "measured + stated assumption",
|
| 142 |
+
"llm_calls_avoided_by_clustering": 103,
|
| 143 |
+
"review_queue_size": 66,
|
| 144 |
+
"assumed_minutes_per_term_review": 3.0,
|
| 145 |
+
"assumed_minutes_per_term_manual": 12.0,
|
| 146 |
+
"est_review_hours": 3.3,
|
| 147 |
+
"est_manual_hours": 13.2,
|
| 148 |
+
"est_hours_saved": 9.9
|
| 149 |
+
},
|
| 150 |
+
"cost_measured": {
|
| 151 |
+
"prompt": 155313,
|
| 152 |
+
"cached": 125184,
|
| 153 |
+
"completion": 11862,
|
| 154 |
+
"usd": 0.006877,
|
| 155 |
+
"latency_s": 242.4
|
| 156 |
+
}
|
| 157 |
+
}
|
eval/knowledge/results/v2_parser_comparison_2026-08-19_152842.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_note": "Parser comparison on the SAME document, gold set and pipeline code. Answers whether MinerU input is interchangeable with the PyMuPDF parse the frozen baseline was measured on.",
|
| 3 |
+
"_caveat": "The MinerU artifacts were built by a SCRATCH converter in the scratchpad, not by production code β section-aware chunking is Sofhia's B5. Treat the chunking-related findings as indicative of what her chunker must handle, not as a measurement of MinerU.",
|
| 4 |
+
"run": {
|
| 5 |
+
"created_at": "2026-08-19_152842",
|
| 6 |
+
"doc_id": "STD_2026_006_MNO",
|
| 7 |
+
"implementation": "src/knowledge_extraction (v2)"
|
| 8 |
+
},
|
| 9 |
+
"variants": {
|
| 10 |
+
"pymupdf_prototype_parse": {
|
| 11 |
+
"E1_recall": 0.8537,
|
| 12 |
+
"true_positives": 35,
|
| 13 |
+
"chunks": 13,
|
| 14 |
+
"note": "what the frozen baseline used"
|
| 15 |
+
},
|
| 16 |
+
"mineru_raw_markup": {
|
| 17 |
+
"E1_recall": 0.7561,
|
| 18 |
+
"true_positives": 31,
|
| 19 |
+
"chunks": 14,
|
| 20 |
+
"misses": [
|
| 21 |
+
"Total Hours",
|
| 22 |
+
"Breakdown",
|
| 23 |
+
"Overburden removal",
|
| 24 |
+
"Grouping (Composite)",
|
| 25 |
+
"Weight average",
|
| 26 |
+
"Plan",
|
| 27 |
+
"Other Activity",
|
| 28 |
+
"Uncontrollable",
|
| 29 |
+
"Fleet management",
|
| 30 |
+
"Mineplan"
|
| 31 |
+
],
|
| 32 |
+
"note": "formulas left as LaTeX, tables as HTML β the span model reads prose, not markup"
|
| 33 |
+
},
|
| 34 |
+
"mineru_rendered_to_prose": {
|
| 35 |
+
"E1_recall": 0.8293,
|
| 36 |
+
"true_positives": 34,
|
| 37 |
+
"chunks": 14,
|
| 38 |
+
"misses": [
|
| 39 |
+
"Overburden removal",
|
| 40 |
+
"Grouping (Composite)",
|
| 41 |
+
"Weight average",
|
| 42 |
+
"Other Activity",
|
| 43 |
+
"Uncontrollable",
|
| 44 |
+
"Fleet management",
|
| 45 |
+
"Mineplan"
|
| 46 |
+
],
|
| 47 |
+
"note": "LaTeX unwrapped, tables flattened to rows; recovers most of the loss"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"n_gold": 41,
|
| 51 |
+
"kill_line": 0.7,
|
| 52 |
+
"findings": [
|
| 53 |
+
"Raw MinerU markup costs ~9.8 recall points: LaTeX-wrapped terms (Total Hours, Breakdown) are invisible to the span filter.",
|
| 54 |
+
"Rendering formulas and tables to prose recovers +7.3 points, to within ~2.4 points of the PyMuPDF parse.",
|
| 55 |
+
"The residual gap is CHUNKING, not content: MinerU marks non-section labels (Keterangan:, Keterangan grafik:) as text_level titles. Opening a new section at each one splits a legend from the figure it explains, and Other Activity / Uncontrollable are then missed despite being present as prose.",
|
| 56 |
+
"Actionable for the parsing half: only NUMBERED headings should open a section. This matches the earlier finding that MinerU flattens every heading to level 2, so numbering is the only reliable section signal.",
|
| 57 |
+
"The artifact should carry formulas and tables as readable text, not raw LaTeX/HTML, or the filter stage must render them before NER."
|
| 58 |
+
]
|
| 59 |
+
}
|
eval/knowledge/results/v2_term_filter_2026-08-19_144318.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_note": "First v2 run. Term-filter (E1) only β the paid extraction stage is not built, so E3 is not scoreable yet.",
|
| 3 |
+
"run": {
|
| 4 |
+
"created_at": "2026-08-19_144318",
|
| 5 |
+
"implementation": "src/knowledge_extraction (v2)",
|
| 6 |
+
"doc_id": "STD_2026_006_MNO"
|
| 7 |
+
},
|
| 8 |
+
"config": {
|
| 9 |
+
"model": "urchade/gliner_multi-v2.1",
|
| 10 |
+
"labels_variant": "broad",
|
| 11 |
+
"threshold": 0.25,
|
| 12 |
+
"window_words": 130,
|
| 13 |
+
"window_overlap": 30
|
| 14 |
+
},
|
| 15 |
+
"gold": {
|
| 16 |
+
"path": "eval/knowledge/knowledge_gold.yaml",
|
| 17 |
+
"status": "provisional β not expert-reviewed",
|
| 18 |
+
"n_terms": 41
|
| 19 |
+
},
|
| 20 |
+
"funnel": {
|
| 21 |
+
"chunks": 13,
|
| 22 |
+
"mentions_raw": 188,
|
| 23 |
+
"mentions_after_noise": 163,
|
| 24 |
+
"clusters": 66,
|
| 25 |
+
"compression_ratio": 2.47
|
| 26 |
+
},
|
| 27 |
+
"E1": {
|
| 28 |
+
"metric": "term_filter_recall",
|
| 29 |
+
"value": 0.8537,
|
| 30 |
+
"kill_line": 0.7,
|
| 31 |
+
"verdict": "PASS",
|
| 32 |
+
"true_positives": 35,
|
| 33 |
+
"precision": 0.4118,
|
| 34 |
+
"precision_note": "reported, NOT the E1 criterion β the filter is deliberately over-inclusive",
|
| 35 |
+
"misses": [
|
| 36 |
+
"Overburden removal",
|
| 37 |
+
"Grouping (Composite)",
|
| 38 |
+
"Weight average",
|
| 39 |
+
"Plan",
|
| 40 |
+
"Fleet management",
|
| 41 |
+
"Mineplan"
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
"baseline_comparison": {
|
| 45 |
+
"baseline_file": "results/baseline_prototype_2026-08-13_145132.json",
|
| 46 |
+
"E1_baseline": 0.8537,
|
| 47 |
+
"E1_v2": 0.8537,
|
| 48 |
+
"delta": 0.0,
|
| 49 |
+
"clusters_baseline": 66,
|
| 50 |
+
"clusters_v2": 66,
|
| 51 |
+
"verdict": "MATCH β v2 reproduces the prototype recall exactly"
|
| 52 |
+
}
|
| 53 |
+
}
|
eval/knowledge/score.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Precision / recall / F1 per branch of the knowledge-extraction pipeline.
|
| 2 |
+
|
| 3 |
+
Migrated verbatim (bar the import bootstrap) from the `kex` prototype, 2026-08-19.
|
| 4 |
+
The prototype is not being ported; this scorer is, because it is the only thing
|
| 5 |
+
that can show extraction v2 matches or beats the measured baseline in
|
| 6 |
+
`results/baseline_prototype_2026-08-13_145132.json`.
|
| 7 |
+
|
| 8 |
+
Pipeline-independent by design: it scores plain lists of surfaces / entry dicts,
|
| 9 |
+
so it works against the prototype's artifacts and against v2 alike.
|
| 10 |
+
|
| 11 |
+
The one thing this module refuses to do is conflate **term-filter recall** with
|
| 12 |
+
**extraction precision** (spec Β§5). They are different failure modes with
|
| 13 |
+
different fixes: recall is fixed at stage 2 (GLiNER labels), precision is fixed
|
| 14 |
+
at stage 3 (model tier / prompt). E1 is the recall number specifically.
|
| 15 |
+
|
| 16 |
+
Gold sets are treated as partial by design β Mas Beta is the labelling
|
| 17 |
+
bottleneck, so scoring reports coverage rather than blocking on a complete file.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import re
|
| 23 |
+
import unicodedata
|
| 24 |
+
from dataclasses import asdict, dataclass
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import yaml
|
| 28 |
+
|
| 29 |
+
GOLD_PATH = Path(__file__).resolve().parent / "knowledge_gold.yaml"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def norm(s: str) -> str:
|
| 33 |
+
s = unicodedata.normalize("NFKC", s).casefold()
|
| 34 |
+
s = re.sub(r"[^\w\s]", " ", s)
|
| 35 |
+
return re.sub(r"\s+", " ", s).strip()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class Score:
|
| 40 |
+
label: str
|
| 41 |
+
n_gold: int
|
| 42 |
+
n_pred: int
|
| 43 |
+
true_positives: int
|
| 44 |
+
precision: float
|
| 45 |
+
recall: float
|
| 46 |
+
f1: float
|
| 47 |
+
misses: list[str]
|
| 48 |
+
|
| 49 |
+
def as_dict(self) -> dict:
|
| 50 |
+
d = asdict(self)
|
| 51 |
+
if hasattr(self, "coverage"):
|
| 52 |
+
d["coverage"] = self.coverage
|
| 53 |
+
return d
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def load_gold(path: Path) -> dict:
|
| 57 |
+
with open(path, encoding="utf-8") as fh:
|
| 58 |
+
return yaml.safe_load(fh)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _prf(tp: int, n_pred: int, n_gold: int) -> tuple[float, float, float]:
|
| 62 |
+
precision = tp / n_pred if n_pred else 0.0
|
| 63 |
+
recall = tp / n_gold if n_gold else 0.0
|
| 64 |
+
f1 = (
|
| 65 |
+
2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
|
| 66 |
+
)
|
| 67 |
+
return round(precision, 4), round(recall, 4), round(f1, 4)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def score_term_filter(gold: dict, surfaces: list[str]) -> Score:
|
| 71 |
+
"""E1. A gold term counts as recalled if ANY of its variants appears among
|
| 72 |
+
the filter's mention surfaces (substring match both ways, so 'PA' inside
|
| 73 |
+
'Physical Availability (PA)' counts).
|
| 74 |
+
|
| 75 |
+
Precision is reported but is NOT E1's criterion: the filter is deliberately
|
| 76 |
+
over-inclusive, and clustering plus evidence ranking absorb the noise.
|
| 77 |
+
"""
|
| 78 |
+
normed = {norm(s) for s in surfaces if norm(s)}
|
| 79 |
+
blob = " | ".join(sorted(normed))
|
| 80 |
+
tp, misses = 0, []
|
| 81 |
+
gold_terms = gold.get("terms", [])
|
| 82 |
+
for entry in gold_terms:
|
| 83 |
+
variants = [entry["term"]] + list(entry.get("variants", []))
|
| 84 |
+
if entry.get("full_name"):
|
| 85 |
+
variants.append(entry["full_name"])
|
| 86 |
+
hit = False
|
| 87 |
+
for v in variants:
|
| 88 |
+
nv = norm(v)
|
| 89 |
+
if not nv:
|
| 90 |
+
continue
|
| 91 |
+
if nv in normed or re.search(rf"(?<![\w]){re.escape(nv)}(?![\w])", blob):
|
| 92 |
+
hit = True
|
| 93 |
+
break
|
| 94 |
+
if hit:
|
| 95 |
+
tp += 1
|
| 96 |
+
else:
|
| 97 |
+
misses.append(entry["term"])
|
| 98 |
+
precision, recall, f1 = _prf(tp, len(normed), len(gold_terms))
|
| 99 |
+
return Score(
|
| 100 |
+
label="term_filter_recall(E1)",
|
| 101 |
+
n_gold=len(gold_terms),
|
| 102 |
+
n_pred=len(normed),
|
| 103 |
+
true_positives=tp,
|
| 104 |
+
precision=precision,
|
| 105 |
+
recall=recall,
|
| 106 |
+
f1=f1,
|
| 107 |
+
misses=misses,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def score_glossary(gold: dict, entries: list[dict]) -> Score:
|
| 112 |
+
"""E3: when nano fills the schema, is it right?
|
| 113 |
+
|
| 114 |
+
Scoring is restricted to the SCOREABLE subset: entries that produced a
|
| 115 |
+
definition AND whose term is in the gold set AND whose gold record carries
|
| 116 |
+
`definition_contains` to check against.
|
| 117 |
+
|
| 118 |
+
Why not simply tp/len(entries): the term filter is deliberately
|
| 119 |
+
over-inclusive and the gold set is deliberately partial, so most entries are
|
| 120 |
+
for terms gold says nothing about. Counting those as errors would measure
|
| 121 |
+
gold coverage while claiming to measure nano's accuracy β precisely the
|
| 122 |
+
conflation spec Β§5 forbids. Coverage is reported separately in as_dict().
|
| 123 |
+
|
| 124 |
+
Substring matching, not exact β exact match would under-report
|
| 125 |
+
correct-but-differently-worded extractions (spec Β§5).
|
| 126 |
+
"""
|
| 127 |
+
gold_by_term: dict[str, dict] = {}
|
| 128 |
+
for entry in gold.get("terms", []):
|
| 129 |
+
for v in [entry["term"], *entry.get("variants", [])]:
|
| 130 |
+
gold_by_term.setdefault(norm(v), entry)
|
| 131 |
+
|
| 132 |
+
checkable_gold = [
|
| 133 |
+
g for g in gold.get("terms", []) if g.get("definition_contains")
|
| 134 |
+
]
|
| 135 |
+
n_checkable_gold = len(checkable_gold)
|
| 136 |
+
|
| 137 |
+
correct, incorrect = 0, 0
|
| 138 |
+
unscoreable_no_gold, unscoreable_no_criteria = 0, 0
|
| 139 |
+
matched_gold, wrong, failures = set(), [], []
|
| 140 |
+
|
| 141 |
+
for pred in entries:
|
| 142 |
+
if not (pred.get("definition") or "").strip():
|
| 143 |
+
continue # abstention is scored separately, not as an error
|
| 144 |
+
g = gold_by_term.get(norm(pred.get("term", "")))
|
| 145 |
+
if not g:
|
| 146 |
+
unscoreable_no_gold += 1
|
| 147 |
+
continue
|
| 148 |
+
required = [norm(x) for x in g.get("definition_contains", [])]
|
| 149 |
+
if not required:
|
| 150 |
+
unscoreable_no_criteria += 1
|
| 151 |
+
continue
|
| 152 |
+
definition = norm(pred.get("definition") or "")
|
| 153 |
+
if all(r in definition for r in required):
|
| 154 |
+
correct += 1
|
| 155 |
+
matched_gold.add(g["term"])
|
| 156 |
+
else:
|
| 157 |
+
incorrect += 1
|
| 158 |
+
wrong.append(f"{pred.get('term')} (definition did not match gold)")
|
| 159 |
+
failures.append(
|
| 160 |
+
{
|
| 161 |
+
"term": pred.get("term"),
|
| 162 |
+
"gold_requires": g.get("definition_contains"),
|
| 163 |
+
"extracted": (pred.get("definition") or "")[:240],
|
| 164 |
+
}
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
misses = [g["term"] for g in checkable_gold if g["term"] not in matched_gold]
|
| 168 |
+
n_scoreable = correct + incorrect
|
| 169 |
+
precision, recall, f1 = _prf(correct, n_scoreable, n_checkable_gold)
|
| 170 |
+
|
| 171 |
+
score = Score(
|
| 172 |
+
label="glossary_schema_fill(E3)",
|
| 173 |
+
n_gold=n_checkable_gold,
|
| 174 |
+
n_pred=n_scoreable,
|
| 175 |
+
true_positives=correct,
|
| 176 |
+
precision=precision,
|
| 177 |
+
recall=recall,
|
| 178 |
+
f1=f1,
|
| 179 |
+
misses=misses + wrong,
|
| 180 |
+
)
|
| 181 |
+
score.coverage = { # type: ignore[attr-defined]
|
| 182 |
+
"entries_total": len(entries),
|
| 183 |
+
"entries_with_definition": sum(
|
| 184 |
+
1 for e in entries if (e.get("definition") or "").strip()
|
| 185 |
+
),
|
| 186 |
+
"abstained_null_definition": sum(
|
| 187 |
+
1 for e in entries if not (e.get("definition") or "").strip()
|
| 188 |
+
),
|
| 189 |
+
"scoreable": n_scoreable,
|
| 190 |
+
"unscoreable_term_not_in_gold": unscoreable_no_gold,
|
| 191 |
+
"unscoreable_gold_has_no_criteria": unscoreable_no_criteria,
|
| 192 |
+
"gold_terms_with_criteria": n_checkable_gold,
|
| 193 |
+
"failures": failures,
|
| 194 |
+
}
|
| 195 |
+
return score
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def score_rules(gold: dict, entries: list[dict]) -> Score:
|
| 199 |
+
gold_rules = gold.get("rules", [])
|
| 200 |
+
pred_blobs = [
|
| 201 |
+
norm(
|
| 202 |
+
" ".join(
|
| 203 |
+
str(v)
|
| 204 |
+
for v in (e.get("statement"), e.get("condition"), e.get("consequence"))
|
| 205 |
+
if v
|
| 206 |
+
)
|
| 207 |
+
)
|
| 208 |
+
for e in entries
|
| 209 |
+
]
|
| 210 |
+
tp, misses = 0, []
|
| 211 |
+
for rule in gold_rules:
|
| 212 |
+
required = [norm(x) for x in rule.get("statement_contains", [])]
|
| 213 |
+
if any(all(r in blob for r in required) for blob in pred_blobs):
|
| 214 |
+
tp += 1
|
| 215 |
+
else:
|
| 216 |
+
misses.append(rule["rule_id"])
|
| 217 |
+
precision, recall, f1 = _prf(tp, len(entries), len(gold_rules))
|
| 218 |
+
return Score(
|
| 219 |
+
label="rule",
|
| 220 |
+
n_gold=len(gold_rules),
|
| 221 |
+
n_pred=len(entries),
|
| 222 |
+
true_positives=tp,
|
| 223 |
+
precision=precision,
|
| 224 |
+
recall=recall,
|
| 225 |
+
f1=f1,
|
| 226 |
+
misses=misses,
|
| 227 |
+
)
|
eval/planner/results/planner_result_2026-07-23_134947.json
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "2026-07-23_134947",
|
| 3 |
+
"total": 27,
|
| 4 |
+
"passed": 20,
|
| 5 |
+
"cases": [
|
| 6 |
+
{
|
| 7 |
+
"id": "count_zero_pa",
|
| 8 |
+
"category": "count",
|
| 9 |
+
"lang": "en",
|
| 10 |
+
"carried_over": true,
|
| 11 |
+
"question": "how many records have PA_Percent = 0?",
|
| 12 |
+
"passed": true,
|
| 13 |
+
"checks": [
|
| 14 |
+
{
|
| 15 |
+
"check": "select_agg=count",
|
| 16 |
+
"ok": true,
|
| 17 |
+
"detail": "aggs=['count']"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"check": "has_filter",
|
| 21 |
+
"ok": true,
|
| 22 |
+
"detail": "filter_present=True"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"check": "no_group_by",
|
| 26 |
+
"ok": true,
|
| 27 |
+
"detail": "group_by_present=False"
|
| 28 |
+
}
|
| 29 |
+
],
|
| 30 |
+
"error": null,
|
| 31 |
+
"latency_ms": 3591,
|
| 32 |
+
"tokens": 14570
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "count_mttr_gt20_id",
|
| 36 |
+
"category": "count",
|
| 37 |
+
"lang": "id",
|
| 38 |
+
"carried_over": true,
|
| 39 |
+
"question": "berapa banyak record dengan MTTR di atas 20?",
|
| 40 |
+
"passed": true,
|
| 41 |
+
"checks": [
|
| 42 |
+
{
|
| 43 |
+
"check": "select_agg=count",
|
| 44 |
+
"ok": true,
|
| 45 |
+
"detail": "aggs=['count']"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"check": "has_filter",
|
| 49 |
+
"ok": true,
|
| 50 |
+
"detail": "filter_present=True"
|
| 51 |
+
}
|
| 52 |
+
],
|
| 53 |
+
"error": null,
|
| 54 |
+
"latency_ms": 2154,
|
| 55 |
+
"tokens": 14571
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "count_section_hauler",
|
| 59 |
+
"category": "count",
|
| 60 |
+
"lang": "en",
|
| 61 |
+
"carried_over": true,
|
| 62 |
+
"question": "how many rows are in section OB HAULER?",
|
| 63 |
+
"passed": true,
|
| 64 |
+
"checks": [
|
| 65 |
+
{
|
| 66 |
+
"check": "select_agg=count",
|
| 67 |
+
"ok": true,
|
| 68 |
+
"detail": "aggs=['count']"
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"check": "has_filter",
|
| 72 |
+
"ok": true,
|
| 73 |
+
"detail": "filter_present=True"
|
| 74 |
+
}
|
| 75 |
+
],
|
| 76 |
+
"error": null,
|
| 77 |
+
"latency_ms": 2063,
|
| 78 |
+
"tokens": 14573
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"id": "rank_units_worst_pa_id",
|
| 82 |
+
"category": "ranking",
|
| 83 |
+
"lang": "id",
|
| 84 |
+
"carried_over": false,
|
| 85 |
+
"question": "5 unit dengan PA terburuk?",
|
| 86 |
+
"passed": true,
|
| 87 |
+
"checks": [
|
| 88 |
+
{
|
| 89 |
+
"check": "group_by",
|
| 90 |
+
"ok": true,
|
| 91 |
+
"detail": "group_by_present=True"
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"check": "group_by_col=Equipment_Number",
|
| 95 |
+
"ok": true,
|
| 96 |
+
"detail": "want_id=c_equipment_number"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"check": "select_agg=avg",
|
| 100 |
+
"ok": true,
|
| 101 |
+
"detail": "aggs=['avg']"
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"check": "order_dir=asc",
|
| 105 |
+
"ok": true,
|
| 106 |
+
"detail": "dirs=['asc']"
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"check": "limit=5",
|
| 110 |
+
"ok": true,
|
| 111 |
+
"detail": "limits=[5]"
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
+
"error": null,
|
| 115 |
+
"latency_ms": 2664,
|
| 116 |
+
"tokens": 14784
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"id": "rank_models_top_mttr_id",
|
| 120 |
+
"category": "ranking",
|
| 121 |
+
"lang": "id",
|
| 122 |
+
"carried_over": true,
|
| 123 |
+
"question": "top 3 model dengan MTTR tertinggi?",
|
| 124 |
+
"passed": true,
|
| 125 |
+
"checks": [
|
| 126 |
+
{
|
| 127 |
+
"check": "group_by",
|
| 128 |
+
"ok": true,
|
| 129 |
+
"detail": "group_by_present=True"
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"check": "group_by_col=Model_Unit",
|
| 133 |
+
"ok": true,
|
| 134 |
+
"detail": "want_id=c_model_unit"
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"check": "select_agg=avg",
|
| 138 |
+
"ok": true,
|
| 139 |
+
"detail": "aggs=['avg']"
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"check": "order_dir=desc",
|
| 143 |
+
"ok": true,
|
| 144 |
+
"detail": "dirs=['desc']"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"check": "limit=3",
|
| 148 |
+
"ok": true,
|
| 149 |
+
"detail": "limits=[3]"
|
| 150 |
+
}
|
| 151 |
+
],
|
| 152 |
+
"error": null,
|
| 153 |
+
"latency_ms": 2757,
|
| 154 |
+
"tokens": 14719
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"id": "rank_sections_lowest_pa_en",
|
| 158 |
+
"category": "ranking",
|
| 159 |
+
"lang": "en",
|
| 160 |
+
"carried_over": true,
|
| 161 |
+
"question": "which section has the lowest average PA?",
|
| 162 |
+
"passed": false,
|
| 163 |
+
"checks": [
|
| 164 |
+
{
|
| 165 |
+
"check": "group_by",
|
| 166 |
+
"ok": false,
|
| 167 |
+
"detail": "group_by_present=False"
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"check": "group_by_col=Section",
|
| 171 |
+
"ok": false,
|
| 172 |
+
"detail": "want_id=c_section"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"check": "select_agg=avg",
|
| 176 |
+
"ok": false,
|
| 177 |
+
"detail": "aggs=[]"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"check": "order_dir=asc",
|
| 181 |
+
"ok": false,
|
| 182 |
+
"detail": "dirs=[]"
|
| 183 |
+
}
|
| 184 |
+
],
|
| 185 |
+
"error": null,
|
| 186 |
+
"latency_ms": 3366,
|
| 187 |
+
"tokens": 14834
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"id": "rank_units_most_breakdown_id",
|
| 191 |
+
"category": "ranking",
|
| 192 |
+
"lang": "id",
|
| 193 |
+
"carried_over": false,
|
| 194 |
+
"question": "unit mana yang paling sering breakdown?",
|
| 195 |
+
"passed": true,
|
| 196 |
+
"checks": [
|
| 197 |
+
{
|
| 198 |
+
"check": "group_by",
|
| 199 |
+
"ok": true,
|
| 200 |
+
"detail": "group_by_present=True"
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"check": "group_by_col=Equipment_Number",
|
| 204 |
+
"ok": true,
|
| 205 |
+
"detail": "want_id=c_equipment_number"
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"check": "order_dir=desc",
|
| 209 |
+
"ok": true,
|
| 210 |
+
"detail": "dirs=['desc']"
|
| 211 |
+
}
|
| 212 |
+
],
|
| 213 |
+
"error": null,
|
| 214 |
+
"latency_ms": 2925,
|
| 215 |
+
"tokens": 14736
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"id": "rank_units_worst_pa_en",
|
| 219 |
+
"category": "ranking",
|
| 220 |
+
"lang": "en",
|
| 221 |
+
"carried_over": false,
|
| 222 |
+
"question": "list the 10 worst units by availability",
|
| 223 |
+
"passed": true,
|
| 224 |
+
"checks": [
|
| 225 |
+
{
|
| 226 |
+
"check": "group_by",
|
| 227 |
+
"ok": true,
|
| 228 |
+
"detail": "group_by_present=True"
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"check": "group_by_col=Equipment_Number",
|
| 232 |
+
"ok": true,
|
| 233 |
+
"detail": "want_id=c_equipment_number"
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"check": "select_agg=avg",
|
| 237 |
+
"ok": true,
|
| 238 |
+
"detail": "aggs=['avg']"
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"check": "order_dir=asc",
|
| 242 |
+
"ok": true,
|
| 243 |
+
"detail": "dirs=['asc']"
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"check": "limit=10",
|
| 247 |
+
"ok": true,
|
| 248 |
+
"detail": "limits=[10]"
|
| 249 |
+
}
|
| 250 |
+
],
|
| 251 |
+
"error": null,
|
| 252 |
+
"latency_ms": 3001,
|
| 253 |
+
"tokens": 14758
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"id": "fuzzy_model_777_id",
|
| 257 |
+
"category": "fuzzy_filter",
|
| 258 |
+
"lang": "id",
|
| 259 |
+
"carried_over": false,
|
| 260 |
+
"question": "berapa banyak model 777?",
|
| 261 |
+
"passed": false,
|
| 262 |
+
"checks": [
|
| 263 |
+
{
|
| 264 |
+
"check": "select_agg=count",
|
| 265 |
+
"ok": true,
|
| 266 |
+
"detail": "aggs=['count']"
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"check": "filter_op=like",
|
| 270 |
+
"ok": false,
|
| 271 |
+
"detail": "ops=['=']"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"check": "no_filter_op=in",
|
| 275 |
+
"ok": true,
|
| 276 |
+
"detail": "ops=['=']"
|
| 277 |
+
}
|
| 278 |
+
],
|
| 279 |
+
"error": null,
|
| 280 |
+
"latency_ms": 1831,
|
| 281 |
+
"tokens": 14578
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
"id": "fuzzy_model_hd785_id",
|
| 285 |
+
"category": "fuzzy_filter",
|
| 286 |
+
"lang": "id",
|
| 287 |
+
"carried_over": true,
|
| 288 |
+
"question": "berapa banyak unit HD785?",
|
| 289 |
+
"passed": false,
|
| 290 |
+
"checks": [
|
| 291 |
+
{
|
| 292 |
+
"check": "select_agg=count",
|
| 293 |
+
"ok": true,
|
| 294 |
+
"detail": "aggs=['count']"
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
"check": "filter_op=like",
|
| 298 |
+
"ok": false,
|
| 299 |
+
"detail": "ops=['=']"
|
| 300 |
+
}
|
| 301 |
+
],
|
| 302 |
+
"error": null,
|
| 303 |
+
"latency_ms": 2219,
|
| 304 |
+
"tokens": 14578
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"id": "fuzzy_model_ex_en",
|
| 308 |
+
"category": "fuzzy_filter",
|
| 309 |
+
"lang": "en",
|
| 310 |
+
"carried_over": false,
|
| 311 |
+
"question": "how many EX excavator units are there?",
|
| 312 |
+
"passed": true,
|
| 313 |
+
"checks": [
|
| 314 |
+
{
|
| 315 |
+
"check": "filter_op=like",
|
| 316 |
+
"ok": true,
|
| 317 |
+
"detail": "ops=['like']"
|
| 318 |
+
}
|
| 319 |
+
],
|
| 320 |
+
"error": null,
|
| 321 |
+
"latency_ms": 5243,
|
| 322 |
+
"tokens": 29547
|
| 323 |
+
},
|
| 324 |
+
{
|
| 325 |
+
"id": "agg_pa_per_section_id",
|
| 326 |
+
"category": "aggregate",
|
| 327 |
+
"lang": "id",
|
| 328 |
+
"carried_over": true,
|
| 329 |
+
"question": "berapa rata-rata PA per section?",
|
| 330 |
+
"passed": false,
|
| 331 |
+
"checks": [
|
| 332 |
+
{
|
| 333 |
+
"check": "group_by",
|
| 334 |
+
"ok": false,
|
| 335 |
+
"detail": "group_by_present=False"
|
| 336 |
+
},
|
| 337 |
+
{
|
| 338 |
+
"check": "group_by_col=Section",
|
| 339 |
+
"ok": false,
|
| 340 |
+
"detail": "want_id=c_section"
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"check": "select_agg=avg",
|
| 344 |
+
"ok": false,
|
| 345 |
+
"detail": "aggs=[]"
|
| 346 |
+
}
|
| 347 |
+
],
|
| 348 |
+
"error": null,
|
| 349 |
+
"latency_ms": 2817,
|
| 350 |
+
"tokens": 14757
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"id": "agg_mttr_per_model_en",
|
| 354 |
+
"category": "aggregate",
|
| 355 |
+
"lang": "en",
|
| 356 |
+
"carried_over": true,
|
| 357 |
+
"question": "what is the average MTTR per model unit?",
|
| 358 |
+
"passed": false,
|
| 359 |
+
"checks": [
|
| 360 |
+
{
|
| 361 |
+
"check": "group_by",
|
| 362 |
+
"ok": false,
|
| 363 |
+
"detail": "group_by_present=False"
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"check": "group_by_col=Model_Unit",
|
| 367 |
+
"ok": false,
|
| 368 |
+
"detail": "want_id=c_model_unit"
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"check": "select_agg=avg",
|
| 372 |
+
"ok": false,
|
| 373 |
+
"detail": "aggs=[]"
|
| 374 |
+
}
|
| 375 |
+
],
|
| 376 |
+
"error": null,
|
| 377 |
+
"latency_ms": 2859,
|
| 378 |
+
"tokens": 14773
|
| 379 |
+
},
|
| 380 |
+
{
|
| 381 |
+
"id": "agg_downhour_per_group_id",
|
| 382 |
+
"category": "aggregate",
|
| 383 |
+
"lang": "id",
|
| 384 |
+
"carried_over": true,
|
| 385 |
+
"question": "total down hour per equipment group?",
|
| 386 |
+
"passed": false,
|
| 387 |
+
"checks": [
|
| 388 |
+
{
|
| 389 |
+
"check": "group_by",
|
| 390 |
+
"ok": false,
|
| 391 |
+
"detail": "group_by_present=False"
|
| 392 |
+
},
|
| 393 |
+
{
|
| 394 |
+
"check": "group_by_col=Equipment_Group_ID",
|
| 395 |
+
"ok": false,
|
| 396 |
+
"detail": "want_id=c_equipment_group_id"
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"check": "select_agg=sum",
|
| 400 |
+
"ok": false,
|
| 401 |
+
"detail": "aggs=[]"
|
| 402 |
+
}
|
| 403 |
+
],
|
| 404 |
+
"error": null,
|
| 405 |
+
"latency_ms": 2855,
|
| 406 |
+
"tokens": 14769
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"id": "desc_mttr_stats_id",
|
| 410 |
+
"category": "descriptive",
|
| 411 |
+
"lang": "id",
|
| 412 |
+
"carried_over": true,
|
| 413 |
+
"question": "berikan ringkasan statistik MTTR",
|
| 414 |
+
"passed": true,
|
| 415 |
+
"checks": [
|
| 416 |
+
{
|
| 417 |
+
"check": "has_tool=analyze_descriptive",
|
| 418 |
+
"ok": true,
|
| 419 |
+
"detail": "tools=['analyze_descriptive', 'check_data', 'retrieve_data']"
|
| 420 |
+
}
|
| 421 |
+
],
|
| 422 |
+
"error": null,
|
| 423 |
+
"latency_ms": 2928,
|
| 424 |
+
"tokens": 14714
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"id": "desc_pa_stats_en",
|
| 428 |
+
"category": "descriptive",
|
| 429 |
+
"lang": "en",
|
| 430 |
+
"carried_over": true,
|
| 431 |
+
"question": "give me the summary statistics for PA_Percent",
|
| 432 |
+
"passed": true,
|
| 433 |
+
"checks": [
|
| 434 |
+
{
|
| 435 |
+
"check": "has_tool=analyze_descriptive",
|
| 436 |
+
"ok": true,
|
| 437 |
+
"detail": "tools=['analyze_descriptive', 'check_data', 'retrieve_data']"
|
| 438 |
+
}
|
| 439 |
+
],
|
| 440 |
+
"error": null,
|
| 441 |
+
"latency_ms": 2862,
|
| 442 |
+
"tokens": 14704
|
| 443 |
+
},
|
| 444 |
+
{
|
| 445 |
+
"id": "corr_mttr_pa_id",
|
| 446 |
+
"category": "correlation",
|
| 447 |
+
"lang": "id",
|
| 448 |
+
"carried_over": true,
|
| 449 |
+
"question": "apakah ada korelasi antara MTTR dan PA?",
|
| 450 |
+
"passed": true,
|
| 451 |
+
"checks": [
|
| 452 |
+
{
|
| 453 |
+
"check": "has_tool=analyze_correlation",
|
| 454 |
+
"ok": true,
|
| 455 |
+
"detail": "tools=['analyze_correlation', 'check_data', 'retrieve_data']"
|
| 456 |
+
}
|
| 457 |
+
],
|
| 458 |
+
"error": null,
|
| 459 |
+
"latency_ms": 3171,
|
| 460 |
+
"tokens": 14866
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"id": "corr_freq_pa_en",
|
| 464 |
+
"category": "correlation",
|
| 465 |
+
"lang": "en",
|
| 466 |
+
"carried_over": true,
|
| 467 |
+
"question": "is breakdown frequency correlated with availability?",
|
| 468 |
+
"passed": true,
|
| 469 |
+
"checks": [
|
| 470 |
+
{
|
| 471 |
+
"check": "has_tool=analyze_correlation",
|
| 472 |
+
"ok": true,
|
| 473 |
+
"detail": "tools=['analyze_correlation', 'check_data', 'retrieve_data']"
|
| 474 |
+
}
|
| 475 |
+
],
|
| 476 |
+
"error": null,
|
| 477 |
+
"latency_ms": 3259,
|
| 478 |
+
"tokens": 14921
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"id": "trend_pa_daily_id",
|
| 482 |
+
"category": "trend",
|
| 483 |
+
"lang": "id",
|
| 484 |
+
"carried_over": true,
|
| 485 |
+
"question": "bagaimana trend PA harian?",
|
| 486 |
+
"passed": false,
|
| 487 |
+
"checks": [],
|
| 488 |
+
"error": "ValidationError: 6 validation errors for TaskList\ntasks.2.id\n Field required [type=missing, input_value={'tool': 'analyze_trend',...: 'day', 'agg': 'mean'}}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/missing\ntasks.2.stage\n Field required [type=missing, input_value={'tool': 'analyze_trend',...: 'day', 'agg': 'mean'}}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/missing\ntasks.2.objective\n Field required [type=missing, input_value={'tool': 'analyze_trend',...: 'day', 'agg': 'mean'}}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/missing\ntasks.2.tool_calls\n Field required [type=missing, input_value={'tool': 'analyze_trend',...: 'day', 'agg': 'mean'}}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/missing\ntasks.2.expected_output\n Field required [type=missing, input_value={'tool': 'analyze_trend',...: 'day', 'agg': 'mean'}}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/missing\ntasks.2.success_criteria\n Field required [type=missing, input_value={'tool': 'analyze_trend',...: 'day', 'agg': 'mean'}}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/missing",
|
| 489 |
+
"latency_ms": 3008,
|
| 490 |
+
"tokens": 14775
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"id": "trend_downhour_en",
|
| 494 |
+
"category": "trend",
|
| 495 |
+
"lang": "en",
|
| 496 |
+
"carried_over": true,
|
| 497 |
+
"question": "show the trend of total down hours over time",
|
| 498 |
+
"passed": true,
|
| 499 |
+
"checks": [
|
| 500 |
+
{
|
| 501 |
+
"check": "has_tool=analyze_trend",
|
| 502 |
+
"ok": true,
|
| 503 |
+
"detail": "tools=['analyze_trend', 'check_data', 'retrieve_data']"
|
| 504 |
+
}
|
| 505 |
+
],
|
| 506 |
+
"error": null,
|
| 507 |
+
"latency_ms": 3193,
|
| 508 |
+
"tokens": 14842
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"id": "merge_worst_pa_and_mttr_id",
|
| 512 |
+
"category": "merge",
|
| 513 |
+
"lang": "id",
|
| 514 |
+
"carried_over": true,
|
| 515 |
+
"question": "model mana yang PA-nya paling buruk sekaligus MTTR-nya paling tinggi?",
|
| 516 |
+
"passed": true,
|
| 517 |
+
"checks": [
|
| 518 |
+
{
|
| 519 |
+
"check": "group_by",
|
| 520 |
+
"ok": true,
|
| 521 |
+
"detail": "group_by_present=True"
|
| 522 |
+
},
|
| 523 |
+
{
|
| 524 |
+
"check": "group_by_col=Model_Unit",
|
| 525 |
+
"ok": true,
|
| 526 |
+
"detail": "want_id=c_model_unit"
|
| 527 |
+
}
|
| 528 |
+
],
|
| 529 |
+
"error": null,
|
| 530 |
+
"latency_ms": 4061,
|
| 531 |
+
"tokens": 15116
|
| 532 |
+
},
|
| 533 |
+
{
|
| 534 |
+
"id": "raw_rows_low_pa_id",
|
| 535 |
+
"category": "counter_raw_rows",
|
| 536 |
+
"lang": "id",
|
| 537 |
+
"carried_over": true,
|
| 538 |
+
"question": "tampilkan 10 record dengan PA di bawah 50",
|
| 539 |
+
"passed": true,
|
| 540 |
+
"checks": [
|
| 541 |
+
{
|
| 542 |
+
"check": "no_group_by",
|
| 543 |
+
"ok": true,
|
| 544 |
+
"detail": "group_by_present=False"
|
| 545 |
+
},
|
| 546 |
+
{
|
| 547 |
+
"check": "has_filter",
|
| 548 |
+
"ok": true,
|
| 549 |
+
"detail": "filter_present=True"
|
| 550 |
+
},
|
| 551 |
+
{
|
| 552 |
+
"check": "limit=10",
|
| 553 |
+
"ok": true,
|
| 554 |
+
"detail": "limits=[10]"
|
| 555 |
+
}
|
| 556 |
+
],
|
| 557 |
+
"error": null,
|
| 558 |
+
"latency_ms": 2944,
|
| 559 |
+
"tokens": 14868
|
| 560 |
+
},
|
| 561 |
+
{
|
| 562 |
+
"id": "raw_rows_head_en",
|
| 563 |
+
"category": "counter_raw_rows",
|
| 564 |
+
"lang": "en",
|
| 565 |
+
"carried_over": true,
|
| 566 |
+
"question": "show me the first 5 rows of the data",
|
| 567 |
+
"passed": true,
|
| 568 |
+
"checks": [
|
| 569 |
+
{
|
| 570 |
+
"check": "no_group_by",
|
| 571 |
+
"ok": true,
|
| 572 |
+
"detail": "group_by_present=False"
|
| 573 |
+
}
|
| 574 |
+
],
|
| 575 |
+
"error": null,
|
| 576 |
+
"latency_ms": 4427,
|
| 577 |
+
"tokens": 15315
|
| 578 |
+
},
|
| 579 |
+
{
|
| 580 |
+
"id": "exact_model_777d_id",
|
| 581 |
+
"category": "counter_exact_filter",
|
| 582 |
+
"lang": "id",
|
| 583 |
+
"carried_over": true,
|
| 584 |
+
"question": "berapa banyak record untuk model 777D?",
|
| 585 |
+
"passed": true,
|
| 586 |
+
"checks": [
|
| 587 |
+
{
|
| 588 |
+
"check": "select_agg=count",
|
| 589 |
+
"ok": true,
|
| 590 |
+
"detail": "aggs=['count']"
|
| 591 |
+
},
|
| 592 |
+
{
|
| 593 |
+
"check": "has_filter",
|
| 594 |
+
"ok": true,
|
| 595 |
+
"detail": "filter_present=True"
|
| 596 |
+
}
|
| 597 |
+
],
|
| 598 |
+
"error": null,
|
| 599 |
+
"latency_ms": 2314,
|
| 600 |
+
"tokens": 14568
|
| 601 |
+
},
|
| 602 |
+
{
|
| 603 |
+
"id": "exact_section_loader_en",
|
| 604 |
+
"category": "counter_exact_filter",
|
| 605 |
+
"lang": "en",
|
| 606 |
+
"carried_over": true,
|
| 607 |
+
"question": "how many records are in the OB LOADER section?",
|
| 608 |
+
"passed": true,
|
| 609 |
+
"checks": [
|
| 610 |
+
{
|
| 611 |
+
"check": "select_agg=count",
|
| 612 |
+
"ok": true,
|
| 613 |
+
"detail": "aggs=['count']"
|
| 614 |
+
},
|
| 615 |
+
{
|
| 616 |
+
"check": "has_filter",
|
| 617 |
+
"ok": true,
|
| 618 |
+
"detail": "filter_present=True"
|
| 619 |
+
}
|
| 620 |
+
],
|
| 621 |
+
"error": null,
|
| 622 |
+
"latency_ms": 2216,
|
| 623 |
+
"tokens": 14595
|
| 624 |
+
},
|
| 625 |
+
{
|
| 626 |
+
"id": "infeasible_churn_id",
|
| 627 |
+
"category": "infeasible",
|
| 628 |
+
"lang": "id",
|
| 629 |
+
"carried_over": true,
|
| 630 |
+
"question": "berapa churn rate pelanggan?",
|
| 631 |
+
"passed": true,
|
| 632 |
+
"checks": [
|
| 633 |
+
{
|
| 634 |
+
"check": "infeasible",
|
| 635 |
+
"ok": true,
|
| 636 |
+
"detail": "infeasible=True"
|
| 637 |
+
}
|
| 638 |
+
],
|
| 639 |
+
"error": null,
|
| 640 |
+
"latency_ms": 1775,
|
| 641 |
+
"tokens": 14477
|
| 642 |
+
},
|
| 643 |
+
{
|
| 644 |
+
"id": "infeasible_profit_en",
|
| 645 |
+
"category": "infeasible",
|
| 646 |
+
"lang": "en",
|
| 647 |
+
"carried_over": true,
|
| 648 |
+
"question": "what is the monthly profit margin?",
|
| 649 |
+
"passed": true,
|
| 650 |
+
"checks": [
|
| 651 |
+
{
|
| 652 |
+
"check": "infeasible",
|
| 653 |
+
"ok": true,
|
| 654 |
+
"detail": "infeasible=True"
|
| 655 |
+
}
|
| 656 |
+
],
|
| 657 |
+
"error": null,
|
| 658 |
+
"latency_ms": 2282,
|
| 659 |
+
"tokens": 14551
|
| 660 |
+
}
|
| 661 |
+
]
|
| 662 |
+
}
|
eval/planner/results/planner_result_2026-07-23_141931.json
ADDED
|
@@ -0,0 +1,1818 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "2026-07-23_141931",
|
| 3 |
+
"total": 27,
|
| 4 |
+
"passed": 25,
|
| 5 |
+
"cases": [
|
| 6 |
+
{
|
| 7 |
+
"id": "count_zero_pa",
|
| 8 |
+
"category": "count",
|
| 9 |
+
"lang": "en",
|
| 10 |
+
"carried_over": true,
|
| 11 |
+
"question": "how many records have PA_Percent = 0?",
|
| 12 |
+
"passed": true,
|
| 13 |
+
"checks": [
|
| 14 |
+
{
|
| 15 |
+
"check": "select_agg=count",
|
| 16 |
+
"ok": true,
|
| 17 |
+
"detail": "aggs=['count']"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"check": "has_filter",
|
| 21 |
+
"ok": true,
|
| 22 |
+
"detail": "filter_present=True"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"check": "no_group_by",
|
| 26 |
+
"ok": true,
|
| 27 |
+
"detail": "grouped=False"
|
| 28 |
+
}
|
| 29 |
+
],
|
| 30 |
+
"facts": {
|
| 31 |
+
"tools": [
|
| 32 |
+
"retrieve_data"
|
| 33 |
+
],
|
| 34 |
+
"irs": [
|
| 35 |
+
{
|
| 36 |
+
"source_id": "src_pa",
|
| 37 |
+
"table_id": "t_pa",
|
| 38 |
+
"select": [
|
| 39 |
+
{
|
| 40 |
+
"kind": "agg",
|
| 41 |
+
"fn": "count",
|
| 42 |
+
"alias": "record_count"
|
| 43 |
+
}
|
| 44 |
+
],
|
| 45 |
+
"filters": [
|
| 46 |
+
{
|
| 47 |
+
"column_id": "c_pa_percent",
|
| 48 |
+
"op": "=",
|
| 49 |
+
"value": 0,
|
| 50 |
+
"value_type": "decimal"
|
| 51 |
+
}
|
| 52 |
+
]
|
| 53 |
+
}
|
| 54 |
+
],
|
| 55 |
+
"agg_args": [],
|
| 56 |
+
"infeasible": false
|
| 57 |
+
},
|
| 58 |
+
"error": null,
|
| 59 |
+
"latency_ms": 3652,
|
| 60 |
+
"tokens": 14570
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "count_mttr_gt20_id",
|
| 64 |
+
"category": "count",
|
| 65 |
+
"lang": "id",
|
| 66 |
+
"carried_over": true,
|
| 67 |
+
"question": "berapa banyak record dengan MTTR di atas 20?",
|
| 68 |
+
"passed": true,
|
| 69 |
+
"checks": [
|
| 70 |
+
{
|
| 71 |
+
"check": "select_agg=count",
|
| 72 |
+
"ok": true,
|
| 73 |
+
"detail": "aggs=['count']"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"check": "has_filter",
|
| 77 |
+
"ok": true,
|
| 78 |
+
"detail": "filter_present=True"
|
| 79 |
+
}
|
| 80 |
+
],
|
| 81 |
+
"facts": {
|
| 82 |
+
"tools": [
|
| 83 |
+
"retrieve_data"
|
| 84 |
+
],
|
| 85 |
+
"irs": [
|
| 86 |
+
{
|
| 87 |
+
"source_id": "src_pa",
|
| 88 |
+
"table_id": "t_pa",
|
| 89 |
+
"select": [
|
| 90 |
+
{
|
| 91 |
+
"kind": "agg",
|
| 92 |
+
"fn": "count",
|
| 93 |
+
"alias": "record_count"
|
| 94 |
+
}
|
| 95 |
+
],
|
| 96 |
+
"filters": [
|
| 97 |
+
{
|
| 98 |
+
"column_id": "c_mttr",
|
| 99 |
+
"op": ">",
|
| 100 |
+
"value": 20,
|
| 101 |
+
"value_type": "decimal"
|
| 102 |
+
}
|
| 103 |
+
]
|
| 104 |
+
}
|
| 105 |
+
],
|
| 106 |
+
"agg_args": [],
|
| 107 |
+
"infeasible": false
|
| 108 |
+
},
|
| 109 |
+
"error": null,
|
| 110 |
+
"latency_ms": 2176,
|
| 111 |
+
"tokens": 14571
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"id": "count_section_hauler",
|
| 115 |
+
"category": "count",
|
| 116 |
+
"lang": "en",
|
| 117 |
+
"carried_over": true,
|
| 118 |
+
"question": "how many rows are in section OB HAULER?",
|
| 119 |
+
"passed": true,
|
| 120 |
+
"checks": [
|
| 121 |
+
{
|
| 122 |
+
"check": "select_agg=count",
|
| 123 |
+
"ok": true,
|
| 124 |
+
"detail": "aggs=['count']"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"check": "has_filter",
|
| 128 |
+
"ok": true,
|
| 129 |
+
"detail": "filter_present=True"
|
| 130 |
+
}
|
| 131 |
+
],
|
| 132 |
+
"facts": {
|
| 133 |
+
"tools": [
|
| 134 |
+
"retrieve_data"
|
| 135 |
+
],
|
| 136 |
+
"irs": [
|
| 137 |
+
{
|
| 138 |
+
"source_id": "src_pa",
|
| 139 |
+
"table_id": "t_pa",
|
| 140 |
+
"select": [
|
| 141 |
+
{
|
| 142 |
+
"kind": "agg",
|
| 143 |
+
"fn": "count",
|
| 144 |
+
"alias": "row_count"
|
| 145 |
+
}
|
| 146 |
+
],
|
| 147 |
+
"filters": [
|
| 148 |
+
{
|
| 149 |
+
"column_id": "c_section",
|
| 150 |
+
"op": "=",
|
| 151 |
+
"value": "OB HAULER",
|
| 152 |
+
"value_type": "string"
|
| 153 |
+
}
|
| 154 |
+
]
|
| 155 |
+
}
|
| 156 |
+
],
|
| 157 |
+
"agg_args": [],
|
| 158 |
+
"infeasible": false
|
| 159 |
+
},
|
| 160 |
+
"error": null,
|
| 161 |
+
"latency_ms": 2342,
|
| 162 |
+
"tokens": 14571
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"id": "rank_units_worst_pa_id",
|
| 166 |
+
"category": "ranking",
|
| 167 |
+
"lang": "id",
|
| 168 |
+
"carried_over": false,
|
| 169 |
+
"question": "5 unit dengan PA terburuk?",
|
| 170 |
+
"passed": true,
|
| 171 |
+
"checks": [
|
| 172 |
+
{
|
| 173 |
+
"check": "group_by",
|
| 174 |
+
"ok": true,
|
| 175 |
+
"detail": "grouped=True"
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"check": "group_by_col=Equipment_Number",
|
| 179 |
+
"ok": true,
|
| 180 |
+
"detail": "ids=['c_equipment_number'] aliases=[]"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"check": "select_agg=avg",
|
| 184 |
+
"ok": true,
|
| 185 |
+
"detail": "aggs=['avg']"
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"check": "order_dir=asc",
|
| 189 |
+
"ok": true,
|
| 190 |
+
"detail": "dirs=['asc']"
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"check": "limit=5",
|
| 194 |
+
"ok": true,
|
| 195 |
+
"detail": "limits=[5]"
|
| 196 |
+
}
|
| 197 |
+
],
|
| 198 |
+
"facts": {
|
| 199 |
+
"tools": [
|
| 200 |
+
"check_data",
|
| 201 |
+
"retrieve_data"
|
| 202 |
+
],
|
| 203 |
+
"irs": [
|
| 204 |
+
{
|
| 205 |
+
"source_id": "src_pa",
|
| 206 |
+
"table_id": "t_pa",
|
| 207 |
+
"select": [
|
| 208 |
+
{
|
| 209 |
+
"kind": "column",
|
| 210 |
+
"column_id": "c_equipment_number",
|
| 211 |
+
"alias": "equipment_number"
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"kind": "agg",
|
| 215 |
+
"fn": "avg",
|
| 216 |
+
"column_id": "c_pa_percent",
|
| 217 |
+
"alias": "avg_pa_percent"
|
| 218 |
+
}
|
| 219 |
+
],
|
| 220 |
+
"group_by": [
|
| 221 |
+
"c_equipment_number"
|
| 222 |
+
],
|
| 223 |
+
"order_by": [
|
| 224 |
+
{
|
| 225 |
+
"column_id": "avg_pa_percent",
|
| 226 |
+
"dir": "asc"
|
| 227 |
+
}
|
| 228 |
+
],
|
| 229 |
+
"limit": 5
|
| 230 |
+
}
|
| 231 |
+
],
|
| 232 |
+
"agg_args": [],
|
| 233 |
+
"infeasible": false
|
| 234 |
+
},
|
| 235 |
+
"error": null,
|
| 236 |
+
"latency_ms": 3519,
|
| 237 |
+
"tokens": 14733
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
"id": "rank_models_top_mttr_id",
|
| 241 |
+
"category": "ranking",
|
| 242 |
+
"lang": "id",
|
| 243 |
+
"carried_over": true,
|
| 244 |
+
"question": "top 3 model dengan MTTR tertinggi?",
|
| 245 |
+
"passed": true,
|
| 246 |
+
"checks": [
|
| 247 |
+
{
|
| 248 |
+
"check": "group_by",
|
| 249 |
+
"ok": true,
|
| 250 |
+
"detail": "grouped=True"
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"check": "group_by_col=Model_Unit",
|
| 254 |
+
"ok": true,
|
| 255 |
+
"detail": "ids=['c_model_unit'] aliases=[]"
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"check": "select_agg=avg",
|
| 259 |
+
"ok": true,
|
| 260 |
+
"detail": "aggs=['avg']"
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"check": "order_dir=desc",
|
| 264 |
+
"ok": true,
|
| 265 |
+
"detail": "dirs=['desc']"
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"check": "limit=3",
|
| 269 |
+
"ok": true,
|
| 270 |
+
"detail": "limits=[3]"
|
| 271 |
+
}
|
| 272 |
+
],
|
| 273 |
+
"facts": {
|
| 274 |
+
"tools": [
|
| 275 |
+
"check_data",
|
| 276 |
+
"retrieve_data"
|
| 277 |
+
],
|
| 278 |
+
"irs": [
|
| 279 |
+
{
|
| 280 |
+
"source_id": "src_pa",
|
| 281 |
+
"table_id": "t_pa",
|
| 282 |
+
"select": [
|
| 283 |
+
{
|
| 284 |
+
"kind": "column",
|
| 285 |
+
"column_id": "c_model_unit",
|
| 286 |
+
"alias": "model"
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"kind": "agg",
|
| 290 |
+
"fn": "avg",
|
| 291 |
+
"column_id": "c_mttr",
|
| 292 |
+
"alias": "avg_mttr"
|
| 293 |
+
}
|
| 294 |
+
],
|
| 295 |
+
"group_by": [
|
| 296 |
+
"c_model_unit"
|
| 297 |
+
],
|
| 298 |
+
"order_by": [
|
| 299 |
+
{
|
| 300 |
+
"column_id": "avg_mttr",
|
| 301 |
+
"dir": "desc"
|
| 302 |
+
}
|
| 303 |
+
],
|
| 304 |
+
"limit": 3
|
| 305 |
+
}
|
| 306 |
+
],
|
| 307 |
+
"agg_args": [],
|
| 308 |
+
"infeasible": false
|
| 309 |
+
},
|
| 310 |
+
"error": null,
|
| 311 |
+
"latency_ms": 2928,
|
| 312 |
+
"tokens": 14723
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"id": "rank_sections_lowest_pa_en",
|
| 316 |
+
"category": "ranking",
|
| 317 |
+
"lang": "en",
|
| 318 |
+
"carried_over": true,
|
| 319 |
+
"question": "which section has the lowest average PA?",
|
| 320 |
+
"passed": false,
|
| 321 |
+
"checks": [
|
| 322 |
+
{
|
| 323 |
+
"check": "group_by",
|
| 324 |
+
"ok": true,
|
| 325 |
+
"detail": "grouped=True"
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
"check": "group_by_col=Section",
|
| 329 |
+
"ok": true,
|
| 330 |
+
"detail": "ids=[] aliases=['section']"
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"check": "select_agg=avg",
|
| 334 |
+
"ok": true,
|
| 335 |
+
"detail": "aggs=['mean']"
|
| 336 |
+
},
|
| 337 |
+
{
|
| 338 |
+
"check": "order_dir=asc",
|
| 339 |
+
"ok": false,
|
| 340 |
+
"detail": "dirs=[]"
|
| 341 |
+
}
|
| 342 |
+
],
|
| 343 |
+
"facts": {
|
| 344 |
+
"tools": [
|
| 345 |
+
"analyze_aggregate",
|
| 346 |
+
"check_data",
|
| 347 |
+
"retrieve_data"
|
| 348 |
+
],
|
| 349 |
+
"irs": [
|
| 350 |
+
{
|
| 351 |
+
"source_id": "src_pa",
|
| 352 |
+
"table_id": "t_pa",
|
| 353 |
+
"select": [
|
| 354 |
+
{
|
| 355 |
+
"kind": "column",
|
| 356 |
+
"column_id": "c_section",
|
| 357 |
+
"alias": "section"
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"kind": "column",
|
| 361 |
+
"column_id": "c_pa_percent",
|
| 362 |
+
"alias": "pa_percent"
|
| 363 |
+
}
|
| 364 |
+
],
|
| 365 |
+
"filters": [
|
| 366 |
+
{
|
| 367 |
+
"column_id": "c_month_id",
|
| 368 |
+
"op": "=",
|
| 369 |
+
"value": 202604,
|
| 370 |
+
"value_type": "int"
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
"column_id": "c_isdeleted",
|
| 374 |
+
"op": "=",
|
| 375 |
+
"value": 0,
|
| 376 |
+
"value_type": "int"
|
| 377 |
+
}
|
| 378 |
+
],
|
| 379 |
+
"limit": 10000
|
| 380 |
+
}
|
| 381 |
+
],
|
| 382 |
+
"agg_args": [
|
| 383 |
+
{
|
| 384 |
+
"data": "${t2}",
|
| 385 |
+
"aggregations": {
|
| 386 |
+
"pa_percent": [
|
| 387 |
+
"mean"
|
| 388 |
+
]
|
| 389 |
+
},
|
| 390 |
+
"group_by": [
|
| 391 |
+
"section"
|
| 392 |
+
]
|
| 393 |
+
}
|
| 394 |
+
],
|
| 395 |
+
"infeasible": false
|
| 396 |
+
},
|
| 397 |
+
"error": null,
|
| 398 |
+
"latency_ms": 3445,
|
| 399 |
+
"tokens": 14833
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
"id": "rank_units_most_breakdown_id",
|
| 403 |
+
"category": "ranking",
|
| 404 |
+
"lang": "id",
|
| 405 |
+
"carried_over": false,
|
| 406 |
+
"question": "unit mana yang paling sering breakdown?",
|
| 407 |
+
"passed": true,
|
| 408 |
+
"checks": [
|
| 409 |
+
{
|
| 410 |
+
"check": "group_by",
|
| 411 |
+
"ok": true,
|
| 412 |
+
"detail": "grouped=True"
|
| 413 |
+
},
|
| 414 |
+
{
|
| 415 |
+
"check": "group_by_col=Equipment_Number",
|
| 416 |
+
"ok": true,
|
| 417 |
+
"detail": "ids=['c_equipment_number'] aliases=[]"
|
| 418 |
+
},
|
| 419 |
+
{
|
| 420 |
+
"check": "order_dir=desc",
|
| 421 |
+
"ok": true,
|
| 422 |
+
"detail": "dirs=['desc']"
|
| 423 |
+
}
|
| 424 |
+
],
|
| 425 |
+
"facts": {
|
| 426 |
+
"tools": [
|
| 427 |
+
"check_data",
|
| 428 |
+
"retrieve_data"
|
| 429 |
+
],
|
| 430 |
+
"irs": [
|
| 431 |
+
{
|
| 432 |
+
"source_id": "src_pa",
|
| 433 |
+
"table_id": "t_pa",
|
| 434 |
+
"select": [
|
| 435 |
+
{
|
| 436 |
+
"kind": "column",
|
| 437 |
+
"column_id": "c_equipment_number",
|
| 438 |
+
"alias": "equipment_number"
|
| 439 |
+
},
|
| 440 |
+
{
|
| 441 |
+
"kind": "agg",
|
| 442 |
+
"fn": "sum",
|
| 443 |
+
"column_id": "c_total_frequency_down",
|
| 444 |
+
"alias": "total_breakdown_frequency"
|
| 445 |
+
}
|
| 446 |
+
],
|
| 447 |
+
"filters": [
|
| 448 |
+
{
|
| 449 |
+
"column_id": "c_month_id",
|
| 450 |
+
"op": "=",
|
| 451 |
+
"value": 202604,
|
| 452 |
+
"value_type": "int"
|
| 453 |
+
},
|
| 454 |
+
{
|
| 455 |
+
"column_id": "c_isdeleted",
|
| 456 |
+
"op": "=",
|
| 457 |
+
"value": 0,
|
| 458 |
+
"value_type": "int"
|
| 459 |
+
}
|
| 460 |
+
],
|
| 461 |
+
"group_by": [
|
| 462 |
+
"c_equipment_number"
|
| 463 |
+
],
|
| 464 |
+
"order_by": [
|
| 465 |
+
{
|
| 466 |
+
"column_id": "total_breakdown_frequency",
|
| 467 |
+
"dir": "desc"
|
| 468 |
+
}
|
| 469 |
+
],
|
| 470 |
+
"limit": 1
|
| 471 |
+
}
|
| 472 |
+
],
|
| 473 |
+
"agg_args": [],
|
| 474 |
+
"infeasible": false
|
| 475 |
+
},
|
| 476 |
+
"error": null,
|
| 477 |
+
"latency_ms": 2590,
|
| 478 |
+
"tokens": 14760
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"id": "rank_units_worst_pa_en",
|
| 482 |
+
"category": "ranking",
|
| 483 |
+
"lang": "en",
|
| 484 |
+
"carried_over": false,
|
| 485 |
+
"question": "list the 10 worst units by availability",
|
| 486 |
+
"passed": true,
|
| 487 |
+
"checks": [
|
| 488 |
+
{
|
| 489 |
+
"check": "group_by",
|
| 490 |
+
"ok": true,
|
| 491 |
+
"detail": "grouped=True"
|
| 492 |
+
},
|
| 493 |
+
{
|
| 494 |
+
"check": "group_by_col=Equipment_Number",
|
| 495 |
+
"ok": true,
|
| 496 |
+
"detail": "ids=['c_equipment_number'] aliases=[]"
|
| 497 |
+
},
|
| 498 |
+
{
|
| 499 |
+
"check": "select_agg=avg",
|
| 500 |
+
"ok": true,
|
| 501 |
+
"detail": "aggs=['avg']"
|
| 502 |
+
},
|
| 503 |
+
{
|
| 504 |
+
"check": "order_dir=asc",
|
| 505 |
+
"ok": true,
|
| 506 |
+
"detail": "dirs=['asc']"
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"check": "limit=10",
|
| 510 |
+
"ok": true,
|
| 511 |
+
"detail": "limits=[10]"
|
| 512 |
+
}
|
| 513 |
+
],
|
| 514 |
+
"facts": {
|
| 515 |
+
"tools": [
|
| 516 |
+
"check_data",
|
| 517 |
+
"retrieve_data"
|
| 518 |
+
],
|
| 519 |
+
"irs": [
|
| 520 |
+
{
|
| 521 |
+
"source_id": "src_pa",
|
| 522 |
+
"table_id": "t_pa",
|
| 523 |
+
"select": [
|
| 524 |
+
{
|
| 525 |
+
"kind": "column",
|
| 526 |
+
"column_id": "c_equipment_number",
|
| 527 |
+
"alias": "equipment_number"
|
| 528 |
+
},
|
| 529 |
+
{
|
| 530 |
+
"kind": "agg",
|
| 531 |
+
"fn": "avg",
|
| 532 |
+
"column_id": "c_pa_percent",
|
| 533 |
+
"alias": "avg_pa_percent"
|
| 534 |
+
}
|
| 535 |
+
],
|
| 536 |
+
"filters": [
|
| 537 |
+
{
|
| 538 |
+
"column_id": "c_month_id",
|
| 539 |
+
"op": "=",
|
| 540 |
+
"value": 202604,
|
| 541 |
+
"value_type": "int"
|
| 542 |
+
},
|
| 543 |
+
{
|
| 544 |
+
"column_id": "c_isdeleted",
|
| 545 |
+
"op": "=",
|
| 546 |
+
"value": 0,
|
| 547 |
+
"value_type": "int"
|
| 548 |
+
}
|
| 549 |
+
],
|
| 550 |
+
"group_by": [
|
| 551 |
+
"c_equipment_number"
|
| 552 |
+
],
|
| 553 |
+
"order_by": [
|
| 554 |
+
{
|
| 555 |
+
"column_id": "avg_pa_percent",
|
| 556 |
+
"dir": "asc"
|
| 557 |
+
}
|
| 558 |
+
],
|
| 559 |
+
"limit": 10
|
| 560 |
+
}
|
| 561 |
+
],
|
| 562 |
+
"agg_args": [],
|
| 563 |
+
"infeasible": false
|
| 564 |
+
},
|
| 565 |
+
"error": null,
|
| 566 |
+
"latency_ms": 3221,
|
| 567 |
+
"tokens": 14791
|
| 568 |
+
},
|
| 569 |
+
{
|
| 570 |
+
"id": "fuzzy_model_777_id",
|
| 571 |
+
"category": "fuzzy_filter",
|
| 572 |
+
"lang": "id",
|
| 573 |
+
"carried_over": false,
|
| 574 |
+
"question": "berapa banyak model 777?",
|
| 575 |
+
"passed": true,
|
| 576 |
+
"checks": [
|
| 577 |
+
{
|
| 578 |
+
"check": "select_agg=count",
|
| 579 |
+
"ok": true,
|
| 580 |
+
"detail": "aggs=['count']"
|
| 581 |
+
},
|
| 582 |
+
{
|
| 583 |
+
"check": "no_filter_op=in",
|
| 584 |
+
"ok": true,
|
| 585 |
+
"detail": "ops=['=']"
|
| 586 |
+
}
|
| 587 |
+
],
|
| 588 |
+
"facts": {
|
| 589 |
+
"tools": [
|
| 590 |
+
"retrieve_data"
|
| 591 |
+
],
|
| 592 |
+
"irs": [
|
| 593 |
+
{
|
| 594 |
+
"source_id": "src_pa",
|
| 595 |
+
"table_id": "t_pa",
|
| 596 |
+
"select": [
|
| 597 |
+
{
|
| 598 |
+
"kind": "agg",
|
| 599 |
+
"fn": "count",
|
| 600 |
+
"alias": "record_count"
|
| 601 |
+
}
|
| 602 |
+
],
|
| 603 |
+
"filters": [
|
| 604 |
+
{
|
| 605 |
+
"column_id": "c_model_unit",
|
| 606 |
+
"op": "=",
|
| 607 |
+
"value": "777",
|
| 608 |
+
"value_type": "string"
|
| 609 |
+
}
|
| 610 |
+
]
|
| 611 |
+
}
|
| 612 |
+
],
|
| 613 |
+
"agg_args": [],
|
| 614 |
+
"infeasible": false
|
| 615 |
+
},
|
| 616 |
+
"error": null,
|
| 617 |
+
"latency_ms": 2332,
|
| 618 |
+
"tokens": 14576
|
| 619 |
+
},
|
| 620 |
+
{
|
| 621 |
+
"id": "fuzzy_model_hd785_id",
|
| 622 |
+
"category": "fuzzy_filter",
|
| 623 |
+
"lang": "id",
|
| 624 |
+
"carried_over": true,
|
| 625 |
+
"question": "berapa banyak unit HD785?",
|
| 626 |
+
"passed": true,
|
| 627 |
+
"checks": [
|
| 628 |
+
{
|
| 629 |
+
"check": "select_agg=count",
|
| 630 |
+
"ok": true,
|
| 631 |
+
"detail": "aggs=['count']"
|
| 632 |
+
},
|
| 633 |
+
{
|
| 634 |
+
"check": "no_filter_op=in",
|
| 635 |
+
"ok": true,
|
| 636 |
+
"detail": "ops=['=']"
|
| 637 |
+
}
|
| 638 |
+
],
|
| 639 |
+
"facts": {
|
| 640 |
+
"tools": [
|
| 641 |
+
"retrieve_data"
|
| 642 |
+
],
|
| 643 |
+
"irs": [
|
| 644 |
+
{
|
| 645 |
+
"source_id": "src_pa",
|
| 646 |
+
"table_id": "t_pa",
|
| 647 |
+
"select": [
|
| 648 |
+
{
|
| 649 |
+
"kind": "agg",
|
| 650 |
+
"fn": "count",
|
| 651 |
+
"alias": "unit_count"
|
| 652 |
+
}
|
| 653 |
+
],
|
| 654 |
+
"filters": [
|
| 655 |
+
{
|
| 656 |
+
"column_id": "c_model_unit",
|
| 657 |
+
"op": "=",
|
| 658 |
+
"value": "HD785",
|
| 659 |
+
"value_type": "string"
|
| 660 |
+
}
|
| 661 |
+
],
|
| 662 |
+
"limit": 10000
|
| 663 |
+
}
|
| 664 |
+
],
|
| 665 |
+
"agg_args": [],
|
| 666 |
+
"infeasible": false
|
| 667 |
+
},
|
| 668 |
+
"error": null,
|
| 669 |
+
"latency_ms": 4613,
|
| 670 |
+
"tokens": 29329
|
| 671 |
+
},
|
| 672 |
+
{
|
| 673 |
+
"id": "fuzzy_model_ex_en",
|
| 674 |
+
"category": "fuzzy_filter",
|
| 675 |
+
"lang": "en",
|
| 676 |
+
"carried_over": false,
|
| 677 |
+
"question": "how many EX excavator units are there?",
|
| 678 |
+
"passed": true,
|
| 679 |
+
"checks": [
|
| 680 |
+
{
|
| 681 |
+
"check": "no_filter_op=in",
|
| 682 |
+
"ok": true,
|
| 683 |
+
"detail": "ops=['like']"
|
| 684 |
+
}
|
| 685 |
+
],
|
| 686 |
+
"facts": {
|
| 687 |
+
"tools": [
|
| 688 |
+
"retrieve_data"
|
| 689 |
+
],
|
| 690 |
+
"irs": [
|
| 691 |
+
{
|
| 692 |
+
"source_id": "src_pa",
|
| 693 |
+
"table_id": "t_pa",
|
| 694 |
+
"select": [
|
| 695 |
+
{
|
| 696 |
+
"kind": "agg",
|
| 697 |
+
"fn": "count",
|
| 698 |
+
"alias": "unit_count"
|
| 699 |
+
}
|
| 700 |
+
],
|
| 701 |
+
"filters": [
|
| 702 |
+
{
|
| 703 |
+
"column_id": "c_equipment_number",
|
| 704 |
+
"op": "like",
|
| 705 |
+
"value": "EX%",
|
| 706 |
+
"value_type": "string"
|
| 707 |
+
}
|
| 708 |
+
]
|
| 709 |
+
}
|
| 710 |
+
],
|
| 711 |
+
"agg_args": [],
|
| 712 |
+
"infeasible": false
|
| 713 |
+
},
|
| 714 |
+
"error": null,
|
| 715 |
+
"latency_ms": 5577,
|
| 716 |
+
"tokens": 29385
|
| 717 |
+
},
|
| 718 |
+
{
|
| 719 |
+
"id": "agg_pa_per_section_id",
|
| 720 |
+
"category": "aggregate",
|
| 721 |
+
"lang": "id",
|
| 722 |
+
"carried_over": true,
|
| 723 |
+
"question": "berapa rata-rata PA per section?",
|
| 724 |
+
"passed": true,
|
| 725 |
+
"checks": [
|
| 726 |
+
{
|
| 727 |
+
"check": "group_by",
|
| 728 |
+
"ok": true,
|
| 729 |
+
"detail": "grouped=True"
|
| 730 |
+
},
|
| 731 |
+
{
|
| 732 |
+
"check": "group_by_col=Section",
|
| 733 |
+
"ok": true,
|
| 734 |
+
"detail": "ids=[] aliases=['section']"
|
| 735 |
+
},
|
| 736 |
+
{
|
| 737 |
+
"check": "select_agg=avg",
|
| 738 |
+
"ok": true,
|
| 739 |
+
"detail": "aggs=['mean']"
|
| 740 |
+
}
|
| 741 |
+
],
|
| 742 |
+
"facts": {
|
| 743 |
+
"tools": [
|
| 744 |
+
"analyze_aggregate",
|
| 745 |
+
"check_data",
|
| 746 |
+
"retrieve_data"
|
| 747 |
+
],
|
| 748 |
+
"irs": [
|
| 749 |
+
{
|
| 750 |
+
"source_id": "src_pa",
|
| 751 |
+
"table_id": "t_pa",
|
| 752 |
+
"select": [
|
| 753 |
+
{
|
| 754 |
+
"kind": "column",
|
| 755 |
+
"column_id": "c_section",
|
| 756 |
+
"alias": "section"
|
| 757 |
+
},
|
| 758 |
+
{
|
| 759 |
+
"kind": "column",
|
| 760 |
+
"column_id": "c_pa_percent",
|
| 761 |
+
"alias": "pa_percent"
|
| 762 |
+
}
|
| 763 |
+
],
|
| 764 |
+
"limit": 10000
|
| 765 |
+
}
|
| 766 |
+
],
|
| 767 |
+
"agg_args": [
|
| 768 |
+
{
|
| 769 |
+
"data": "${t2}",
|
| 770 |
+
"aggregations": {
|
| 771 |
+
"pa_percent": [
|
| 772 |
+
"mean"
|
| 773 |
+
]
|
| 774 |
+
},
|
| 775 |
+
"group_by": [
|
| 776 |
+
"section"
|
| 777 |
+
]
|
| 778 |
+
}
|
| 779 |
+
],
|
| 780 |
+
"infeasible": false
|
| 781 |
+
},
|
| 782 |
+
"error": null,
|
| 783 |
+
"latency_ms": 3785,
|
| 784 |
+
"tokens": 14754
|
| 785 |
+
},
|
| 786 |
+
{
|
| 787 |
+
"id": "agg_mttr_per_model_en",
|
| 788 |
+
"category": "aggregate",
|
| 789 |
+
"lang": "en",
|
| 790 |
+
"carried_over": true,
|
| 791 |
+
"question": "what is the average MTTR per model unit?",
|
| 792 |
+
"passed": true,
|
| 793 |
+
"checks": [
|
| 794 |
+
{
|
| 795 |
+
"check": "group_by",
|
| 796 |
+
"ok": true,
|
| 797 |
+
"detail": "grouped=True"
|
| 798 |
+
},
|
| 799 |
+
{
|
| 800 |
+
"check": "group_by_col=Model_Unit",
|
| 801 |
+
"ok": true,
|
| 802 |
+
"detail": "ids=[] aliases=['model_unit']"
|
| 803 |
+
},
|
| 804 |
+
{
|
| 805 |
+
"check": "select_agg=avg",
|
| 806 |
+
"ok": true,
|
| 807 |
+
"detail": "aggs=['mean']"
|
| 808 |
+
}
|
| 809 |
+
],
|
| 810 |
+
"facts": {
|
| 811 |
+
"tools": [
|
| 812 |
+
"analyze_aggregate",
|
| 813 |
+
"check_data",
|
| 814 |
+
"retrieve_data"
|
| 815 |
+
],
|
| 816 |
+
"irs": [
|
| 817 |
+
{
|
| 818 |
+
"source_id": "src_pa",
|
| 819 |
+
"table_id": "t_pa",
|
| 820 |
+
"select": [
|
| 821 |
+
{
|
| 822 |
+
"kind": "column",
|
| 823 |
+
"column_id": "c_model_unit",
|
| 824 |
+
"alias": "model_unit"
|
| 825 |
+
},
|
| 826 |
+
{
|
| 827 |
+
"kind": "column",
|
| 828 |
+
"column_id": "c_mttr",
|
| 829 |
+
"alias": "mttr"
|
| 830 |
+
}
|
| 831 |
+
],
|
| 832 |
+
"limit": 10000
|
| 833 |
+
}
|
| 834 |
+
],
|
| 835 |
+
"agg_args": [
|
| 836 |
+
{
|
| 837 |
+
"data": "${t2}",
|
| 838 |
+
"aggregations": {
|
| 839 |
+
"mttr": [
|
| 840 |
+
"mean"
|
| 841 |
+
]
|
| 842 |
+
},
|
| 843 |
+
"group_by": [
|
| 844 |
+
"model_unit"
|
| 845 |
+
]
|
| 846 |
+
}
|
| 847 |
+
],
|
| 848 |
+
"infeasible": false
|
| 849 |
+
},
|
| 850 |
+
"error": null,
|
| 851 |
+
"latency_ms": 2925,
|
| 852 |
+
"tokens": 14790
|
| 853 |
+
},
|
| 854 |
+
{
|
| 855 |
+
"id": "agg_downhour_per_group_id",
|
| 856 |
+
"category": "aggregate",
|
| 857 |
+
"lang": "id",
|
| 858 |
+
"carried_over": true,
|
| 859 |
+
"question": "total down hour per equipment group?",
|
| 860 |
+
"passed": false,
|
| 861 |
+
"checks": [
|
| 862 |
+
{
|
| 863 |
+
"check": "group_by",
|
| 864 |
+
"ok": true,
|
| 865 |
+
"detail": "grouped=True"
|
| 866 |
+
},
|
| 867 |
+
{
|
| 868 |
+
"check": "group_by_col=Equipment_Group_ID",
|
| 869 |
+
"ok": false,
|
| 870 |
+
"detail": "ids=[] aliases=['equipment_group']"
|
| 871 |
+
},
|
| 872 |
+
{
|
| 873 |
+
"check": "select_agg=sum",
|
| 874 |
+
"ok": true,
|
| 875 |
+
"detail": "aggs=['sum']"
|
| 876 |
+
}
|
| 877 |
+
],
|
| 878 |
+
"facts": {
|
| 879 |
+
"tools": [
|
| 880 |
+
"analyze_aggregate",
|
| 881 |
+
"check_data",
|
| 882 |
+
"retrieve_data"
|
| 883 |
+
],
|
| 884 |
+
"irs": [
|
| 885 |
+
{
|
| 886 |
+
"source_id": "src_pa",
|
| 887 |
+
"table_id": "t_pa",
|
| 888 |
+
"select": [
|
| 889 |
+
{
|
| 890 |
+
"kind": "column",
|
| 891 |
+
"column_id": "c_equipment_group_id",
|
| 892 |
+
"alias": "equipment_group"
|
| 893 |
+
},
|
| 894 |
+
{
|
| 895 |
+
"kind": "column",
|
| 896 |
+
"column_id": "c_total_down_hour",
|
| 897 |
+
"alias": "total_down_hour"
|
| 898 |
+
}
|
| 899 |
+
],
|
| 900 |
+
"limit": 10000
|
| 901 |
+
}
|
| 902 |
+
],
|
| 903 |
+
"agg_args": [
|
| 904 |
+
{
|
| 905 |
+
"data": "${t2}",
|
| 906 |
+
"aggregations": {
|
| 907 |
+
"total_down_hour": [
|
| 908 |
+
"sum"
|
| 909 |
+
]
|
| 910 |
+
},
|
| 911 |
+
"group_by": [
|
| 912 |
+
"equipment_group"
|
| 913 |
+
]
|
| 914 |
+
}
|
| 915 |
+
],
|
| 916 |
+
"infeasible": false
|
| 917 |
+
},
|
| 918 |
+
"error": null,
|
| 919 |
+
"latency_ms": 3449,
|
| 920 |
+
"tokens": 14785
|
| 921 |
+
},
|
| 922 |
+
{
|
| 923 |
+
"id": "desc_mttr_stats_id",
|
| 924 |
+
"category": "descriptive",
|
| 925 |
+
"lang": "id",
|
| 926 |
+
"carried_over": true,
|
| 927 |
+
"question": "berikan ringkasan statistik MTTR",
|
| 928 |
+
"passed": true,
|
| 929 |
+
"checks": [
|
| 930 |
+
{
|
| 931 |
+
"check": "has_tool=analyze_descriptive",
|
| 932 |
+
"ok": true,
|
| 933 |
+
"detail": "tools=['analyze_descriptive', 'check_data', 'retrieve_data']"
|
| 934 |
+
}
|
| 935 |
+
],
|
| 936 |
+
"facts": {
|
| 937 |
+
"tools": [
|
| 938 |
+
"analyze_descriptive",
|
| 939 |
+
"check_data",
|
| 940 |
+
"retrieve_data"
|
| 941 |
+
],
|
| 942 |
+
"irs": [
|
| 943 |
+
{
|
| 944 |
+
"source_id": "src_pa",
|
| 945 |
+
"table_id": "t_pa",
|
| 946 |
+
"select": [
|
| 947 |
+
{
|
| 948 |
+
"kind": "column",
|
| 949 |
+
"column_id": "c_mttr",
|
| 950 |
+
"alias": "mttr"
|
| 951 |
+
}
|
| 952 |
+
],
|
| 953 |
+
"filters": [
|
| 954 |
+
{
|
| 955 |
+
"column_id": "c_month_id",
|
| 956 |
+
"op": "=",
|
| 957 |
+
"value": 202604,
|
| 958 |
+
"value_type": "int"
|
| 959 |
+
},
|
| 960 |
+
{
|
| 961 |
+
"column_id": "c_isdeleted",
|
| 962 |
+
"op": "=",
|
| 963 |
+
"value": 0,
|
| 964 |
+
"value_type": "int"
|
| 965 |
+
}
|
| 966 |
+
],
|
| 967 |
+
"limit": 10000
|
| 968 |
+
}
|
| 969 |
+
],
|
| 970 |
+
"agg_args": [],
|
| 971 |
+
"infeasible": false
|
| 972 |
+
},
|
| 973 |
+
"error": null,
|
| 974 |
+
"latency_ms": 2895,
|
| 975 |
+
"tokens": 14767
|
| 976 |
+
},
|
| 977 |
+
{
|
| 978 |
+
"id": "desc_pa_stats_en",
|
| 979 |
+
"category": "descriptive",
|
| 980 |
+
"lang": "en",
|
| 981 |
+
"carried_over": true,
|
| 982 |
+
"question": "give me the summary statistics for PA_Percent",
|
| 983 |
+
"passed": true,
|
| 984 |
+
"checks": [
|
| 985 |
+
{
|
| 986 |
+
"check": "has_tool=analyze_descriptive",
|
| 987 |
+
"ok": true,
|
| 988 |
+
"detail": "tools=['analyze_descriptive', 'check_data', 'retrieve_data']"
|
| 989 |
+
}
|
| 990 |
+
],
|
| 991 |
+
"facts": {
|
| 992 |
+
"tools": [
|
| 993 |
+
"analyze_descriptive",
|
| 994 |
+
"check_data",
|
| 995 |
+
"retrieve_data"
|
| 996 |
+
],
|
| 997 |
+
"irs": [
|
| 998 |
+
{
|
| 999 |
+
"source_id": "src_pa",
|
| 1000 |
+
"table_id": "t_pa",
|
| 1001 |
+
"select": [
|
| 1002 |
+
{
|
| 1003 |
+
"kind": "column",
|
| 1004 |
+
"column_id": "c_pa_percent",
|
| 1005 |
+
"alias": "pa_percent"
|
| 1006 |
+
}
|
| 1007 |
+
],
|
| 1008 |
+
"limit": 10000
|
| 1009 |
+
}
|
| 1010 |
+
],
|
| 1011 |
+
"agg_args": [],
|
| 1012 |
+
"infeasible": false
|
| 1013 |
+
},
|
| 1014 |
+
"error": null,
|
| 1015 |
+
"latency_ms": 2850,
|
| 1016 |
+
"tokens": 14703
|
| 1017 |
+
},
|
| 1018 |
+
{
|
| 1019 |
+
"id": "corr_mttr_pa_id",
|
| 1020 |
+
"category": "correlation",
|
| 1021 |
+
"lang": "id",
|
| 1022 |
+
"carried_over": true,
|
| 1023 |
+
"question": "apakah ada korelasi antara MTTR dan PA?",
|
| 1024 |
+
"passed": true,
|
| 1025 |
+
"checks": [
|
| 1026 |
+
{
|
| 1027 |
+
"check": "has_tool=analyze_correlation",
|
| 1028 |
+
"ok": true,
|
| 1029 |
+
"detail": "tools=['analyze_correlation', 'check_data', 'retrieve_data']"
|
| 1030 |
+
}
|
| 1031 |
+
],
|
| 1032 |
+
"facts": {
|
| 1033 |
+
"tools": [
|
| 1034 |
+
"analyze_correlation",
|
| 1035 |
+
"check_data",
|
| 1036 |
+
"retrieve_data"
|
| 1037 |
+
],
|
| 1038 |
+
"irs": [
|
| 1039 |
+
{
|
| 1040 |
+
"source_id": "src_pa",
|
| 1041 |
+
"table_id": "t_pa",
|
| 1042 |
+
"select": [
|
| 1043 |
+
{
|
| 1044 |
+
"kind": "column",
|
| 1045 |
+
"column_id": "c_mttr",
|
| 1046 |
+
"alias": "mttr"
|
| 1047 |
+
},
|
| 1048 |
+
{
|
| 1049 |
+
"kind": "column",
|
| 1050 |
+
"column_id": "c_pa_percent",
|
| 1051 |
+
"alias": "pa_percent"
|
| 1052 |
+
}
|
| 1053 |
+
],
|
| 1054 |
+
"filters": [
|
| 1055 |
+
{
|
| 1056 |
+
"column_id": "c_month_id",
|
| 1057 |
+
"op": "=",
|
| 1058 |
+
"value": 202604,
|
| 1059 |
+
"value_type": "int"
|
| 1060 |
+
},
|
| 1061 |
+
{
|
| 1062 |
+
"column_id": "c_isdeleted",
|
| 1063 |
+
"op": "=",
|
| 1064 |
+
"value": 0,
|
| 1065 |
+
"value_type": "int"
|
| 1066 |
+
}
|
| 1067 |
+
],
|
| 1068 |
+
"limit": 10000
|
| 1069 |
+
}
|
| 1070 |
+
],
|
| 1071 |
+
"agg_args": [],
|
| 1072 |
+
"infeasible": false
|
| 1073 |
+
},
|
| 1074 |
+
"error": null,
|
| 1075 |
+
"latency_ms": 3665,
|
| 1076 |
+
"tokens": 14850
|
| 1077 |
+
},
|
| 1078 |
+
{
|
| 1079 |
+
"id": "corr_freq_pa_en",
|
| 1080 |
+
"category": "correlation",
|
| 1081 |
+
"lang": "en",
|
| 1082 |
+
"carried_over": true,
|
| 1083 |
+
"question": "is breakdown frequency correlated with availability?",
|
| 1084 |
+
"passed": true,
|
| 1085 |
+
"checks": [
|
| 1086 |
+
{
|
| 1087 |
+
"check": "has_tool=analyze_correlation",
|
| 1088 |
+
"ok": true,
|
| 1089 |
+
"detail": "tools=['analyze_correlation', 'check_data', 'retrieve_data']"
|
| 1090 |
+
}
|
| 1091 |
+
],
|
| 1092 |
+
"facts": {
|
| 1093 |
+
"tools": [
|
| 1094 |
+
"analyze_correlation",
|
| 1095 |
+
"check_data",
|
| 1096 |
+
"retrieve_data"
|
| 1097 |
+
],
|
| 1098 |
+
"irs": [
|
| 1099 |
+
{
|
| 1100 |
+
"source_id": "src_pa",
|
| 1101 |
+
"table_id": "t_pa",
|
| 1102 |
+
"select": [
|
| 1103 |
+
{
|
| 1104 |
+
"kind": "column",
|
| 1105 |
+
"column_id": "c_total_frequency_breakdown_schedule",
|
| 1106 |
+
"alias": "breakdown_freq_schedule"
|
| 1107 |
+
},
|
| 1108 |
+
{
|
| 1109 |
+
"kind": "column",
|
| 1110 |
+
"column_id": "c_total_frequency_breakdown_unschedule",
|
| 1111 |
+
"alias": "breakdown_freq_unschedule"
|
| 1112 |
+
},
|
| 1113 |
+
{
|
| 1114 |
+
"kind": "column",
|
| 1115 |
+
"column_id": "c_pa_percent",
|
| 1116 |
+
"alias": "pa_percent"
|
| 1117 |
+
}
|
| 1118 |
+
],
|
| 1119 |
+
"filters": [
|
| 1120 |
+
{
|
| 1121 |
+
"column_id": "c_isdeleted",
|
| 1122 |
+
"op": "=",
|
| 1123 |
+
"value": 0,
|
| 1124 |
+
"value_type": "int"
|
| 1125 |
+
}
|
| 1126 |
+
],
|
| 1127 |
+
"limit": 10000
|
| 1128 |
+
}
|
| 1129 |
+
],
|
| 1130 |
+
"agg_args": [],
|
| 1131 |
+
"infeasible": false
|
| 1132 |
+
},
|
| 1133 |
+
"error": null,
|
| 1134 |
+
"latency_ms": 3299,
|
| 1135 |
+
"tokens": 14859
|
| 1136 |
+
},
|
| 1137 |
+
{
|
| 1138 |
+
"id": "trend_pa_daily_id",
|
| 1139 |
+
"category": "trend",
|
| 1140 |
+
"lang": "id",
|
| 1141 |
+
"carried_over": true,
|
| 1142 |
+
"question": "bagaimana trend PA harian?",
|
| 1143 |
+
"passed": true,
|
| 1144 |
+
"checks": [
|
| 1145 |
+
{
|
| 1146 |
+
"check": "has_tool=analyze_trend",
|
| 1147 |
+
"ok": true,
|
| 1148 |
+
"detail": "tools=['analyze_trend', 'check_data', 'retrieve_data']"
|
| 1149 |
+
}
|
| 1150 |
+
],
|
| 1151 |
+
"facts": {
|
| 1152 |
+
"tools": [
|
| 1153 |
+
"analyze_trend",
|
| 1154 |
+
"check_data",
|
| 1155 |
+
"retrieve_data"
|
| 1156 |
+
],
|
| 1157 |
+
"irs": [
|
| 1158 |
+
{
|
| 1159 |
+
"source_id": "src_pa",
|
| 1160 |
+
"table_id": "t_pa",
|
| 1161 |
+
"select": [
|
| 1162 |
+
{
|
| 1163 |
+
"kind": "column",
|
| 1164 |
+
"column_id": "c_from_date",
|
| 1165 |
+
"alias": "date"
|
| 1166 |
+
},
|
| 1167 |
+
{
|
| 1168 |
+
"kind": "column",
|
| 1169 |
+
"column_id": "c_pa_percent",
|
| 1170 |
+
"alias": "pa_percent"
|
| 1171 |
+
}
|
| 1172 |
+
],
|
| 1173 |
+
"filters": [
|
| 1174 |
+
{
|
| 1175 |
+
"column_id": "c_month_id",
|
| 1176 |
+
"op": "=",
|
| 1177 |
+
"value": 202604,
|
| 1178 |
+
"value_type": "int"
|
| 1179 |
+
},
|
| 1180 |
+
{
|
| 1181 |
+
"column_id": "c_isdeleted",
|
| 1182 |
+
"op": "=",
|
| 1183 |
+
"value": 0,
|
| 1184 |
+
"value_type": "int"
|
| 1185 |
+
}
|
| 1186 |
+
],
|
| 1187 |
+
"limit": 10000
|
| 1188 |
+
}
|
| 1189 |
+
],
|
| 1190 |
+
"agg_args": [],
|
| 1191 |
+
"infeasible": false
|
| 1192 |
+
},
|
| 1193 |
+
"error": null,
|
| 1194 |
+
"latency_ms": 3472,
|
| 1195 |
+
"tokens": 14861
|
| 1196 |
+
},
|
| 1197 |
+
{
|
| 1198 |
+
"id": "trend_downhour_en",
|
| 1199 |
+
"category": "trend",
|
| 1200 |
+
"lang": "en",
|
| 1201 |
+
"carried_over": true,
|
| 1202 |
+
"question": "show the trend of total down hours over time",
|
| 1203 |
+
"passed": true,
|
| 1204 |
+
"checks": [
|
| 1205 |
+
{
|
| 1206 |
+
"check": "has_tool=analyze_trend",
|
| 1207 |
+
"ok": true,
|
| 1208 |
+
"detail": "tools=['analyze_trend', 'check_data', 'retrieve_data']"
|
| 1209 |
+
}
|
| 1210 |
+
],
|
| 1211 |
+
"facts": {
|
| 1212 |
+
"tools": [
|
| 1213 |
+
"analyze_trend",
|
| 1214 |
+
"check_data",
|
| 1215 |
+
"retrieve_data"
|
| 1216 |
+
],
|
| 1217 |
+
"irs": [
|
| 1218 |
+
{
|
| 1219 |
+
"source_id": "src_pa",
|
| 1220 |
+
"table_id": "t_pa",
|
| 1221 |
+
"select": [
|
| 1222 |
+
{
|
| 1223 |
+
"kind": "column",
|
| 1224 |
+
"column_id": "c_from_date",
|
| 1225 |
+
"alias": "date"
|
| 1226 |
+
},
|
| 1227 |
+
{
|
| 1228 |
+
"kind": "column",
|
| 1229 |
+
"column_id": "c_total_down_hour",
|
| 1230 |
+
"alias": "total_down_hours"
|
| 1231 |
+
}
|
| 1232 |
+
],
|
| 1233 |
+
"filters": [
|
| 1234 |
+
{
|
| 1235 |
+
"column_id": "c_month_id",
|
| 1236 |
+
"op": "=",
|
| 1237 |
+
"value": 202604,
|
| 1238 |
+
"value_type": "int"
|
| 1239 |
+
}
|
| 1240 |
+
],
|
| 1241 |
+
"limit": 10000
|
| 1242 |
+
}
|
| 1243 |
+
],
|
| 1244 |
+
"agg_args": [],
|
| 1245 |
+
"infeasible": false
|
| 1246 |
+
},
|
| 1247 |
+
"error": null,
|
| 1248 |
+
"latency_ms": 3703,
|
| 1249 |
+
"tokens": 14825
|
| 1250 |
+
},
|
| 1251 |
+
{
|
| 1252 |
+
"id": "merge_worst_pa_and_mttr_id",
|
| 1253 |
+
"category": "merge",
|
| 1254 |
+
"lang": "id",
|
| 1255 |
+
"carried_over": true,
|
| 1256 |
+
"question": "model mana yang PA-nya paling buruk sekaligus MTTR-nya paling tinggi?",
|
| 1257 |
+
"passed": true,
|
| 1258 |
+
"checks": [
|
| 1259 |
+
{
|
| 1260 |
+
"check": "group_by",
|
| 1261 |
+
"ok": true,
|
| 1262 |
+
"detail": "grouped=True"
|
| 1263 |
+
},
|
| 1264 |
+
{
|
| 1265 |
+
"check": "group_by_col=Model_Unit",
|
| 1266 |
+
"ok": true,
|
| 1267 |
+
"detail": "ids=['c_model_unit', 'c_model_unit'] aliases=[]"
|
| 1268 |
+
}
|
| 1269 |
+
],
|
| 1270 |
+
"facts": {
|
| 1271 |
+
"tools": [
|
| 1272 |
+
"analyze_merge",
|
| 1273 |
+
"check_data",
|
| 1274 |
+
"retrieve_data"
|
| 1275 |
+
],
|
| 1276 |
+
"irs": [
|
| 1277 |
+
{
|
| 1278 |
+
"source_id": "src_pa",
|
| 1279 |
+
"table_id": "t_pa",
|
| 1280 |
+
"select": [
|
| 1281 |
+
{
|
| 1282 |
+
"kind": "column",
|
| 1283 |
+
"column_id": "c_model_unit",
|
| 1284 |
+
"alias": "model"
|
| 1285 |
+
},
|
| 1286 |
+
{
|
| 1287 |
+
"kind": "agg",
|
| 1288 |
+
"fn": "avg",
|
| 1289 |
+
"column_id": "c_pa_percent",
|
| 1290 |
+
"alias": "avg_pa"
|
| 1291 |
+
}
|
| 1292 |
+
],
|
| 1293 |
+
"group_by": [
|
| 1294 |
+
"c_model_unit"
|
| 1295 |
+
],
|
| 1296 |
+
"order_by": [
|
| 1297 |
+
{
|
| 1298 |
+
"column_id": "avg_pa",
|
| 1299 |
+
"dir": "asc"
|
| 1300 |
+
}
|
| 1301 |
+
],
|
| 1302 |
+
"limit": 1
|
| 1303 |
+
},
|
| 1304 |
+
{
|
| 1305 |
+
"source_id": "src_pa",
|
| 1306 |
+
"table_id": "t_pa",
|
| 1307 |
+
"select": [
|
| 1308 |
+
{
|
| 1309 |
+
"kind": "column",
|
| 1310 |
+
"column_id": "c_model_unit",
|
| 1311 |
+
"alias": "model"
|
| 1312 |
+
},
|
| 1313 |
+
{
|
| 1314 |
+
"kind": "agg",
|
| 1315 |
+
"fn": "avg",
|
| 1316 |
+
"column_id": "c_mttr",
|
| 1317 |
+
"alias": "avg_mttr"
|
| 1318 |
+
}
|
| 1319 |
+
],
|
| 1320 |
+
"group_by": [
|
| 1321 |
+
"c_model_unit"
|
| 1322 |
+
],
|
| 1323 |
+
"order_by": [
|
| 1324 |
+
{
|
| 1325 |
+
"column_id": "avg_mttr",
|
| 1326 |
+
"dir": "desc"
|
| 1327 |
+
}
|
| 1328 |
+
],
|
| 1329 |
+
"limit": 1
|
| 1330 |
+
}
|
| 1331 |
+
],
|
| 1332 |
+
"agg_args": [],
|
| 1333 |
+
"infeasible": false
|
| 1334 |
+
},
|
| 1335 |
+
"error": null,
|
| 1336 |
+
"latency_ms": 4058,
|
| 1337 |
+
"tokens": 15042
|
| 1338 |
+
},
|
| 1339 |
+
{
|
| 1340 |
+
"id": "raw_rows_low_pa_id",
|
| 1341 |
+
"category": "counter_raw_rows",
|
| 1342 |
+
"lang": "id",
|
| 1343 |
+
"carried_over": true,
|
| 1344 |
+
"question": "tampilkan 10 record dengan PA di bawah 50",
|
| 1345 |
+
"passed": true,
|
| 1346 |
+
"checks": [
|
| 1347 |
+
{
|
| 1348 |
+
"check": "no_group_by",
|
| 1349 |
+
"ok": true,
|
| 1350 |
+
"detail": "grouped=False"
|
| 1351 |
+
},
|
| 1352 |
+
{
|
| 1353 |
+
"check": "has_filter",
|
| 1354 |
+
"ok": true,
|
| 1355 |
+
"detail": "filter_present=True"
|
| 1356 |
+
},
|
| 1357 |
+
{
|
| 1358 |
+
"check": "limit=10",
|
| 1359 |
+
"ok": true,
|
| 1360 |
+
"detail": "limits=[10]"
|
| 1361 |
+
}
|
| 1362 |
+
],
|
| 1363 |
+
"facts": {
|
| 1364 |
+
"tools": [
|
| 1365 |
+
"retrieve_data"
|
| 1366 |
+
],
|
| 1367 |
+
"irs": [
|
| 1368 |
+
{
|
| 1369 |
+
"source_id": "src_pa",
|
| 1370 |
+
"table_id": "t_pa",
|
| 1371 |
+
"select": [
|
| 1372 |
+
{
|
| 1373 |
+
"kind": "column",
|
| 1374 |
+
"column_id": "c_keyid",
|
| 1375 |
+
"alias": "key_id"
|
| 1376 |
+
},
|
| 1377 |
+
{
|
| 1378 |
+
"kind": "column",
|
| 1379 |
+
"column_id": "c_month_id",
|
| 1380 |
+
"alias": "month_id"
|
| 1381 |
+
},
|
| 1382 |
+
{
|
| 1383 |
+
"kind": "column",
|
| 1384 |
+
"column_id": "c_site_id",
|
| 1385 |
+
"alias": "site_id"
|
| 1386 |
+
},
|
| 1387 |
+
{
|
| 1388 |
+
"kind": "column",
|
| 1389 |
+
"column_id": "c_from_date",
|
| 1390 |
+
"alias": "from_date"
|
| 1391 |
+
},
|
| 1392 |
+
{
|
| 1393 |
+
"kind": "column",
|
| 1394 |
+
"column_id": "c_to_date",
|
| 1395 |
+
"alias": "to_date"
|
| 1396 |
+
},
|
| 1397 |
+
{
|
| 1398 |
+
"kind": "column",
|
| 1399 |
+
"column_id": "c_time_description",
|
| 1400 |
+
"alias": "time_description"
|
| 1401 |
+
},
|
| 1402 |
+
{
|
| 1403 |
+
"kind": "column",
|
| 1404 |
+
"column_id": "c_model_unit",
|
| 1405 |
+
"alias": "model_unit"
|
| 1406 |
+
},
|
| 1407 |
+
{
|
| 1408 |
+
"kind": "column",
|
| 1409 |
+
"column_id": "c_equipment_number",
|
| 1410 |
+
"alias": "equipment_number"
|
| 1411 |
+
},
|
| 1412 |
+
{
|
| 1413 |
+
"kind": "column",
|
| 1414 |
+
"column_id": "c_equipment_group_id",
|
| 1415 |
+
"alias": "equipment_group_id"
|
| 1416 |
+
},
|
| 1417 |
+
{
|
| 1418 |
+
"kind": "column",
|
| 1419 |
+
"column_id": "c_unit_status",
|
| 1420 |
+
"alias": "unit_status"
|
| 1421 |
+
},
|
| 1422 |
+
{
|
| 1423 |
+
"kind": "column",
|
| 1424 |
+
"column_id": "c_pa_percent",
|
| 1425 |
+
"alias": "pa_percent"
|
| 1426 |
+
}
|
| 1427 |
+
],
|
| 1428 |
+
"filters": [
|
| 1429 |
+
{
|
| 1430 |
+
"column_id": "c_pa_percent",
|
| 1431 |
+
"op": "<",
|
| 1432 |
+
"value": 50,
|
| 1433 |
+
"value_type": "decimal"
|
| 1434 |
+
}
|
| 1435 |
+
],
|
| 1436 |
+
"order_by": [
|
| 1437 |
+
{
|
| 1438 |
+
"column_id": "pa_percent",
|
| 1439 |
+
"dir": "asc"
|
| 1440 |
+
}
|
| 1441 |
+
],
|
| 1442 |
+
"limit": 10
|
| 1443 |
+
}
|
| 1444 |
+
],
|
| 1445 |
+
"agg_args": [],
|
| 1446 |
+
"infeasible": false
|
| 1447 |
+
},
|
| 1448 |
+
"error": null,
|
| 1449 |
+
"latency_ms": 3493,
|
| 1450 |
+
"tokens": 14790
|
| 1451 |
+
},
|
| 1452 |
+
{
|
| 1453 |
+
"id": "raw_rows_head_en",
|
| 1454 |
+
"category": "counter_raw_rows",
|
| 1455 |
+
"lang": "en",
|
| 1456 |
+
"carried_over": true,
|
| 1457 |
+
"question": "show me the first 5 rows of the data",
|
| 1458 |
+
"passed": true,
|
| 1459 |
+
"checks": [
|
| 1460 |
+
{
|
| 1461 |
+
"check": "no_group_by",
|
| 1462 |
+
"ok": true,
|
| 1463 |
+
"detail": "grouped=False"
|
| 1464 |
+
}
|
| 1465 |
+
],
|
| 1466 |
+
"facts": {
|
| 1467 |
+
"tools": [
|
| 1468 |
+
"check_data",
|
| 1469 |
+
"retrieve_data"
|
| 1470 |
+
],
|
| 1471 |
+
"irs": [
|
| 1472 |
+
{
|
| 1473 |
+
"source_id": "src_pa",
|
| 1474 |
+
"table_id": "t_pa",
|
| 1475 |
+
"select": [
|
| 1476 |
+
{
|
| 1477 |
+
"kind": "column",
|
| 1478 |
+
"column_id": "c_keyid",
|
| 1479 |
+
"alias": "KeyId"
|
| 1480 |
+
},
|
| 1481 |
+
{
|
| 1482 |
+
"kind": "column",
|
| 1483 |
+
"column_id": "c_month_id",
|
| 1484 |
+
"alias": "Month_ID"
|
| 1485 |
+
},
|
| 1486 |
+
{
|
| 1487 |
+
"kind": "column",
|
| 1488 |
+
"column_id": "c_site_id",
|
| 1489 |
+
"alias": "Site_ID"
|
| 1490 |
+
},
|
| 1491 |
+
{
|
| 1492 |
+
"kind": "column",
|
| 1493 |
+
"column_id": "c_from_date",
|
| 1494 |
+
"alias": "From_Date"
|
| 1495 |
+
},
|
| 1496 |
+
{
|
| 1497 |
+
"kind": "column",
|
| 1498 |
+
"column_id": "c_to_date",
|
| 1499 |
+
"alias": "To_Date"
|
| 1500 |
+
},
|
| 1501 |
+
{
|
| 1502 |
+
"kind": "column",
|
| 1503 |
+
"column_id": "c_time_description",
|
| 1504 |
+
"alias": "Time_Description"
|
| 1505 |
+
},
|
| 1506 |
+
{
|
| 1507 |
+
"kind": "column",
|
| 1508 |
+
"column_id": "c_model_unit",
|
| 1509 |
+
"alias": "Model_Unit"
|
| 1510 |
+
},
|
| 1511 |
+
{
|
| 1512 |
+
"kind": "column",
|
| 1513 |
+
"column_id": "c_equipment_number",
|
| 1514 |
+
"alias": "Equipment_Number"
|
| 1515 |
+
},
|
| 1516 |
+
{
|
| 1517 |
+
"kind": "column",
|
| 1518 |
+
"column_id": "c_equipment_group_id",
|
| 1519 |
+
"alias": "Equipment_Group_ID"
|
| 1520 |
+
},
|
| 1521 |
+
{
|
| 1522 |
+
"kind": "column",
|
| 1523 |
+
"column_id": "c_unit_status",
|
| 1524 |
+
"alias": "Unit_Status"
|
| 1525 |
+
},
|
| 1526 |
+
{
|
| 1527 |
+
"kind": "column",
|
| 1528 |
+
"column_id": "c_total_breakdown_schedule_hour",
|
| 1529 |
+
"alias": "Total_Breakdown_Schedule_Hour"
|
| 1530 |
+
},
|
| 1531 |
+
{
|
| 1532 |
+
"kind": "column",
|
| 1533 |
+
"column_id": "c_total_breakdown_unschedule_hour",
|
| 1534 |
+
"alias": "Total_Breakdown_Unschedule_Hour"
|
| 1535 |
+
},
|
| 1536 |
+
{
|
| 1537 |
+
"kind": "column",
|
| 1538 |
+
"column_id": "c_total_adj_breakdown_schedule_hour",
|
| 1539 |
+
"alias": "Total_Adj_Breakdown_Schedule_Hour"
|
| 1540 |
+
},
|
| 1541 |
+
{
|
| 1542 |
+
"kind": "column",
|
| 1543 |
+
"column_id": "c_total_adj_breakdown_unschedule_hour",
|
| 1544 |
+
"alias": "Total_Adj_Breakdown_Unschedule_Hour"
|
| 1545 |
+
},
|
| 1546 |
+
{
|
| 1547 |
+
"kind": "column",
|
| 1548 |
+
"column_id": "c_total_mtc_hour",
|
| 1549 |
+
"alias": "Total_MTC_Hour"
|
| 1550 |
+
},
|
| 1551 |
+
{
|
| 1552 |
+
"kind": "column",
|
| 1553 |
+
"column_id": "c_total_down_hour",
|
| 1554 |
+
"alias": "Total_Down_Hour"
|
| 1555 |
+
},
|
| 1556 |
+
{
|
| 1557 |
+
"kind": "column",
|
| 1558 |
+
"column_id": "c_total_frequency_breakdown_schedule",
|
| 1559 |
+
"alias": "Total_Frequency_Breakdown_Schedule"
|
| 1560 |
+
},
|
| 1561 |
+
{
|
| 1562 |
+
"kind": "column",
|
| 1563 |
+
"column_id": "c_total_frequency_breakdown_unschedule",
|
| 1564 |
+
"alias": "Total_Frequency_Breakdown_Unschedule"
|
| 1565 |
+
},
|
| 1566 |
+
{
|
| 1567 |
+
"kind": "column",
|
| 1568 |
+
"column_id": "c_total_frequency_maintenance",
|
| 1569 |
+
"alias": "Total_Frequency_Maintenance"
|
| 1570 |
+
},
|
| 1571 |
+
{
|
| 1572 |
+
"kind": "column",
|
| 1573 |
+
"column_id": "c_total_frequency_tire",
|
| 1574 |
+
"alias": "Total_Frequency_Tire"
|
| 1575 |
+
},
|
| 1576 |
+
{
|
| 1577 |
+
"kind": "column",
|
| 1578 |
+
"column_id": "c_total_frequency_down",
|
| 1579 |
+
"alias": "Total_Frequency_Down"
|
| 1580 |
+
},
|
| 1581 |
+
{
|
| 1582 |
+
"kind": "column",
|
| 1583 |
+
"column_id": "c_total_hours",
|
| 1584 |
+
"alias": "Total_Hours"
|
| 1585 |
+
},
|
| 1586 |
+
{
|
| 1587 |
+
"kind": "column",
|
| 1588 |
+
"column_id": "c_total_inpr_hour",
|
| 1589 |
+
"alias": "Total_INPR_Hour"
|
| 1590 |
+
},
|
| 1591 |
+
{
|
| 1592 |
+
"kind": "column",
|
| 1593 |
+
"column_id": "c_total_record_hm_hour",
|
| 1594 |
+
"alias": "Total_Record_HM_Hour"
|
| 1595 |
+
},
|
| 1596 |
+
{
|
| 1597 |
+
"kind": "column",
|
| 1598 |
+
"column_id": "c_total_hm_mtc_down_hour",
|
| 1599 |
+
"alias": "Total_HM_Mtc_Down_Hour"
|
| 1600 |
+
},
|
| 1601 |
+
{
|
| 1602 |
+
"kind": "column",
|
| 1603 |
+
"column_id": "c_plan_pa_percent",
|
| 1604 |
+
"alias": "Plan_PA_Percent"
|
| 1605 |
+
},
|
| 1606 |
+
{
|
| 1607 |
+
"kind": "column",
|
| 1608 |
+
"column_id": "c_pa_percent",
|
| 1609 |
+
"alias": "PA_Percent"
|
| 1610 |
+
},
|
| 1611 |
+
{
|
| 1612 |
+
"kind": "column",
|
| 1613 |
+
"column_id": "c_mtbs",
|
| 1614 |
+
"alias": "MTBS"
|
| 1615 |
+
},
|
| 1616 |
+
{
|
| 1617 |
+
"kind": "column",
|
| 1618 |
+
"column_id": "c_mttr",
|
| 1619 |
+
"alias": "MTTR"
|
| 1620 |
+
},
|
| 1621 |
+
{
|
| 1622 |
+
"kind": "column",
|
| 1623 |
+
"column_id": "c_sm_percent",
|
| 1624 |
+
"alias": "SM_Percent"
|
| 1625 |
+
},
|
| 1626 |
+
{
|
| 1627 |
+
"kind": "column",
|
| 1628 |
+
"column_id": "c_unschedule_sm_percent",
|
| 1629 |
+
"alias": "Unschedule_SM_Percent"
|
| 1630 |
+
},
|
| 1631 |
+
{
|
| 1632 |
+
"kind": "column",
|
| 1633 |
+
"column_id": "c_isdeleted",
|
| 1634 |
+
"alias": "IsDeleted"
|
| 1635 |
+
},
|
| 1636 |
+
{
|
| 1637 |
+
"kind": "column",
|
| 1638 |
+
"column_id": "c_section",
|
| 1639 |
+
"alias": "Section"
|
| 1640 |
+
},
|
| 1641 |
+
{
|
| 1642 |
+
"kind": "column",
|
| 1643 |
+
"column_id": "c_week_id",
|
| 1644 |
+
"alias": "Week_ID"
|
| 1645 |
+
},
|
| 1646 |
+
{
|
| 1647 |
+
"kind": "column",
|
| 1648 |
+
"column_id": "c_plan_pa_percent_2",
|
| 1649 |
+
"alias": "Plan_PA_Percent_2"
|
| 1650 |
+
},
|
| 1651 |
+
{
|
| 1652 |
+
"kind": "column",
|
| 1653 |
+
"column_id": "c_updated_date",
|
| 1654 |
+
"alias": "Updated_Date"
|
| 1655 |
+
}
|
| 1656 |
+
],
|
| 1657 |
+
"limit": 5
|
| 1658 |
+
}
|
| 1659 |
+
],
|
| 1660 |
+
"agg_args": [],
|
| 1661 |
+
"infeasible": false
|
| 1662 |
+
},
|
| 1663 |
+
"error": null,
|
| 1664 |
+
"latency_ms": 4440,
|
| 1665 |
+
"tokens": 15314
|
| 1666 |
+
},
|
| 1667 |
+
{
|
| 1668 |
+
"id": "exact_model_777d_id",
|
| 1669 |
+
"category": "counter_exact_filter",
|
| 1670 |
+
"lang": "id",
|
| 1671 |
+
"carried_over": true,
|
| 1672 |
+
"question": "berapa banyak record untuk model 777D?",
|
| 1673 |
+
"passed": true,
|
| 1674 |
+
"checks": [
|
| 1675 |
+
{
|
| 1676 |
+
"check": "select_agg=count",
|
| 1677 |
+
"ok": true,
|
| 1678 |
+
"detail": "aggs=['count']"
|
| 1679 |
+
},
|
| 1680 |
+
{
|
| 1681 |
+
"check": "has_filter",
|
| 1682 |
+
"ok": true,
|
| 1683 |
+
"detail": "filter_present=True"
|
| 1684 |
+
}
|
| 1685 |
+
],
|
| 1686 |
+
"facts": {
|
| 1687 |
+
"tools": [
|
| 1688 |
+
"retrieve_data"
|
| 1689 |
+
],
|
| 1690 |
+
"irs": [
|
| 1691 |
+
{
|
| 1692 |
+
"source_id": "src_pa",
|
| 1693 |
+
"table_id": "t_pa",
|
| 1694 |
+
"select": [
|
| 1695 |
+
{
|
| 1696 |
+
"kind": "agg",
|
| 1697 |
+
"fn": "count",
|
| 1698 |
+
"alias": "record_count"
|
| 1699 |
+
}
|
| 1700 |
+
],
|
| 1701 |
+
"filters": [
|
| 1702 |
+
{
|
| 1703 |
+
"column_id": "c_model_unit",
|
| 1704 |
+
"op": "=",
|
| 1705 |
+
"value": "777D",
|
| 1706 |
+
"value_type": "string"
|
| 1707 |
+
}
|
| 1708 |
+
]
|
| 1709 |
+
}
|
| 1710 |
+
],
|
| 1711 |
+
"agg_args": [],
|
| 1712 |
+
"infeasible": false
|
| 1713 |
+
},
|
| 1714 |
+
"error": null,
|
| 1715 |
+
"latency_ms": 2035,
|
| 1716 |
+
"tokens": 14580
|
| 1717 |
+
},
|
| 1718 |
+
{
|
| 1719 |
+
"id": "exact_section_loader_en",
|
| 1720 |
+
"category": "counter_exact_filter",
|
| 1721 |
+
"lang": "en",
|
| 1722 |
+
"carried_over": true,
|
| 1723 |
+
"question": "how many records are in the OB LOADER section?",
|
| 1724 |
+
"passed": true,
|
| 1725 |
+
"checks": [
|
| 1726 |
+
{
|
| 1727 |
+
"check": "select_agg=count",
|
| 1728 |
+
"ok": true,
|
| 1729 |
+
"detail": "aggs=['count']"
|
| 1730 |
+
},
|
| 1731 |
+
{
|
| 1732 |
+
"check": "has_filter",
|
| 1733 |
+
"ok": true,
|
| 1734 |
+
"detail": "filter_present=True"
|
| 1735 |
+
}
|
| 1736 |
+
],
|
| 1737 |
+
"facts": {
|
| 1738 |
+
"tools": [
|
| 1739 |
+
"retrieve_data"
|
| 1740 |
+
],
|
| 1741 |
+
"irs": [
|
| 1742 |
+
{
|
| 1743 |
+
"source_id": "src_pa",
|
| 1744 |
+
"table_id": "t_pa",
|
| 1745 |
+
"select": [
|
| 1746 |
+
{
|
| 1747 |
+
"kind": "agg",
|
| 1748 |
+
"fn": "count",
|
| 1749 |
+
"alias": "record_count"
|
| 1750 |
+
}
|
| 1751 |
+
],
|
| 1752 |
+
"filters": [
|
| 1753 |
+
{
|
| 1754 |
+
"column_id": "c_section",
|
| 1755 |
+
"op": "=",
|
| 1756 |
+
"value": "OB LOADER",
|
| 1757 |
+
"value_type": "string"
|
| 1758 |
+
}
|
| 1759 |
+
]
|
| 1760 |
+
}
|
| 1761 |
+
],
|
| 1762 |
+
"agg_args": [],
|
| 1763 |
+
"infeasible": false
|
| 1764 |
+
},
|
| 1765 |
+
"error": null,
|
| 1766 |
+
"latency_ms": 2133,
|
| 1767 |
+
"tokens": 14570
|
| 1768 |
+
},
|
| 1769 |
+
{
|
| 1770 |
+
"id": "infeasible_churn_id",
|
| 1771 |
+
"category": "infeasible",
|
| 1772 |
+
"lang": "id",
|
| 1773 |
+
"carried_over": true,
|
| 1774 |
+
"question": "berapa churn rate pelanggan?",
|
| 1775 |
+
"passed": true,
|
| 1776 |
+
"checks": [
|
| 1777 |
+
{
|
| 1778 |
+
"check": "infeasible",
|
| 1779 |
+
"ok": true,
|
| 1780 |
+
"detail": "infeasible=True"
|
| 1781 |
+
}
|
| 1782 |
+
],
|
| 1783 |
+
"facts": {
|
| 1784 |
+
"tools": [],
|
| 1785 |
+
"irs": [],
|
| 1786 |
+
"agg_args": [],
|
| 1787 |
+
"infeasible": true
|
| 1788 |
+
},
|
| 1789 |
+
"error": null,
|
| 1790 |
+
"latency_ms": 2586,
|
| 1791 |
+
"tokens": 14512
|
| 1792 |
+
},
|
| 1793 |
+
{
|
| 1794 |
+
"id": "infeasible_profit_en",
|
| 1795 |
+
"category": "infeasible",
|
| 1796 |
+
"lang": "en",
|
| 1797 |
+
"carried_over": true,
|
| 1798 |
+
"question": "what is the monthly profit margin?",
|
| 1799 |
+
"passed": true,
|
| 1800 |
+
"checks": [
|
| 1801 |
+
{
|
| 1802 |
+
"check": "infeasible",
|
| 1803 |
+
"ok": true,
|
| 1804 |
+
"detail": "infeasible=True"
|
| 1805 |
+
}
|
| 1806 |
+
],
|
| 1807 |
+
"facts": {
|
| 1808 |
+
"tools": [],
|
| 1809 |
+
"irs": [],
|
| 1810 |
+
"agg_args": [],
|
| 1811 |
+
"infeasible": true
|
| 1812 |
+
},
|
| 1813 |
+
"error": null,
|
| 1814 |
+
"latency_ms": 2206,
|
| 1815 |
+
"tokens": 14551
|
| 1816 |
+
}
|
| 1817 |
+
]
|
| 1818 |
+
}
|
eval/planner/results/planner_result_2026-07-23_152650.json
ADDED
|
@@ -0,0 +1,2123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "2026-07-23_152650",
|
| 3 |
+
"total": 31,
|
| 4 |
+
"passed": 30,
|
| 5 |
+
"cases": [
|
| 6 |
+
{
|
| 7 |
+
"id": "count_zero_pa",
|
| 8 |
+
"category": "count",
|
| 9 |
+
"lang": "en",
|
| 10 |
+
"carried_over": true,
|
| 11 |
+
"question": "how many records have PA_Percent = 0?",
|
| 12 |
+
"passed": true,
|
| 13 |
+
"checks": [
|
| 14 |
+
{
|
| 15 |
+
"check": "select_agg=count",
|
| 16 |
+
"ok": true,
|
| 17 |
+
"detail": "aggs=['count']"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"check": "has_filter",
|
| 21 |
+
"ok": true,
|
| 22 |
+
"detail": "filter_present=True"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"check": "no_group_by",
|
| 26 |
+
"ok": true,
|
| 27 |
+
"detail": "grouped=False"
|
| 28 |
+
}
|
| 29 |
+
],
|
| 30 |
+
"facts": {
|
| 31 |
+
"tools": [
|
| 32 |
+
"retrieve_data"
|
| 33 |
+
],
|
| 34 |
+
"irs": [
|
| 35 |
+
{
|
| 36 |
+
"source_id": "src_pa",
|
| 37 |
+
"table_id": "t_pa",
|
| 38 |
+
"select": [
|
| 39 |
+
{
|
| 40 |
+
"kind": "agg",
|
| 41 |
+
"fn": "count",
|
| 42 |
+
"alias": "record_count"
|
| 43 |
+
}
|
| 44 |
+
],
|
| 45 |
+
"filters": [
|
| 46 |
+
{
|
| 47 |
+
"column_id": "c_pa_percent",
|
| 48 |
+
"op": "=",
|
| 49 |
+
"value": 0,
|
| 50 |
+
"value_type": "decimal"
|
| 51 |
+
}
|
| 52 |
+
]
|
| 53 |
+
}
|
| 54 |
+
],
|
| 55 |
+
"agg_args": [],
|
| 56 |
+
"infeasible": false
|
| 57 |
+
},
|
| 58 |
+
"error": null,
|
| 59 |
+
"latency_ms": 4042,
|
| 60 |
+
"tokens": 14569
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "count_mttr_gt20_id",
|
| 64 |
+
"category": "count",
|
| 65 |
+
"lang": "id",
|
| 66 |
+
"carried_over": true,
|
| 67 |
+
"question": "berapa banyak record dengan MTTR di atas 20?",
|
| 68 |
+
"passed": true,
|
| 69 |
+
"checks": [
|
| 70 |
+
{
|
| 71 |
+
"check": "select_agg=count",
|
| 72 |
+
"ok": true,
|
| 73 |
+
"detail": "aggs=['count']"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"check": "has_filter",
|
| 77 |
+
"ok": true,
|
| 78 |
+
"detail": "filter_present=True"
|
| 79 |
+
}
|
| 80 |
+
],
|
| 81 |
+
"facts": {
|
| 82 |
+
"tools": [
|
| 83 |
+
"retrieve_data"
|
| 84 |
+
],
|
| 85 |
+
"irs": [
|
| 86 |
+
{
|
| 87 |
+
"source_id": "src_pa",
|
| 88 |
+
"table_id": "t_pa",
|
| 89 |
+
"select": [
|
| 90 |
+
{
|
| 91 |
+
"kind": "agg",
|
| 92 |
+
"fn": "count",
|
| 93 |
+
"alias": "record_count"
|
| 94 |
+
}
|
| 95 |
+
],
|
| 96 |
+
"filters": [
|
| 97 |
+
{
|
| 98 |
+
"column_id": "c_mttr",
|
| 99 |
+
"op": ">",
|
| 100 |
+
"value": 20,
|
| 101 |
+
"value_type": "decimal"
|
| 102 |
+
}
|
| 103 |
+
]
|
| 104 |
+
}
|
| 105 |
+
],
|
| 106 |
+
"agg_args": [],
|
| 107 |
+
"infeasible": false
|
| 108 |
+
},
|
| 109 |
+
"error": null,
|
| 110 |
+
"latency_ms": 2495,
|
| 111 |
+
"tokens": 14571
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"id": "count_section_hauler",
|
| 115 |
+
"category": "count",
|
| 116 |
+
"lang": "en",
|
| 117 |
+
"carried_over": true,
|
| 118 |
+
"question": "how many rows are in section OB HAULER?",
|
| 119 |
+
"passed": true,
|
| 120 |
+
"checks": [
|
| 121 |
+
{
|
| 122 |
+
"check": "select_agg=count",
|
| 123 |
+
"ok": true,
|
| 124 |
+
"detail": "aggs=['count']"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"check": "has_filter",
|
| 128 |
+
"ok": true,
|
| 129 |
+
"detail": "filter_present=True"
|
| 130 |
+
}
|
| 131 |
+
],
|
| 132 |
+
"facts": {
|
| 133 |
+
"tools": [
|
| 134 |
+
"retrieve_data"
|
| 135 |
+
],
|
| 136 |
+
"irs": [
|
| 137 |
+
{
|
| 138 |
+
"source_id": "src_pa",
|
| 139 |
+
"table_id": "t_pa",
|
| 140 |
+
"select": [
|
| 141 |
+
{
|
| 142 |
+
"kind": "agg",
|
| 143 |
+
"fn": "count",
|
| 144 |
+
"alias": "row_count"
|
| 145 |
+
}
|
| 146 |
+
],
|
| 147 |
+
"filters": [
|
| 148 |
+
{
|
| 149 |
+
"column_id": "c_section",
|
| 150 |
+
"op": "=",
|
| 151 |
+
"value": "OB HAULER",
|
| 152 |
+
"value_type": "string"
|
| 153 |
+
}
|
| 154 |
+
]
|
| 155 |
+
}
|
| 156 |
+
],
|
| 157 |
+
"agg_args": [],
|
| 158 |
+
"infeasible": false
|
| 159 |
+
},
|
| 160 |
+
"error": null,
|
| 161 |
+
"latency_ms": 2162,
|
| 162 |
+
"tokens": 14574
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"id": "rank_units_worst_pa_id",
|
| 166 |
+
"category": "ranking",
|
| 167 |
+
"lang": "id",
|
| 168 |
+
"carried_over": false,
|
| 169 |
+
"question": "5 unit dengan PA terburuk?",
|
| 170 |
+
"passed": true,
|
| 171 |
+
"checks": [
|
| 172 |
+
{
|
| 173 |
+
"check": "group_by",
|
| 174 |
+
"ok": true,
|
| 175 |
+
"detail": "grouped=True"
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"check": "group_by_col=Equipment_Number",
|
| 179 |
+
"ok": true,
|
| 180 |
+
"detail": "ids=['c_equipment_number'] aliases=[] resolved=['c_equipment_number']"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"check": "select_agg=avg",
|
| 184 |
+
"ok": true,
|
| 185 |
+
"detail": "aggs=['avg']"
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"check": "order_dir=asc",
|
| 189 |
+
"ok": true,
|
| 190 |
+
"detail": "dirs=['asc']"
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"check": "limit=5",
|
| 194 |
+
"ok": true,
|
| 195 |
+
"detail": "limits=[5]"
|
| 196 |
+
}
|
| 197 |
+
],
|
| 198 |
+
"facts": {
|
| 199 |
+
"tools": [
|
| 200 |
+
"check_data",
|
| 201 |
+
"retrieve_data"
|
| 202 |
+
],
|
| 203 |
+
"irs": [
|
| 204 |
+
{
|
| 205 |
+
"source_id": "src_pa",
|
| 206 |
+
"table_id": "t_pa",
|
| 207 |
+
"select": [
|
| 208 |
+
{
|
| 209 |
+
"kind": "column",
|
| 210 |
+
"column_id": "c_equipment_number",
|
| 211 |
+
"alias": "equipment_number"
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"kind": "agg",
|
| 215 |
+
"fn": "avg",
|
| 216 |
+
"column_id": "c_pa_percent",
|
| 217 |
+
"alias": "avg_pa_percent"
|
| 218 |
+
}
|
| 219 |
+
],
|
| 220 |
+
"group_by": [
|
| 221 |
+
"c_equipment_number"
|
| 222 |
+
],
|
| 223 |
+
"order_by": [
|
| 224 |
+
{
|
| 225 |
+
"column_id": "avg_pa_percent",
|
| 226 |
+
"dir": "asc"
|
| 227 |
+
}
|
| 228 |
+
],
|
| 229 |
+
"limit": 5
|
| 230 |
+
}
|
| 231 |
+
],
|
| 232 |
+
"agg_args": [],
|
| 233 |
+
"infeasible": false
|
| 234 |
+
},
|
| 235 |
+
"error": null,
|
| 236 |
+
"latency_ms": 3918,
|
| 237 |
+
"tokens": 14745
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
"id": "rank_models_top_mttr_id",
|
| 241 |
+
"category": "ranking",
|
| 242 |
+
"lang": "id",
|
| 243 |
+
"carried_over": true,
|
| 244 |
+
"question": "top 3 model dengan MTTR tertinggi?",
|
| 245 |
+
"passed": true,
|
| 246 |
+
"checks": [
|
| 247 |
+
{
|
| 248 |
+
"check": "group_by",
|
| 249 |
+
"ok": true,
|
| 250 |
+
"detail": "grouped=True"
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"check": "group_by_col=Model_Unit",
|
| 254 |
+
"ok": true,
|
| 255 |
+
"detail": "ids=['c_model_unit'] aliases=[] resolved=['c_model_unit']"
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"check": "select_agg=avg",
|
| 259 |
+
"ok": true,
|
| 260 |
+
"detail": "aggs=['avg']"
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"check": "order_dir=desc",
|
| 264 |
+
"ok": true,
|
| 265 |
+
"detail": "dirs=['desc']"
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"check": "limit=3",
|
| 269 |
+
"ok": true,
|
| 270 |
+
"detail": "limits=[3]"
|
| 271 |
+
}
|
| 272 |
+
],
|
| 273 |
+
"facts": {
|
| 274 |
+
"tools": [
|
| 275 |
+
"check_data",
|
| 276 |
+
"retrieve_data"
|
| 277 |
+
],
|
| 278 |
+
"irs": [
|
| 279 |
+
{
|
| 280 |
+
"source_id": "src_pa",
|
| 281 |
+
"table_id": "t_pa",
|
| 282 |
+
"select": [
|
| 283 |
+
{
|
| 284 |
+
"kind": "column",
|
| 285 |
+
"column_id": "c_model_unit",
|
| 286 |
+
"alias": "model"
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"kind": "agg",
|
| 290 |
+
"fn": "avg",
|
| 291 |
+
"column_id": "c_mttr",
|
| 292 |
+
"alias": "avg_mttr"
|
| 293 |
+
}
|
| 294 |
+
],
|
| 295 |
+
"group_by": [
|
| 296 |
+
"c_model_unit"
|
| 297 |
+
],
|
| 298 |
+
"order_by": [
|
| 299 |
+
{
|
| 300 |
+
"column_id": "avg_mttr",
|
| 301 |
+
"dir": "desc"
|
| 302 |
+
}
|
| 303 |
+
],
|
| 304 |
+
"limit": 3
|
| 305 |
+
}
|
| 306 |
+
],
|
| 307 |
+
"agg_args": [],
|
| 308 |
+
"infeasible": false
|
| 309 |
+
},
|
| 310 |
+
"error": null,
|
| 311 |
+
"latency_ms": 2820,
|
| 312 |
+
"tokens": 14719
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"id": "rank_sections_lowest_pa_en",
|
| 316 |
+
"category": "ranking",
|
| 317 |
+
"lang": "en",
|
| 318 |
+
"carried_over": true,
|
| 319 |
+
"question": "which section has the lowest average PA?",
|
| 320 |
+
"passed": true,
|
| 321 |
+
"checks": [
|
| 322 |
+
{
|
| 323 |
+
"check": "group_by",
|
| 324 |
+
"ok": true,
|
| 325 |
+
"detail": "grouped=True"
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
"check": "group_by_col=Section",
|
| 329 |
+
"ok": true,
|
| 330 |
+
"detail": "ids=[] aliases=['section'] resolved=['c_section']"
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"check": "select_agg=avg",
|
| 334 |
+
"ok": true,
|
| 335 |
+
"detail": "aggs=['mean']"
|
| 336 |
+
}
|
| 337 |
+
],
|
| 338 |
+
"facts": {
|
| 339 |
+
"tools": [
|
| 340 |
+
"analyze_aggregate",
|
| 341 |
+
"check_data",
|
| 342 |
+
"retrieve_data"
|
| 343 |
+
],
|
| 344 |
+
"irs": [
|
| 345 |
+
{
|
| 346 |
+
"source_id": "src_pa",
|
| 347 |
+
"table_id": "t_pa",
|
| 348 |
+
"select": [
|
| 349 |
+
{
|
| 350 |
+
"kind": "column",
|
| 351 |
+
"column_id": "c_section",
|
| 352 |
+
"alias": "section"
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"kind": "column",
|
| 356 |
+
"column_id": "c_pa_percent",
|
| 357 |
+
"alias": "pa_percent"
|
| 358 |
+
}
|
| 359 |
+
],
|
| 360 |
+
"filters": [
|
| 361 |
+
{
|
| 362 |
+
"column_id": "c_month_id",
|
| 363 |
+
"op": "=",
|
| 364 |
+
"value": 202604,
|
| 365 |
+
"value_type": "int"
|
| 366 |
+
},
|
| 367 |
+
{
|
| 368 |
+
"column_id": "c_isdeleted",
|
| 369 |
+
"op": "=",
|
| 370 |
+
"value": 0,
|
| 371 |
+
"value_type": "int"
|
| 372 |
+
}
|
| 373 |
+
],
|
| 374 |
+
"limit": 10000
|
| 375 |
+
}
|
| 376 |
+
],
|
| 377 |
+
"agg_args": [
|
| 378 |
+
{
|
| 379 |
+
"data": "${t2}",
|
| 380 |
+
"aggregations": {
|
| 381 |
+
"pa_percent": [
|
| 382 |
+
"mean"
|
| 383 |
+
]
|
| 384 |
+
},
|
| 385 |
+
"group_by": [
|
| 386 |
+
"section"
|
| 387 |
+
]
|
| 388 |
+
}
|
| 389 |
+
],
|
| 390 |
+
"infeasible": false
|
| 391 |
+
},
|
| 392 |
+
"error": null,
|
| 393 |
+
"latency_ms": 3103,
|
| 394 |
+
"tokens": 14818
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
"id": "rank_units_most_breakdown_id",
|
| 398 |
+
"category": "ranking",
|
| 399 |
+
"lang": "id",
|
| 400 |
+
"carried_over": false,
|
| 401 |
+
"question": "unit mana yang paling sering breakdown?",
|
| 402 |
+
"passed": true,
|
| 403 |
+
"checks": [
|
| 404 |
+
{
|
| 405 |
+
"check": "group_by",
|
| 406 |
+
"ok": true,
|
| 407 |
+
"detail": "grouped=True"
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"check": "group_by_col=Equipment_Number",
|
| 411 |
+
"ok": true,
|
| 412 |
+
"detail": "ids=['c_equipment_number'] aliases=[] resolved=['c_equipment_number']"
|
| 413 |
+
},
|
| 414 |
+
{
|
| 415 |
+
"check": "order_dir=desc",
|
| 416 |
+
"ok": true,
|
| 417 |
+
"detail": "dirs=['desc']"
|
| 418 |
+
}
|
| 419 |
+
],
|
| 420 |
+
"facts": {
|
| 421 |
+
"tools": [
|
| 422 |
+
"check_data",
|
| 423 |
+
"retrieve_data"
|
| 424 |
+
],
|
| 425 |
+
"irs": [
|
| 426 |
+
{
|
| 427 |
+
"source_id": "src_pa",
|
| 428 |
+
"table_id": "t_pa",
|
| 429 |
+
"select": [
|
| 430 |
+
{
|
| 431 |
+
"kind": "column",
|
| 432 |
+
"column_id": "c_equipment_number",
|
| 433 |
+
"alias": "equipment_number"
|
| 434 |
+
},
|
| 435 |
+
{
|
| 436 |
+
"kind": "agg",
|
| 437 |
+
"fn": "sum",
|
| 438 |
+
"column_id": "c_total_frequency_down",
|
| 439 |
+
"alias": "total_breakdown_frequency"
|
| 440 |
+
}
|
| 441 |
+
],
|
| 442 |
+
"filters": [
|
| 443 |
+
{
|
| 444 |
+
"column_id": "c_month_id",
|
| 445 |
+
"op": "=",
|
| 446 |
+
"value": 202604,
|
| 447 |
+
"value_type": "int"
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"column_id": "c_isdeleted",
|
| 451 |
+
"op": "=",
|
| 452 |
+
"value": 0,
|
| 453 |
+
"value_type": "int"
|
| 454 |
+
}
|
| 455 |
+
],
|
| 456 |
+
"group_by": [
|
| 457 |
+
"c_equipment_number"
|
| 458 |
+
],
|
| 459 |
+
"order_by": [
|
| 460 |
+
{
|
| 461 |
+
"column_id": "total_breakdown_frequency",
|
| 462 |
+
"dir": "desc"
|
| 463 |
+
}
|
| 464 |
+
],
|
| 465 |
+
"limit": 1
|
| 466 |
+
}
|
| 467 |
+
],
|
| 468 |
+
"agg_args": [],
|
| 469 |
+
"infeasible": false
|
| 470 |
+
},
|
| 471 |
+
"error": null,
|
| 472 |
+
"latency_ms": 2902,
|
| 473 |
+
"tokens": 14761
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"id": "rank_units_worst_pa_en",
|
| 477 |
+
"category": "ranking",
|
| 478 |
+
"lang": "en",
|
| 479 |
+
"carried_over": false,
|
| 480 |
+
"question": "list the 10 worst units by availability",
|
| 481 |
+
"passed": true,
|
| 482 |
+
"checks": [
|
| 483 |
+
{
|
| 484 |
+
"check": "group_by",
|
| 485 |
+
"ok": true,
|
| 486 |
+
"detail": "grouped=True"
|
| 487 |
+
},
|
| 488 |
+
{
|
| 489 |
+
"check": "group_by_col=Equipment_Number",
|
| 490 |
+
"ok": true,
|
| 491 |
+
"detail": "ids=['c_equipment_number'] aliases=[] resolved=['c_equipment_number']"
|
| 492 |
+
},
|
| 493 |
+
{
|
| 494 |
+
"check": "select_agg=avg",
|
| 495 |
+
"ok": true,
|
| 496 |
+
"detail": "aggs=['avg']"
|
| 497 |
+
},
|
| 498 |
+
{
|
| 499 |
+
"check": "order_dir=asc",
|
| 500 |
+
"ok": true,
|
| 501 |
+
"detail": "dirs=['asc']"
|
| 502 |
+
},
|
| 503 |
+
{
|
| 504 |
+
"check": "limit=10",
|
| 505 |
+
"ok": true,
|
| 506 |
+
"detail": "limits=[10]"
|
| 507 |
+
}
|
| 508 |
+
],
|
| 509 |
+
"facts": {
|
| 510 |
+
"tools": [
|
| 511 |
+
"check_data",
|
| 512 |
+
"retrieve_data"
|
| 513 |
+
],
|
| 514 |
+
"irs": [
|
| 515 |
+
{
|
| 516 |
+
"source_id": "src_pa",
|
| 517 |
+
"table_id": "t_pa",
|
| 518 |
+
"select": [
|
| 519 |
+
{
|
| 520 |
+
"kind": "column",
|
| 521 |
+
"column_id": "c_equipment_number",
|
| 522 |
+
"alias": "equipment_number"
|
| 523 |
+
},
|
| 524 |
+
{
|
| 525 |
+
"kind": "agg",
|
| 526 |
+
"fn": "avg",
|
| 527 |
+
"column_id": "c_pa_percent",
|
| 528 |
+
"alias": "avg_pa_percent"
|
| 529 |
+
}
|
| 530 |
+
],
|
| 531 |
+
"filters": [
|
| 532 |
+
{
|
| 533 |
+
"column_id": "c_isdeleted",
|
| 534 |
+
"op": "=",
|
| 535 |
+
"value": 0,
|
| 536 |
+
"value_type": "int"
|
| 537 |
+
},
|
| 538 |
+
{
|
| 539 |
+
"column_id": "c_month_id",
|
| 540 |
+
"op": "=",
|
| 541 |
+
"value": 202604,
|
| 542 |
+
"value_type": "int"
|
| 543 |
+
}
|
| 544 |
+
],
|
| 545 |
+
"group_by": [
|
| 546 |
+
"c_equipment_number"
|
| 547 |
+
],
|
| 548 |
+
"order_by": [
|
| 549 |
+
{
|
| 550 |
+
"column_id": "avg_pa_percent",
|
| 551 |
+
"dir": "asc"
|
| 552 |
+
}
|
| 553 |
+
],
|
| 554 |
+
"limit": 10
|
| 555 |
+
}
|
| 556 |
+
],
|
| 557 |
+
"agg_args": [],
|
| 558 |
+
"infeasible": false
|
| 559 |
+
},
|
| 560 |
+
"error": null,
|
| 561 |
+
"latency_ms": 3112,
|
| 562 |
+
"tokens": 14762
|
| 563 |
+
},
|
| 564 |
+
{
|
| 565 |
+
"id": "fuzzy_model_777_id",
|
| 566 |
+
"category": "fuzzy_filter",
|
| 567 |
+
"lang": "id",
|
| 568 |
+
"carried_over": false,
|
| 569 |
+
"question": "berapa banyak model 777?",
|
| 570 |
+
"passed": true,
|
| 571 |
+
"checks": [
|
| 572 |
+
{
|
| 573 |
+
"check": "select_agg=count",
|
| 574 |
+
"ok": true,
|
| 575 |
+
"detail": "aggs=['count']"
|
| 576 |
+
},
|
| 577 |
+
{
|
| 578 |
+
"check": "no_filter_op=in",
|
| 579 |
+
"ok": true,
|
| 580 |
+
"detail": "ops=['=']"
|
| 581 |
+
}
|
| 582 |
+
],
|
| 583 |
+
"facts": {
|
| 584 |
+
"tools": [
|
| 585 |
+
"retrieve_data"
|
| 586 |
+
],
|
| 587 |
+
"irs": [
|
| 588 |
+
{
|
| 589 |
+
"source_id": "src_pa",
|
| 590 |
+
"table_id": "t_pa",
|
| 591 |
+
"select": [
|
| 592 |
+
{
|
| 593 |
+
"kind": "agg",
|
| 594 |
+
"fn": "count",
|
| 595 |
+
"alias": "record_count"
|
| 596 |
+
}
|
| 597 |
+
],
|
| 598 |
+
"filters": [
|
| 599 |
+
{
|
| 600 |
+
"column_id": "c_model_unit",
|
| 601 |
+
"op": "=",
|
| 602 |
+
"value": "777",
|
| 603 |
+
"value_type": "string"
|
| 604 |
+
}
|
| 605 |
+
],
|
| 606 |
+
"limit": 10000
|
| 607 |
+
}
|
| 608 |
+
],
|
| 609 |
+
"agg_args": [],
|
| 610 |
+
"infeasible": false
|
| 611 |
+
},
|
| 612 |
+
"error": null,
|
| 613 |
+
"latency_ms": 13617,
|
| 614 |
+
"tokens": 14582
|
| 615 |
+
},
|
| 616 |
+
{
|
| 617 |
+
"id": "fuzzy_model_hd785_id",
|
| 618 |
+
"category": "fuzzy_filter",
|
| 619 |
+
"lang": "id",
|
| 620 |
+
"carried_over": true,
|
| 621 |
+
"question": "berapa banyak unit HD785?",
|
| 622 |
+
"passed": true,
|
| 623 |
+
"checks": [
|
| 624 |
+
{
|
| 625 |
+
"check": "select_agg=count",
|
| 626 |
+
"ok": true,
|
| 627 |
+
"detail": "aggs=['count']"
|
| 628 |
+
},
|
| 629 |
+
{
|
| 630 |
+
"check": "no_filter_op=in",
|
| 631 |
+
"ok": true,
|
| 632 |
+
"detail": "ops=['=']"
|
| 633 |
+
}
|
| 634 |
+
],
|
| 635 |
+
"facts": {
|
| 636 |
+
"tools": [
|
| 637 |
+
"check_data",
|
| 638 |
+
"retrieve_data"
|
| 639 |
+
],
|
| 640 |
+
"irs": [
|
| 641 |
+
{
|
| 642 |
+
"source_id": "src_pa",
|
| 643 |
+
"table_id": "t_pa",
|
| 644 |
+
"select": [
|
| 645 |
+
{
|
| 646 |
+
"kind": "agg",
|
| 647 |
+
"fn": "count",
|
| 648 |
+
"alias": "unit_count"
|
| 649 |
+
}
|
| 650 |
+
],
|
| 651 |
+
"filters": [
|
| 652 |
+
{
|
| 653 |
+
"column_id": "c_model_unit",
|
| 654 |
+
"op": "=",
|
| 655 |
+
"value": "HD785",
|
| 656 |
+
"value_type": "string"
|
| 657 |
+
}
|
| 658 |
+
]
|
| 659 |
+
}
|
| 660 |
+
],
|
| 661 |
+
"agg_args": [],
|
| 662 |
+
"infeasible": false
|
| 663 |
+
},
|
| 664 |
+
"error": null,
|
| 665 |
+
"latency_ms": 3472,
|
| 666 |
+
"tokens": 14660
|
| 667 |
+
},
|
| 668 |
+
{
|
| 669 |
+
"id": "fuzzy_model_ex_en",
|
| 670 |
+
"category": "fuzzy_filter",
|
| 671 |
+
"lang": "en",
|
| 672 |
+
"carried_over": false,
|
| 673 |
+
"question": "how many EX excavator units are there?",
|
| 674 |
+
"passed": true,
|
| 675 |
+
"checks": [
|
| 676 |
+
{
|
| 677 |
+
"check": "no_filter_op=in",
|
| 678 |
+
"ok": true,
|
| 679 |
+
"detail": "ops=['like']"
|
| 680 |
+
}
|
| 681 |
+
],
|
| 682 |
+
"facts": {
|
| 683 |
+
"tools": [
|
| 684 |
+
"retrieve_data"
|
| 685 |
+
],
|
| 686 |
+
"irs": [
|
| 687 |
+
{
|
| 688 |
+
"source_id": "src_pa",
|
| 689 |
+
"table_id": "t_pa",
|
| 690 |
+
"select": [
|
| 691 |
+
{
|
| 692 |
+
"kind": "agg",
|
| 693 |
+
"fn": "count",
|
| 694 |
+
"alias": "unit_count"
|
| 695 |
+
}
|
| 696 |
+
],
|
| 697 |
+
"filters": [
|
| 698 |
+
{
|
| 699 |
+
"column_id": "c_model_unit",
|
| 700 |
+
"op": "like",
|
| 701 |
+
"value": "EX%",
|
| 702 |
+
"value_type": "string"
|
| 703 |
+
}
|
| 704 |
+
],
|
| 705 |
+
"limit": 10000
|
| 706 |
+
}
|
| 707 |
+
],
|
| 708 |
+
"agg_args": [],
|
| 709 |
+
"infeasible": false
|
| 710 |
+
},
|
| 711 |
+
"error": null,
|
| 712 |
+
"latency_ms": 5809,
|
| 713 |
+
"tokens": 29353
|
| 714 |
+
},
|
| 715 |
+
{
|
| 716 |
+
"id": "agg_pa_per_section_id",
|
| 717 |
+
"category": "aggregate",
|
| 718 |
+
"lang": "id",
|
| 719 |
+
"carried_over": true,
|
| 720 |
+
"question": "berapa rata-rata PA per section?",
|
| 721 |
+
"passed": true,
|
| 722 |
+
"checks": [
|
| 723 |
+
{
|
| 724 |
+
"check": "group_by",
|
| 725 |
+
"ok": true,
|
| 726 |
+
"detail": "grouped=True"
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"check": "group_by_col=Section",
|
| 730 |
+
"ok": true,
|
| 731 |
+
"detail": "ids=[] aliases=['section'] resolved=['c_section']"
|
| 732 |
+
},
|
| 733 |
+
{
|
| 734 |
+
"check": "select_agg=avg",
|
| 735 |
+
"ok": true,
|
| 736 |
+
"detail": "aggs=['mean']"
|
| 737 |
+
}
|
| 738 |
+
],
|
| 739 |
+
"facts": {
|
| 740 |
+
"tools": [
|
| 741 |
+
"analyze_aggregate",
|
| 742 |
+
"check_data",
|
| 743 |
+
"retrieve_data"
|
| 744 |
+
],
|
| 745 |
+
"irs": [
|
| 746 |
+
{
|
| 747 |
+
"source_id": "src_pa",
|
| 748 |
+
"table_id": "t_pa",
|
| 749 |
+
"select": [
|
| 750 |
+
{
|
| 751 |
+
"kind": "column",
|
| 752 |
+
"column_id": "c_section",
|
| 753 |
+
"alias": "section"
|
| 754 |
+
},
|
| 755 |
+
{
|
| 756 |
+
"kind": "column",
|
| 757 |
+
"column_id": "c_pa_percent",
|
| 758 |
+
"alias": "pa_percent"
|
| 759 |
+
}
|
| 760 |
+
],
|
| 761 |
+
"filters": [
|
| 762 |
+
{
|
| 763 |
+
"column_id": "c_isdeleted",
|
| 764 |
+
"op": "=",
|
| 765 |
+
"value": 0,
|
| 766 |
+
"value_type": "int"
|
| 767 |
+
}
|
| 768 |
+
],
|
| 769 |
+
"limit": 10000
|
| 770 |
+
}
|
| 771 |
+
],
|
| 772 |
+
"agg_args": [
|
| 773 |
+
{
|
| 774 |
+
"data": "${t2}",
|
| 775 |
+
"aggregations": {
|
| 776 |
+
"pa_percent": [
|
| 777 |
+
"mean"
|
| 778 |
+
]
|
| 779 |
+
},
|
| 780 |
+
"group_by": [
|
| 781 |
+
"section"
|
| 782 |
+
]
|
| 783 |
+
}
|
| 784 |
+
],
|
| 785 |
+
"infeasible": false
|
| 786 |
+
},
|
| 787 |
+
"error": null,
|
| 788 |
+
"latency_ms": 3942,
|
| 789 |
+
"tokens": 14785
|
| 790 |
+
},
|
| 791 |
+
{
|
| 792 |
+
"id": "agg_mttr_per_model_en",
|
| 793 |
+
"category": "aggregate",
|
| 794 |
+
"lang": "en",
|
| 795 |
+
"carried_over": true,
|
| 796 |
+
"question": "what is the average MTTR per model unit?",
|
| 797 |
+
"passed": true,
|
| 798 |
+
"checks": [
|
| 799 |
+
{
|
| 800 |
+
"check": "group_by",
|
| 801 |
+
"ok": true,
|
| 802 |
+
"detail": "grouped=True"
|
| 803 |
+
},
|
| 804 |
+
{
|
| 805 |
+
"check": "group_by_col=Model_Unit",
|
| 806 |
+
"ok": true,
|
| 807 |
+
"detail": "ids=[] aliases=['model_unit'] resolved=['c_model_unit']"
|
| 808 |
+
},
|
| 809 |
+
{
|
| 810 |
+
"check": "select_agg=avg",
|
| 811 |
+
"ok": true,
|
| 812 |
+
"detail": "aggs=['mean']"
|
| 813 |
+
}
|
| 814 |
+
],
|
| 815 |
+
"facts": {
|
| 816 |
+
"tools": [
|
| 817 |
+
"analyze_aggregate",
|
| 818 |
+
"check_data",
|
| 819 |
+
"retrieve_data"
|
| 820 |
+
],
|
| 821 |
+
"irs": [
|
| 822 |
+
{
|
| 823 |
+
"source_id": "src_pa",
|
| 824 |
+
"table_id": "t_pa",
|
| 825 |
+
"select": [
|
| 826 |
+
{
|
| 827 |
+
"kind": "column",
|
| 828 |
+
"column_id": "c_model_unit",
|
| 829 |
+
"alias": "model_unit"
|
| 830 |
+
},
|
| 831 |
+
{
|
| 832 |
+
"kind": "column",
|
| 833 |
+
"column_id": "c_mttr",
|
| 834 |
+
"alias": "mttr"
|
| 835 |
+
}
|
| 836 |
+
],
|
| 837 |
+
"limit": 10000
|
| 838 |
+
}
|
| 839 |
+
],
|
| 840 |
+
"agg_args": [
|
| 841 |
+
{
|
| 842 |
+
"data": "${t2}",
|
| 843 |
+
"aggregations": {
|
| 844 |
+
"mttr": [
|
| 845 |
+
"mean"
|
| 846 |
+
]
|
| 847 |
+
},
|
| 848 |
+
"group_by": [
|
| 849 |
+
"model_unit"
|
| 850 |
+
]
|
| 851 |
+
}
|
| 852 |
+
],
|
| 853 |
+
"infeasible": false
|
| 854 |
+
},
|
| 855 |
+
"error": null,
|
| 856 |
+
"latency_ms": 3648,
|
| 857 |
+
"tokens": 14781
|
| 858 |
+
},
|
| 859 |
+
{
|
| 860 |
+
"id": "agg_downhour_per_group_id",
|
| 861 |
+
"category": "aggregate",
|
| 862 |
+
"lang": "id",
|
| 863 |
+
"carried_over": true,
|
| 864 |
+
"question": "total down hour per equipment group?",
|
| 865 |
+
"passed": true,
|
| 866 |
+
"checks": [
|
| 867 |
+
{
|
| 868 |
+
"check": "group_by",
|
| 869 |
+
"ok": true,
|
| 870 |
+
"detail": "grouped=True"
|
| 871 |
+
},
|
| 872 |
+
{
|
| 873 |
+
"check": "group_by_col=Equipment_Group_ID",
|
| 874 |
+
"ok": true,
|
| 875 |
+
"detail": "ids=[] aliases=['equipment_group'] resolved=['c_equipment_group_id']"
|
| 876 |
+
},
|
| 877 |
+
{
|
| 878 |
+
"check": "select_agg=sum",
|
| 879 |
+
"ok": true,
|
| 880 |
+
"detail": "aggs=['sum']"
|
| 881 |
+
}
|
| 882 |
+
],
|
| 883 |
+
"facts": {
|
| 884 |
+
"tools": [
|
| 885 |
+
"analyze_aggregate",
|
| 886 |
+
"check_data",
|
| 887 |
+
"retrieve_data"
|
| 888 |
+
],
|
| 889 |
+
"irs": [
|
| 890 |
+
{
|
| 891 |
+
"source_id": "src_pa",
|
| 892 |
+
"table_id": "t_pa",
|
| 893 |
+
"select": [
|
| 894 |
+
{
|
| 895 |
+
"kind": "column",
|
| 896 |
+
"column_id": "c_equipment_group_id",
|
| 897 |
+
"alias": "equipment_group"
|
| 898 |
+
},
|
| 899 |
+
{
|
| 900 |
+
"kind": "column",
|
| 901 |
+
"column_id": "c_total_down_hour",
|
| 902 |
+
"alias": "down_hour"
|
| 903 |
+
}
|
| 904 |
+
],
|
| 905 |
+
"filters": [
|
| 906 |
+
{
|
| 907 |
+
"column_id": "c_month_id",
|
| 908 |
+
"op": "=",
|
| 909 |
+
"value": 202604,
|
| 910 |
+
"value_type": "int"
|
| 911 |
+
},
|
| 912 |
+
{
|
| 913 |
+
"column_id": "c_isdeleted",
|
| 914 |
+
"op": "=",
|
| 915 |
+
"value": 0,
|
| 916 |
+
"value_type": "int"
|
| 917 |
+
}
|
| 918 |
+
],
|
| 919 |
+
"limit": 10000
|
| 920 |
+
}
|
| 921 |
+
],
|
| 922 |
+
"agg_args": [
|
| 923 |
+
{
|
| 924 |
+
"data": "${t2}",
|
| 925 |
+
"aggregations": {
|
| 926 |
+
"down_hour": [
|
| 927 |
+
"sum"
|
| 928 |
+
]
|
| 929 |
+
},
|
| 930 |
+
"group_by": [
|
| 931 |
+
"equipment_group"
|
| 932 |
+
]
|
| 933 |
+
}
|
| 934 |
+
],
|
| 935 |
+
"infeasible": false
|
| 936 |
+
},
|
| 937 |
+
"error": null,
|
| 938 |
+
"latency_ms": 5057,
|
| 939 |
+
"tokens": 14830
|
| 940 |
+
},
|
| 941 |
+
{
|
| 942 |
+
"id": "desc_mttr_stats_id",
|
| 943 |
+
"category": "descriptive",
|
| 944 |
+
"lang": "id",
|
| 945 |
+
"carried_over": true,
|
| 946 |
+
"question": "berikan ringkasan statistik MTTR",
|
| 947 |
+
"passed": true,
|
| 948 |
+
"checks": [
|
| 949 |
+
{
|
| 950 |
+
"check": "has_tool=analyze_descriptive",
|
| 951 |
+
"ok": true,
|
| 952 |
+
"detail": "tools=['analyze_descriptive', 'check_data', 'retrieve_data']"
|
| 953 |
+
}
|
| 954 |
+
],
|
| 955 |
+
"facts": {
|
| 956 |
+
"tools": [
|
| 957 |
+
"analyze_descriptive",
|
| 958 |
+
"check_data",
|
| 959 |
+
"retrieve_data"
|
| 960 |
+
],
|
| 961 |
+
"irs": [
|
| 962 |
+
{
|
| 963 |
+
"source_id": "src_pa",
|
| 964 |
+
"table_id": "t_pa",
|
| 965 |
+
"select": [
|
| 966 |
+
{
|
| 967 |
+
"kind": "column",
|
| 968 |
+
"column_id": "c_mttr",
|
| 969 |
+
"alias": "mttr"
|
| 970 |
+
}
|
| 971 |
+
],
|
| 972 |
+
"limit": 10000
|
| 973 |
+
}
|
| 974 |
+
],
|
| 975 |
+
"agg_args": [],
|
| 976 |
+
"infeasible": false
|
| 977 |
+
},
|
| 978 |
+
"error": null,
|
| 979 |
+
"latency_ms": 3173,
|
| 980 |
+
"tokens": 14707
|
| 981 |
+
},
|
| 982 |
+
{
|
| 983 |
+
"id": "desc_pa_stats_en",
|
| 984 |
+
"category": "descriptive",
|
| 985 |
+
"lang": "en",
|
| 986 |
+
"carried_over": true,
|
| 987 |
+
"question": "give me the summary statistics for PA_Percent",
|
| 988 |
+
"passed": true,
|
| 989 |
+
"checks": [
|
| 990 |
+
{
|
| 991 |
+
"check": "has_tool=analyze_descriptive",
|
| 992 |
+
"ok": true,
|
| 993 |
+
"detail": "tools=['analyze_descriptive', 'check_data', 'retrieve_data']"
|
| 994 |
+
}
|
| 995 |
+
],
|
| 996 |
+
"facts": {
|
| 997 |
+
"tools": [
|
| 998 |
+
"analyze_descriptive",
|
| 999 |
+
"check_data",
|
| 1000 |
+
"retrieve_data"
|
| 1001 |
+
],
|
| 1002 |
+
"irs": [
|
| 1003 |
+
{
|
| 1004 |
+
"source_id": "src_pa",
|
| 1005 |
+
"table_id": "t_pa",
|
| 1006 |
+
"select": [
|
| 1007 |
+
{
|
| 1008 |
+
"kind": "column",
|
| 1009 |
+
"column_id": "c_pa_percent",
|
| 1010 |
+
"alias": "PA_Percent"
|
| 1011 |
+
}
|
| 1012 |
+
],
|
| 1013 |
+
"limit": 10000
|
| 1014 |
+
}
|
| 1015 |
+
],
|
| 1016 |
+
"agg_args": [],
|
| 1017 |
+
"infeasible": false
|
| 1018 |
+
},
|
| 1019 |
+
"error": null,
|
| 1020 |
+
"latency_ms": 3135,
|
| 1021 |
+
"tokens": 14717
|
| 1022 |
+
},
|
| 1023 |
+
{
|
| 1024 |
+
"id": "corr_mttr_pa_id",
|
| 1025 |
+
"category": "correlation",
|
| 1026 |
+
"lang": "id",
|
| 1027 |
+
"carried_over": true,
|
| 1028 |
+
"question": "apakah ada korelasi antara MTTR dan PA?",
|
| 1029 |
+
"passed": true,
|
| 1030 |
+
"checks": [
|
| 1031 |
+
{
|
| 1032 |
+
"check": "has_tool=analyze_correlation",
|
| 1033 |
+
"ok": true,
|
| 1034 |
+
"detail": "tools=['analyze_correlation', 'check_data', 'retrieve_data']"
|
| 1035 |
+
}
|
| 1036 |
+
],
|
| 1037 |
+
"facts": {
|
| 1038 |
+
"tools": [
|
| 1039 |
+
"analyze_correlation",
|
| 1040 |
+
"check_data",
|
| 1041 |
+
"retrieve_data"
|
| 1042 |
+
],
|
| 1043 |
+
"irs": [
|
| 1044 |
+
{
|
| 1045 |
+
"source_id": "src_pa",
|
| 1046 |
+
"table_id": "t_pa",
|
| 1047 |
+
"select": [
|
| 1048 |
+
{
|
| 1049 |
+
"kind": "column",
|
| 1050 |
+
"column_id": "c_mttr",
|
| 1051 |
+
"alias": "mttr"
|
| 1052 |
+
},
|
| 1053 |
+
{
|
| 1054 |
+
"kind": "column",
|
| 1055 |
+
"column_id": "c_pa_percent",
|
| 1056 |
+
"alias": "pa_percent"
|
| 1057 |
+
}
|
| 1058 |
+
],
|
| 1059 |
+
"filters": [
|
| 1060 |
+
{
|
| 1061 |
+
"column_id": "c_month_id",
|
| 1062 |
+
"op": "=",
|
| 1063 |
+
"value": 202604,
|
| 1064 |
+
"value_type": "int"
|
| 1065 |
+
},
|
| 1066 |
+
{
|
| 1067 |
+
"column_id": "c_isdeleted",
|
| 1068 |
+
"op": "=",
|
| 1069 |
+
"value": 0,
|
| 1070 |
+
"value_type": "int"
|
| 1071 |
+
}
|
| 1072 |
+
],
|
| 1073 |
+
"limit": 10000
|
| 1074 |
+
}
|
| 1075 |
+
],
|
| 1076 |
+
"agg_args": [],
|
| 1077 |
+
"infeasible": false
|
| 1078 |
+
},
|
| 1079 |
+
"error": null,
|
| 1080 |
+
"latency_ms": 3920,
|
| 1081 |
+
"tokens": 14798
|
| 1082 |
+
},
|
| 1083 |
+
{
|
| 1084 |
+
"id": "corr_freq_pa_en",
|
| 1085 |
+
"category": "correlation",
|
| 1086 |
+
"lang": "en",
|
| 1087 |
+
"carried_over": true,
|
| 1088 |
+
"question": "is breakdown frequency correlated with availability?",
|
| 1089 |
+
"passed": true,
|
| 1090 |
+
"checks": [
|
| 1091 |
+
{
|
| 1092 |
+
"check": "has_tool=analyze_correlation",
|
| 1093 |
+
"ok": true,
|
| 1094 |
+
"detail": "tools=['analyze_correlation', 'check_data', 'retrieve_data']"
|
| 1095 |
+
}
|
| 1096 |
+
],
|
| 1097 |
+
"facts": {
|
| 1098 |
+
"tools": [
|
| 1099 |
+
"analyze_correlation",
|
| 1100 |
+
"check_data",
|
| 1101 |
+
"retrieve_data"
|
| 1102 |
+
],
|
| 1103 |
+
"irs": [
|
| 1104 |
+
{
|
| 1105 |
+
"source_id": "src_pa",
|
| 1106 |
+
"table_id": "t_pa",
|
| 1107 |
+
"select": [
|
| 1108 |
+
{
|
| 1109 |
+
"kind": "column",
|
| 1110 |
+
"column_id": "c_total_frequency_down",
|
| 1111 |
+
"alias": "breakdown_frequency"
|
| 1112 |
+
},
|
| 1113 |
+
{
|
| 1114 |
+
"kind": "column",
|
| 1115 |
+
"column_id": "c_pa_percent",
|
| 1116 |
+
"alias": "availability"
|
| 1117 |
+
}
|
| 1118 |
+
],
|
| 1119 |
+
"filters": [
|
| 1120 |
+
{
|
| 1121 |
+
"column_id": "c_month_id",
|
| 1122 |
+
"op": "=",
|
| 1123 |
+
"value": 202604,
|
| 1124 |
+
"value_type": "int"
|
| 1125 |
+
},
|
| 1126 |
+
{
|
| 1127 |
+
"column_id": "c_isdeleted",
|
| 1128 |
+
"op": "=",
|
| 1129 |
+
"value": 0,
|
| 1130 |
+
"value_type": "int"
|
| 1131 |
+
}
|
| 1132 |
+
],
|
| 1133 |
+
"limit": 10000
|
| 1134 |
+
}
|
| 1135 |
+
],
|
| 1136 |
+
"agg_args": [],
|
| 1137 |
+
"infeasible": false
|
| 1138 |
+
},
|
| 1139 |
+
"error": null,
|
| 1140 |
+
"latency_ms": 3774,
|
| 1141 |
+
"tokens": 14845
|
| 1142 |
+
},
|
| 1143 |
+
{
|
| 1144 |
+
"id": "trend_pa_daily_id",
|
| 1145 |
+
"category": "trend",
|
| 1146 |
+
"lang": "id",
|
| 1147 |
+
"carried_over": true,
|
| 1148 |
+
"question": "bagaimana trend PA harian?",
|
| 1149 |
+
"passed": true,
|
| 1150 |
+
"checks": [
|
| 1151 |
+
{
|
| 1152 |
+
"check": "has_tool=analyze_trend",
|
| 1153 |
+
"ok": true,
|
| 1154 |
+
"detail": "tools=['analyze_trend', 'check_data', 'retrieve_data']"
|
| 1155 |
+
}
|
| 1156 |
+
],
|
| 1157 |
+
"facts": {
|
| 1158 |
+
"tools": [
|
| 1159 |
+
"analyze_trend",
|
| 1160 |
+
"check_data",
|
| 1161 |
+
"retrieve_data"
|
| 1162 |
+
],
|
| 1163 |
+
"irs": [
|
| 1164 |
+
{
|
| 1165 |
+
"source_id": "src_pa",
|
| 1166 |
+
"table_id": "t_pa",
|
| 1167 |
+
"select": [
|
| 1168 |
+
{
|
| 1169 |
+
"kind": "column",
|
| 1170 |
+
"column_id": "c_from_date",
|
| 1171 |
+
"alias": "date"
|
| 1172 |
+
},
|
| 1173 |
+
{
|
| 1174 |
+
"kind": "column",
|
| 1175 |
+
"column_id": "c_pa_percent",
|
| 1176 |
+
"alias": "pa_percent"
|
| 1177 |
+
}
|
| 1178 |
+
],
|
| 1179 |
+
"filters": [
|
| 1180 |
+
{
|
| 1181 |
+
"column_id": "c_month_id",
|
| 1182 |
+
"op": "=",
|
| 1183 |
+
"value": 202604,
|
| 1184 |
+
"value_type": "int"
|
| 1185 |
+
},
|
| 1186 |
+
{
|
| 1187 |
+
"column_id": "c_isdeleted",
|
| 1188 |
+
"op": "=",
|
| 1189 |
+
"value": 0,
|
| 1190 |
+
"value_type": "int"
|
| 1191 |
+
}
|
| 1192 |
+
],
|
| 1193 |
+
"limit": 10000
|
| 1194 |
+
}
|
| 1195 |
+
],
|
| 1196 |
+
"agg_args": [],
|
| 1197 |
+
"infeasible": false
|
| 1198 |
+
},
|
| 1199 |
+
"error": null,
|
| 1200 |
+
"latency_ms": 4485,
|
| 1201 |
+
"tokens": 14852
|
| 1202 |
+
},
|
| 1203 |
+
{
|
| 1204 |
+
"id": "trend_downhour_en",
|
| 1205 |
+
"category": "trend",
|
| 1206 |
+
"lang": "en",
|
| 1207 |
+
"carried_over": true,
|
| 1208 |
+
"question": "show the trend of total down hours over time",
|
| 1209 |
+
"passed": true,
|
| 1210 |
+
"checks": [
|
| 1211 |
+
{
|
| 1212 |
+
"check": "has_tool=analyze_trend",
|
| 1213 |
+
"ok": true,
|
| 1214 |
+
"detail": "tools=['analyze_trend', 'check_data', 'retrieve_data']"
|
| 1215 |
+
}
|
| 1216 |
+
],
|
| 1217 |
+
"facts": {
|
| 1218 |
+
"tools": [
|
| 1219 |
+
"analyze_trend",
|
| 1220 |
+
"check_data",
|
| 1221 |
+
"retrieve_data"
|
| 1222 |
+
],
|
| 1223 |
+
"irs": [
|
| 1224 |
+
{
|
| 1225 |
+
"source_id": "src_pa",
|
| 1226 |
+
"table_id": "t_pa",
|
| 1227 |
+
"select": [
|
| 1228 |
+
{
|
| 1229 |
+
"kind": "column",
|
| 1230 |
+
"column_id": "c_from_date",
|
| 1231 |
+
"alias": "from_date"
|
| 1232 |
+
},
|
| 1233 |
+
{
|
| 1234 |
+
"kind": "column",
|
| 1235 |
+
"column_id": "c_total_down_hour",
|
| 1236 |
+
"alias": "total_down_hour"
|
| 1237 |
+
}
|
| 1238 |
+
],
|
| 1239 |
+
"filters": [
|
| 1240 |
+
{
|
| 1241 |
+
"column_id": "c_month_id",
|
| 1242 |
+
"op": "=",
|
| 1243 |
+
"value": 202604,
|
| 1244 |
+
"value_type": "int"
|
| 1245 |
+
}
|
| 1246 |
+
],
|
| 1247 |
+
"limit": 10000
|
| 1248 |
+
}
|
| 1249 |
+
],
|
| 1250 |
+
"agg_args": [],
|
| 1251 |
+
"infeasible": false
|
| 1252 |
+
},
|
| 1253 |
+
"error": null,
|
| 1254 |
+
"latency_ms": 3686,
|
| 1255 |
+
"tokens": 14832
|
| 1256 |
+
},
|
| 1257 |
+
{
|
| 1258 |
+
"id": "merge_worst_pa_and_mttr_id",
|
| 1259 |
+
"category": "merge",
|
| 1260 |
+
"lang": "id",
|
| 1261 |
+
"carried_over": true,
|
| 1262 |
+
"question": "model mana yang PA-nya paling buruk sekaligus MTTR-nya paling tinggi?",
|
| 1263 |
+
"passed": true,
|
| 1264 |
+
"checks": [
|
| 1265 |
+
{
|
| 1266 |
+
"check": "group_by",
|
| 1267 |
+
"ok": true,
|
| 1268 |
+
"detail": "grouped=True"
|
| 1269 |
+
},
|
| 1270 |
+
{
|
| 1271 |
+
"check": "group_by_col=Model_Unit",
|
| 1272 |
+
"ok": true,
|
| 1273 |
+
"detail": "ids=['c_model_unit', 'c_model_unit'] aliases=[] resolved=['c_model_unit', 'c_model_unit']"
|
| 1274 |
+
}
|
| 1275 |
+
],
|
| 1276 |
+
"facts": {
|
| 1277 |
+
"tools": [
|
| 1278 |
+
"analyze_merge",
|
| 1279 |
+
"check_data",
|
| 1280 |
+
"retrieve_data"
|
| 1281 |
+
],
|
| 1282 |
+
"irs": [
|
| 1283 |
+
{
|
| 1284 |
+
"source_id": "src_pa",
|
| 1285 |
+
"table_id": "t_pa",
|
| 1286 |
+
"select": [
|
| 1287 |
+
{
|
| 1288 |
+
"kind": "column",
|
| 1289 |
+
"column_id": "c_model_unit",
|
| 1290 |
+
"alias": "model"
|
| 1291 |
+
},
|
| 1292 |
+
{
|
| 1293 |
+
"kind": "agg",
|
| 1294 |
+
"fn": "avg",
|
| 1295 |
+
"column_id": "c_pa_percent",
|
| 1296 |
+
"alias": "avg_pa"
|
| 1297 |
+
}
|
| 1298 |
+
],
|
| 1299 |
+
"filters": [
|
| 1300 |
+
{
|
| 1301 |
+
"column_id": "c_from_date",
|
| 1302 |
+
"op": "between",
|
| 1303 |
+
"value": [
|
| 1304 |
+
"2026-04-01",
|
| 1305 |
+
"2026-04-30"
|
| 1306 |
+
],
|
| 1307 |
+
"value_type": "date"
|
| 1308 |
+
}
|
| 1309 |
+
],
|
| 1310 |
+
"group_by": [
|
| 1311 |
+
"c_model_unit"
|
| 1312 |
+
],
|
| 1313 |
+
"order_by": [
|
| 1314 |
+
{
|
| 1315 |
+
"column_id": "avg_pa",
|
| 1316 |
+
"dir": "asc"
|
| 1317 |
+
}
|
| 1318 |
+
],
|
| 1319 |
+
"limit": 1
|
| 1320 |
+
},
|
| 1321 |
+
{
|
| 1322 |
+
"source_id": "src_pa",
|
| 1323 |
+
"table_id": "t_pa",
|
| 1324 |
+
"select": [
|
| 1325 |
+
{
|
| 1326 |
+
"kind": "column",
|
| 1327 |
+
"column_id": "c_model_unit",
|
| 1328 |
+
"alias": "model"
|
| 1329 |
+
},
|
| 1330 |
+
{
|
| 1331 |
+
"kind": "agg",
|
| 1332 |
+
"fn": "avg",
|
| 1333 |
+
"column_id": "c_mttr",
|
| 1334 |
+
"alias": "avg_mttr"
|
| 1335 |
+
}
|
| 1336 |
+
],
|
| 1337 |
+
"filters": [
|
| 1338 |
+
{
|
| 1339 |
+
"column_id": "c_from_date",
|
| 1340 |
+
"op": "between",
|
| 1341 |
+
"value": [
|
| 1342 |
+
"2026-04-01",
|
| 1343 |
+
"2026-04-30"
|
| 1344 |
+
],
|
| 1345 |
+
"value_type": "date"
|
| 1346 |
+
}
|
| 1347 |
+
],
|
| 1348 |
+
"group_by": [
|
| 1349 |
+
"c_model_unit"
|
| 1350 |
+
],
|
| 1351 |
+
"order_by": [
|
| 1352 |
+
{
|
| 1353 |
+
"column_id": "avg_mttr",
|
| 1354 |
+
"dir": "desc"
|
| 1355 |
+
}
|
| 1356 |
+
],
|
| 1357 |
+
"limit": 1
|
| 1358 |
+
}
|
| 1359 |
+
],
|
| 1360 |
+
"agg_args": [],
|
| 1361 |
+
"infeasible": false
|
| 1362 |
+
},
|
| 1363 |
+
"error": null,
|
| 1364 |
+
"latency_ms": 5546,
|
| 1365 |
+
"tokens": 15165
|
| 1366 |
+
},
|
| 1367 |
+
{
|
| 1368 |
+
"id": "raw_rows_low_pa_id",
|
| 1369 |
+
"category": "counter_raw_rows",
|
| 1370 |
+
"lang": "id",
|
| 1371 |
+
"carried_over": true,
|
| 1372 |
+
"question": "tampilkan 10 record dengan PA di bawah 50",
|
| 1373 |
+
"passed": true,
|
| 1374 |
+
"checks": [
|
| 1375 |
+
{
|
| 1376 |
+
"check": "no_group_by",
|
| 1377 |
+
"ok": true,
|
| 1378 |
+
"detail": "grouped=False"
|
| 1379 |
+
},
|
| 1380 |
+
{
|
| 1381 |
+
"check": "has_filter",
|
| 1382 |
+
"ok": true,
|
| 1383 |
+
"detail": "filter_present=True"
|
| 1384 |
+
},
|
| 1385 |
+
{
|
| 1386 |
+
"check": "limit=10",
|
| 1387 |
+
"ok": true,
|
| 1388 |
+
"detail": "limits=[10]"
|
| 1389 |
+
}
|
| 1390 |
+
],
|
| 1391 |
+
"facts": {
|
| 1392 |
+
"tools": [
|
| 1393 |
+
"check_data",
|
| 1394 |
+
"retrieve_data"
|
| 1395 |
+
],
|
| 1396 |
+
"irs": [
|
| 1397 |
+
{
|
| 1398 |
+
"source_id": "src_pa",
|
| 1399 |
+
"table_id": "t_pa",
|
| 1400 |
+
"select": [
|
| 1401 |
+
{
|
| 1402 |
+
"kind": "column",
|
| 1403 |
+
"column_id": "c_keyid",
|
| 1404 |
+
"alias": "key_id"
|
| 1405 |
+
},
|
| 1406 |
+
{
|
| 1407 |
+
"kind": "column",
|
| 1408 |
+
"column_id": "c_month_id",
|
| 1409 |
+
"alias": "month_id"
|
| 1410 |
+
},
|
| 1411 |
+
{
|
| 1412 |
+
"kind": "column",
|
| 1413 |
+
"column_id": "c_site_id",
|
| 1414 |
+
"alias": "site_id"
|
| 1415 |
+
},
|
| 1416 |
+
{
|
| 1417 |
+
"kind": "column",
|
| 1418 |
+
"column_id": "c_from_date",
|
| 1419 |
+
"alias": "from_date"
|
| 1420 |
+
},
|
| 1421 |
+
{
|
| 1422 |
+
"kind": "column",
|
| 1423 |
+
"column_id": "c_to_date",
|
| 1424 |
+
"alias": "to_date"
|
| 1425 |
+
},
|
| 1426 |
+
{
|
| 1427 |
+
"kind": "column",
|
| 1428 |
+
"column_id": "c_time_description",
|
| 1429 |
+
"alias": "time_description"
|
| 1430 |
+
},
|
| 1431 |
+
{
|
| 1432 |
+
"kind": "column",
|
| 1433 |
+
"column_id": "c_model_unit",
|
| 1434 |
+
"alias": "model_unit"
|
| 1435 |
+
},
|
| 1436 |
+
{
|
| 1437 |
+
"kind": "column",
|
| 1438 |
+
"column_id": "c_equipment_number",
|
| 1439 |
+
"alias": "equipment_number"
|
| 1440 |
+
},
|
| 1441 |
+
{
|
| 1442 |
+
"kind": "column",
|
| 1443 |
+
"column_id": "c_equipment_group_id",
|
| 1444 |
+
"alias": "equipment_group_id"
|
| 1445 |
+
},
|
| 1446 |
+
{
|
| 1447 |
+
"kind": "column",
|
| 1448 |
+
"column_id": "c_unit_status",
|
| 1449 |
+
"alias": "unit_status"
|
| 1450 |
+
},
|
| 1451 |
+
{
|
| 1452 |
+
"kind": "column",
|
| 1453 |
+
"column_id": "c_pa_percent",
|
| 1454 |
+
"alias": "pa_percent"
|
| 1455 |
+
}
|
| 1456 |
+
],
|
| 1457 |
+
"filters": [
|
| 1458 |
+
{
|
| 1459 |
+
"column_id": "c_pa_percent",
|
| 1460 |
+
"op": "<",
|
| 1461 |
+
"value": 50,
|
| 1462 |
+
"value_type": "decimal"
|
| 1463 |
+
}
|
| 1464 |
+
],
|
| 1465 |
+
"order_by": [
|
| 1466 |
+
{
|
| 1467 |
+
"column_id": "pa_percent",
|
| 1468 |
+
"dir": "asc"
|
| 1469 |
+
}
|
| 1470 |
+
],
|
| 1471 |
+
"limit": 10
|
| 1472 |
+
}
|
| 1473 |
+
],
|
| 1474 |
+
"agg_args": [],
|
| 1475 |
+
"infeasible": false
|
| 1476 |
+
},
|
| 1477 |
+
"error": null,
|
| 1478 |
+
"latency_ms": 3525,
|
| 1479 |
+
"tokens": 14840
|
| 1480 |
+
},
|
| 1481 |
+
{
|
| 1482 |
+
"id": "raw_rows_head_en",
|
| 1483 |
+
"category": "counter_raw_rows",
|
| 1484 |
+
"lang": "en",
|
| 1485 |
+
"carried_over": true,
|
| 1486 |
+
"question": "show me the first 5 rows of the data",
|
| 1487 |
+
"passed": true,
|
| 1488 |
+
"checks": [
|
| 1489 |
+
{
|
| 1490 |
+
"check": "no_group_by",
|
| 1491 |
+
"ok": true,
|
| 1492 |
+
"detail": "grouped=False"
|
| 1493 |
+
}
|
| 1494 |
+
],
|
| 1495 |
+
"facts": {
|
| 1496 |
+
"tools": [
|
| 1497 |
+
"check_data",
|
| 1498 |
+
"retrieve_data"
|
| 1499 |
+
],
|
| 1500 |
+
"irs": [
|
| 1501 |
+
{
|
| 1502 |
+
"source_id": "src_pa",
|
| 1503 |
+
"table_id": "t_pa",
|
| 1504 |
+
"select": [
|
| 1505 |
+
{
|
| 1506 |
+
"kind": "column",
|
| 1507 |
+
"column_id": "c_keyid",
|
| 1508 |
+
"alias": "KeyId"
|
| 1509 |
+
},
|
| 1510 |
+
{
|
| 1511 |
+
"kind": "column",
|
| 1512 |
+
"column_id": "c_month_id",
|
| 1513 |
+
"alias": "Month_ID"
|
| 1514 |
+
},
|
| 1515 |
+
{
|
| 1516 |
+
"kind": "column",
|
| 1517 |
+
"column_id": "c_site_id",
|
| 1518 |
+
"alias": "Site_ID"
|
| 1519 |
+
},
|
| 1520 |
+
{
|
| 1521 |
+
"kind": "column",
|
| 1522 |
+
"column_id": "c_from_date",
|
| 1523 |
+
"alias": "From_Date"
|
| 1524 |
+
},
|
| 1525 |
+
{
|
| 1526 |
+
"kind": "column",
|
| 1527 |
+
"column_id": "c_to_date",
|
| 1528 |
+
"alias": "To_Date"
|
| 1529 |
+
},
|
| 1530 |
+
{
|
| 1531 |
+
"kind": "column",
|
| 1532 |
+
"column_id": "c_time_description",
|
| 1533 |
+
"alias": "Time_Description"
|
| 1534 |
+
},
|
| 1535 |
+
{
|
| 1536 |
+
"kind": "column",
|
| 1537 |
+
"column_id": "c_model_unit",
|
| 1538 |
+
"alias": "Model_Unit"
|
| 1539 |
+
},
|
| 1540 |
+
{
|
| 1541 |
+
"kind": "column",
|
| 1542 |
+
"column_id": "c_equipment_number",
|
| 1543 |
+
"alias": "Equipment_Number"
|
| 1544 |
+
},
|
| 1545 |
+
{
|
| 1546 |
+
"kind": "column",
|
| 1547 |
+
"column_id": "c_equipment_group_id",
|
| 1548 |
+
"alias": "Equipment_Group_ID"
|
| 1549 |
+
},
|
| 1550 |
+
{
|
| 1551 |
+
"kind": "column",
|
| 1552 |
+
"column_id": "c_unit_status",
|
| 1553 |
+
"alias": "Unit_Status"
|
| 1554 |
+
},
|
| 1555 |
+
{
|
| 1556 |
+
"kind": "column",
|
| 1557 |
+
"column_id": "c_total_breakdown_schedule_hour",
|
| 1558 |
+
"alias": "Total_Breakdown_Schedule_Hour"
|
| 1559 |
+
},
|
| 1560 |
+
{
|
| 1561 |
+
"kind": "column",
|
| 1562 |
+
"column_id": "c_total_breakdown_unschedule_hour",
|
| 1563 |
+
"alias": "Total_Breakdown_Unschedule_Hour"
|
| 1564 |
+
},
|
| 1565 |
+
{
|
| 1566 |
+
"kind": "column",
|
| 1567 |
+
"column_id": "c_total_adj_breakdown_schedule_hour",
|
| 1568 |
+
"alias": "Total_Adj_Breakdown_Schedule_Hour"
|
| 1569 |
+
},
|
| 1570 |
+
{
|
| 1571 |
+
"kind": "column",
|
| 1572 |
+
"column_id": "c_total_adj_breakdown_unschedule_hour",
|
| 1573 |
+
"alias": "Total_Adj_Breakdown_Unschedule_Hour"
|
| 1574 |
+
},
|
| 1575 |
+
{
|
| 1576 |
+
"kind": "column",
|
| 1577 |
+
"column_id": "c_total_mtc_hour",
|
| 1578 |
+
"alias": "Total_MTC_Hour"
|
| 1579 |
+
},
|
| 1580 |
+
{
|
| 1581 |
+
"kind": "column",
|
| 1582 |
+
"column_id": "c_total_down_hour",
|
| 1583 |
+
"alias": "Total_Down_Hour"
|
| 1584 |
+
},
|
| 1585 |
+
{
|
| 1586 |
+
"kind": "column",
|
| 1587 |
+
"column_id": "c_total_frequency_breakdown_schedule",
|
| 1588 |
+
"alias": "Total_Frequency_Breakdown_Schedule"
|
| 1589 |
+
},
|
| 1590 |
+
{
|
| 1591 |
+
"kind": "column",
|
| 1592 |
+
"column_id": "c_total_frequency_breakdown_unschedule",
|
| 1593 |
+
"alias": "Total_Frequency_Breakdown_Unschedule"
|
| 1594 |
+
},
|
| 1595 |
+
{
|
| 1596 |
+
"kind": "column",
|
| 1597 |
+
"column_id": "c_total_frequency_maintenance",
|
| 1598 |
+
"alias": "Total_Frequency_Maintenance"
|
| 1599 |
+
},
|
| 1600 |
+
{
|
| 1601 |
+
"kind": "column",
|
| 1602 |
+
"column_id": "c_total_frequency_tire",
|
| 1603 |
+
"alias": "Total_Frequency_Tire"
|
| 1604 |
+
},
|
| 1605 |
+
{
|
| 1606 |
+
"kind": "column",
|
| 1607 |
+
"column_id": "c_total_frequency_down",
|
| 1608 |
+
"alias": "Total_Frequency_Down"
|
| 1609 |
+
},
|
| 1610 |
+
{
|
| 1611 |
+
"kind": "column",
|
| 1612 |
+
"column_id": "c_total_hours",
|
| 1613 |
+
"alias": "Total_Hours"
|
| 1614 |
+
},
|
| 1615 |
+
{
|
| 1616 |
+
"kind": "column",
|
| 1617 |
+
"column_id": "c_total_inpr_hour",
|
| 1618 |
+
"alias": "Total_INPR_Hour"
|
| 1619 |
+
},
|
| 1620 |
+
{
|
| 1621 |
+
"kind": "column",
|
| 1622 |
+
"column_id": "c_total_record_hm_hour",
|
| 1623 |
+
"alias": "Total_Record_HM_Hour"
|
| 1624 |
+
},
|
| 1625 |
+
{
|
| 1626 |
+
"kind": "column",
|
| 1627 |
+
"column_id": "c_total_hm_mtc_down_hour",
|
| 1628 |
+
"alias": "Total_HM_Mtc_Down_Hour"
|
| 1629 |
+
},
|
| 1630 |
+
{
|
| 1631 |
+
"kind": "column",
|
| 1632 |
+
"column_id": "c_plan_pa_percent",
|
| 1633 |
+
"alias": "Plan_PA_Percent"
|
| 1634 |
+
},
|
| 1635 |
+
{
|
| 1636 |
+
"kind": "column",
|
| 1637 |
+
"column_id": "c_pa_percent",
|
| 1638 |
+
"alias": "PA_Percent"
|
| 1639 |
+
},
|
| 1640 |
+
{
|
| 1641 |
+
"kind": "column",
|
| 1642 |
+
"column_id": "c_mtbs",
|
| 1643 |
+
"alias": "MTBS"
|
| 1644 |
+
},
|
| 1645 |
+
{
|
| 1646 |
+
"kind": "column",
|
| 1647 |
+
"column_id": "c_mttr",
|
| 1648 |
+
"alias": "MTTR"
|
| 1649 |
+
},
|
| 1650 |
+
{
|
| 1651 |
+
"kind": "column",
|
| 1652 |
+
"column_id": "c_sm_percent",
|
| 1653 |
+
"alias": "SM_Percent"
|
| 1654 |
+
},
|
| 1655 |
+
{
|
| 1656 |
+
"kind": "column",
|
| 1657 |
+
"column_id": "c_unschedule_sm_percent",
|
| 1658 |
+
"alias": "Unschedule_SM_Percent"
|
| 1659 |
+
},
|
| 1660 |
+
{
|
| 1661 |
+
"kind": "column",
|
| 1662 |
+
"column_id": "c_isdeleted",
|
| 1663 |
+
"alias": "IsDeleted"
|
| 1664 |
+
},
|
| 1665 |
+
{
|
| 1666 |
+
"kind": "column",
|
| 1667 |
+
"column_id": "c_section",
|
| 1668 |
+
"alias": "Section"
|
| 1669 |
+
},
|
| 1670 |
+
{
|
| 1671 |
+
"kind": "column",
|
| 1672 |
+
"column_id": "c_week_id",
|
| 1673 |
+
"alias": "Week_ID"
|
| 1674 |
+
},
|
| 1675 |
+
{
|
| 1676 |
+
"kind": "column",
|
| 1677 |
+
"column_id": "c_plan_pa_percent_2",
|
| 1678 |
+
"alias": "Plan_PA_Percent_2"
|
| 1679 |
+
},
|
| 1680 |
+
{
|
| 1681 |
+
"kind": "column",
|
| 1682 |
+
"column_id": "c_updated_date",
|
| 1683 |
+
"alias": "Updated_Date"
|
| 1684 |
+
}
|
| 1685 |
+
],
|
| 1686 |
+
"limit": 5
|
| 1687 |
+
}
|
| 1688 |
+
],
|
| 1689 |
+
"agg_args": [],
|
| 1690 |
+
"infeasible": false
|
| 1691 |
+
},
|
| 1692 |
+
"error": null,
|
| 1693 |
+
"latency_ms": 6662,
|
| 1694 |
+
"tokens": 15316
|
| 1695 |
+
},
|
| 1696 |
+
{
|
| 1697 |
+
"id": "exact_model_777d_id",
|
| 1698 |
+
"category": "counter_exact_filter",
|
| 1699 |
+
"lang": "id",
|
| 1700 |
+
"carried_over": true,
|
| 1701 |
+
"question": "berapa banyak record untuk model 777D?",
|
| 1702 |
+
"passed": true,
|
| 1703 |
+
"checks": [
|
| 1704 |
+
{
|
| 1705 |
+
"check": "select_agg=count",
|
| 1706 |
+
"ok": true,
|
| 1707 |
+
"detail": "aggs=['count']"
|
| 1708 |
+
},
|
| 1709 |
+
{
|
| 1710 |
+
"check": "has_filter",
|
| 1711 |
+
"ok": true,
|
| 1712 |
+
"detail": "filter_present=True"
|
| 1713 |
+
}
|
| 1714 |
+
],
|
| 1715 |
+
"facts": {
|
| 1716 |
+
"tools": [
|
| 1717 |
+
"retrieve_data"
|
| 1718 |
+
],
|
| 1719 |
+
"irs": [
|
| 1720 |
+
{
|
| 1721 |
+
"source_id": "src_pa",
|
| 1722 |
+
"table_id": "t_pa",
|
| 1723 |
+
"select": [
|
| 1724 |
+
{
|
| 1725 |
+
"kind": "agg",
|
| 1726 |
+
"fn": "count",
|
| 1727 |
+
"alias": "record_count"
|
| 1728 |
+
}
|
| 1729 |
+
],
|
| 1730 |
+
"filters": [
|
| 1731 |
+
{
|
| 1732 |
+
"column_id": "c_model_unit",
|
| 1733 |
+
"op": "=",
|
| 1734 |
+
"value": "777D",
|
| 1735 |
+
"value_type": "string"
|
| 1736 |
+
}
|
| 1737 |
+
]
|
| 1738 |
+
}
|
| 1739 |
+
],
|
| 1740 |
+
"agg_args": [],
|
| 1741 |
+
"infeasible": false
|
| 1742 |
+
},
|
| 1743 |
+
"error": null,
|
| 1744 |
+
"latency_ms": 2422,
|
| 1745 |
+
"tokens": 14573
|
| 1746 |
+
},
|
| 1747 |
+
{
|
| 1748 |
+
"id": "exact_section_loader_en",
|
| 1749 |
+
"category": "counter_exact_filter",
|
| 1750 |
+
"lang": "en",
|
| 1751 |
+
"carried_over": true,
|
| 1752 |
+
"question": "how many records are in the OB LOADER section?",
|
| 1753 |
+
"passed": true,
|
| 1754 |
+
"checks": [
|
| 1755 |
+
{
|
| 1756 |
+
"check": "select_agg=count",
|
| 1757 |
+
"ok": true,
|
| 1758 |
+
"detail": "aggs=['count']"
|
| 1759 |
+
},
|
| 1760 |
+
{
|
| 1761 |
+
"check": "has_filter",
|
| 1762 |
+
"ok": true,
|
| 1763 |
+
"detail": "filter_present=True"
|
| 1764 |
+
}
|
| 1765 |
+
],
|
| 1766 |
+
"facts": {
|
| 1767 |
+
"tools": [
|
| 1768 |
+
"retrieve_data"
|
| 1769 |
+
],
|
| 1770 |
+
"irs": [
|
| 1771 |
+
{
|
| 1772 |
+
"source_id": "src_pa",
|
| 1773 |
+
"table_id": "t_pa",
|
| 1774 |
+
"select": [
|
| 1775 |
+
{
|
| 1776 |
+
"kind": "agg",
|
| 1777 |
+
"fn": "count",
|
| 1778 |
+
"alias": "record_count"
|
| 1779 |
+
}
|
| 1780 |
+
],
|
| 1781 |
+
"filters": [
|
| 1782 |
+
{
|
| 1783 |
+
"column_id": "c_section",
|
| 1784 |
+
"op": "=",
|
| 1785 |
+
"value": "OB LOADER",
|
| 1786 |
+
"value_type": "string"
|
| 1787 |
+
}
|
| 1788 |
+
]
|
| 1789 |
+
}
|
| 1790 |
+
],
|
| 1791 |
+
"agg_args": [],
|
| 1792 |
+
"infeasible": false
|
| 1793 |
+
},
|
| 1794 |
+
"error": null,
|
| 1795 |
+
"latency_ms": 2378,
|
| 1796 |
+
"tokens": 14570
|
| 1797 |
+
},
|
| 1798 |
+
{
|
| 1799 |
+
"id": "disambig_trend_pa_id",
|
| 1800 |
+
"category": "column_disambiguation",
|
| 1801 |
+
"lang": "id",
|
| 1802 |
+
"carried_over": false,
|
| 1803 |
+
"question": "bagaimana trend PA?",
|
| 1804 |
+
"passed": true,
|
| 1805 |
+
"checks": [
|
| 1806 |
+
{
|
| 1807 |
+
"check": "selects_col=PA_Percent",
|
| 1808 |
+
"ok": true,
|
| 1809 |
+
"detail": "selected=['c_from_date', 'c_pa_percent']"
|
| 1810 |
+
},
|
| 1811 |
+
{
|
| 1812 |
+
"check": "not_selects_col=Plan_PA_Percent",
|
| 1813 |
+
"ok": true,
|
| 1814 |
+
"detail": "selected=['c_from_date', 'c_pa_percent']"
|
| 1815 |
+
}
|
| 1816 |
+
],
|
| 1817 |
+
"facts": {
|
| 1818 |
+
"tools": [
|
| 1819 |
+
"analyze_trend",
|
| 1820 |
+
"check_data",
|
| 1821 |
+
"retrieve_data"
|
| 1822 |
+
],
|
| 1823 |
+
"irs": [
|
| 1824 |
+
{
|
| 1825 |
+
"source_id": "src_pa",
|
| 1826 |
+
"table_id": "t_pa",
|
| 1827 |
+
"select": [
|
| 1828 |
+
{
|
| 1829 |
+
"kind": "column",
|
| 1830 |
+
"column_id": "c_from_date",
|
| 1831 |
+
"alias": "date"
|
| 1832 |
+
},
|
| 1833 |
+
{
|
| 1834 |
+
"kind": "column",
|
| 1835 |
+
"column_id": "c_pa_percent",
|
| 1836 |
+
"alias": "pa_percent"
|
| 1837 |
+
}
|
| 1838 |
+
],
|
| 1839 |
+
"filters": [
|
| 1840 |
+
{
|
| 1841 |
+
"column_id": "c_month_id",
|
| 1842 |
+
"op": "=",
|
| 1843 |
+
"value": 202604,
|
| 1844 |
+
"value_type": "int"
|
| 1845 |
+
},
|
| 1846 |
+
{
|
| 1847 |
+
"column_id": "c_isdeleted",
|
| 1848 |
+
"op": "=",
|
| 1849 |
+
"value": 0,
|
| 1850 |
+
"value_type": "int"
|
| 1851 |
+
}
|
| 1852 |
+
],
|
| 1853 |
+
"limit": 10000
|
| 1854 |
+
}
|
| 1855 |
+
],
|
| 1856 |
+
"agg_args": [],
|
| 1857 |
+
"infeasible": false
|
| 1858 |
+
},
|
| 1859 |
+
"error": null,
|
| 1860 |
+
"latency_ms": 3527,
|
| 1861 |
+
"tokens": 14860
|
| 1862 |
+
},
|
| 1863 |
+
{
|
| 1864 |
+
"id": "disambig_avg_pa_en",
|
| 1865 |
+
"category": "column_disambiguation",
|
| 1866 |
+
"lang": "en",
|
| 1867 |
+
"carried_over": false,
|
| 1868 |
+
"question": "what is the average PA per section?",
|
| 1869 |
+
"passed": true,
|
| 1870 |
+
"checks": [
|
| 1871 |
+
{
|
| 1872 |
+
"check": "selects_col=PA_Percent",
|
| 1873 |
+
"ok": true,
|
| 1874 |
+
"detail": "selected=['c_section', 'c_pa_percent']"
|
| 1875 |
+
},
|
| 1876 |
+
{
|
| 1877 |
+
"check": "not_selects_col=Plan_PA_Percent",
|
| 1878 |
+
"ok": true,
|
| 1879 |
+
"detail": "selected=['c_section', 'c_pa_percent']"
|
| 1880 |
+
}
|
| 1881 |
+
],
|
| 1882 |
+
"facts": {
|
| 1883 |
+
"tools": [
|
| 1884 |
+
"analyze_aggregate",
|
| 1885 |
+
"check_data",
|
| 1886 |
+
"retrieve_data"
|
| 1887 |
+
],
|
| 1888 |
+
"irs": [
|
| 1889 |
+
{
|
| 1890 |
+
"source_id": "src_pa",
|
| 1891 |
+
"table_id": "t_pa",
|
| 1892 |
+
"select": [
|
| 1893 |
+
{
|
| 1894 |
+
"kind": "column",
|
| 1895 |
+
"column_id": "c_section",
|
| 1896 |
+
"alias": "section"
|
| 1897 |
+
},
|
| 1898 |
+
{
|
| 1899 |
+
"kind": "column",
|
| 1900 |
+
"column_id": "c_pa_percent",
|
| 1901 |
+
"alias": "pa_percent"
|
| 1902 |
+
}
|
| 1903 |
+
],
|
| 1904 |
+
"filters": [
|
| 1905 |
+
{
|
| 1906 |
+
"column_id": "c_month_id",
|
| 1907 |
+
"op": "=",
|
| 1908 |
+
"value": 202604,
|
| 1909 |
+
"value_type": "int"
|
| 1910 |
+
},
|
| 1911 |
+
{
|
| 1912 |
+
"column_id": "c_isdeleted",
|
| 1913 |
+
"op": "=",
|
| 1914 |
+
"value": 0,
|
| 1915 |
+
"value_type": "int"
|
| 1916 |
+
}
|
| 1917 |
+
],
|
| 1918 |
+
"limit": 10000
|
| 1919 |
+
}
|
| 1920 |
+
],
|
| 1921 |
+
"agg_args": [
|
| 1922 |
+
{
|
| 1923 |
+
"data": "${t2}",
|
| 1924 |
+
"aggregations": {
|
| 1925 |
+
"pa_percent": [
|
| 1926 |
+
"mean"
|
| 1927 |
+
]
|
| 1928 |
+
},
|
| 1929 |
+
"group_by": [
|
| 1930 |
+
"section"
|
| 1931 |
+
]
|
| 1932 |
+
}
|
| 1933 |
+
],
|
| 1934 |
+
"infeasible": false
|
| 1935 |
+
},
|
| 1936 |
+
"error": null,
|
| 1937 |
+
"latency_ms": 3972,
|
| 1938 |
+
"tokens": 14810
|
| 1939 |
+
},
|
| 1940 |
+
{
|
| 1941 |
+
"id": "chart_trend_pa_id",
|
| 1942 |
+
"category": "chart",
|
| 1943 |
+
"lang": "id",
|
| 1944 |
+
"carried_over": false,
|
| 1945 |
+
"question": "bagaimana visualisasi trend PA?",
|
| 1946 |
+
"passed": false,
|
| 1947 |
+
"checks": [
|
| 1948 |
+
{
|
| 1949 |
+
"check": "has_tool=render_chart",
|
| 1950 |
+
"ok": true,
|
| 1951 |
+
"detail": "tools=['check_data', 'render_chart', 'retrieve_data']"
|
| 1952 |
+
},
|
| 1953 |
+
{
|
| 1954 |
+
"check": "any_tool=['analyze_aggregate', 'analyze_trend']",
|
| 1955 |
+
"ok": false,
|
| 1956 |
+
"detail": "tools=['check_data', 'render_chart', 'retrieve_data']"
|
| 1957 |
+
},
|
| 1958 |
+
{
|
| 1959 |
+
"check": "selects_col=PA_Percent",
|
| 1960 |
+
"ok": true,
|
| 1961 |
+
"detail": "selected=['c_from_date', 'c_pa_percent']"
|
| 1962 |
+
},
|
| 1963 |
+
{
|
| 1964 |
+
"check": "not_selects_col=Plan_PA_Percent",
|
| 1965 |
+
"ok": true,
|
| 1966 |
+
"detail": "selected=['c_from_date', 'c_pa_percent']"
|
| 1967 |
+
}
|
| 1968 |
+
],
|
| 1969 |
+
"facts": {
|
| 1970 |
+
"tools": [
|
| 1971 |
+
"check_data",
|
| 1972 |
+
"render_chart",
|
| 1973 |
+
"retrieve_data"
|
| 1974 |
+
],
|
| 1975 |
+
"irs": [
|
| 1976 |
+
{
|
| 1977 |
+
"source_id": "src_pa",
|
| 1978 |
+
"table_id": "t_pa",
|
| 1979 |
+
"select": [
|
| 1980 |
+
{
|
| 1981 |
+
"kind": "column",
|
| 1982 |
+
"column_id": "c_from_date",
|
| 1983 |
+
"alias": "from_date"
|
| 1984 |
+
},
|
| 1985 |
+
{
|
| 1986 |
+
"kind": "column",
|
| 1987 |
+
"column_id": "c_pa_percent",
|
| 1988 |
+
"alias": "pa_percent"
|
| 1989 |
+
}
|
| 1990 |
+
],
|
| 1991 |
+
"filters": [
|
| 1992 |
+
{
|
| 1993 |
+
"column_id": "c_month_id",
|
| 1994 |
+
"op": "=",
|
| 1995 |
+
"value": 202604,
|
| 1996 |
+
"value_type": "int"
|
| 1997 |
+
},
|
| 1998 |
+
{
|
| 1999 |
+
"column_id": "c_isdeleted",
|
| 2000 |
+
"op": "=",
|
| 2001 |
+
"value": 0,
|
| 2002 |
+
"value_type": "int"
|
| 2003 |
+
}
|
| 2004 |
+
],
|
| 2005 |
+
"limit": 10000
|
| 2006 |
+
}
|
| 2007 |
+
],
|
| 2008 |
+
"agg_args": [],
|
| 2009 |
+
"infeasible": false
|
| 2010 |
+
},
|
| 2011 |
+
"error": null,
|
| 2012 |
+
"latency_ms": 3641,
|
| 2013 |
+
"tokens": 14859
|
| 2014 |
+
},
|
| 2015 |
+
{
|
| 2016 |
+
"id": "chart_avg_pa_by_section_en",
|
| 2017 |
+
"category": "chart",
|
| 2018 |
+
"lang": "en",
|
| 2019 |
+
"carried_over": false,
|
| 2020 |
+
"question": "show me a bar chart of average PA per section",
|
| 2021 |
+
"passed": true,
|
| 2022 |
+
"checks": [
|
| 2023 |
+
{
|
| 2024 |
+
"check": "has_tool=render_chart",
|
| 2025 |
+
"ok": true,
|
| 2026 |
+
"detail": "tools=['check_data', 'render_chart', 'retrieve_data']"
|
| 2027 |
+
},
|
| 2028 |
+
{
|
| 2029 |
+
"check": "group_by",
|
| 2030 |
+
"ok": true,
|
| 2031 |
+
"detail": "grouped=True"
|
| 2032 |
+
},
|
| 2033 |
+
{
|
| 2034 |
+
"check": "selects_col=PA_Percent",
|
| 2035 |
+
"ok": true,
|
| 2036 |
+
"detail": "selected=['c_section', 'c_pa_percent']"
|
| 2037 |
+
}
|
| 2038 |
+
],
|
| 2039 |
+
"facts": {
|
| 2040 |
+
"tools": [
|
| 2041 |
+
"check_data",
|
| 2042 |
+
"render_chart",
|
| 2043 |
+
"retrieve_data"
|
| 2044 |
+
],
|
| 2045 |
+
"irs": [
|
| 2046 |
+
{
|
| 2047 |
+
"source_id": "src_pa",
|
| 2048 |
+
"table_id": "t_pa",
|
| 2049 |
+
"select": [
|
| 2050 |
+
{
|
| 2051 |
+
"kind": "column",
|
| 2052 |
+
"column_id": "c_section",
|
| 2053 |
+
"alias": "section"
|
| 2054 |
+
},
|
| 2055 |
+
{
|
| 2056 |
+
"kind": "agg",
|
| 2057 |
+
"fn": "avg",
|
| 2058 |
+
"column_id": "c_pa_percent",
|
| 2059 |
+
"alias": "avg_pa"
|
| 2060 |
+
}
|
| 2061 |
+
],
|
| 2062 |
+
"group_by": [
|
| 2063 |
+
"c_section"
|
| 2064 |
+
]
|
| 2065 |
+
}
|
| 2066 |
+
],
|
| 2067 |
+
"agg_args": [],
|
| 2068 |
+
"infeasible": false
|
| 2069 |
+
},
|
| 2070 |
+
"error": null,
|
| 2071 |
+
"latency_ms": 3166,
|
| 2072 |
+
"tokens": 14769
|
| 2073 |
+
},
|
| 2074 |
+
{
|
| 2075 |
+
"id": "infeasible_churn_id",
|
| 2076 |
+
"category": "infeasible",
|
| 2077 |
+
"lang": "id",
|
| 2078 |
+
"carried_over": true,
|
| 2079 |
+
"question": "berapa churn rate pelanggan?",
|
| 2080 |
+
"passed": true,
|
| 2081 |
+
"checks": [
|
| 2082 |
+
{
|
| 2083 |
+
"check": "infeasible",
|
| 2084 |
+
"ok": true,
|
| 2085 |
+
"detail": "infeasible=True"
|
| 2086 |
+
}
|
| 2087 |
+
],
|
| 2088 |
+
"facts": {
|
| 2089 |
+
"tools": [],
|
| 2090 |
+
"irs": [],
|
| 2091 |
+
"agg_args": [],
|
| 2092 |
+
"infeasible": true
|
| 2093 |
+
},
|
| 2094 |
+
"error": null,
|
| 2095 |
+
"latency_ms": 2055,
|
| 2096 |
+
"tokens": 14486
|
| 2097 |
+
},
|
| 2098 |
+
{
|
| 2099 |
+
"id": "infeasible_profit_en",
|
| 2100 |
+
"category": "infeasible",
|
| 2101 |
+
"lang": "en",
|
| 2102 |
+
"carried_over": true,
|
| 2103 |
+
"question": "what is the monthly profit margin?",
|
| 2104 |
+
"passed": true,
|
| 2105 |
+
"checks": [
|
| 2106 |
+
{
|
| 2107 |
+
"check": "infeasible",
|
| 2108 |
+
"ok": true,
|
| 2109 |
+
"detail": "infeasible=True"
|
| 2110 |
+
}
|
| 2111 |
+
],
|
| 2112 |
+
"facts": {
|
| 2113 |
+
"tools": [],
|
| 2114 |
+
"irs": [],
|
| 2115 |
+
"agg_args": [],
|
| 2116 |
+
"infeasible": true
|
| 2117 |
+
},
|
| 2118 |
+
"error": null,
|
| 2119 |
+
"latency_ms": 2072,
|
| 2120 |
+
"tokens": 14501
|
| 2121 |
+
}
|
| 2122 |
+
]
|
| 2123 |
+
}
|
eval/readiness/results/readiness_result_2026-06-23_094622.json
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run": {
|
| 3 |
+
"timestamp": "2026-06-23T09:46:22",
|
| 4 |
+
"dataset": "readiness_dataset.json",
|
| 5 |
+
"target": "src/agents/report/readiness.is_report_ready",
|
| 6 |
+
"total": 17,
|
| 7 |
+
"passed": 17,
|
| 8 |
+
"accuracy": 1.0,
|
| 9 |
+
"runtime_avg_ms": 0.0
|
| 10 |
+
},
|
| 11 |
+
"alignment_gap": {
|
| 12 |
+
"count": 2,
|
| 13 |
+
"ids": [
|
| 14 |
+
"align_01",
|
| 15 |
+
"align_02"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"by_group": {
|
| 19 |
+
"floor": {
|
| 20 |
+
"n": 8,
|
| 21 |
+
"passed": 8,
|
| 22 |
+
"accuracy": 1.0
|
| 23 |
+
},
|
| 24 |
+
"delta": {
|
| 25 |
+
"n": 5,
|
| 26 |
+
"passed": 5,
|
| 27 |
+
"accuracy": 1.0
|
| 28 |
+
},
|
| 29 |
+
"edge": {
|
| 30 |
+
"n": 1,
|
| 31 |
+
"passed": 1,
|
| 32 |
+
"accuracy": 1.0
|
| 33 |
+
},
|
| 34 |
+
"alignment": {
|
| 35 |
+
"n": 3,
|
| 36 |
+
"passed": 3,
|
| 37 |
+
"accuracy": 1.0
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"cases": [
|
| 41 |
+
{
|
| 42 |
+
"id": "floor_01",
|
| 43 |
+
"group": "floor",
|
| 44 |
+
"expected_ready": false,
|
| 45 |
+
"got_ready": false,
|
| 46 |
+
"expected_missing": [
|
| 47 |
+
"a validated problem statement",
|
| 48 |
+
"at least one completed analysis"
|
| 49 |
+
],
|
| 50 |
+
"got_missing": [
|
| 51 |
+
"a validated problem statement",
|
| 52 |
+
"at least one completed analysis"
|
| 53 |
+
],
|
| 54 |
+
"correct": true,
|
| 55 |
+
"aligned": false,
|
| 56 |
+
"gap": false,
|
| 57 |
+
"latency_ms": 0.0
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"id": "floor_02",
|
| 61 |
+
"group": "floor",
|
| 62 |
+
"expected_ready": false,
|
| 63 |
+
"got_ready": false,
|
| 64 |
+
"expected_missing": [
|
| 65 |
+
"a validated problem statement"
|
| 66 |
+
],
|
| 67 |
+
"got_missing": [
|
| 68 |
+
"a validated problem statement"
|
| 69 |
+
],
|
| 70 |
+
"correct": true,
|
| 71 |
+
"aligned": true,
|
| 72 |
+
"gap": false,
|
| 73 |
+
"latency_ms": 0.0
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"id": "floor_03",
|
| 77 |
+
"group": "floor",
|
| 78 |
+
"expected_ready": false,
|
| 79 |
+
"got_ready": false,
|
| 80 |
+
"expected_missing": [
|
| 81 |
+
"at least one completed analysis"
|
| 82 |
+
],
|
| 83 |
+
"got_missing": [
|
| 84 |
+
"at least one completed analysis"
|
| 85 |
+
],
|
| 86 |
+
"correct": true,
|
| 87 |
+
"aligned": false,
|
| 88 |
+
"gap": false,
|
| 89 |
+
"latency_ms": 0.0
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "floor_04",
|
| 93 |
+
"group": "floor",
|
| 94 |
+
"expected_ready": false,
|
| 95 |
+
"got_ready": false,
|
| 96 |
+
"expected_missing": [
|
| 97 |
+
"at least one completed analysis"
|
| 98 |
+
],
|
| 99 |
+
"got_missing": [
|
| 100 |
+
"at least one completed analysis"
|
| 101 |
+
],
|
| 102 |
+
"correct": true,
|
| 103 |
+
"aligned": false,
|
| 104 |
+
"gap": false,
|
| 105 |
+
"latency_ms": 0.0
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"id": "floor_05",
|
| 109 |
+
"group": "floor",
|
| 110 |
+
"expected_ready": false,
|
| 111 |
+
"got_ready": false,
|
| 112 |
+
"expected_missing": [
|
| 113 |
+
"at least one completed analysis"
|
| 114 |
+
],
|
| 115 |
+
"got_missing": [
|
| 116 |
+
"at least one completed analysis"
|
| 117 |
+
],
|
| 118 |
+
"correct": true,
|
| 119 |
+
"aligned": false,
|
| 120 |
+
"gap": false,
|
| 121 |
+
"latency_ms": 0.0
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"id": "floor_06",
|
| 125 |
+
"group": "floor",
|
| 126 |
+
"expected_ready": true,
|
| 127 |
+
"got_ready": true,
|
| 128 |
+
"expected_missing": [],
|
| 129 |
+
"got_missing": [],
|
| 130 |
+
"correct": true,
|
| 131 |
+
"aligned": true,
|
| 132 |
+
"gap": false,
|
| 133 |
+
"latency_ms": 0.0
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": "floor_07",
|
| 137 |
+
"group": "floor",
|
| 138 |
+
"expected_ready": true,
|
| 139 |
+
"got_ready": true,
|
| 140 |
+
"expected_missing": [],
|
| 141 |
+
"got_missing": [],
|
| 142 |
+
"correct": true,
|
| 143 |
+
"aligned": true,
|
| 144 |
+
"gap": false,
|
| 145 |
+
"latency_ms": 0.0
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"id": "floor_08",
|
| 149 |
+
"group": "floor",
|
| 150 |
+
"expected_ready": true,
|
| 151 |
+
"got_ready": true,
|
| 152 |
+
"expected_missing": [],
|
| 153 |
+
"got_missing": [],
|
| 154 |
+
"correct": true,
|
| 155 |
+
"aligned": true,
|
| 156 |
+
"gap": false,
|
| 157 |
+
"latency_ms": 0.0
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"id": "delta_01",
|
| 161 |
+
"group": "delta",
|
| 162 |
+
"expected_ready": false,
|
| 163 |
+
"got_ready": false,
|
| 164 |
+
"expected_missing": [
|
| 165 |
+
"a new analysis since the last report"
|
| 166 |
+
],
|
| 167 |
+
"got_missing": [
|
| 168 |
+
"a new analysis since the last report"
|
| 169 |
+
],
|
| 170 |
+
"correct": true,
|
| 171 |
+
"aligned": true,
|
| 172 |
+
"gap": false,
|
| 173 |
+
"latency_ms": 0.0
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"id": "delta_02",
|
| 177 |
+
"group": "delta",
|
| 178 |
+
"expected_ready": true,
|
| 179 |
+
"got_ready": true,
|
| 180 |
+
"expected_missing": [],
|
| 181 |
+
"got_missing": [],
|
| 182 |
+
"correct": true,
|
| 183 |
+
"aligned": true,
|
| 184 |
+
"gap": false,
|
| 185 |
+
"latency_ms": 0.0
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"id": "delta_03",
|
| 189 |
+
"group": "delta",
|
| 190 |
+
"expected_ready": true,
|
| 191 |
+
"got_ready": true,
|
| 192 |
+
"expected_missing": [],
|
| 193 |
+
"got_missing": [],
|
| 194 |
+
"correct": true,
|
| 195 |
+
"aligned": true,
|
| 196 |
+
"gap": false,
|
| 197 |
+
"latency_ms": 0.0
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"id": "delta_04",
|
| 201 |
+
"group": "delta",
|
| 202 |
+
"expected_ready": false,
|
| 203 |
+
"got_ready": false,
|
| 204 |
+
"expected_missing": [
|
| 205 |
+
"a new analysis since the last report"
|
| 206 |
+
],
|
| 207 |
+
"got_missing": [
|
| 208 |
+
"a new analysis since the last report"
|
| 209 |
+
],
|
| 210 |
+
"correct": true,
|
| 211 |
+
"aligned": true,
|
| 212 |
+
"gap": false,
|
| 213 |
+
"latency_ms": 0.0
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
"id": "delta_05",
|
| 217 |
+
"group": "delta",
|
| 218 |
+
"expected_ready": false,
|
| 219 |
+
"got_ready": false,
|
| 220 |
+
"expected_missing": [
|
| 221 |
+
"a new analysis since the last report"
|
| 222 |
+
],
|
| 223 |
+
"got_missing": [
|
| 224 |
+
"a new analysis since the last report"
|
| 225 |
+
],
|
| 226 |
+
"correct": true,
|
| 227 |
+
"aligned": true,
|
| 228 |
+
"gap": false,
|
| 229 |
+
"latency_ms": 0.0
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"id": "edge_01",
|
| 233 |
+
"group": "edge",
|
| 234 |
+
"expected_ready": false,
|
| 235 |
+
"got_ready": false,
|
| 236 |
+
"expected_missing": [
|
| 237 |
+
"at least one completed analysis"
|
| 238 |
+
],
|
| 239 |
+
"got_missing": [
|
| 240 |
+
"at least one completed analysis"
|
| 241 |
+
],
|
| 242 |
+
"correct": true,
|
| 243 |
+
"aligned": false,
|
| 244 |
+
"gap": false,
|
| 245 |
+
"latency_ms": 0.0
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"id": "align_01",
|
| 249 |
+
"group": "alignment",
|
| 250 |
+
"expected_ready": true,
|
| 251 |
+
"got_ready": true,
|
| 252 |
+
"expected_missing": [],
|
| 253 |
+
"got_missing": [],
|
| 254 |
+
"correct": true,
|
| 255 |
+
"aligned": false,
|
| 256 |
+
"gap": true,
|
| 257 |
+
"latency_ms": 0.0
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"id": "align_02",
|
| 261 |
+
"group": "alignment",
|
| 262 |
+
"expected_ready": true,
|
| 263 |
+
"got_ready": true,
|
| 264 |
+
"expected_missing": [],
|
| 265 |
+
"got_missing": [],
|
| 266 |
+
"correct": true,
|
| 267 |
+
"aligned": false,
|
| 268 |
+
"gap": true,
|
| 269 |
+
"latency_ms": 0.0
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
"id": "align_03",
|
| 273 |
+
"group": "alignment",
|
| 274 |
+
"expected_ready": true,
|
| 275 |
+
"got_ready": true,
|
| 276 |
+
"expected_missing": [],
|
| 277 |
+
"got_missing": [],
|
| 278 |
+
"correct": true,
|
| 279 |
+
"aligned": true,
|
| 280 |
+
"gap": false,
|
| 281 |
+
"latency_ms": 0.0
|
| 282 |
+
}
|
| 283 |
+
]
|
| 284 |
+
}
|
eval/readiness/results/readiness_result_2026-07-02_083335.json
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run": {
|
| 3 |
+
"timestamp": "2026-07-02T08:33:35",
|
| 4 |
+
"dataset": "readiness_dataset.json",
|
| 5 |
+
"target": "src/agents/report/readiness.is_report_ready",
|
| 6 |
+
"total": 15,
|
| 7 |
+
"passed": 15,
|
| 8 |
+
"accuracy": 1.0,
|
| 9 |
+
"runtime_avg_ms": 0.0
|
| 10 |
+
},
|
| 11 |
+
"alignment_gap": {
|
| 12 |
+
"count": 2,
|
| 13 |
+
"ids": [
|
| 14 |
+
"align_01",
|
| 15 |
+
"align_02"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"by_group": {
|
| 19 |
+
"floor": {
|
| 20 |
+
"n": 6,
|
| 21 |
+
"passed": 6,
|
| 22 |
+
"accuracy": 1.0
|
| 23 |
+
},
|
| 24 |
+
"delta": {
|
| 25 |
+
"n": 5,
|
| 26 |
+
"passed": 5,
|
| 27 |
+
"accuracy": 1.0
|
| 28 |
+
},
|
| 29 |
+
"edge": {
|
| 30 |
+
"n": 1,
|
| 31 |
+
"passed": 1,
|
| 32 |
+
"accuracy": 1.0
|
| 33 |
+
},
|
| 34 |
+
"alignment": {
|
| 35 |
+
"n": 3,
|
| 36 |
+
"passed": 3,
|
| 37 |
+
"accuracy": 1.0
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"cases": [
|
| 41 |
+
{
|
| 42 |
+
"id": "floor_01",
|
| 43 |
+
"group": "floor",
|
| 44 |
+
"expected_ready": false,
|
| 45 |
+
"got_ready": false,
|
| 46 |
+
"expected_missing": [
|
| 47 |
+
"at least one completed analysis"
|
| 48 |
+
],
|
| 49 |
+
"got_missing": [
|
| 50 |
+
"at least one completed analysis"
|
| 51 |
+
],
|
| 52 |
+
"correct": true,
|
| 53 |
+
"aligned": false,
|
| 54 |
+
"gap": false,
|
| 55 |
+
"latency_ms": 0.0
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "floor_02",
|
| 59 |
+
"group": "floor",
|
| 60 |
+
"expected_ready": false,
|
| 61 |
+
"got_ready": false,
|
| 62 |
+
"expected_missing": [
|
| 63 |
+
"at least one completed analysis"
|
| 64 |
+
],
|
| 65 |
+
"got_missing": [
|
| 66 |
+
"at least one completed analysis"
|
| 67 |
+
],
|
| 68 |
+
"correct": true,
|
| 69 |
+
"aligned": false,
|
| 70 |
+
"gap": false,
|
| 71 |
+
"latency_ms": 0.0
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"id": "floor_03",
|
| 75 |
+
"group": "floor",
|
| 76 |
+
"expected_ready": false,
|
| 77 |
+
"got_ready": false,
|
| 78 |
+
"expected_missing": [
|
| 79 |
+
"at least one completed analysis"
|
| 80 |
+
],
|
| 81 |
+
"got_missing": [
|
| 82 |
+
"at least one completed analysis"
|
| 83 |
+
],
|
| 84 |
+
"correct": true,
|
| 85 |
+
"aligned": false,
|
| 86 |
+
"gap": false,
|
| 87 |
+
"latency_ms": 0.0
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"id": "floor_04",
|
| 91 |
+
"group": "floor",
|
| 92 |
+
"expected_ready": true,
|
| 93 |
+
"got_ready": true,
|
| 94 |
+
"expected_missing": [],
|
| 95 |
+
"got_missing": [],
|
| 96 |
+
"correct": true,
|
| 97 |
+
"aligned": true,
|
| 98 |
+
"gap": false,
|
| 99 |
+
"latency_ms": 0.0
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"id": "floor_05",
|
| 103 |
+
"group": "floor",
|
| 104 |
+
"expected_ready": true,
|
| 105 |
+
"got_ready": true,
|
| 106 |
+
"expected_missing": [],
|
| 107 |
+
"got_missing": [],
|
| 108 |
+
"correct": true,
|
| 109 |
+
"aligned": true,
|
| 110 |
+
"gap": false,
|
| 111 |
+
"latency_ms": 0.0
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"id": "floor_06",
|
| 115 |
+
"group": "floor",
|
| 116 |
+
"expected_ready": true,
|
| 117 |
+
"got_ready": true,
|
| 118 |
+
"expected_missing": [],
|
| 119 |
+
"got_missing": [],
|
| 120 |
+
"correct": true,
|
| 121 |
+
"aligned": true,
|
| 122 |
+
"gap": false,
|
| 123 |
+
"latency_ms": 0.0
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"id": "delta_01",
|
| 127 |
+
"group": "delta",
|
| 128 |
+
"expected_ready": false,
|
| 129 |
+
"got_ready": false,
|
| 130 |
+
"expected_missing": [
|
| 131 |
+
"a new analysis since the last report"
|
| 132 |
+
],
|
| 133 |
+
"got_missing": [
|
| 134 |
+
"a new analysis since the last report"
|
| 135 |
+
],
|
| 136 |
+
"correct": true,
|
| 137 |
+
"aligned": true,
|
| 138 |
+
"gap": false,
|
| 139 |
+
"latency_ms": 0.0
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"id": "delta_02",
|
| 143 |
+
"group": "delta",
|
| 144 |
+
"expected_ready": true,
|
| 145 |
+
"got_ready": true,
|
| 146 |
+
"expected_missing": [],
|
| 147 |
+
"got_missing": [],
|
| 148 |
+
"correct": true,
|
| 149 |
+
"aligned": true,
|
| 150 |
+
"gap": false,
|
| 151 |
+
"latency_ms": 0.0
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "delta_03",
|
| 155 |
+
"group": "delta",
|
| 156 |
+
"expected_ready": true,
|
| 157 |
+
"got_ready": true,
|
| 158 |
+
"expected_missing": [],
|
| 159 |
+
"got_missing": [],
|
| 160 |
+
"correct": true,
|
| 161 |
+
"aligned": true,
|
| 162 |
+
"gap": false,
|
| 163 |
+
"latency_ms": 0.0
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"id": "delta_04",
|
| 167 |
+
"group": "delta",
|
| 168 |
+
"expected_ready": false,
|
| 169 |
+
"got_ready": false,
|
| 170 |
+
"expected_missing": [
|
| 171 |
+
"a new analysis since the last report"
|
| 172 |
+
],
|
| 173 |
+
"got_missing": [
|
| 174 |
+
"a new analysis since the last report"
|
| 175 |
+
],
|
| 176 |
+
"correct": true,
|
| 177 |
+
"aligned": true,
|
| 178 |
+
"gap": false,
|
| 179 |
+
"latency_ms": 0.0
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"id": "delta_05",
|
| 183 |
+
"group": "delta",
|
| 184 |
+
"expected_ready": false,
|
| 185 |
+
"got_ready": false,
|
| 186 |
+
"expected_missing": [
|
| 187 |
+
"a new analysis since the last report"
|
| 188 |
+
],
|
| 189 |
+
"got_missing": [
|
| 190 |
+
"a new analysis since the last report"
|
| 191 |
+
],
|
| 192 |
+
"correct": true,
|
| 193 |
+
"aligned": true,
|
| 194 |
+
"gap": false,
|
| 195 |
+
"latency_ms": 0.0
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"id": "edge_01",
|
| 199 |
+
"group": "edge",
|
| 200 |
+
"expected_ready": false,
|
| 201 |
+
"got_ready": false,
|
| 202 |
+
"expected_missing": [
|
| 203 |
+
"at least one completed analysis"
|
| 204 |
+
],
|
| 205 |
+
"got_missing": [
|
| 206 |
+
"at least one completed analysis"
|
| 207 |
+
],
|
| 208 |
+
"correct": true,
|
| 209 |
+
"aligned": false,
|
| 210 |
+
"gap": false,
|
| 211 |
+
"latency_ms": 0.0
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"id": "align_01",
|
| 215 |
+
"group": "alignment",
|
| 216 |
+
"expected_ready": true,
|
| 217 |
+
"got_ready": true,
|
| 218 |
+
"expected_missing": [],
|
| 219 |
+
"got_missing": [],
|
| 220 |
+
"correct": true,
|
| 221 |
+
"aligned": false,
|
| 222 |
+
"gap": true,
|
| 223 |
+
"latency_ms": 0.0
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"id": "align_02",
|
| 227 |
+
"group": "alignment",
|
| 228 |
+
"expected_ready": true,
|
| 229 |
+
"got_ready": true,
|
| 230 |
+
"expected_missing": [],
|
| 231 |
+
"got_missing": [],
|
| 232 |
+
"correct": true,
|
| 233 |
+
"aligned": false,
|
| 234 |
+
"gap": true,
|
| 235 |
+
"latency_ms": 0.0
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"id": "align_03",
|
| 239 |
+
"group": "alignment",
|
| 240 |
+
"expected_ready": true,
|
| 241 |
+
"got_ready": true,
|
| 242 |
+
"expected_missing": [],
|
| 243 |
+
"got_missing": [],
|
| 244 |
+
"correct": true,
|
| 245 |
+
"aligned": true,
|
| 246 |
+
"gap": false,
|
| 247 |
+
"latency_ms": 0.0
|
| 248 |
+
}
|
| 249 |
+
]
|
| 250 |
+
}
|
eval/readiness/results/readiness_result_2026-07-02_093621.json
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run": {
|
| 3 |
+
"timestamp": "2026-07-02T09:36:21",
|
| 4 |
+
"dataset": "readiness_dataset.json",
|
| 5 |
+
"target": "src/agents/report/readiness.is_report_ready",
|
| 6 |
+
"total": 15,
|
| 7 |
+
"passed": 15,
|
| 8 |
+
"accuracy": 1.0,
|
| 9 |
+
"runtime_avg_ms": 0.0
|
| 10 |
+
},
|
| 11 |
+
"alignment_gap": {
|
| 12 |
+
"count": 2,
|
| 13 |
+
"ids": [
|
| 14 |
+
"align_01",
|
| 15 |
+
"align_02"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"by_group": {
|
| 19 |
+
"floor": {
|
| 20 |
+
"n": 6,
|
| 21 |
+
"passed": 6,
|
| 22 |
+
"accuracy": 1.0
|
| 23 |
+
},
|
| 24 |
+
"delta": {
|
| 25 |
+
"n": 5,
|
| 26 |
+
"passed": 5,
|
| 27 |
+
"accuracy": 1.0
|
| 28 |
+
},
|
| 29 |
+
"edge": {
|
| 30 |
+
"n": 1,
|
| 31 |
+
"passed": 1,
|
| 32 |
+
"accuracy": 1.0
|
| 33 |
+
},
|
| 34 |
+
"alignment": {
|
| 35 |
+
"n": 3,
|
| 36 |
+
"passed": 3,
|
| 37 |
+
"accuracy": 1.0
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"cases": [
|
| 41 |
+
{
|
| 42 |
+
"id": "floor_01",
|
| 43 |
+
"group": "floor",
|
| 44 |
+
"expected_ready": false,
|
| 45 |
+
"got_ready": false,
|
| 46 |
+
"expected_missing": [
|
| 47 |
+
"at least one completed analysis"
|
| 48 |
+
],
|
| 49 |
+
"got_missing": [
|
| 50 |
+
"at least one completed analysis"
|
| 51 |
+
],
|
| 52 |
+
"correct": true,
|
| 53 |
+
"aligned": false,
|
| 54 |
+
"gap": false,
|
| 55 |
+
"latency_ms": 0.0
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "floor_02",
|
| 59 |
+
"group": "floor",
|
| 60 |
+
"expected_ready": false,
|
| 61 |
+
"got_ready": false,
|
| 62 |
+
"expected_missing": [
|
| 63 |
+
"at least one completed analysis"
|
| 64 |
+
],
|
| 65 |
+
"got_missing": [
|
| 66 |
+
"at least one completed analysis"
|
| 67 |
+
],
|
| 68 |
+
"correct": true,
|
| 69 |
+
"aligned": false,
|
| 70 |
+
"gap": false,
|
| 71 |
+
"latency_ms": 0.0
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"id": "floor_03",
|
| 75 |
+
"group": "floor",
|
| 76 |
+
"expected_ready": false,
|
| 77 |
+
"got_ready": false,
|
| 78 |
+
"expected_missing": [
|
| 79 |
+
"at least one completed analysis"
|
| 80 |
+
],
|
| 81 |
+
"got_missing": [
|
| 82 |
+
"at least one completed analysis"
|
| 83 |
+
],
|
| 84 |
+
"correct": true,
|
| 85 |
+
"aligned": false,
|
| 86 |
+
"gap": false,
|
| 87 |
+
"latency_ms": 0.0
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"id": "floor_04",
|
| 91 |
+
"group": "floor",
|
| 92 |
+
"expected_ready": true,
|
| 93 |
+
"got_ready": true,
|
| 94 |
+
"expected_missing": [],
|
| 95 |
+
"got_missing": [],
|
| 96 |
+
"correct": true,
|
| 97 |
+
"aligned": true,
|
| 98 |
+
"gap": false,
|
| 99 |
+
"latency_ms": 0.0
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"id": "floor_05",
|
| 103 |
+
"group": "floor",
|
| 104 |
+
"expected_ready": true,
|
| 105 |
+
"got_ready": true,
|
| 106 |
+
"expected_missing": [],
|
| 107 |
+
"got_missing": [],
|
| 108 |
+
"correct": true,
|
| 109 |
+
"aligned": true,
|
| 110 |
+
"gap": false,
|
| 111 |
+
"latency_ms": 0.0
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"id": "floor_06",
|
| 115 |
+
"group": "floor",
|
| 116 |
+
"expected_ready": true,
|
| 117 |
+
"got_ready": true,
|
| 118 |
+
"expected_missing": [],
|
| 119 |
+
"got_missing": [],
|
| 120 |
+
"correct": true,
|
| 121 |
+
"aligned": true,
|
| 122 |
+
"gap": false,
|
| 123 |
+
"latency_ms": 0.0
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"id": "delta_01",
|
| 127 |
+
"group": "delta",
|
| 128 |
+
"expected_ready": false,
|
| 129 |
+
"got_ready": false,
|
| 130 |
+
"expected_missing": [
|
| 131 |
+
"a new analysis since the last report"
|
| 132 |
+
],
|
| 133 |
+
"got_missing": [
|
| 134 |
+
"a new analysis since the last report"
|
| 135 |
+
],
|
| 136 |
+
"correct": true,
|
| 137 |
+
"aligned": true,
|
| 138 |
+
"gap": false,
|
| 139 |
+
"latency_ms": 0.0
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"id": "delta_02",
|
| 143 |
+
"group": "delta",
|
| 144 |
+
"expected_ready": true,
|
| 145 |
+
"got_ready": true,
|
| 146 |
+
"expected_missing": [],
|
| 147 |
+
"got_missing": [],
|
| 148 |
+
"correct": true,
|
| 149 |
+
"aligned": true,
|
| 150 |
+
"gap": false,
|
| 151 |
+
"latency_ms": 0.0
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "delta_03",
|
| 155 |
+
"group": "delta",
|
| 156 |
+
"expected_ready": true,
|
| 157 |
+
"got_ready": true,
|
| 158 |
+
"expected_missing": [],
|
| 159 |
+
"got_missing": [],
|
| 160 |
+
"correct": true,
|
| 161 |
+
"aligned": true,
|
| 162 |
+
"gap": false,
|
| 163 |
+
"latency_ms": 0.0
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"id": "delta_04",
|
| 167 |
+
"group": "delta",
|
| 168 |
+
"expected_ready": false,
|
| 169 |
+
"got_ready": false,
|
| 170 |
+
"expected_missing": [
|
| 171 |
+
"a new analysis since the last report"
|
| 172 |
+
],
|
| 173 |
+
"got_missing": [
|
| 174 |
+
"a new analysis since the last report"
|
| 175 |
+
],
|
| 176 |
+
"correct": true,
|
| 177 |
+
"aligned": true,
|
| 178 |
+
"gap": false,
|
| 179 |
+
"latency_ms": 0.0
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"id": "delta_05",
|
| 183 |
+
"group": "delta",
|
| 184 |
+
"expected_ready": false,
|
| 185 |
+
"got_ready": false,
|
| 186 |
+
"expected_missing": [
|
| 187 |
+
"a new analysis since the last report"
|
| 188 |
+
],
|
| 189 |
+
"got_missing": [
|
| 190 |
+
"a new analysis since the last report"
|
| 191 |
+
],
|
| 192 |
+
"correct": true,
|
| 193 |
+
"aligned": true,
|
| 194 |
+
"gap": false,
|
| 195 |
+
"latency_ms": 0.0
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"id": "edge_01",
|
| 199 |
+
"group": "edge",
|
| 200 |
+
"expected_ready": false,
|
| 201 |
+
"got_ready": false,
|
| 202 |
+
"expected_missing": [
|
| 203 |
+
"at least one completed analysis"
|
| 204 |
+
],
|
| 205 |
+
"got_missing": [
|
| 206 |
+
"at least one completed analysis"
|
| 207 |
+
],
|
| 208 |
+
"correct": true,
|
| 209 |
+
"aligned": false,
|
| 210 |
+
"gap": false,
|
| 211 |
+
"latency_ms": 0.0
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"id": "align_01",
|
| 215 |
+
"group": "alignment",
|
| 216 |
+
"expected_ready": true,
|
| 217 |
+
"got_ready": true,
|
| 218 |
+
"expected_missing": [],
|
| 219 |
+
"got_missing": [],
|
| 220 |
+
"correct": true,
|
| 221 |
+
"aligned": false,
|
| 222 |
+
"gap": true,
|
| 223 |
+
"latency_ms": 0.0
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"id": "align_02",
|
| 227 |
+
"group": "alignment",
|
| 228 |
+
"expected_ready": true,
|
| 229 |
+
"got_ready": true,
|
| 230 |
+
"expected_missing": [],
|
| 231 |
+
"got_missing": [],
|
| 232 |
+
"correct": true,
|
| 233 |
+
"aligned": false,
|
| 234 |
+
"gap": true,
|
| 235 |
+
"latency_ms": 0.0
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"id": "align_03",
|
| 239 |
+
"group": "alignment",
|
| 240 |
+
"expected_ready": true,
|
| 241 |
+
"got_ready": true,
|
| 242 |
+
"expected_missing": [],
|
| 243 |
+
"got_missing": [],
|
| 244 |
+
"correct": true,
|
| 245 |
+
"aligned": true,
|
| 246 |
+
"gap": false,
|
| 247 |
+
"latency_ms": 0.0
|
| 248 |
+
}
|
| 249 |
+
]
|
| 250 |
+
}
|
knowledge_pipeline_context.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Knowledge Pipeline β Context for the Data Eyond Python Repo (19 Aug 2026)
|
| 2 |
+
|
| 3 |
+
> **Who this is for:** a Claude Code session working in `Agentic-Service-Data-Eyond-Catalog`
|
| 4 |
+
> (the Python agentic service). This doc carries *context and intent* only β the why, the
|
| 5 |
+
> ownership split, the shape of the pipeline, and the decisions already settled. It does not
|
| 6 |
+
> prescribe folder layout, module names, or endpoint signatures; work those out against
|
| 7 |
+
> `CLAUDE.md`, `REPO_STATUS.md` and the existing subsystem patterns in the repo.
|
| 8 |
+
>
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. What we are doing and why now
|
| 12 |
+
|
| 13 |
+
Data Eyond is an AI data-analyst platform. The strategic thesis from the 30 July exec review
|
| 14 |
+
is that **domain knowledge is the differentiator, not architecture** β orchestrators and
|
| 15 |
+
connectors are replicable in a quarter, model capability is rented, only encoded domain
|
| 16 |
+
knowledge compounds. The end goal is an **MCP product** exposing modular domain knowledge
|
| 17 |
+
that EMA and later clients connect to.
|
| 18 |
+
|
| 19 |
+
That knowledge currently comes from experts typing it in. **Mas Beta** (plant & maintenance)
|
| 20 |
+
is the single validation bottleneck β nothing becomes an artifact without him. The knowledge
|
| 21 |
+
pipeline exists to change the expert's job from *authoring* to *reviewing*: the pipeline reads
|
| 22 |
+
the client's own documents (standards, SOPs, handbooks) and proposes candidate knowledge
|
| 23 |
+
entries; the expert approves, edits or rejects them.
|
| 24 |
+
|
| 25 |
+
**Why it lands in this repo now.** We want to integrate the pipeline into Data Eyond to test
|
| 26 |
+
it end-to-end on real documents, in the place where the resulting knowledge will actually be
|
| 27 |
+
consumed. This is a **test integration**, not a productionisation β treat it as a new, gated
|
| 28 |
+
subsystem alongside the existing document/catalog pipelines, not as a rewrite of them.
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
## 2. Ownership β two items, two people, one seam
|
| 33 |
+
|
| 34 |
+
The pipeline splits cleanly in two, and the split is deliberate: **they are separate,
|
| 35 |
+
independently runnable, independently swappable pipelines.**
|
| 36 |
+
|
| 37 |
+
| Half | Owner | Scope |
|
| 38 |
+
|---|---|---|
|
| 39 |
+
| **β Parsing (MinerU)** | **Sofhia Az-Zahra** | PDF/DOCX in β structured, typed extracted content out. MinerU replaces the current text+table-only extraction: it adds formula, image, chart and diagram detection, and emits Markdown or JSON with type labels. Presented to the execs 13 Aug as the **prerequisite for Scenario 2**; a written cost justification for MinerU is owed for the budget proposal. |
|
| 40 |
+
| **β‘ Knowledge extraction** | **Rifqi (me)** | Parsed content in β validated candidate knowledge entries out. Term filtering, clustering, evidence ranking, schema-guided LLM attribute fill, span validation, diff against the active glossary version, and the frequency-sorted review queue. |
|
| 41 |
+
|
| 42 |
+
**The seam.** The two halves must meet only at a **persisted, versioned parsed-document
|
| 43 |
+
artifact** β extraction consumes that artifact, never a raw file, never the parser's API.
|
| 44 |
+
This is the single most important structural constraint in the whole plan, because:
|
| 45 |
+
|
| 46 |
+
- MinerU can be swapped (or fall back to the existing Tesseract / Azure Document Intelligence
|
| 47 |
+
paths) without extraction knowing;
|
| 48 |
+
- extraction can be re-run cheaply on an already-parsed corpus when a prompt or schema changes
|
| 49 |
+
β and it will be, often, because prompt iteration is the main development loop here;
|
| 50 |
+
- Sofhia and I can work and test independently, against a fixture artifact, without blocking
|
| 51 |
+
each other;
|
| 52 |
+
- parsing is the slow/expensive-to-rerun half; extraction is the fast-to-iterate half. Keeping
|
| 53 |
+
the boundary hard means we never re-parse to fix an extraction bug.
|
| 54 |
+
|
| 55 |
+
Anything that makes the two halves import each other's internals, or that makes extraction
|
| 56 |
+
take a file path, breaks the point of the split.
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## 3. The pipeline
|
| 61 |
+
|
| 62 |
+
Six stages. The cost story matters as much as the flow: almost everything is free CPU/regex
|
| 63 |
+
work, and the LLM appears in exactly one stage.
|
| 64 |
+
|
| 65 |
+
```mermaid
|
| 66 |
+
flowchart TD
|
| 67 |
+
A["Admin / expert triggers ingest<br/><i>not continuous, not per-user</i>"] --> B
|
| 68 |
+
|
| 69 |
+
subgraph P["β PARSING β Sofhia"]
|
| 70 |
+
B["MinerU<br/>text Β· tables Β· formulas Β· figures Β· charts"] --> C["Section-aware chunking<br/>split on numbered headings 2.1.1, 2.1.2 β¦"]
|
| 71 |
+
end
|
| 72 |
+
|
| 73 |
+
C --> SEAM[("Parsed-document artifact<br/>versioned Β· persisted<br/><b>THE SEAM</b>")]
|
| 74 |
+
SEAM --> D
|
| 75 |
+
|
| 76 |
+
subgraph F["β‘ TERM FILTER β CPU / regex, free"]
|
| 77 |
+
D["GLiNER span filter on ALL chunks β term mentions"] --> H
|
| 78 |
+
E["Discourse-cue regex β rule-of-thumb candidates"] --> H
|
| 79 |
+
G["Legend-block regex β formula vars + abbreviation dictionary"] --> H
|
| 80 |
+
G2["Section pass β summary units"] --> H
|
| 81 |
+
H["Normalise + cluster mentions<br/>PA Β· P.A. Β· Physical Availability β 1 cluster"] --> I["Evidence ranking<br/>cue Β· heading Β· legend Β· formula Β· formatting Β· first-occurrence"]
|
| 82 |
+
I --> J["Top-3 evidence chunks per cluster"]
|
| 83 |
+
end
|
| 84 |
+
|
| 85 |
+
SEAM --> E
|
| 86 |
+
SEAM --> G
|
| 87 |
+
SEAM --> G2
|
| 88 |
+
|
| 89 |
+
J --> K
|
| 90 |
+
|
| 91 |
+
subgraph X["β’ KNOWLEDGE EXTRACTION β the only paid stage"]
|
| 92 |
+
K["1 LLM call per TERM CLUSTER<br/>(not per mention, not per chunk)"]
|
| 93 |
+
L["1 call per cue chunk Β· 1 per unique formula Β· 1 per document (summary)"]
|
| 94 |
+
end
|
| 95 |
+
|
| 96 |
+
K --> M
|
| 97 |
+
L --> M
|
| 98 |
+
|
| 99 |
+
subgraph V["β£ VALIDATION β deterministic"]
|
| 100 |
+
M["Verbatim-span check β reject field if span not locatable"] --> N["Null definition β escalate to next 3 evidence chunks<br/>max 2 rounds β else flag 'term found, no definition located'"]
|
| 101 |
+
N --> O["Conflicting definitions β definition_conflict + variants[]"]
|
| 102 |
+
end
|
| 103 |
+
|
| 104 |
+
O --> Q["β€ DIFF vs active glossary version<br/>duplicate (skip) Β· new Β· conflicting"]
|
| 105 |
+
Q --> R
|
| 106 |
+
|
| 107 |
+
subgraph RV["β₯ REVIEW β human"]
|
| 108 |
+
R["Queue sorted by mention frequency<br/>PA 250 β UA 180 β β¦ β long tail"] --> S["Expert (Mas Beta) approves / edits / rejects"]
|
| 109 |
+
S --> T["Glossary Β· Interpretation Pack Β· Brief Context Β· Formula"]
|
| 110 |
+
T --> U["Versioning + approval audit trail"]
|
| 111 |
+
end
|
| 112 |
+
|
| 113 |
+
U --> W["MCP server β the consumption surface"]
|
| 114 |
+
|
| 115 |
+
style P fill:#e8f4ff,stroke:#4a90d9
|
| 116 |
+
style F fill:#e6f7e6,stroke:#4caf50
|
| 117 |
+
style X fill:#dbe9ff,stroke:#2f6fd0
|
| 118 |
+
style V fill:#f0e6ff,stroke:#8e5fd0
|
| 119 |
+
style RV fill:#fff0e0,stroke:#e08b3c
|
| 120 |
+
style SEAM fill:#fffbe0,stroke:#c9a227,stroke-width:3px
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
Colour convention, carried from the diagrams shown to the team:
|
| 124 |
+
**green = free** (CPU/regex β most of the pipeline) Β· **blue = LLM** (the only paid part) Β·
|
| 125 |
+
**purple = deterministic code** Β· **orange = human**.
|
| 126 |
+
|
| 127 |
+
### The five-box version (how this is explained to execs)
|
| 128 |
+
|
| 129 |
+
```mermaid
|
| 130 |
+
flowchart LR
|
| 131 |
+
A["PARSE<br/>MinerU"] --> B["FILTER<br/>free, CPU"] --> C["EXTRACT<br/>LLM, per term cluster"] --> D["VALIDATE + DIFF<br/>deterministic"] --> E["EXPERT REVIEW<br/>frequency-sorted queue"]
|
| 132 |
+
style A fill:#e8f4ff,stroke:#4a90d9
|
| 133 |
+
style B fill:#e6f7e6,stroke:#4caf50
|
| 134 |
+
style C fill:#dbe9ff,stroke:#2f6fd0
|
| 135 |
+
style D fill:#f0e6ff,stroke:#8e5fd0
|
| 136 |
+
style E fill:#fff0e0,stroke:#e08b3c
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
### Ownership view
|
| 140 |
+
|
| 141 |
+
```mermaid
|
| 142 |
+
flowchart LR
|
| 143 |
+
subgraph S["Sofhia"]
|
| 144 |
+
P["Parsing pipeline<br/>MinerU + backends"]
|
| 145 |
+
end
|
| 146 |
+
subgraph R["Rifqi"]
|
| 147 |
+
K["Extraction pipeline<br/>filter β LLM β validate β diff β queue"]
|
| 148 |
+
end
|
| 149 |
+
subgraph H["Mas Har / Mas Beta"]
|
| 150 |
+
C["Curation UI + expert review<br/>versioning, approval"]
|
| 151 |
+
end
|
| 152 |
+
P -->|"versioned parsed artifact<br/>(the only contract)"| K
|
| 153 |
+
K -->|"candidate entries + provenance"| C
|
| 154 |
+
C -->|"active versions"| M["MCP server"]
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## 4. Decisions already settled β do not reopen these
|
| 160 |
+
|
| 161 |
+
1. **The LLM call unit is the term cluster** β not the chunk, not the mention. Per-chunk works
|
| 162 |
+
for a 9-page document and breaks at 1,000+ pages: "PA" mentioned 250Γ would produce 250
|
| 163 |
+
near-identical candidates. Clustering first cuts expert review burden ~6.2Γ (3,125 raw
|
| 164 |
+
extractions β ~500 term entries on a 1,000-page document). The token saving is trivial
|
| 165 |
+
(~$0.45); **the review-burden reduction is the justification.**
|
| 166 |
+
2. **Conflict detection depends on the clustering.** Two contradictory definitions of the same
|
| 167 |
+
term can only be spotted because all evidence for that term arrives in one call. Two
|
| 168 |
+
separate calls would never meet, and the conflict would surface as two silently-accepted
|
| 169 |
+
entries.
|
| 170 |
+
3. **No standalone relevance gate.** A binary relevance classifier before the term filter is a
|
| 171 |
+
second imperfect classifier whose false negatives drop content *before* extraction sees it β
|
| 172 |
+
and dropped content never reaches expert review to be caught. GLiNER is cheap enough (CPU,
|
| 173 |
+
~400MB) to run unconditionally on every chunk; "zero candidate spans" becomes the relevance
|
| 174 |
+
signal as a byproduct of looking, rather than a judgment made before looking.
|
| 175 |
+
4. **Evidence ranking is allowed even though it is also a filter** β because no term is
|
| 176 |
+
dropped (only which passages feed the call is narrowed), all mentions stay in provenance so
|
| 177 |
+
a bad top-3 is visible, and the escalation loop self-corrects. The rejected relevance gate
|
| 178 |
+
had none of those three properties. This distinction is the reasoning to preserve if the
|
| 179 |
+
ranking is ever revisited.
|
| 180 |
+
5. **`provenance.span` is mandatory and verbatim.** A field whose span cannot be located in the
|
| 181 |
+
source is rejected. This is the primary anti-hallucination control and the thing that makes
|
| 182 |
+
expert review *finishable* β the reviewer checks a quote against a page, not a claim against
|
| 183 |
+
their memory.
|
| 184 |
+
6. **All content fields are Optional in the schema.** The model must be able to answer null; a
|
| 185 |
+
required field is a fabrication invitation for smaller models. `subdomain_tags` is an enum β
|
| 186 |
+
classification, not generation.
|
| 187 |
+
7. **Frequency-sorted review queue.** Directly targets the Mas Beta bottleneck: validate the
|
| 188 |
+
~40 terms carrying the document's meaning first, let the long tail wait.
|
| 189 |
+
8. **Model tier is per branch.** Glossary and Formula are extraction tasks well-guarded by span
|
| 190 |
+
verification β a small/cheap tier is fine. Rule-of-thumb and Summary are reasoning/generation
|
| 191 |
+
tasks where a small model's failure mode is least detectable (a plausible summary cannot be
|
| 192 |
+
span-checked) β larger tier. Test before committing the small tier on the rule-of-thumb
|
| 193 |
+
branch.
|
| 194 |
+
9. **GLiNER-hybrid stays the glossary-branch default.** Pure NER can only fill 2 of 6 schema
|
| 195 |
+
fields (definition, formula and interpretation are not spans β they are assembled from text
|
| 196 |
+
that may be pages apart). Pure small-LLM shows recall 0.93β1.00 at precision 0.15β0.43;
|
| 197 |
+
perfect recall is the failure signature of a model fabricating a value for every field.
|
| 198 |
+
10. **The pipeline records the literal source wording.** It recorded "Physical of Availability"
|
| 199 |
+
from the BUMA standard rather than the textbook's "Physical Availability" β surfacing the
|
| 200 |
+
discrepancy to the expert instead of silently normalising it. Keep that behaviour.
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## 5. Deployment reality β this shapes every engineering trade-off
|
| 205 |
+
|
| 206 |
+
The pipeline runs **once at setup (bulk corpus ingest)** and **occasionally when an admin or
|
| 207 |
+
expert adds a file**. It is **not continuous** and **not per-user** β normal engineers cannot
|
| 208 |
+
add data. Consequences, in the order they matter:
|
| 209 |
+
|
| 210 |
+
- **Extraction quality and expert-review efficiency dominate.** A missed term has no
|
| 211 |
+
self-correcting mechanism, because engineers can't add data to fill a gap later.
|
| 212 |
+
- **Maintainability matters more than it looks.** A glue chain invoked every few months is
|
| 213 |
+
exactly the thing nobody remembers how to debug. Prefer boring, inspectable stages over
|
| 214 |
+
clever ones.
|
| 215 |
+
- **API cost is negligible and latency is irrelevant.** ~$8β12 for a 6,000-page corpus;
|
| 216 |
+
an incremental single 9-page file is ~1.5 US cents. Do not spend engineering time optimising
|
| 217 |
+
tokens β engineering time on the review experience is worth more than any token saving
|
| 218 |
+
available here.
|
| 219 |
+
- **No GPU required.** GLiNER is a CPU encoder model (~1.6 seq/s Python, ~6.7 seq/s via the
|
| 220 |
+
Rust port on an i9 8-core). ~15,000 chunks β 2.6h Python / ~40min Rust for a job that runs a
|
| 221 |
+
few times a year. The LLM half is an API call. (Runtime as reported to the team on 13 Aug:
|
| 222 |
+
~11 s/page GLiNER, ~26 s/page LLM β with the caveat that **cost scales on term count, not
|
| 223 |
+
page count.**)
|
| 224 |
+
- **Two ingest modes to build:** *bulk setup ingest* (corpus-scale β the only place
|
| 225 |
+
corpus-frequency statistics are usable as a candidate-term booster) and *incremental
|
| 226 |
+
single-file add* (no corpus context; diff against the active glossary version).
|
| 227 |
+
- **The summary branch is the quiet cost risk** β only ~300 calls but a quarter of all input
|
| 228 |
+
tokens, because summarisation can't be filtered; it needs whole documents.
|
| 229 |
+
|
| 230 |
+
---
|
| 231 |
+
|
| 232 |
+
## 6. Where the output goes
|
| 233 |
+
|
| 234 |
+
Approved candidates become the project's **four artifacts** (see the `context` doc for the
|
| 235 |
+
full architecture):
|
| 236 |
+
|
| 237 |
+
1. **Domain knowledge** β system-prompt material, capped ~1500 tokens: operating and equipment
|
| 238 |
+
hierarchy, time convention (WITA), seasonality, most-important glossary terms, high-level
|
| 239 |
+
business process, and agent limitations.
|
| 240 |
+
2. **Data dictionary** β from DB column profiling; used for query generation. Pak Ricky
|
| 241 |
+
connected the extraction work directly to **automating this**.
|
| 242 |
+
3. **Interpretation pack** β expert-curated interpretation logic, action rules, benchmark
|
| 243 |
+
history, tied to a use case ("PA missed" reads differently from "PA missed but MTBS
|
| 244 |
+
achieved"). Revived on 13 Aug after earlier removal for being too normative β reintroduce
|
| 245 |
+
carefully.
|
| 246 |
+
4. **Skill registry** β query rules and expert-approved formulas. Approval is mandatory because
|
| 247 |
+
the same term computes differently per company (MTTR at BUMA = breakdown duration Γ·
|
| 248 |
+
breakdown frequency; MTTR in IT = mean time to resolve).
|
| 249 |
+
|
| 250 |
+
All four carry **versioning plus an approval audit trail**, and the expert chooses which
|
| 251 |
+
version is active. Delivery format is **MCP** β skill registry β MCP skills; data dictionary β
|
| 252 |
+
MCP on demand; interpretation pack β attached to the skill; domain knowledge β system prompt
|
| 253 |
+
but preferably served via MCP so owners can change it without redeploying the engine.
|
| 254 |
+
|
| 255 |
+
---
|
| 256 |
+
|
| 257 |
+
## 7. Grounding material
|
| 258 |
+
|
| 259 |
+
Two real sample documents anchor every design claim above, and should anchor test fixtures too:
|
| 260 |
+
|
| 261 |
+
- **BUMA STD/2026/006/MNO Rev.0.0** β Production Parameter & ECA. 9 pages, majority Bahasa
|
| 262 |
+
Indonesia. Carries: PA (glossary, p.4), Other Activity + the controllability hierarchy
|
| 263 |
+
(rule-of-thumb, pp.8β9), the MOHH/Qty/PA/UA/Pty legend block (formula branch, p.2).
|
| 264 |
+
- **Open Pit Mine Planning & Design** (textbook excerpt) β carries NSR as a **cross-page
|
| 265 |
+
definition**: intro p.92, formula p.93, interpretive remark p.94 (Β§2.3.5). This is the case
|
| 266 |
+
that justifies section-aware chunking over fixed token windows β the whole definition must
|
| 267 |
+
stay inside one semantic unit.
|
| 268 |
+
|
| 269 |
+
**Known open risk:** GLiNER multi-v2.1's Indonesian performance is unverified, and the BUMA
|
| 270 |
+
standard is majority Bahasa Indonesia. This is the standing "Indonesian degradation" question,
|
| 271 |
+
now with a concrete test case. Verify before trusting recall numbers.
|
| 272 |
+
|
| 273 |
+
**Flagged as scope creep, proposed not decided:** ensembling GLiNER + LangExtract on every
|
| 274 |
+
ingest. Cheap in API terms, but it adds a reconciliation/dedup step between two candidate sets
|
| 275 |
+
that may disagree β new engineering surface for uncertain gain.
|
| 276 |
+
|
| 277 |
+
---
|
| 278 |
+
|
| 279 |
+
## 8. Integration guidance for this repo
|
| 280 |
+
|
| 281 |
+
Deliberately shape-agnostic β decide the specifics against `CLAUDE.md` and the existing
|
| 282 |
+
subsystem patterns, not against this doc.
|
| 283 |
+
|
| 284 |
+
- **Both halves are new subsystems, not modifications** to the existing document/catalog
|
| 285 |
+
pipelines. The current unstructured path (Tesseract OCR β chunk β pgvector) stays as it is;
|
| 286 |
+
the MinerU path is additive and, at least initially, gated.
|
| 287 |
+
- **Follow the repo's existing multi-stage precedent.** The query subsystem (IR β validator β
|
| 288 |
+
compiler β executor, orchestrated from a service facade, triggered by thin entry points) is
|
| 289 |
+
the closest structural analogue to what both halves need.
|
| 290 |
+
- **Respect the hard boundaries.** Go owns the dedorch schema β any new table needs a
|
| 291 |
+
Harry-ready DDL handoff, never DDL executed from Python. Any new endpoint on the live surface
|
| 292 |
+
needs a contract-doc entry, and an admin ingest surface is a different risk class than the
|
| 293 |
+
read-only chat surface β raise the authentication question rather than assuming.
|
| 294 |
+
- **Build the offline path first.** Because this runs a few times a year and is triggered by an
|
| 295 |
+
admin, a script-driven run over a fixture document is the honest first milestone; HTTP
|
| 296 |
+
endpoints are a convenience layer over it, not the other way round.
|
| 297 |
+
- **Fixtures over live parsing in tests.** A committed parsed-artifact fixture from one of the
|
| 298 |
+
two sample documents lets the extraction half be tested without MinerU installed at all β
|
| 299 |
+
which is the seam doing its job.
|
pyproject.toml
CHANGED
|
@@ -90,6 +90,9 @@ dependencies = [
|
|
| 90 |
"pytesseract>=0.3.13",
|
| 91 |
"pypdf2>=3.0.1",
|
| 92 |
"pyarrow>=24.0.0",
|
|
|
|
|
|
|
|
|
|
| 93 |
]
|
| 94 |
|
| 95 |
[project.optional-dependencies]
|
|
@@ -103,6 +106,18 @@ dev = [
|
|
| 103 |
"pre-commit==4.0.1",
|
| 104 |
]
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
[tool.hatch.build.targets.wheel]
|
| 107 |
packages = ["src/agent_service"]
|
| 108 |
|
|
@@ -125,6 +140,13 @@ ignore = [
|
|
| 125 |
"tests/**" = ["S101", "S105", "S106", "S608"]
|
| 126 |
# T201: eval/ scripts are CLIs β print() is their intended output channel.
|
| 127 |
"eval/**" = ["T201"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
[tool.mypy]
|
| 130 |
python_version = "3.12"
|
|
|
|
| 90 |
"pytesseract>=0.3.13",
|
| 91 |
"pypdf2>=3.0.1",
|
| 92 |
"pyarrow>=24.0.0",
|
| 93 |
+
"rapidfuzz>=3.14.5",
|
| 94 |
+
"gliner>=0.2.13",
|
| 95 |
+
"torch>=2.6",
|
| 96 |
]
|
| 97 |
|
| 98 |
[project.optional-dependencies]
|
|
|
|
| 106 |
"pre-commit==4.0.1",
|
| 107 |
]
|
| 108 |
|
| 109 |
+
# Document parsing for the knowledge pipeline (src/knowledge_parsing/).
|
| 110 |
+
#
|
| 111 |
+
# Deliberately an EXTRA, not a main dependency: MinerU pulls torch and its model
|
| 112 |
+
# tail (GBs), and the agent service never parses documents at request time β
|
| 113 |
+
# parsing is an offline, admin-triggered batch job. Keeping it here means the
|
| 114 |
+
# deployed Space does not build or ship any of it.
|
| 115 |
+
#
|
| 116 |
+
# pip install -e ".[parsing]"
|
| 117 |
+
knowledge-parsing = [
|
| 118 |
+
"mineru==3.4.4",
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
[tool.hatch.build.targets.wheel]
|
| 122 |
packages = ["src/agent_service"]
|
| 123 |
|
|
|
|
| 140 |
"tests/**" = ["S101", "S105", "S106", "S608"]
|
| 141 |
# T201: eval/ scripts are CLIs β print() is their intended output channel.
|
| 142 |
"eval/**" = ["T201"]
|
| 143 |
+
# Same rule, same reason: this is an operator-run CLI, and its printed output IS
|
| 144 |
+
# the deliverable.
|
| 145 |
+
"src/knowledge_extraction/cli.py" = ["T201"]
|
| 146 |
+
# Same again for the knowledge-parsing CLI entry points. Scoped to the two files
|
| 147 |
+
# rather than the package, so the library modules stay print-free.
|
| 148 |
+
"src/knowledge_parsing/run.py" = ["T201"]
|
| 149 |
+
"src/knowledge_parsing/report.py" = ["T201"]
|
| 150 |
|
| 151 |
[tool.mypy]
|
| 152 |
python_version = "3.12"
|
src/config/settings.py
CHANGED
|
@@ -47,6 +47,13 @@ class Settings(BaseSettings):
|
|
| 47 |
azureai_deployment_name_54m: str = Field(alias="azureai__deployment__name__54m", default="")
|
| 48 |
azureai_api_version_54m: str = Field(alias="azureai__api__version__54m", default="")
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# Azure OpenAI - Embeddings
|
| 51 |
azureai_api_key_embedding: str = Field(alias="azureai__api_key__embedding", default="")
|
| 52 |
azureai_endpoint_url_embedding: str = Field(alias="azureai__endpoint__url__embedding", default="")
|
|
|
|
| 47 |
azureai_deployment_name_54m: str = Field(alias="azureai__deployment__name__54m", default="")
|
| 48 |
azureai_api_version_54m: str = Field(alias="azureai__api__version__54m", default="")
|
| 49 |
|
| 50 |
+
# Azure OpenAI - GPT-5.4-nano (knowledge-extraction pipeline; all four
|
| 51 |
+
# branches route here β see KNOWLEDGE_PIPELINE_TODO.md D3)
|
| 52 |
+
azureai_api_key_54n: str = Field(alias="azureai__api_key__54n", default="")
|
| 53 |
+
azureai_endpoint_url_54n: str = Field(alias="azureai__endpoint__url__54n", default="")
|
| 54 |
+
azureai_deployment_name_54n: str = Field(alias="azureai__deployment__name__54n", default="")
|
| 55 |
+
azureai_api_version_54n: str = Field(alias="azureai__api__version__54n", default="")
|
| 56 |
+
|
| 57 |
# Azure OpenAI - Embeddings
|
| 58 |
azureai_api_key_embedding: str = Field(alias="azureai__api_key__embedding", default="")
|
| 59 |
azureai_endpoint_url_embedding: str = Field(alias="azureai__endpoint__url__embedding", default="")
|
src/knowledge_extraction/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Knowledge-extraction pipeline: parsed document -> candidate knowledge entries.
|
| 2 |
+
|
| 3 |
+
Distinct from `src/knowledge/`, which is the existing OCR -> chunk -> pgvector
|
| 4 |
+
ingestion path for unstructured RAG. This package does not touch it.
|
| 5 |
+
|
| 6 |
+
Stage order, and which stages cost money:
|
| 7 |
+
adapter seam artifact -> internal Chunk free
|
| 8 |
+
filters cue / legend / span NER -> mentions free (CPU)
|
| 9 |
+
cluster normalise + cluster mentions free
|
| 10 |
+
rank evidence scoring, top-K selection free
|
| 11 |
+
extract one LLM call per TERM CLUSTER PAID
|
| 12 |
+
validate verbatim span check, escalation free
|
| 13 |
+
diff new / duplicate / conflicting free
|
| 14 |
+
queue frequency-sorted review queue free
|
| 15 |
+
|
| 16 |
+
`service.py` is the facade; `cli.py` is the operator entry point.
|
| 17 |
+
|
| 18 |
+
Design rationale: knowledge_pipeline_context.md
|
| 19 |
+
Calibrated constants and why: KNOWLEDGE_PIPELINE_CALIBRATION.md
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from .adapter import parsed_doc_from_artifact
|
| 23 |
+
from .models import (
|
| 24 |
+
AbbrevPair,
|
| 25 |
+
BriefContext,
|
| 26 |
+
CallUsage,
|
| 27 |
+
Chunk,
|
| 28 |
+
ClusterResult,
|
| 29 |
+
FilterResult,
|
| 30 |
+
FormulaEntry,
|
| 31 |
+
GlossaryEntry,
|
| 32 |
+
Mention,
|
| 33 |
+
ParsedDoc,
|
| 34 |
+
Provenance,
|
| 35 |
+
RuleCandidate,
|
| 36 |
+
RuleEntry,
|
| 37 |
+
TermCluster,
|
| 38 |
+
)
|
| 39 |
+
from .service import ExtractionResult, build_clusters, estimate_cost, extract_all, run_filters
|
| 40 |
+
|
| 41 |
+
__all__ = [
|
| 42 |
+
"AbbrevPair",
|
| 43 |
+
"BriefContext",
|
| 44 |
+
"CallUsage",
|
| 45 |
+
"Chunk",
|
| 46 |
+
"ClusterResult",
|
| 47 |
+
"ExtractionResult",
|
| 48 |
+
"FilterResult",
|
| 49 |
+
"FormulaEntry",
|
| 50 |
+
"GlossaryEntry",
|
| 51 |
+
"Mention",
|
| 52 |
+
"ParsedDoc",
|
| 53 |
+
"Provenance",
|
| 54 |
+
"RuleCandidate",
|
| 55 |
+
"RuleEntry",
|
| 56 |
+
"TermCluster",
|
| 57 |
+
"build_clusters",
|
| 58 |
+
"estimate_cost",
|
| 59 |
+
"extract_all",
|
| 60 |
+
"parsed_doc_from_artifact",
|
| 61 |
+
"run_filters",
|
| 62 |
+
]
|
src/knowledge_extraction/adapter.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Seam adapter: parsed-document artifact β the pipeline's internal `Chunk`.
|
| 2 |
+
|
| 3 |
+
**This is the only file that knows the seam's shape.** Every stage downstream
|
| 4 |
+
depends on `models.Chunk` alone, so when the artifact contract settles with
|
| 5 |
+
Sofhia the change lands here and nowhere else.
|
| 6 |
+
|
| 7 |
+
The seam is still under discussion (KNOWLEDGE_PIPELINE_TODO.md Β§3), so this
|
| 8 |
+
reads defensively: it accepts either the draft's field names or the prototype's,
|
| 9 |
+
takes plain dicts, and tolerates missing optional structure. It deliberately
|
| 10 |
+
does **not** accept a file path β extraction never opens a document. That
|
| 11 |
+
constraint is the point of the split, not an implementation detail.
|
| 12 |
+
|
| 13 |
+
Two things it must never do:
|
| 14 |
+
- reflow, strip or whitespace-normalise `text`. Span validation locates quoted
|
| 15 |
+
spans literally inside it; cleaning the text makes the lookup fail and the
|
| 16 |
+
field go silently null, which presents as a bad model.
|
| 17 |
+
- infer a page number it was not given. A wrong page sends the reviewer to the
|
| 18 |
+
wrong part of the document, which is worse than no page at all.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import hashlib
|
| 24 |
+
import json
|
| 25 |
+
from typing import Any
|
| 26 |
+
|
| 27 |
+
from .models import Chunk, ParsedDoc
|
| 28 |
+
|
| 29 |
+
# Field names accepted for the same concept. `page_idx`/`page_idxs` are the
|
| 30 |
+
# parsing half's contract; the rest are earlier drafts and the prototype's shape,
|
| 31 |
+
# kept so old fixtures still load.
|
| 32 |
+
#
|
| 33 |
+
# Page numbers are 0-BASED throughout, exactly as the parser reports them. No
|
| 34 |
+
# conversion happens anywhere in this pipeline: converting to 1-based is the
|
| 35 |
+
# review UI's job, done once at display time. An off-by-one here would be
|
| 36 |
+
# invisible until an expert opened the wrong page.
|
| 37 |
+
_PAGE_KEYS = ("page_idx", "page_start", "page")
|
| 38 |
+
_PAGES_KEYS = ("page_idxs", "pages", "page_list")
|
| 39 |
+
_PAGE_END_KEYS = ("page_idx_end", "page_end")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def chunk_from_dict(raw: dict[str, Any], doc_id: str, ordinal: int = 0) -> Chunk:
|
| 43 |
+
"""Map one artifact item onto the internal chunk.
|
| 44 |
+
|
| 45 |
+
`kind`/`is_tabular` are reconciled: the draft carries a `kind` discriminator
|
| 46 |
+
while the prototype carried booleans. Either is accepted.
|
| 47 |
+
"""
|
| 48 |
+
kind = raw.get("kind")
|
| 49 |
+
pages = _first(raw, _PAGES_KEYS) or []
|
| 50 |
+
page_start = _first(raw, _PAGE_KEYS)
|
| 51 |
+
if page_start is None:
|
| 52 |
+
page_start = min(pages) if pages else 0
|
| 53 |
+
page_end = _first(raw, _PAGE_END_KEYS)
|
| 54 |
+
if page_end is None:
|
| 55 |
+
page_end = max(pages) if pages else page_start
|
| 56 |
+
|
| 57 |
+
return Chunk(
|
| 58 |
+
chunk_id=raw.get("chunk_id") or f"{doc_id}#{ordinal:04d}",
|
| 59 |
+
doc_id=raw.get("doc_id") or doc_id,
|
| 60 |
+
text=raw["text"], # verbatim, never cleaned
|
| 61 |
+
page_start=int(page_start),
|
| 62 |
+
page_end=int(page_end),
|
| 63 |
+
ordinal=int(raw.get("ordinal", ordinal)),
|
| 64 |
+
section_no=raw.get("section_no"),
|
| 65 |
+
heading=raw.get("heading"),
|
| 66 |
+
has_formula=bool(raw.get("has_formula", kind == "equation")),
|
| 67 |
+
is_tabular=bool(raw.get("is_tabular", kind == "table")),
|
| 68 |
+
bold_spans=list(raw.get("bold_spans") or []),
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def parsed_doc_from_artifact(
|
| 73 |
+
artifact: Any,
|
| 74 |
+
doc_id: str | None = None,
|
| 75 |
+
source_ref: str = "",
|
| 76 |
+
parser_name: str = "unknown",
|
| 77 |
+
parser_version: str = "",
|
| 78 |
+
) -> ParsedDoc:
|
| 79 |
+
"""Build a `ParsedDoc` from either shape of the artifact.
|
| 80 |
+
|
| 81 |
+
Accepts a bare `list[chunk]` (the draft's current shape) or a mapping with a
|
| 82 |
+
`chunks` key (the shape proposed for the document-level envelope). When the
|
| 83 |
+
envelope lands, its `content_hash`/`n_pages`/`version` are preferred over
|
| 84 |
+
the values derived here.
|
| 85 |
+
"""
|
| 86 |
+
if hasattr(artifact, "model_dump"): # a ParsedDocument from the parsing half
|
| 87 |
+
artifact = artifact.model_dump(mode="json")
|
| 88 |
+
|
| 89 |
+
if isinstance(artifact, dict):
|
| 90 |
+
items = artifact.get("chunks") or []
|
| 91 |
+
doc_id = doc_id or artifact.get("doc_id")
|
| 92 |
+
source_ref = source_ref or artifact.get("source_path") or artifact.get("source_ref") or ""
|
| 93 |
+
parser_name = artifact.get("parser_name") or parser_name
|
| 94 |
+
parser_version = artifact.get("parser_version") or parser_version
|
| 95 |
+
# The backend matters as much as the version: the same MinerU build can
|
| 96 |
+
# emit different text from `pipeline` and `vlm`, so a shift in extraction
|
| 97 |
+
# output has to be attributable to one or the other.
|
| 98 |
+
backend = artifact.get("parser_backend")
|
| 99 |
+
if backend:
|
| 100 |
+
parser_version = f"{parser_version}/{backend}" if parser_version else backend
|
| 101 |
+
declared_hash = artifact.get("content_hash")
|
| 102 |
+
declared_pages = artifact.get("n_pages")
|
| 103 |
+
else:
|
| 104 |
+
items = list(artifact)
|
| 105 |
+
declared_hash, declared_pages = None, None
|
| 106 |
+
|
| 107 |
+
if not doc_id:
|
| 108 |
+
doc_id = (items[0].get("doc_id") if items else None) or "unknown"
|
| 109 |
+
|
| 110 |
+
chunks = [chunk_from_dict(raw, doc_id, i) for i, raw in enumerate(items)]
|
| 111 |
+
pages = {p for c in chunks for p in (c.page_start, c.page_end)}
|
| 112 |
+
|
| 113 |
+
return ParsedDoc(
|
| 114 |
+
doc_id=doc_id,
|
| 115 |
+
source_ref=source_ref,
|
| 116 |
+
content_hash=declared_hash or content_hash(chunks),
|
| 117 |
+
n_pages=int(declared_pages) if declared_pages else (max(pages) + 1 if pages else 0),
|
| 118 |
+
chunks=chunks,
|
| 119 |
+
parser_name=parser_name,
|
| 120 |
+
parser_version=parser_version,
|
| 121 |
+
used_heading_split=any(c.section_no for c in chunks),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def content_hash(chunks: list[Chunk]) -> str:
|
| 126 |
+
"""Stable hash of the chunk text, so a re-parse that changed nothing can be
|
| 127 |
+
detected and the expensive stages skipped."""
|
| 128 |
+
blob = json.dumps([c.text for c in chunks], ensure_ascii=False).encode()
|
| 129 |
+
return hashlib.sha256(blob).hexdigest()[:16]
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _first(raw: dict[str, Any], keys: tuple[str, ...]) -> Any:
|
| 133 |
+
for key in keys:
|
| 134 |
+
if raw.get(key) is not None:
|
| 135 |
+
return raw[key]
|
| 136 |
+
return None
|
src/knowledge_extraction/cli.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Offline runner for the knowledge-extraction pipeline.
|
| 2 |
+
|
| 3 |
+
The pipeline runs a few times a year, triggered by an admin β so a script over a
|
| 4 |
+
parsed artifact is the honest entry point, and any HTTP surface is a convenience
|
| 5 |
+
layer over this, never the other way round.
|
| 6 |
+
|
| 7 |
+
Takes a **parsed-document artifact**, never a PDF: extraction does not parse.
|
| 8 |
+
Every stage writes its own JSON so a later stage can be re-run without repeating
|
| 9 |
+
an earlier one, which matters because prompt iteration is the main development
|
| 10 |
+
loop and the span filter is the slow part.
|
| 11 |
+
|
| 12 |
+
# free stages only (default) β no API calls, no spend
|
| 13 |
+
uv run --no-sync python -m src.knowledge_extraction.cli <artifact.json>
|
| 14 |
+
|
| 15 |
+
# cost estimate before spending anything
|
| 16 |
+
uv run --no-sync python -m src.knowledge_extraction.cli <artifact.json> --dry-run
|
| 17 |
+
|
| 18 |
+
# small pilot, then the full run
|
| 19 |
+
uv run --no-sync python -m src.knowledge_extraction.cli <artifact.json> --extract --limit 5
|
| 20 |
+
uv run --no-sync python -m src.knowledge_extraction.cli <artifact.json> --extract
|
| 21 |
+
|
| 22 |
+
# exercise the wiring with no credentials and no spend
|
| 23 |
+
uv run --no-sync python -m src.knowledge_extraction.cli <artifact.json> --extract --mock
|
| 24 |
+
|
| 25 |
+
Lives inside the package rather than in `scripts/`, which is gitignored: this
|
| 26 |
+
runner is the pipeline's operator entry point and has to ship with the module.
|
| 27 |
+
|
| 28 |
+
**Always --dry-run before a corpus-scale run.** It builds the exact prompts,
|
| 29 |
+
prints the token estimate, and makes zero API calls.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import argparse
|
| 35 |
+
import json
|
| 36 |
+
import re
|
| 37 |
+
import sys
|
| 38 |
+
from pathlib import Path
|
| 39 |
+
|
| 40 |
+
from .adapter import parsed_doc_from_artifact
|
| 41 |
+
from .cluster import cluster_mentions
|
| 42 |
+
from .extract import MockExtractor, cacheable, prefix_tokens
|
| 43 |
+
from .models import Mention
|
| 44 |
+
from .rank import rank_evidence
|
| 45 |
+
from .service import build_clusters, estimate_cost, extract_all, run_filters
|
| 46 |
+
from .settings import EVIDENCE_K
|
| 47 |
+
|
| 48 |
+
BRANCHES = ("glossary", "rule", "formula", "summary")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def main(argv: list[str] | None = None) -> int:
|
| 52 |
+
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 53 |
+
parser.add_argument("artifact", type=Path, help="parsed-document artifact JSON")
|
| 54 |
+
parser.add_argument("--out-dir", type=Path, default=Path("out/knowledge"))
|
| 55 |
+
parser.add_argument("--doc-id", help="override the artifact's doc_id")
|
| 56 |
+
parser.add_argument("--mentions", type=Path, help="span-NER mentions JSON")
|
| 57 |
+
parser.add_argument(
|
| 58 |
+
"--no-span-filter",
|
| 59 |
+
action="store_true",
|
| 60 |
+
help="skip the span model; legend terms only (wiring check, NOT a recall run)",
|
| 61 |
+
)
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--extract", action="store_true", help="run the PAID extraction stage"
|
| 64 |
+
)
|
| 65 |
+
parser.add_argument(
|
| 66 |
+
"--dry-run",
|
| 67 |
+
action="store_true",
|
| 68 |
+
help="build the prompts and print a token estimate; makes no API calls",
|
| 69 |
+
)
|
| 70 |
+
parser.add_argument("--mock", action="store_true", help="mock extractor: no network, no spend")
|
| 71 |
+
parser.add_argument("--limit", type=int, help="cap the number of items per branch (pilot)")
|
| 72 |
+
parser.add_argument(
|
| 73 |
+
"--branches", nargs="+", choices=BRANCHES, default=list(BRANCHES)
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument(
|
| 76 |
+
"--active-glossary", type=Path, help="approved glossary to diff against"
|
| 77 |
+
)
|
| 78 |
+
args = parser.parse_args(argv)
|
| 79 |
+
|
| 80 |
+
if not args.artifact.exists():
|
| 81 |
+
print(f"artifact not found: {args.artifact}", file=sys.stderr)
|
| 82 |
+
return 2
|
| 83 |
+
|
| 84 |
+
raw = json.loads(args.artifact.read_text(encoding="utf-8"))
|
| 85 |
+
doc = parsed_doc_from_artifact(raw, doc_id=args.doc_id, source_ref=str(args.artifact))
|
| 86 |
+
print(
|
| 87 |
+
f"[parse ] {doc.doc_id}: {len(doc.chunks)} chunks, {doc.n_pages} pages, "
|
| 88 |
+
f"hash {doc.content_hash}, heading-split={doc.used_heading_split}"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
filtered = run_filters(doc, use_span_filter=not (args.no_span_filter or args.mentions))
|
| 92 |
+
if args.mentions:
|
| 93 |
+
filtered.mentions = _load_mentions(args.mentions)
|
| 94 |
+
source = "file"
|
| 95 |
+
elif args.no_span_filter:
|
| 96 |
+
filtered.mentions = _from_pairs(doc, filtered.abbrev_pairs)
|
| 97 |
+
source = "legend stand-in (NOT a recall run)"
|
| 98 |
+
else:
|
| 99 |
+
source = "span filter"
|
| 100 |
+
|
| 101 |
+
capped = sum(m.hit_span_cap for m in filtered.mentions)
|
| 102 |
+
cap_note = f", {capped} hit the span cap" if capped else ""
|
| 103 |
+
print(
|
| 104 |
+
f"[filter] {len(filtered.abbrev_pairs)} abbreviation pairs, "
|
| 105 |
+
f"{len(filtered.rule_candidates)} rule candidates"
|
| 106 |
+
)
|
| 107 |
+
print(f"[filter] {len(filtered.mentions)} mentions from {source}{cap_note}")
|
| 108 |
+
|
| 109 |
+
if args.mentions or args.no_span_filter:
|
| 110 |
+
clustered = cluster_mentions(filtered.mentions, filtered.abbrev_pairs, doc.doc_id)
|
| 111 |
+
rank_evidence(clustered.clusters, doc.chunks)
|
| 112 |
+
else:
|
| 113 |
+
clustered = build_clusters(doc, filtered)
|
| 114 |
+
print(
|
| 115 |
+
f"[cluster] {clustered.n_mentions} mentions -> {clustered.n_clusters} clusters "
|
| 116 |
+
f"(compression {clustered.compression_ratio}x)"
|
| 117 |
+
)
|
| 118 |
+
for cluster in clustered.clusters[:8]:
|
| 119 |
+
print(
|
| 120 |
+
f" {cluster.canonical:<26} mentions={cluster.mention_count:<4} "
|
| 121 |
+
f"evidence={len(cluster.evidence_chunk_ids)} "
|
| 122 |
+
f"top={cluster.evidence_chunk_ids[:EVIDENCE_K]}"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
args.out_dir.mkdir(parents=True, exist_ok=True)
|
| 126 |
+
_dump(args.out_dir / f"{doc.doc_id}.chunks.json", doc.model_dump(mode="json"))
|
| 127 |
+
_dump(args.out_dir / f"{doc.doc_id}.filters.json", filtered.model_dump(mode="json"))
|
| 128 |
+
_dump(args.out_dir / f"{doc.doc_id}.clusters.json", clustered.model_dump(mode="json"))
|
| 129 |
+
|
| 130 |
+
if args.dry_run:
|
| 131 |
+
est = estimate_cost(doc, clustered, filtered, args.limit)
|
| 132 |
+
print("[dry-run] NO API CALLS MADE")
|
| 133 |
+
for key, value in est.items():
|
| 134 |
+
print(f" {key}: {value}")
|
| 135 |
+
for branch in args.branches:
|
| 136 |
+
print(
|
| 137 |
+
f" prefix[{branch}]: {prefix_tokens(branch)} tokens, "
|
| 138 |
+
f"cacheable={cacheable(branch)}"
|
| 139 |
+
)
|
| 140 |
+
return 0
|
| 141 |
+
|
| 142 |
+
if not args.extract:
|
| 143 |
+
print(f"[write ] {args.out_dir} (free stages only; --extract to run the paid stage)")
|
| 144 |
+
return 0
|
| 145 |
+
|
| 146 |
+
extractor = MockExtractor() if args.mock else _azure_extractor()
|
| 147 |
+
if extractor is None:
|
| 148 |
+
return 3
|
| 149 |
+
active = (
|
| 150 |
+
json.loads(args.active_glossary.read_text(encoding="utf-8"))
|
| 151 |
+
if args.active_glossary
|
| 152 |
+
else []
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
result = extract_all(
|
| 156 |
+
doc,
|
| 157 |
+
clustered,
|
| 158 |
+
filtered,
|
| 159 |
+
extractor,
|
| 160 |
+
limit=args.limit,
|
| 161 |
+
active_glossary=active,
|
| 162 |
+
branches=tuple(args.branches),
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
prompt, cached, completion = result.total_tokens
|
| 166 |
+
simulated = " [SIMULATED β not a quality measurement]" if args.mock else ""
|
| 167 |
+
print(f"[extract] {len(result.usages)} calls{simulated}")
|
| 168 |
+
print(
|
| 169 |
+
f" glossary={len(result.glossary)} rules={len(result.rules)} "
|
| 170 |
+
f"formulas={len(result.formulas)}"
|
| 171 |
+
)
|
| 172 |
+
print(f" tokens prompt={prompt} cached={cached} completion={completion}")
|
| 173 |
+
print(f" fields rejected by span check: {len(result.rejected)}")
|
| 174 |
+
no_def = sum(1 for e in result.glossary if e.get("extraction_status") == "no_definition_found")
|
| 175 |
+
print(f" abstained (no definition in document): {no_def}/{len(result.glossary)}")
|
| 176 |
+
|
| 177 |
+
print("[queue ] top of the review queue:")
|
| 178 |
+
for row in result.review_queue[:10]:
|
| 179 |
+
term = str(row.get("term"))[:26]
|
| 180 |
+
print(
|
| 181 |
+
f" {row['rank']:>3}. {term:<26} "
|
| 182 |
+
f"n={row['mention_count']:<4} {row['review_reason']}"
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
_dump(args.out_dir / "glossary.json", result.glossary)
|
| 186 |
+
_dump(args.out_dir / "interpretation_pack.json", result.rules)
|
| 187 |
+
_dump(args.out_dir / "formulas.json", result.formulas)
|
| 188 |
+
_dump(args.out_dir / "review_queue.json", result.review_queue)
|
| 189 |
+
_dump(args.out_dir / "rejected.json", [r.model_dump(mode="json") for r in result.rejected])
|
| 190 |
+
if result.brief:
|
| 191 |
+
_dump(args.out_dir / "brief_context.json", result.brief)
|
| 192 |
+
_dump(args.out_dir / "usage.json", [u.model_dump(mode="json") for u in result.usages])
|
| 193 |
+
print(f"[write ] {args.out_dir}")
|
| 194 |
+
return 0
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _azure_extractor():
|
| 198 |
+
from .extract import AzureExtractor
|
| 199 |
+
|
| 200 |
+
try:
|
| 201 |
+
return AzureExtractor()
|
| 202 |
+
except Exception as exc:
|
| 203 |
+
print(f"cannot build the Azure client: {exc}", file=sys.stderr)
|
| 204 |
+
print("use --mock to exercise the pipeline without credentials", file=sys.stderr)
|
| 205 |
+
return None
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _load_mentions(path: Path) -> list[Mention]:
|
| 209 |
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
| 210 |
+
items = raw.get("mentions", raw) if isinstance(raw, dict) else raw
|
| 211 |
+
return [Mention.model_validate(m) for m in items]
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _from_pairs(doc, pairs) -> list[Mention]:
|
| 215 |
+
"""Stand-in mentions from legend abbreviations, so the wiring is runnable
|
| 216 |
+
without the span model.
|
| 217 |
+
|
| 218 |
+
NOT a recall measurement β it only sees terms a legend block already named.
|
| 219 |
+
|
| 220 |
+
Word-boundary matching, never substring: "PA" occurs inside "parameter",
|
| 221 |
+
"pada", "capacity" and "composite", and substring matching produced 126
|
| 222 |
+
spurious PA mentions on a 9-page document (77x compression instead of the
|
| 223 |
+
measured 2.56x). Same trap the evidence ranker documents for headings.
|
| 224 |
+
"""
|
| 225 |
+
surfaces = {p.abbrev for p in pairs} | {p.expansion for p in pairs}
|
| 226 |
+
patterns = [
|
| 227 |
+
(s, re.compile(rf"(?<!\w){re.escape(s)}(?!\w)", re.IGNORECASE)) for s in surfaces
|
| 228 |
+
]
|
| 229 |
+
out: list[Mention] = []
|
| 230 |
+
for chunk in doc.chunks:
|
| 231 |
+
for surface, pattern in patterns:
|
| 232 |
+
for match in pattern.finditer(chunk.text):
|
| 233 |
+
out.append(
|
| 234 |
+
Mention(
|
| 235 |
+
surface=surface,
|
| 236 |
+
chunk_id=chunk.chunk_id,
|
| 237 |
+
char_start=match.start(),
|
| 238 |
+
char_end=match.end(),
|
| 239 |
+
label="legend",
|
| 240 |
+
score=1.0,
|
| 241 |
+
)
|
| 242 |
+
)
|
| 243 |
+
return out
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def _dump(path: Path, payload) -> None:
|
| 247 |
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
if __name__ == "__main__":
|
| 251 |
+
raise SystemExit(main())
|
src/knowledge_extraction/cluster/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .cluster import cluster_mentions
|
| 2 |
+
from .normalize import AbbrevIndex, is_noise, normalize
|
| 3 |
+
|
| 4 |
+
__all__ = ["AbbrevIndex", "cluster_mentions", "is_noise", "normalize"]
|
src/knowledge_extraction/cluster/cluster.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mention[] β TermCluster[].
|
| 2 |
+
|
| 3 |
+
Matching order, cheapest and most certain first:
|
| 4 |
+
1. exact match after normalisation
|
| 5 |
+
2. abbreviation β expansion (from legend blocks)
|
| 6 |
+
3. conservative fuzzy (token_set_ratio >= 92, and only above 5 characters)
|
| 7 |
+
|
| 8 |
+
**Over-merging is much worse than under-merging.** An under-merge costs one
|
| 9 |
+
extra LLM call and one extra review-queue row. A wrong merge silently destroys a
|
| 10 |
+
distinct term, and no downstream stage recovers it β the expert never learns the
|
| 11 |
+
term existed. Every threshold here is set on that asymmetry.
|
| 12 |
+
|
| 13 |
+
Clustering is also what makes conflict detection possible at all: contradictory
|
| 14 |
+
definitions can only be compared if all evidence for a term reaches the same
|
| 15 |
+
call.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from rapidfuzz import fuzz
|
| 21 |
+
|
| 22 |
+
from ..models import AbbrevPair, ClusterResult, Mention, TermCluster
|
| 23 |
+
from ..settings import FUZZY_MIN_LEN, FUZZY_THRESHOLD
|
| 24 |
+
from .normalize import AbbrevIndex, is_noise
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def cluster_mentions(
|
| 28 |
+
mentions: list[Mention],
|
| 29 |
+
abbrev_pairs: list[AbbrevPair],
|
| 30 |
+
doc_id: str,
|
| 31 |
+
fuzzy_threshold: int = FUZZY_THRESHOLD,
|
| 32 |
+
) -> ClusterResult:
|
| 33 |
+
index = AbbrevIndex(abbrev_pairs)
|
| 34 |
+
kept = [m for m in mentions if not is_noise(m.surface)]
|
| 35 |
+
|
| 36 |
+
buckets: dict[str, dict] = {}
|
| 37 |
+
|
| 38 |
+
for mention in kept:
|
| 39 |
+
key = index.canonical_key(mention.surface)
|
| 40 |
+
|
| 41 |
+
if key in buckets:
|
| 42 |
+
_add(buckets[key], mention, "exact")
|
| 43 |
+
continue
|
| 44 |
+
|
| 45 |
+
linked = next((k for k in buckets if index.linked(mention.surface, k)), None)
|
| 46 |
+
if linked:
|
| 47 |
+
_add(buckets[linked], mention, "abbrev")
|
| 48 |
+
continue
|
| 49 |
+
|
| 50 |
+
match = _fuzzy_match(key, buckets.keys(), fuzzy_threshold)
|
| 51 |
+
if match:
|
| 52 |
+
_add(buckets[match], mention, "fuzzy")
|
| 53 |
+
continue
|
| 54 |
+
|
| 55 |
+
buckets[key] = {
|
| 56 |
+
"surfaces": {mention.surface},
|
| 57 |
+
"mentions": [mention],
|
| 58 |
+
"reasons": set(),
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
clusters: list[TermCluster] = []
|
| 62 |
+
ordered = sorted(buckets.items(), key=lambda kv: -len(kv[1]["mentions"]))
|
| 63 |
+
for i, (key, data) in enumerate(ordered):
|
| 64 |
+
variants = sorted(data["surfaces"], key=lambda s: (len(s), s))
|
| 65 |
+
clusters.append(
|
| 66 |
+
TermCluster(
|
| 67 |
+
cluster_id=f"{doc_id}#c{i:03d}",
|
| 68 |
+
canonical=_canonical(variants, key),
|
| 69 |
+
variants=variants,
|
| 70 |
+
mentions=data["mentions"],
|
| 71 |
+
mention_count=len(data["mentions"]),
|
| 72 |
+
merge_reasons=sorted(data["reasons"]),
|
| 73 |
+
)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
n_mentions, n_clusters = len(kept), len(clusters)
|
| 77 |
+
return ClusterResult(
|
| 78 |
+
doc_id=doc_id,
|
| 79 |
+
clusters=clusters,
|
| 80 |
+
n_mentions=n_mentions,
|
| 81 |
+
n_clusters=n_clusters,
|
| 82 |
+
compression_ratio=round(n_mentions / n_clusters, 3) if n_clusters else 0.0,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _add(bucket: dict, mention: Mention, reason: str) -> None:
|
| 87 |
+
bucket["surfaces"].add(mention.surface)
|
| 88 |
+
bucket["mentions"].append(mention)
|
| 89 |
+
bucket["reasons"].add(reason)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _fuzzy_match(key: str, existing, threshold: int) -> str | None:
|
| 93 |
+
best, best_score = None, 0.0
|
| 94 |
+
for other in existing:
|
| 95 |
+
# Short strings fuzzy-match far too easily: "PA" vs "UA" scores high on
|
| 96 |
+
# token_set_ratio. Below FUZZY_MIN_LEN only exact matching is allowed.
|
| 97 |
+
if min(len(key), len(other)) < FUZZY_MIN_LEN:
|
| 98 |
+
continue
|
| 99 |
+
score = fuzz.token_set_ratio(key, other)
|
| 100 |
+
if score >= threshold and score > best_score:
|
| 101 |
+
best, best_score = other, score
|
| 102 |
+
return best
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _canonical(variants: list[str], key: str) -> str:
|
| 106 |
+
"""Prefer the shortest non-trivial surface β usually the term as a reader
|
| 107 |
+
would look it up ("PA", not "Physical Availability (PA) untuk ...")."""
|
| 108 |
+
for v in variants:
|
| 109 |
+
if len(v) >= 2:
|
| 110 |
+
return v
|
| 111 |
+
return key
|
src/knowledge_extraction/cluster/normalize.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Surface normalisation and the abbreviation index used by clustering.
|
| 2 |
+
|
| 3 |
+
**This normalisation is for clustering only.** Span validation normalises
|
| 4 |
+
whitespace and nothing else β every additional normalisation there is a hole a
|
| 5 |
+
fabrication can fit through. Do not reuse `normalize()` in that path.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
import unicodedata
|
| 12 |
+
|
| 13 |
+
from ..models import AbbrevPair
|
| 14 |
+
|
| 15 |
+
# Surfaces that carry no discriminating power on their own. A mention of just
|
| 16 |
+
# "unit" or "parameter" is not a term. These are dropped as WHOLE surface forms
|
| 17 |
+
# only, never as substrings β so no term containing them is ever lost.
|
| 18 |
+
STOP_SURFACES = {
|
| 19 |
+
"unit",
|
| 20 |
+
"type",
|
| 21 |
+
"class",
|
| 22 |
+
"equipment",
|
| 23 |
+
"equipment unit",
|
| 24 |
+
"parameter",
|
| 25 |
+
"activity",
|
| 26 |
+
"data",
|
| 27 |
+
"nilai",
|
| 28 |
+
"proses",
|
| 29 |
+
"hasil",
|
| 30 |
+
"total",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def normalize(surface: str) -> str:
|
| 35 |
+
s = unicodedata.normalize("NFKC", surface).casefold()
|
| 36 |
+
s = s.replace("-", " ").replace("_", " ")
|
| 37 |
+
s = re.sub(r"[.β']", "", s)
|
| 38 |
+
s = re.sub(r"[^\w\s/()]", " ", s)
|
| 39 |
+
s = re.sub(r"\s+", " ", s)
|
| 40 |
+
return s.strip(" ()/")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def is_noise(surface: str) -> bool:
|
| 44 |
+
n = normalize(surface)
|
| 45 |
+
if len(n) < 2:
|
| 46 |
+
return True
|
| 47 |
+
if n in STOP_SURFACES:
|
| 48 |
+
return True
|
| 49 |
+
return not re.search(r"[a-z]", n) # pure numbers / symbols
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class AbbrevIndex:
|
| 53 |
+
"""Bidirectional abbreviation β expansion lookup built from legend blocks.
|
| 54 |
+
|
| 55 |
+
This is why the legend filter runs before clustering: without it, `PA` and
|
| 56 |
+
`Physical Availability` never meet.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(self, pairs: list[AbbrevPair]):
|
| 60 |
+
self.to_expansion: dict[str, str] = {}
|
| 61 |
+
self.to_abbrev: dict[str, str] = {}
|
| 62 |
+
for pair in pairs:
|
| 63 |
+
abbrev, expansion = normalize(pair.abbrev), normalize(pair.expansion)
|
| 64 |
+
if not abbrev or not expansion:
|
| 65 |
+
continue
|
| 66 |
+
self.to_expansion[abbrev] = expansion
|
| 67 |
+
self.to_abbrev[expansion] = abbrev
|
| 68 |
+
|
| 69 |
+
def canonical_key(self, surface: str) -> str:
|
| 70 |
+
"""Map a surface to a shared key so an abbreviation and its expansion
|
| 71 |
+
collide into the same bucket."""
|
| 72 |
+
n = normalize(surface)
|
| 73 |
+
return self.to_abbrev.get(n, n)
|
| 74 |
+
|
| 75 |
+
def linked(self, a: str, b: str) -> bool:
|
| 76 |
+
na, nb = normalize(a), normalize(b)
|
| 77 |
+
return self.to_expansion.get(na) == nb or self.to_expansion.get(nb) == na
|
src/knowledge_extraction/config/__init__.py
ADDED
|
File without changes
|
src/knowledge_extraction/config/cues.yaml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Discourse cues (definitional) and legend-block patterns.
|
| 2 |
+
# Used by cue_filter (rule candidates) and evidence ranking (+5.0 signal).
|
| 3 |
+
|
| 4 |
+
definitional_id:
|
| 5 |
+
- '\badalah kondisi ketika\b'
|
| 6 |
+
- '\badalah\b'
|
| 7 |
+
- '\bmerupakan\b'
|
| 8 |
+
- '\byaitu\b'
|
| 9 |
+
- '\bdidefinisikan sebagai\b'
|
| 10 |
+
- '\bdimaksud dengan\b'
|
| 11 |
+
|
| 12 |
+
definitional_en:
|
| 13 |
+
- '\bis defined as\b'
|
| 14 |
+
- '\brefers to\b'
|
| 15 |
+
- '\bis the\b'
|
| 16 |
+
- '\bmeans\b'
|
| 17 |
+
|
| 18 |
+
conditional_id:
|
| 19 |
+
- '\bapabila\b'
|
| 20 |
+
- '\bjika\b.{0,80}\bmaka\b'
|
| 21 |
+
- '\bbila\b'
|
| 22 |
+
- '\bharus\b'
|
| 23 |
+
- '\btidak boleh\b'
|
| 24 |
+
- '\bwajib\b'
|
| 25 |
+
|
| 26 |
+
conditional_en:
|
| 27 |
+
- '\bif\b.{0,80}\bthen\b'
|
| 28 |
+
- '\bmust\b'
|
| 29 |
+
- '\bshall\b'
|
| 30 |
+
- '\bNote that\b'
|
| 31 |
+
|
| 32 |
+
note_id:
|
| 33 |
+
- '^\s*Catatan\b'
|
| 34 |
+
- '^\s*Sebagai contoh\b'
|
| 35 |
+
- '^\s*Contoh\b'
|
| 36 |
+
|
| 37 |
+
# Legend blocks: 'Keterangan:' / 'Dimana:' / 'where' introduce symbol tables.
|
| 38 |
+
legend_headers:
|
| 39 |
+
- '^\s*Keterangan\s*[::]'
|
| 40 |
+
- '^\s*Dimana\s*[::]'
|
| 41 |
+
- '^\s*Di\s*mana\s*[::]'
|
| 42 |
+
- '^\s*[Ww]here\s*[::]'
|
| 43 |
+
|
| 44 |
+
legend_entry: '^\s*([A-Za-z][A-Za-z0-9_\.]{0,12})\s*[:=]\s*(.+)$'
|
| 45 |
+
|
| 46 |
+
# A chunk is 'formula present' if any of these appear.
|
| 47 |
+
formula_markers:
|
| 48 |
+
- '='
|
| 49 |
+
- 'Γ'
|
| 50 |
+
- '\bx\s*100\s*%'
|
| 51 |
+
- '/'
|
src/knowledge_extraction/config/labels.yaml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GLiNER entity labels. E1 tunes these WITHOUT touching code.
|
| 2 |
+
# Two variants so E1 can A/B label phrasing (GLiNER is sensitive to label wording).
|
| 3 |
+
|
| 4 |
+
default:
|
| 5 |
+
- mining production metric
|
| 6 |
+
- equipment availability term
|
| 7 |
+
- operational abbreviation
|
| 8 |
+
- machine or equipment type
|
| 9 |
+
- time category
|
| 10 |
+
- production activity
|
| 11 |
+
- measurement unit
|
| 12 |
+
|
| 13 |
+
# Variant B: Indonesian-language label phrasing. GLiNER multi is multilingual;
|
| 14 |
+
# whether ID label strings improve ID recall is exactly what E1 tests.
|
| 15 |
+
id_phrasing:
|
| 16 |
+
- istilah produksi tambang
|
| 17 |
+
- parameter ketersediaan alat
|
| 18 |
+
- singkatan operasional
|
| 19 |
+
- jenis alat berat
|
| 20 |
+
- kategori waktu kerja
|
| 21 |
+
- aktivitas produksi
|
| 22 |
+
- satuan pengukuran
|
| 23 |
+
|
| 24 |
+
# Variant C: the first two variants both missed the same class of terms β
|
| 25 |
+
# mining activities and materials (coal mining, land clearing, waste removal,
|
| 26 |
+
# joint survey). This variant adds labels for that class specifically.
|
| 27 |
+
broad:
|
| 28 |
+
- mining production metric
|
| 29 |
+
- equipment availability term
|
| 30 |
+
- operational abbreviation
|
| 31 |
+
- machine or equipment type
|
| 32 |
+
- time category
|
| 33 |
+
- mining activity
|
| 34 |
+
- material or commodity
|
| 35 |
+
- measurement unit
|
| 36 |
+
- data source or system
|
| 37 |
+
- work category
|
| 38 |
+
- analysis method
|
| 39 |
+
|
| 40 |
+
# 0.25, not 0.35: the E1 sweep measured recall 0.854 @ 0.25 vs 0.658 @ 0.35 on the
|
| 41 |
+
# `broad` variant. Precision falls (0.41 vs 0.50) but the term filter is meant to be
|
| 42 |
+
# over-inclusive β clustering and evidence ranking absorb the noise, and a term the
|
| 43 |
+
# filter never proposes can never be recovered downstream.
|
| 44 |
+
threshold: 0.25
|
src/knowledge_extraction/diff/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .glossary_diff import classify, diff_glossary
|
| 2 |
+
|
| 3 |
+
__all__ = ["classify", "diff_glossary"]
|
src/knowledge_extraction/diff/glossary_diff.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Classify each candidate against the ACTIVE glossary version: new, duplicate
|
| 2 |
+
or conflicting.
|
| 3 |
+
|
| 4 |
+
The prototype diffed against the file it then overwrote, so every entry came
|
| 5 |
+
back `new` and the interesting paths never ran. The baseline must therefore be
|
| 6 |
+
supplied explicitly β an approved, versioned set β rather than read from
|
| 7 |
+
wherever the last run happened to write.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from ..models import DiffStatus
|
| 13 |
+
from ..settings import DUPLICATE_OVERLAP_THRESHOLD
|
| 14 |
+
from ..validate.conflict import overlap
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def classify(entry: dict, existing_by_term: dict[str, dict]) -> DiffStatus:
|
| 18 |
+
prior = existing_by_term.get((entry.get("term") or "").casefold())
|
| 19 |
+
if prior is None:
|
| 20 |
+
return "new"
|
| 21 |
+
a = (entry.get("definition") or "").strip()
|
| 22 |
+
b = (prior.get("definition") or "").strip()
|
| 23 |
+
if a and a == b:
|
| 24 |
+
return "duplicate"
|
| 25 |
+
if not a or not b:
|
| 26 |
+
# One side abstained: not a contradiction, just less information.
|
| 27 |
+
return "new"
|
| 28 |
+
return "duplicate" if overlap(a, b) >= DUPLICATE_OVERLAP_THRESHOLD else "conflicting"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def diff_glossary(entries: list[dict], active: list[dict]) -> list[dict]:
|
| 32 |
+
existing_by_term = {(e.get("term") or "").casefold(): e for e in active}
|
| 33 |
+
return [{**entry, "diff_status": classify(entry, existing_by_term)} for entry in entries]
|
src/knowledge_extraction/extract/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base import cacheable, est_tokens, evidence_block, load_prompt, prefix_tokens
|
| 2 |
+
from .branches import (
|
| 3 |
+
build_glossary_prompt,
|
| 4 |
+
extract_formula,
|
| 5 |
+
extract_glossary,
|
| 6 |
+
extract_rule,
|
| 7 |
+
extract_summary,
|
| 8 |
+
)
|
| 9 |
+
from .client import AzureExtractor, LLMResult, MockExtractor
|
| 10 |
+
from .schemas import schema_for
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"AzureExtractor",
|
| 14 |
+
"LLMResult",
|
| 15 |
+
"MockExtractor",
|
| 16 |
+
"build_glossary_prompt",
|
| 17 |
+
"cacheable",
|
| 18 |
+
"est_tokens",
|
| 19 |
+
"evidence_block",
|
| 20 |
+
"extract_formula",
|
| 21 |
+
"extract_glossary",
|
| 22 |
+
"extract_rule",
|
| 23 |
+
"extract_summary",
|
| 24 |
+
"load_prompt",
|
| 25 |
+
"prefix_tokens",
|
| 26 |
+
"schema_for",
|
| 27 |
+
]
|
src/knowledge_extraction/extract/base.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared plumbing for the four extraction branches.
|
| 2 |
+
|
| 3 |
+
Prompts live in `prompts/*.txt`, never in code, for two reasons: a prompt change
|
| 4 |
+
is not a code change, and **the fixed prefix must stay byte-identical across
|
| 5 |
+
calls** or prompt caching silently stops engaging at roughly 10x the input cost.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from functools import lru_cache
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from ..models import Chunk
|
| 14 |
+
from ..settings import CACHE_MIN_TOKENS
|
| 15 |
+
|
| 16 |
+
PROMPT_DIR = Path(__file__).parent / "prompts"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def est_tokens(text: str) -> int:
|
| 20 |
+
"""Cheap estimate, for dry-run budgeting only. Real counts come from the
|
| 21 |
+
API's usage object β never report a cached price from an estimate."""
|
| 22 |
+
return max(1, int(len(text) / 3.6))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@lru_cache(maxsize=8)
|
| 26 |
+
def load_prompt(branch: str) -> str:
|
| 27 |
+
return (PROMPT_DIR / f"{branch}.txt").read_text(encoding="utf-8")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def prefix_tokens(branch: str) -> int:
|
| 31 |
+
return est_tokens(load_prompt(branch))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def cacheable(branch: str) -> bool:
|
| 35 |
+
"""Whether the fixed prefix is long enough to cache at all.
|
| 36 |
+
|
| 37 |
+
Reported, never assumed: caching does not engage below the floor, so a
|
| 38 |
+
shorter prefix caches nothing. Only the API's `cached_tokens` proves a hit.
|
| 39 |
+
"""
|
| 40 |
+
return prefix_tokens(branch) >= CACHE_MIN_TOKENS
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def evidence_block(chunks: list[Chunk], scores: list[float] | None = None) -> str:
|
| 44 |
+
"""Evidence labelled with chunk_id, section and page so the model can cite
|
| 45 |
+
provenance and we can trace which evidence produced which field.
|
| 46 |
+
|
| 47 |
+
**The heading is included, and must stay included.** Two reasons:
|
| 48 |
+
|
| 49 |
+
1. Indonesian standards name the term in the heading and open the body with
|
| 50 |
+
the definition β "2.1.3 Physical of Availability (PA)" / "Adalah
|
| 51 |
+
ketersediaan fisikβ¦" β so the body often never repeats the term. Without
|
| 52 |
+
the heading the model is asked to define a term the evidence never names.
|
| 53 |
+
2. It keeps one invariant true: **what the model reads is exactly what the
|
| 54 |
+
span check searches.** `validate.evidence_text` composes heading + text;
|
| 55 |
+
if this block showed only the text, the model could never quote a
|
| 56 |
+
section title, and any field that did quote one would be rejected as
|
| 57 |
+
unlocatable.
|
| 58 |
+
"""
|
| 59 |
+
parts = []
|
| 60 |
+
for i, chunk in enumerate(chunks):
|
| 61 |
+
score = f" score={scores[i]:.1f}" if scores and i < len(scores) else ""
|
| 62 |
+
head = f"{chunk.heading}\n" if chunk.heading else ""
|
| 63 |
+
parts.append(
|
| 64 |
+
f"[chunk_id={chunk.chunk_id} section={chunk.section_no or '-'} "
|
| 65 |
+
f"page={chunk.page_start}{score}]\n{head}{chunk.text}"
|
| 66 |
+
)
|
| 67 |
+
return "EVIDENCE\n" + "\n\n---\n\n".join(parts)
|
src/knowledge_extraction/extract/branches.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The four extraction branches. This is the only stage that costs money.
|
| 2 |
+
|
| 3 |
+
**One call per TERM CLUSTER** β not per mention, not per chunk. That is the
|
| 4 |
+
whole economic argument for clustering: 200 mentions of "PA" cost one call, not
|
| 5 |
+
200. It is also what makes conflict detection possible, since contradictory
|
| 6 |
+
definitions can only be compared when they arrive together.
|
| 7 |
+
|
| 8 |
+
Each branch returns `(entry, usage)`, with `None` for the entry when the
|
| 9 |
+
response fails schema validation. A failed parse is not an exception: one bad
|
| 10 |
+
response must not abort a corpus-scale run that has already paid for parsing.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from ...middlewares.logging import get_logger
|
| 16 |
+
from ..models import (
|
| 17 |
+
BriefContext,
|
| 18 |
+
CallUsage,
|
| 19 |
+
Chunk,
|
| 20 |
+
FormulaEntry,
|
| 21 |
+
FormulaVariable,
|
| 22 |
+
GlossaryEntry,
|
| 23 |
+
Provenance,
|
| 24 |
+
RuleCandidate,
|
| 25 |
+
RuleEntry,
|
| 26 |
+
TermCluster,
|
| 27 |
+
)
|
| 28 |
+
from ..rank import top_k
|
| 29 |
+
from ..settings import EVIDENCE_K
|
| 30 |
+
from .base import evidence_block, load_prompt
|
| 31 |
+
from .schemas import FormulaDraft, GlossaryDraft, RuleDraft, SummaryDraft, schema_for
|
| 32 |
+
|
| 33 |
+
logger = get_logger("knowledge_extract")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _prov(draft_prov, doc_id: str, chunk_id: str | None = None) -> Provenance:
|
| 37 |
+
return Provenance(
|
| 38 |
+
doc_id=doc_id,
|
| 39 |
+
span=draft_prov.span,
|
| 40 |
+
page=draft_prov.page,
|
| 41 |
+
section_no=draft_prov.section_no,
|
| 42 |
+
chunk_id=chunk_id,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _evidence_for(
|
| 47 |
+
cluster: TermCluster, chunks: list[Chunk], k: int, round_index: int
|
| 48 |
+
) -> tuple[list[Chunk], list[float]]:
|
| 49 |
+
by_id = {c.chunk_id: c for c in chunks}
|
| 50 |
+
ids = top_k(cluster, k=k, round_index=round_index)
|
| 51 |
+
scores = cluster.evidence_scores[round_index * k : round_index * k + k]
|
| 52 |
+
return [by_id[i] for i in ids if i in by_id], scores
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# ββ glossary ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def build_glossary_prompt(
|
| 59 |
+
cluster: TermCluster, chunks: list[Chunk], k: int = EVIDENCE_K, round_index: int = 0
|
| 60 |
+
) -> tuple[str, str]:
|
| 61 |
+
evidence, scores = _evidence_for(cluster, chunks, k, round_index)
|
| 62 |
+
user = (
|
| 63 |
+
f"CANDIDATE TERM: {cluster.canonical}\n"
|
| 64 |
+
f"KNOWN VARIANTS: {', '.join(cluster.variants)}\n"
|
| 65 |
+
f"MENTION COUNT: {cluster.mention_count}\n\n"
|
| 66 |
+
+ evidence_block(evidence, scores)
|
| 67 |
+
)
|
| 68 |
+
return load_prompt("glossary"), user
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def extract_glossary(
|
| 72 |
+
cluster: TermCluster,
|
| 73 |
+
chunks: list[Chunk],
|
| 74 |
+
extractor,
|
| 75 |
+
doc_id: str,
|
| 76 |
+
k: int = EVIDENCE_K,
|
| 77 |
+
round_index: int = 0,
|
| 78 |
+
) -> tuple[GlossaryEntry | None, CallUsage]:
|
| 79 |
+
system, user = build_glossary_prompt(cluster, chunks, k, round_index)
|
| 80 |
+
result = extractor.complete(
|
| 81 |
+
"glossary", system, user, schema_for("glossary"), "GlossaryEntry"
|
| 82 |
+
)
|
| 83 |
+
try:
|
| 84 |
+
draft = GlossaryDraft.model_validate(result.data)
|
| 85 |
+
except Exception as exc:
|
| 86 |
+
logger.warning(
|
| 87 |
+
"glossary draft invalid", cluster=cluster.canonical, error=repr(exc)
|
| 88 |
+
)
|
| 89 |
+
return None, result.usage
|
| 90 |
+
|
| 91 |
+
evidence, _ = _evidence_for(cluster, chunks, k, round_index)
|
| 92 |
+
entry = GlossaryEntry(
|
| 93 |
+
term=draft.term,
|
| 94 |
+
full_name=draft.full_name,
|
| 95 |
+
source_wording=_heading_wording(cluster, evidence) or draft.source_wording,
|
| 96 |
+
definition=draft.definition,
|
| 97 |
+
formula_latex=draft.formula_latex,
|
| 98 |
+
interpretation=draft.interpretation,
|
| 99 |
+
subdomain_tags=draft.subdomain_tags,
|
| 100 |
+
domain=draft.domain,
|
| 101 |
+
company=draft.company,
|
| 102 |
+
language=draft.language,
|
| 103 |
+
mention_count=cluster.mention_count,
|
| 104 |
+
provenance=_prov(
|
| 105 |
+
draft.provenance, doc_id, evidence[0].chunk_id if evidence else None
|
| 106 |
+
),
|
| 107 |
+
)
|
| 108 |
+
return entry, result.usage
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _heading_wording(cluster: TermCluster, evidence: list[Chunk]) -> str | None:
|
| 112 |
+
"""The verbatim heading of the evidence chunk whose title names this term.
|
| 113 |
+
|
| 114 |
+
Preferred over whatever the model chose to quote, because the section
|
| 115 |
+
heading is where the document formally names the term. Measured on the
|
| 116 |
+
reference standard: the model quoted "Physical Availability (PA)" from the
|
| 117 |
+
page-1 intro β a real verbatim quote β while the section itself is headed
|
| 118 |
+
"Physical **of** Availability (PA)". Both occur in the document; only the
|
| 119 |
+
heading form reveals that the two disagree.
|
| 120 |
+
|
| 121 |
+
Recording the literal form is a locked decision: the discrepancy belongs to
|
| 122 |
+
the expert, not to us. Taking it deterministically rather than asking the
|
| 123 |
+
model to volunteer it means it cannot be normalised away.
|
| 124 |
+
"""
|
| 125 |
+
from ..cluster.normalize import normalize
|
| 126 |
+
from ..rank.evidence import _word_match
|
| 127 |
+
|
| 128 |
+
variants = [normalize(v) for v in cluster.variants]
|
| 129 |
+
for chunk in evidence:
|
| 130 |
+
heading = chunk.heading
|
| 131 |
+
if heading and any(_word_match(v, normalize(heading)) for v in variants):
|
| 132 |
+
return heading
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ββ rule of thumb βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def extract_rule(
|
| 140 |
+
candidate: RuleCandidate, chunk: Chunk, extractor, doc_id: str
|
| 141 |
+
) -> tuple[RuleEntry | None, CallUsage]:
|
| 142 |
+
user = (
|
| 143 |
+
f"CUE: {candidate.cue}\n\n"
|
| 144 |
+
+ evidence_block([chunk])
|
| 145 |
+
+ f"\n\nFOCUS PASSAGE:\n{candidate.snippet}"
|
| 146 |
+
)
|
| 147 |
+
result = extractor.complete(
|
| 148 |
+
"rule", load_prompt("rule"), user, schema_for("rule"), "RuleEntry"
|
| 149 |
+
)
|
| 150 |
+
try:
|
| 151 |
+
draft = RuleDraft.model_validate(result.data)
|
| 152 |
+
except Exception as exc:
|
| 153 |
+
logger.warning("rule draft invalid", chunk_id=candidate.chunk_id, error=repr(exc))
|
| 154 |
+
return None, result.usage
|
| 155 |
+
|
| 156 |
+
return (
|
| 157 |
+
RuleEntry(
|
| 158 |
+
rule_id=draft.rule_id,
|
| 159 |
+
statement=draft.statement,
|
| 160 |
+
condition=draft.condition,
|
| 161 |
+
consequence=draft.consequence,
|
| 162 |
+
applies_to=draft.applies_to,
|
| 163 |
+
subdomain_tags=draft.subdomain_tags,
|
| 164 |
+
language=draft.language,
|
| 165 |
+
provenance=_prov(draft.provenance, doc_id, chunk.chunk_id),
|
| 166 |
+
),
|
| 167 |
+
result.usage,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# ββ formula βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def extract_formula(
|
| 175 |
+
chunk: Chunk, extractor, doc_id: str
|
| 176 |
+
) -> tuple[FormulaEntry | None, CallUsage]:
|
| 177 |
+
user = evidence_block([chunk])
|
| 178 |
+
result = extractor.complete(
|
| 179 |
+
"formula", load_prompt("formula"), user, schema_for("formula"), "FormulaEntry"
|
| 180 |
+
)
|
| 181 |
+
try:
|
| 182 |
+
draft = FormulaDraft.model_validate(result.data)
|
| 183 |
+
except Exception as exc:
|
| 184 |
+
logger.warning("formula draft invalid", chunk_id=chunk.chunk_id, error=repr(exc))
|
| 185 |
+
return None, result.usage
|
| 186 |
+
|
| 187 |
+
return (
|
| 188 |
+
FormulaEntry(
|
| 189 |
+
name=draft.name,
|
| 190 |
+
formula_latex=draft.formula_latex,
|
| 191 |
+
variables=[
|
| 192 |
+
FormulaVariable(symbol=v.symbol, meaning=v.meaning) for v in draft.variables
|
| 193 |
+
],
|
| 194 |
+
unit=draft.unit,
|
| 195 |
+
provenance=_prov(draft.provenance, doc_id, chunk.chunk_id),
|
| 196 |
+
),
|
| 197 |
+
result.usage,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# ββ summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def extract_summary(
|
| 205 |
+
chunks: list[Chunk], extractor, doc_id: str
|
| 206 |
+
) -> tuple[BriefContext | None, CallUsage]:
|
| 207 |
+
"""Whole-document summary β the quiet cost risk. Few calls, but a large
|
| 208 |
+
share of all input tokens, because summarisation cannot be filtered: it
|
| 209 |
+
needs the whole document.
|
| 210 |
+
|
| 211 |
+
It is also the only branch that cannot be span-checked at all. A plausible
|
| 212 |
+
summary is indistinguishable from a correct one, which is exactly why it
|
| 213 |
+
belongs on a larger tier as soon as one exists.
|
| 214 |
+
"""
|
| 215 |
+
user = evidence_block(chunks)
|
| 216 |
+
result = extractor.complete(
|
| 217 |
+
"summary", load_prompt("summary"), user, schema_for("summary"), "BriefContext"
|
| 218 |
+
)
|
| 219 |
+
try:
|
| 220 |
+
draft = SummaryDraft.model_validate(result.data)
|
| 221 |
+
except Exception as exc:
|
| 222 |
+
logger.warning("summary draft invalid", error=repr(exc))
|
| 223 |
+
return None, result.usage
|
| 224 |
+
|
| 225 |
+
return (
|
| 226 |
+
BriefContext(
|
| 227 |
+
title=draft.title,
|
| 228 |
+
purpose=draft.purpose,
|
| 229 |
+
scope=draft.scope,
|
| 230 |
+
key_parameters=draft.key_parameters,
|
| 231 |
+
summary_md=draft.summary_md,
|
| 232 |
+
provenance=_prov(draft.provenance, doc_id),
|
| 233 |
+
),
|
| 234 |
+
result.usage,
|
| 235 |
+
)
|
src/knowledge_extraction/extract/client.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM clients for the extraction stage β **the only place this pipeline spends
|
| 2 |
+
money.**
|
| 3 |
+
|
| 4 |
+
Two things this module is careful about:
|
| 5 |
+
|
| 6 |
+
- **Structured output is probed, not assumed.** `json_schema` needs a recent
|
| 7 |
+
api_version and we cannot confirm from here what the resource exposes. The
|
| 8 |
+
first call tries it; on rejection it falls back to `json_object` plus
|
| 9 |
+
validate-and-retry, and records which mode actually applied.
|
| 10 |
+
- **Cached tokens are read from the API, never modelled.** Caching does not
|
| 11 |
+
engage below the token floor, so an under-length prefix caches nothing.
|
| 12 |
+
`usage.prompt_tokens_details.cached_tokens` is the only source of truth, and a
|
| 13 |
+
cached price must never be reported without it.
|
| 14 |
+
|
| 15 |
+
All four branches route to the **nano** deployment (`__54n`). That is a recorded
|
| 16 |
+
decision, not an oversight: nano measured 0.75 schema-fill precision against a
|
| 17 |
+
0.80 line, and `rule`/`summary` β whose failure mode is least detectable, since
|
| 18 |
+
a plausible summary cannot be span-checked β run there too until a larger
|
| 19 |
+
deployment exists.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import json
|
| 25 |
+
import time
|
| 26 |
+
from typing import Any
|
| 27 |
+
|
| 28 |
+
from ...config.settings import settings as app_settings
|
| 29 |
+
from ...middlewares.logging import get_logger
|
| 30 |
+
from ..models import Branch, CallUsage
|
| 31 |
+
from ..settings import TEMPERATURE
|
| 32 |
+
|
| 33 |
+
logger = get_logger("knowledge_extract_client")
|
| 34 |
+
|
| 35 |
+
MAX_RETRIES = 3
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class LLMResult:
|
| 39 |
+
def __init__(self, data: dict, usage: CallUsage, raw: str = ""):
|
| 40 |
+
self.data = data
|
| 41 |
+
self.usage = usage
|
| 42 |
+
self.raw = raw
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class AzureExtractor:
|
| 46 |
+
"""Real calls, real spend. Always dry-run before a corpus-scale run."""
|
| 47 |
+
|
| 48 |
+
def __init__(self, client=None, deployment: str | None = None):
|
| 49 |
+
self.deployment = deployment or app_settings.azureai_deployment_name_54n
|
| 50 |
+
self._client = client or self._build_client()
|
| 51 |
+
self._mode: str | None = None # resolved on the first successful call
|
| 52 |
+
|
| 53 |
+
@staticmethod
|
| 54 |
+
def _build_client():
|
| 55 |
+
from openai import AzureOpenAI
|
| 56 |
+
|
| 57 |
+
endpoint = app_settings.azureai_endpoint_url_54n
|
| 58 |
+
api_key = app_settings.azureai_api_key_54n
|
| 59 |
+
if not endpoint or not api_key:
|
| 60 |
+
raise RuntimeError(
|
| 61 |
+
"azureai__endpoint__url__54n / azureai__api_key__54n are not set. "
|
| 62 |
+
"Use the mock extractor to run without Azure."
|
| 63 |
+
)
|
| 64 |
+
return AzureOpenAI(
|
| 65 |
+
azure_endpoint=endpoint,
|
| 66 |
+
api_key=api_key,
|
| 67 |
+
api_version=app_settings.azureai_api_version_54n,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def complete(
|
| 71 |
+
self,
|
| 72 |
+
branch: Branch,
|
| 73 |
+
system_prompt: str,
|
| 74 |
+
user_prompt: str,
|
| 75 |
+
schema: dict,
|
| 76 |
+
schema_name: str,
|
| 77 |
+
) -> LLMResult:
|
| 78 |
+
messages = [
|
| 79 |
+
{"role": "system", "content": system_prompt},
|
| 80 |
+
{"role": "user", "content": user_prompt},
|
| 81 |
+
]
|
| 82 |
+
last_error: Exception | None = None
|
| 83 |
+
|
| 84 |
+
for attempt in range(MAX_RETRIES):
|
| 85 |
+
mode = self._mode or "json_schema"
|
| 86 |
+
started = time.time()
|
| 87 |
+
try:
|
| 88 |
+
response = self._client.chat.completions.create(
|
| 89 |
+
model=self.deployment,
|
| 90 |
+
messages=messages,
|
| 91 |
+
temperature=TEMPERATURE,
|
| 92 |
+
response_format=self._response_format(mode, schema, schema_name),
|
| 93 |
+
)
|
| 94 |
+
except Exception as exc:
|
| 95 |
+
if mode == "json_schema" and self._looks_unsupported(exc):
|
| 96 |
+
logger.info(
|
| 97 |
+
"json_schema unsupported β falling back to json_object",
|
| 98 |
+
error=repr(exc),
|
| 99 |
+
)
|
| 100 |
+
self._mode = "json_object"
|
| 101 |
+
continue
|
| 102 |
+
last_error = exc
|
| 103 |
+
logger.warning("call failed", branch=branch, attempt=attempt, error=repr(exc))
|
| 104 |
+
time.sleep(2**attempt)
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
self._mode = mode
|
| 108 |
+
content = response.choices[0].message.content or "{}"
|
| 109 |
+
try:
|
| 110 |
+
data = json.loads(content)
|
| 111 |
+
except json.JSONDecodeError as exc:
|
| 112 |
+
last_error = exc
|
| 113 |
+
logger.warning("unparseable JSON", branch=branch, attempt=attempt)
|
| 114 |
+
continue
|
| 115 |
+
|
| 116 |
+
usage = self._usage(response, branch, time.time() - started, attempt, mode)
|
| 117 |
+
return LLMResult(data, usage, content)
|
| 118 |
+
|
| 119 |
+
raise RuntimeError(f"{branch}: all {MAX_RETRIES} attempts failed: {last_error!r}")
|
| 120 |
+
|
| 121 |
+
@staticmethod
|
| 122 |
+
def _response_format(mode: str, schema: dict, schema_name: str) -> dict:
|
| 123 |
+
if mode == "json_schema":
|
| 124 |
+
return {
|
| 125 |
+
"type": "json_schema",
|
| 126 |
+
"json_schema": {"name": schema_name, "schema": schema, "strict": False},
|
| 127 |
+
}
|
| 128 |
+
return {"type": "json_object"}
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def _looks_unsupported(exc: Exception) -> bool:
|
| 132 |
+
text = str(exc).lower()
|
| 133 |
+
return any(
|
| 134 |
+
s in text
|
| 135 |
+
for s in ("response_format", "json_schema", "unsupported", "invalid_request")
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
def _usage(
|
| 139 |
+
self, response: Any, branch: Branch, latency: float, retries: int, mode: str
|
| 140 |
+
) -> CallUsage:
|
| 141 |
+
usage = getattr(response, "usage", None)
|
| 142 |
+
details = getattr(usage, "prompt_tokens_details", None)
|
| 143 |
+
# The ONLY source of truth for caching. Absent -> cached stays 0 and the
|
| 144 |
+
# uncached regime is what gets reported.
|
| 145 |
+
cached = int(getattr(details, "cached_tokens", 0) or 0) if details else 0
|
| 146 |
+
return CallUsage(
|
| 147 |
+
branch=branch,
|
| 148 |
+
deployment=self.deployment,
|
| 149 |
+
tier="nano",
|
| 150 |
+
prompt_tokens=int(getattr(usage, "prompt_tokens", 0) or 0),
|
| 151 |
+
cached_tokens=cached,
|
| 152 |
+
completion_tokens=int(getattr(usage, "completion_tokens", 0) or 0),
|
| 153 |
+
latency_s=round(latency, 3),
|
| 154 |
+
retries=retries,
|
| 155 |
+
structured_output_mode=mode,
|
| 156 |
+
simulated=False,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class MockExtractor:
|
| 161 |
+
"""No network, no spend. Every record it produces is stamped `simulated`.
|
| 162 |
+
|
| 163 |
+
Exercises the wiring β schema validation, span checking, escalation,
|
| 164 |
+
conflicts, diff, queue β without credentials. It is **not** a model-quality
|
| 165 |
+
measurement and its output must never be reported as one.
|
| 166 |
+
|
| 167 |
+
It abstains by default (returns null definitions), because abstention is the
|
| 168 |
+
dominant real behaviour: on the reference document 56 of 66 entries had no
|
| 169 |
+
definition. A mock that always answers would make the downstream stages look
|
| 170 |
+
far better exercised than they are.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
def __init__(self, responses: dict[str, dict] | None = None, deployment: str = "mock"):
|
| 174 |
+
self.responses = responses or {}
|
| 175 |
+
self.deployment = deployment
|
| 176 |
+
self.calls: list[tuple[str, str]] = []
|
| 177 |
+
|
| 178 |
+
def complete(
|
| 179 |
+
self,
|
| 180 |
+
branch: Branch,
|
| 181 |
+
system_prompt: str,
|
| 182 |
+
user_prompt: str,
|
| 183 |
+
schema: dict,
|
| 184 |
+
schema_name: str,
|
| 185 |
+
) -> LLMResult:
|
| 186 |
+
self.calls.append((branch, user_prompt))
|
| 187 |
+
data = self.responses.get(branch) or self._abstain(branch, user_prompt)
|
| 188 |
+
usage = CallUsage(
|
| 189 |
+
branch=branch,
|
| 190 |
+
deployment=self.deployment,
|
| 191 |
+
prompt_tokens=len(system_prompt) // 4 + len(user_prompt) // 4,
|
| 192 |
+
completion_tokens=40,
|
| 193 |
+
structured_output_mode="mock",
|
| 194 |
+
simulated=True,
|
| 195 |
+
)
|
| 196 |
+
return LLMResult(data, usage, json.dumps(data))
|
| 197 |
+
|
| 198 |
+
@staticmethod
|
| 199 |
+
def _abstain(branch: Branch, user_prompt: str) -> dict:
|
| 200 |
+
# Quote a real fragment so the span check has something locatable and is
|
| 201 |
+
# genuinely exercised rather than trivially passed.
|
| 202 |
+
span = ""
|
| 203 |
+
if "EVIDENCE" in user_prompt:
|
| 204 |
+
body = user_prompt.split("EVIDENCE", 1)[1]
|
| 205 |
+
for line in body.splitlines():
|
| 206 |
+
if line.strip() and not line.startswith("["):
|
| 207 |
+
span = line.strip()[:60]
|
| 208 |
+
break
|
| 209 |
+
prov = {"section_no": None, "page": 1, "span": span}
|
| 210 |
+
if branch == "glossary":
|
| 211 |
+
term = "unknown"
|
| 212 |
+
for line in user_prompt.splitlines():
|
| 213 |
+
if line.startswith("CANDIDATE TERM:"):
|
| 214 |
+
term = line.split(":", 1)[1].strip()
|
| 215 |
+
break
|
| 216 |
+
return {"term": term, "definition": None, "provenance": prov}
|
| 217 |
+
if branch == "rule":
|
| 218 |
+
return {"rule_id": "r_mock", "statement": None, "provenance": prov}
|
| 219 |
+
if branch == "formula":
|
| 220 |
+
return {"name": None, "formula_latex": None, "provenance": prov}
|
| 221 |
+
return {"title": None, "summary_md": None, "provenance": prov}
|
src/knowledge_extraction/extract/prompts/formula.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You transcribe FORMULAS from Indonesian and English mining-operations standards.
|
| 2 |
+
|
| 3 |
+
This is a TRANSCRIPTION task, not a derivation task. You are converting a formula
|
| 4 |
+
that is already written in the evidence into LaTeX. You must never derive, simplify,
|
| 5 |
+
correct, or complete a formula.
|
| 6 |
+
|
| 7 |
+
RULES β correctness requirements:
|
| 8 |
+
1. If no formula is present in the evidence, set "formula_latex" to null. A null
|
| 9 |
+
answer is a CORRECT answer.
|
| 10 |
+
2. Transcribe exactly. If the source writes "x 100%", keep the percentage.
|
| 11 |
+
3. List every variable that appears, with the meaning ONLY if the evidence states it
|
| 12 |
+
(usually in a "Keterangan:" or "Dimana:" legend block). Otherwise meaning is null.
|
| 13 |
+
4. "provenance.span" must be copied VERBATIM from the evidence and is checked
|
| 14 |
+
automatically against the source.
|
| 15 |
+
|
| 16 |
+
FIELD GUIDE:
|
| 17 |
+
- name: what the formula computes, as named in the source
|
| 18 |
+
- formula_latex: LaTeX transcription
|
| 19 |
+
- variables: [{"symbol": "...", "meaning": "..." or null}, ...]
|
| 20 |
+
- unit: the result unit if stated (BCM, ton, %, hours), else null
|
| 21 |
+
|
| 22 |
+
WORKED EXAMPLE:
|
| 23 |
+
Evidence: "Secara umum, PA dihitung menggunakan rumus berikut:\nPA = Total Hours -
|
| 24 |
+
Breakdown / Total Hours x 100%\nKeterangan:\nPA : Physical Availability"
|
| 25 |
+
Output:
|
| 26 |
+
{"name": "Physical Availability (PA)", "formula_latex": "PA = \\frac{Total\\ Hours - Breakdown}{Total\\ Hours} \\times 100\\%", "variables": [{"symbol": "PA", "meaning": "Physical Availability"}, {"symbol": "Total Hours", "meaning": null}, {"symbol": "Breakdown", "meaning": null}], "unit": "%", "provenance": {"section_no": "2.1.3", "page": 4, "span": "PA = Total Hours - Breakdown"}}
|
| 27 |
+
|
| 28 |
+
Return a single JSON object. No prose, no markdown fence.
|
src/knowledge_extraction/extract/prompts/glossary.txt
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You extract glossary entries from Indonesian and English mining-operations standards.
|
| 2 |
+
|
| 3 |
+
RULES β these are not style preferences, they are correctness requirements:
|
| 4 |
+
1. Extract ONLY what the evidence states. Never infer, complete, or generalise.
|
| 5 |
+
2. If the evidence does not define the term, set "definition" to null. A null answer
|
| 6 |
+
is a CORRECT answer. Guessing is the single worst failure mode here.
|
| 7 |
+
3. "provenance.span" must be copied VERBATIM from the evidence β character for
|
| 8 |
+
character, including Indonesian spelling. It is automatically checked against the
|
| 9 |
+
source and the field is discarded if it does not match exactly.
|
| 10 |
+
4. "subdomain_tags" must come from the allowed list only. Do not invent tags.
|
| 11 |
+
5. Preserve the source language. Do not translate an Indonesian definition to English.
|
| 12 |
+
|
| 13 |
+
ALLOWED subdomain_tags:
|
| 14 |
+
production, maintenance, hauling, loading, drilling_blasting, equipment, safety,
|
| 15 |
+
quality, planning, cost, geology, other
|
| 16 |
+
|
| 17 |
+
FIELD GUIDE:
|
| 18 |
+
- term: the term as a reader would look it up (usually the abbreviation, e.g. "PA")
|
| 19 |
+
- full_name: the expanded form exactly as written in the source, else null
|
| 20 |
+
- definition: the definition as stated, else null
|
| 21 |
+
- formula_latex: LaTeX of the formula IF the evidence shows one, else null
|
| 22 |
+
- interpretation: what a high or low value means operationally, ONLY if stated, else null
|
| 23 |
+
- language: "id", "en", or "mixed" β the language of the definition you extracted
|
| 24 |
+
- provenance.span: the exact sentence or phrase you took the definition from
|
| 25 |
+
|
| 26 |
+
WORKED EXAMPLE 1 (definition present):
|
| 27 |
+
Evidence: "2.1.3. Physical of Availability (PA)\nAdalah ketersediaan fisik suatu
|
| 28 |
+
equipment/unit yang menunjukkan proporsi waktu equipment/unit tersebut berada pada
|
| 29 |
+
kondisi available (siap pakai) selama suatu periode tertentu."
|
| 30 |
+
Output:
|
| 31 |
+
{"term": "PA", "full_name": "Physical of Availability", "definition": "Adalah ketersediaan fisik suatu equipment/unit yang menunjukkan proporsi waktu equipment/unit tersebut berada pada kondisi available (siap pakai) selama suatu periode tertentu.", "formula_latex": null, "interpretation": null, "subdomain_tags": ["equipment", "production"], "domain": "mining", "company": null, "language": "id", "provenance": {"section_no": "2.1.3", "page": 4, "span": "Adalah ketersediaan fisik suatu equipment/unit"}}
|
| 32 |
+
|
| 33 |
+
WORKED EXAMPLE 2 (term appears but is NOT defined β the important case):
|
| 34 |
+
Evidence: "Gain/Loss menggunakan satuan BCM atau ton, Gap Standby menggunakan satuan
|
| 35 |
+
jam (hours)"
|
| 36 |
+
Candidate term: "BCM"
|
| 37 |
+
Output:
|
| 38 |
+
{"term": "BCM", "full_name": null, "definition": null, "formula_latex": null, "interpretation": null, "subdomain_tags": ["production"], "domain": "mining", "company": null, "language": "id", "provenance": {"section_no": "2.2.1", "page": 6, "span": "Gain/Loss menggunakan satuan BCM atau ton"}}
|
| 39 |
+
Note: the term is mentioned but never defined, so definition is null. This is correct.
|
| 40 |
+
|
| 41 |
+
WORKED EXAMPLE 3 (formula present):
|
| 42 |
+
Evidence: "Secara umum, PA dihitung menggunakan rumus berikut:\nPA = Total Hours -
|
| 43 |
+
Breakdown / Total Hours x 100%"
|
| 44 |
+
Output field: "formula_latex": "PA = \\frac{Total\\ Hours - Breakdown}{Total\\ Hours} \\times 100\\%"
|
| 45 |
+
|
| 46 |
+
WORKED EXAMPLE 4 (mixed-language definition, abbreviation and expansion both present):
|
| 47 |
+
Evidence: "2.1.4. Utilization of Availability (UA)\nAdalah tingkat efektifitas
|
| 48 |
+
penggunaan suatu alat yang tersedia secara fisik yang menunjukkan seberapa lama suatu
|
| 49 |
+
unit digunakan secara efektif untuk bekerja - working hours selama berada dalam
|
| 50 |
+
kondisi siap dioperasikan (available)."
|
| 51 |
+
Output:
|
| 52 |
+
{"term": "UA", "full_name": "Utilization of Availability", "definition": "Adalah tingkat efektifitas penggunaan suatu alat yang tersedia secara fisik yang menunjukkan seberapa lama suatu unit digunakan secara efektif untuk bekerja - working hours selama berada dalam kondisi siap dioperasikan (available).", "formula_latex": null, "interpretation": null, "subdomain_tags": ["equipment", "production"], "domain": "mining", "company": null, "language": "mixed", "provenance": {"section_no": "2.1.4", "page": 4, "span": "Adalah tingkat efektifitas penggunaan suatu alat yang tersedia secara fisik"}}
|
| 53 |
+
Note: language is "mixed" because the Indonesian definition embeds English terms.
|
| 54 |
+
|
| 55 |
+
COMMON MISTAKES TO AVOID:
|
| 56 |
+
- Do NOT translate an Indonesian definition into English. Keep the source language.
|
| 57 |
+
- Do NOT expand an abbreviation yourself. If the source never writes the expansion,
|
| 58 |
+
full_name is null.
|
| 59 |
+
- Do NOT copy a definition from your own knowledge of mining. If this document does
|
| 60 |
+
not define the term, the answer is null, even if you know what the term means.
|
| 61 |
+
- Do NOT paraphrase the provenance span to make it shorter or cleaner. It is compared
|
| 62 |
+
character by character against the source and a paraphrase will be rejected.
|
| 63 |
+
- Do NOT merge two different terms into one entry. Extract only the candidate term.
|
| 64 |
+
- Do NOT put units, table captions, or figure labels in the definition field.
|
| 65 |
+
- If the evidence contains only a formula and no prose, set definition to null and
|
| 66 |
+
fill formula_latex only.
|
| 67 |
+
|
| 68 |
+
Return a single JSON object. No prose, no markdown fence.
|
src/knowledge_extraction/extract/prompts/rule.txt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You extract operational RULES from Indonesian and English mining-operations standards.
|
| 2 |
+
|
| 3 |
+
A rule is a statement that constrains or directs behaviour: a condition and what
|
| 4 |
+
follows from it, a requirement, a prohibition, or a calculation policy.
|
| 5 |
+
|
| 6 |
+
RULES β correctness requirements, not style preferences:
|
| 7 |
+
1. Extract ONLY what the evidence states. Never infer or complete a rule.
|
| 8 |
+
2. If the evidence is descriptive rather than prescriptive, set "statement" to null.
|
| 9 |
+
A null answer is a CORRECT answer.
|
| 10 |
+
3. "provenance.span" must be copied VERBATIM from the evidence. It is checked
|
| 11 |
+
automatically against the source and discarded if it does not match exactly.
|
| 12 |
+
4. "subdomain_tags" must come from the allowed list only.
|
| 13 |
+
5. Preserve the source language. Do not translate.
|
| 14 |
+
|
| 15 |
+
ALLOWED subdomain_tags:
|
| 16 |
+
production, maintenance, hauling, loading, drilling_blasting, equipment, safety,
|
| 17 |
+
quality, planning, cost, geology, other
|
| 18 |
+
|
| 19 |
+
FIELD GUIDE:
|
| 20 |
+
- rule_id: SCREAMING_SNAKE_CASE, descriptive, derived from the rule's subject
|
| 21 |
+
- statement: the rule in one sentence, as stated in the source
|
| 22 |
+
- condition: the triggering condition if the rule is conditional, else null
|
| 23 |
+
- consequence: what must happen when the condition holds, else null
|
| 24 |
+
- applies_to: the parameter, equipment, or activity the rule governs, else null
|
| 25 |
+
|
| 26 |
+
WORKED EXAMPLE 1 (conditional rule):
|
| 27 |
+
Evidence: "Production yang digunakan dalam perhitungan adalah produksi hasil joint
|
| 28 |
+
survey. Apabila data joint survey belum tersedia, maka digunakan data produksi
|
| 29 |
+
berdasarkan truck count sebagai dasar perhitungan."
|
| 30 |
+
Output:
|
| 31 |
+
{"rule_id": "PTY_PRODUCTION_SOURCE", "statement": "Production yang digunakan dalam perhitungan adalah produksi hasil joint survey.", "condition": "Apabila data joint survey belum tersedia", "consequence": "digunakan data produksi berdasarkan truck count sebagai dasar perhitungan", "applies_to": "Productivity (Pty)", "subdomain_tags": ["production", "planning"], "language": "id", "provenance": {"section_no": "2.1.5", "page": 5, "span": "Apabila data joint survey belum tersedia, maka digunakan data produksi berdasarkan truck count"}}
|
| 32 |
+
|
| 33 |
+
WORKED EXAMPLE 2 (descriptive, NOT a rule β the important case):
|
| 34 |
+
Evidence: "Waterfall Analysis dapat di-review melalui dua metode."
|
| 35 |
+
Output:
|
| 36 |
+
{"rule_id": "NONE", "statement": null, "condition": null, "consequence": null, "applies_to": null, "subdomain_tags": ["other"], "language": "id", "provenance": {"section_no": "2.2.5", "page": 8, "span": "Waterfall Analysis dapat di-review melalui dua metode"}}
|
| 37 |
+
Note: this describes a capability, it does not constrain behaviour. statement is null.
|
| 38 |
+
|
| 39 |
+
Return a single JSON object. No prose, no markdown fence.
|
src/knowledge_extraction/extract/prompts/summary.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You write a short orientation brief for a mining-operations standard document.
|
| 2 |
+
|
| 3 |
+
This is the ONE branch whose output cannot be span-checked, because a summary is by
|
| 4 |
+
nature not verbatim. Treat that as a reason for restraint, not licence: state only
|
| 5 |
+
what the document states, and prefer omission to elaboration.
|
| 6 |
+
|
| 7 |
+
RULES:
|
| 8 |
+
1. Use only the section headings and text provided. Do not add industry background.
|
| 9 |
+
2. If the document does not state a purpose or scope, set that field to null.
|
| 10 |
+
3. "provenance.span" must still be copied VERBATIM from the evidence β use the most
|
| 11 |
+
representative sentence from the document's purpose section.
|
| 12 |
+
4. Keep summary_md under 200 words. It orients a reader; it does not replace the doc.
|
| 13 |
+
|
| 14 |
+
FIELD GUIDE:
|
| 15 |
+
- title: the document's title as written
|
| 16 |
+
- purpose: what the document is for, as stated
|
| 17 |
+
- scope: what it covers, as stated
|
| 18 |
+
- key_parameters: the main parameters or concepts the document defines
|
| 19 |
+
- summary_md: a short markdown orientation, under 200 words
|
| 20 |
+
|
| 21 |
+
Return a single JSON object. No prose, no markdown fence.
|
src/knowledge_extraction/extract/schemas.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""What the MODEL is asked to return.
|
| 2 |
+
|
| 3 |
+
Deliberately separate from `models.py`. The model never supplies `doc_id` (we
|
| 4 |
+
know it), never sets `extraction_status`, and never sets the conflict or diff
|
| 5 |
+
fields β validation owns those. **Asking a model for fields it cannot know is an
|
| 6 |
+
invitation to fabricate**, so the request schema is narrower than the stored one.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from typing import Literal
|
| 12 |
+
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
|
| 15 |
+
from ..models import SubdomainEnum
|
| 16 |
+
|
| 17 |
+
Language = Literal["id", "en", "mixed"]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ProvenanceDraft(BaseModel):
|
| 21 |
+
section_no: str | None = None
|
| 22 |
+
page: int
|
| 23 |
+
span: str
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class GlossaryDraft(BaseModel):
|
| 27 |
+
term: str
|
| 28 |
+
full_name: str | None = None
|
| 29 |
+
# The wording exactly as the document writes it, even when "wrong" β the
|
| 30 |
+
# standard heads its section "Physical of Availability (PA)". Surfacing the
|
| 31 |
+
# discrepancy is the point; normalising it hides a decision the expert owns.
|
| 32 |
+
source_wording: str | None = None
|
| 33 |
+
definition: str | None = None
|
| 34 |
+
formula_latex: str | None = None
|
| 35 |
+
interpretation: str | None = None
|
| 36 |
+
subdomain_tags: list[SubdomainEnum] = Field(default_factory=list)
|
| 37 |
+
domain: str | None = None
|
| 38 |
+
company: str | None = None
|
| 39 |
+
language: Language | None = None
|
| 40 |
+
provenance: ProvenanceDraft
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class RuleDraft(BaseModel):
|
| 44 |
+
rule_id: str
|
| 45 |
+
statement: str | None = None
|
| 46 |
+
condition: str | None = None
|
| 47 |
+
consequence: str | None = None
|
| 48 |
+
applies_to: str | None = None
|
| 49 |
+
subdomain_tags: list[SubdomainEnum] = Field(default_factory=list)
|
| 50 |
+
language: Language | None = None
|
| 51 |
+
provenance: ProvenanceDraft
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class VariableDraft(BaseModel):
|
| 55 |
+
symbol: str
|
| 56 |
+
meaning: str | None = None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class FormulaDraft(BaseModel):
|
| 60 |
+
name: str | None = None
|
| 61 |
+
formula_latex: str | None = None
|
| 62 |
+
variables: list[VariableDraft] = Field(default_factory=list)
|
| 63 |
+
unit: str | None = None
|
| 64 |
+
provenance: ProvenanceDraft
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class SummaryDraft(BaseModel):
|
| 68 |
+
title: str | None = None
|
| 69 |
+
purpose: str | None = None
|
| 70 |
+
scope: str | None = None
|
| 71 |
+
key_parameters: list[str] = Field(default_factory=list)
|
| 72 |
+
summary_md: str | None = None
|
| 73 |
+
provenance: ProvenanceDraft
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
DRAFTS = {
|
| 77 |
+
"glossary": GlossaryDraft,
|
| 78 |
+
"rule": RuleDraft,
|
| 79 |
+
"formula": FormulaDraft,
|
| 80 |
+
"summary": SummaryDraft,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def schema_for(branch: str) -> dict:
|
| 85 |
+
return DRAFTS[branch].model_json_schema()
|
src/knowledge_extraction/filters/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .cue_filter import definitional_hits, rule_candidates
|
| 2 |
+
from .legend_filter import abbrev_pairs, find_legend_lines
|
| 3 |
+
from .span_filter import extract_mentions, load_model
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"abbrev_pairs",
|
| 7 |
+
"definitional_hits",
|
| 8 |
+
"extract_mentions",
|
| 9 |
+
"find_legend_lines",
|
| 10 |
+
"load_model",
|
| 11 |
+
"rule_candidates",
|
| 12 |
+
]
|
src/knowledge_extraction/filters/cue_filter.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Discourse-cue regex β rule-of-thumb candidates. Free: zero API calls.
|
| 2 |
+
|
| 3 |
+
Also supplies `definitional_hits`, which evidence ranking uses for its strongest
|
| 4 |
+
signal (+5.0 when a cue sits within 100 characters of a mention). Cue patterns
|
| 5 |
+
live in `config/cues.yaml` so they can be tuned per language without a code
|
| 6 |
+
change.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
from functools import lru_cache
|
| 13 |
+
|
| 14 |
+
from ..models import Chunk, RuleCandidate
|
| 15 |
+
from ..settings import load_yaml
|
| 16 |
+
|
| 17 |
+
SNIPPET_CHARS = 240
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@lru_cache(maxsize=8)
|
| 21 |
+
def _compiled(group: str) -> tuple[re.Pattern[str], ...]:
|
| 22 |
+
cues = load_yaml("cues.yaml")
|
| 23 |
+
return tuple(
|
| 24 |
+
re.compile(p, re.IGNORECASE | re.MULTILINE) for p in cues.get(group, [])
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _groups(*names: str) -> tuple[re.Pattern[str], ...]:
|
| 29 |
+
out: list[re.Pattern[str]] = []
|
| 30 |
+
for name in names:
|
| 31 |
+
out.extend(_compiled(name))
|
| 32 |
+
return tuple(out)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def definitional_hits(text: str) -> list[tuple[int, int]]:
|
| 36 |
+
"""(start, end) of every definitional cue. Consumed by evidence ranking."""
|
| 37 |
+
spans: list[tuple[int, int]] = []
|
| 38 |
+
for pattern in _groups("definitional_id", "definitional_en"):
|
| 39 |
+
spans.extend((m.start(), m.end()) for m in pattern.finditer(text))
|
| 40 |
+
return sorted(spans)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def rule_candidates(chunks: list[Chunk]) -> list[RuleCandidate]:
|
| 44 |
+
"""Passages a conditional or note cue marks as possibly stating a rule.
|
| 45 |
+
|
| 46 |
+
Over-inclusive on purpose β this is a candidate generator, and the paid
|
| 47 |
+
stage decides whether a rule is actually present. Nothing is dropped here.
|
| 48 |
+
"""
|
| 49 |
+
patterns = _groups("conditional_id", "conditional_en", "note_id")
|
| 50 |
+
out: list[RuleCandidate] = []
|
| 51 |
+
for chunk in chunks:
|
| 52 |
+
seen: set[int] = set()
|
| 53 |
+
for pattern in patterns:
|
| 54 |
+
for match in pattern.finditer(chunk.text):
|
| 55 |
+
start = match.start()
|
| 56 |
+
# One candidate per sentence-ish region: several cues in the
|
| 57 |
+
# same clause would otherwise produce near-identical rows.
|
| 58 |
+
bucket = start // 200
|
| 59 |
+
if bucket in seen:
|
| 60 |
+
continue
|
| 61 |
+
seen.add(bucket)
|
| 62 |
+
out.append(
|
| 63 |
+
RuleCandidate(
|
| 64 |
+
chunk_id=chunk.chunk_id,
|
| 65 |
+
cue=match.group(0).strip(),
|
| 66 |
+
char_start=start,
|
| 67 |
+
char_end=match.end(),
|
| 68 |
+
snippet=_snippet(chunk.text, start),
|
| 69 |
+
)
|
| 70 |
+
)
|
| 71 |
+
return out
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _snippet(text: str, start: int) -> str:
|
| 75 |
+
lo = max(0, start - SNIPPET_CHARS // 4)
|
| 76 |
+
return text[lo : lo + SNIPPET_CHARS].strip()
|
src/knowledge_extraction/filters/legend_filter.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Legend blocks β abbreviation pairs + formula variables. Free: zero API calls.
|
| 2 |
+
|
| 3 |
+
A legend block is the `Keterangan:` / `Dimana:` / `where` table that follows a
|
| 4 |
+
formula and expands its symbols:
|
| 5 |
+
|
| 6 |
+
Keterangan:
|
| 7 |
+
MOHH : Machine on Hand Hours
|
| 8 |
+
Qty : Quantity
|
| 9 |
+
|
| 10 |
+
This runs **before clustering** and that ordering is load-bearing: without the
|
| 11 |
+
abbreviation pairs it produces, `PA` and `Physical Availability` cluster as two
|
| 12 |
+
unrelated terms and the expert sees the same concept twice.
|
| 13 |
+
|
| 14 |
+
`find_legend_lines` additionally feeds evidence ranking (+3.5 when a mention
|
| 15 |
+
sits inside a legend block).
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import re
|
| 21 |
+
from functools import lru_cache
|
| 22 |
+
|
| 23 |
+
from ..models import AbbrevPair, Chunk
|
| 24 |
+
from ..settings import load_yaml
|
| 25 |
+
|
| 26 |
+
# How far below a legend header the block is assumed to run before a blank line
|
| 27 |
+
# or a non-entry line closes it.
|
| 28 |
+
MAX_BLOCK_LINES = 30
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@lru_cache(maxsize=2)
|
| 32 |
+
def _headers() -> tuple[re.Pattern[str], ...]:
|
| 33 |
+
cues = load_yaml("cues.yaml")
|
| 34 |
+
return tuple(re.compile(p) for p in cues.get("legend_headers", []))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@lru_cache(maxsize=2)
|
| 38 |
+
def _entry() -> re.Pattern[str]:
|
| 39 |
+
cues = load_yaml("cues.yaml")
|
| 40 |
+
return re.compile(cues["legend_entry"])
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def find_legend_lines(text: str) -> set[int]:
|
| 44 |
+
"""Zero-based line numbers that fall inside a legend block."""
|
| 45 |
+
lines = text.split("\n")
|
| 46 |
+
inside: set[int] = set()
|
| 47 |
+
for i, line in enumerate(lines):
|
| 48 |
+
if not any(h.search(line) for h in _headers()):
|
| 49 |
+
continue
|
| 50 |
+
for j in range(i + 1, min(i + 1 + MAX_BLOCK_LINES, len(lines))):
|
| 51 |
+
if not lines[j].strip():
|
| 52 |
+
break
|
| 53 |
+
if not _entry().match(lines[j]):
|
| 54 |
+
break
|
| 55 |
+
inside.add(j)
|
| 56 |
+
return inside
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def abbrev_pairs(chunks: list[Chunk]) -> list[AbbrevPair]:
|
| 60 |
+
"""Extract `abbrev : expansion` rows from every legend block.
|
| 61 |
+
|
| 62 |
+
Precision matters more than recall here: a wrong pair merges two distinct
|
| 63 |
+
terms into one cluster, which destroys a term silently. So only lines
|
| 64 |
+
inside a detected block are read, never `X : Y` anywhere in the text.
|
| 65 |
+
"""
|
| 66 |
+
out: list[AbbrevPair] = []
|
| 67 |
+
seen: set[tuple[str, str]] = set()
|
| 68 |
+
for chunk in chunks:
|
| 69 |
+
lines = chunk.text.split("\n")
|
| 70 |
+
for line_no in sorted(find_legend_lines(chunk.text)):
|
| 71 |
+
match = _entry().match(lines[line_no])
|
| 72 |
+
if not match:
|
| 73 |
+
continue
|
| 74 |
+
abbrev, expansion = match.group(1).strip(), match.group(2).strip()
|
| 75 |
+
if not _plausible(abbrev, expansion):
|
| 76 |
+
continue
|
| 77 |
+
key = (abbrev.casefold(), expansion.casefold())
|
| 78 |
+
if key in seen:
|
| 79 |
+
continue
|
| 80 |
+
seen.add(key)
|
| 81 |
+
out.append(
|
| 82 |
+
AbbrevPair(
|
| 83 |
+
abbrev=abbrev, expansion=expansion, chunk_id=chunk.chunk_id
|
| 84 |
+
)
|
| 85 |
+
)
|
| 86 |
+
return out
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _plausible(abbrev: str, expansion: str) -> bool:
|
| 90 |
+
"""Reject rows that are formula lines rather than legend entries."""
|
| 91 |
+
if not abbrev or not expansion:
|
| 92 |
+
return False
|
| 93 |
+
if len(expansion) < 3 or len(expansion) > 120:
|
| 94 |
+
return False
|
| 95 |
+
# An expansion that is mostly digits/operators is the right-hand side of an
|
| 96 |
+
# equation, not a term ("Qty = 360/720").
|
| 97 |
+
letters = sum(c.isalpha() for c in expansion)
|
| 98 |
+
return letters >= max(3, len(expansion) // 3)
|
src/knowledge_extraction/filters/span_filter.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Span NER over ALL chunks β Mention[]. Free stage: zero API calls, CPU only.
|
| 2 |
+
|
| 3 |
+
**Runs unconditionally on every chunk.** There is deliberately no relevance gate
|
| 4 |
+
in front of it: a binary pre-filter's false negatives drop content *before*
|
| 5 |
+
extraction ever sees it, and dropped content never reaches expert review to be
|
| 6 |
+
caught. The model is cheap enough (CPU encoder, ~400 MB) to just look at
|
| 7 |
+
everything, which turns "zero candidate spans" into the relevance signal as a
|
| 8 |
+
byproduct of looking rather than a judgment made before looking.
|
| 9 |
+
|
| 10 |
+
Two mechanical traps, both of which silently cost recall if reintroduced:
|
| 11 |
+
|
| 12 |
+
1. **The model truncates long inputs and warns rather than failing.** Feeding a
|
| 13 |
+
whole chunk drops its tail without erroring. Chunks are therefore fed as
|
| 14 |
+
overlapping word windows with offsets remapped back to chunk coordinates.
|
| 15 |
+
2. **There is a ~12-token span cap.** Compound terms ("UA plan grouping
|
| 16 |
+
(composite) actual") exceed it. Candidates that hit the cap are *flagged*,
|
| 17 |
+
never silently truncated, so a recall shortfall can be attributed to it.
|
| 18 |
+
|
| 19 |
+
The model is loaded lazily and cached: it is heavy, and only this stage needs
|
| 20 |
+
it, so importing the package must not pay for it.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import time
|
| 26 |
+
from functools import lru_cache
|
| 27 |
+
|
| 28 |
+
from ...middlewares.logging import get_logger
|
| 29 |
+
from ..models import Chunk, Mention
|
| 30 |
+
from ..settings import (
|
| 31 |
+
SPAN_TOKEN_CAP,
|
| 32 |
+
WINDOW_OVERLAP,
|
| 33 |
+
WINDOW_WORDS,
|
| 34 |
+
labels_for,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
logger = get_logger("knowledge_span_filter")
|
| 38 |
+
|
| 39 |
+
MODEL_NAME = "urchade/gliner_multi-v2.1"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@lru_cache(maxsize=2)
|
| 43 |
+
def load_model(model_name: str = MODEL_NAME):
|
| 44 |
+
"""Lazy, cached. Imported inside the function so the package stays importable
|
| 45 |
+
(and the app stays fast to boot) without the model stack."""
|
| 46 |
+
from gliner import GLiNER
|
| 47 |
+
|
| 48 |
+
logger.info("loading span model", model=model_name)
|
| 49 |
+
return GLiNER.from_pretrained(model_name)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def extract_mentions(
|
| 53 |
+
chunks: list[Chunk],
|
| 54 |
+
variant: str | None = None,
|
| 55 |
+
threshold: float | None = None,
|
| 56 |
+
model=None,
|
| 57 |
+
) -> list[Mention]:
|
| 58 |
+
"""Candidate term mentions across every chunk.
|
| 59 |
+
|
| 60 |
+
Never raises: the span filter is the widest net in the pipeline, and a model
|
| 61 |
+
failure must degrade to "no candidates from this chunk" rather than abort a
|
| 62 |
+
corpus-scale ingest that has already paid for parsing. Failures are logged
|
| 63 |
+
with `repr(e)` β an empty error string is how a real cause once went missing
|
| 64 |
+
for a day elsewhere in this repo.
|
| 65 |
+
"""
|
| 66 |
+
labels, configured_threshold = labels_for(variant) if variant else labels_for()
|
| 67 |
+
score_floor = threshold if threshold is not None else configured_threshold
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
model = model or load_model()
|
| 71 |
+
except Exception as exc: # pragma: no cover - depends on the model stack
|
| 72 |
+
logger.error("span model unavailable", error=repr(exc))
|
| 73 |
+
return []
|
| 74 |
+
|
| 75 |
+
mentions: list[Mention] = []
|
| 76 |
+
capped = 0
|
| 77 |
+
started = time.time()
|
| 78 |
+
|
| 79 |
+
for chunk in chunks:
|
| 80 |
+
for window_text, offset in _windows(chunk.text):
|
| 81 |
+
try:
|
| 82 |
+
found = model.predict_entities(
|
| 83 |
+
window_text, labels, threshold=score_floor
|
| 84 |
+
)
|
| 85 |
+
except Exception as exc:
|
| 86 |
+
logger.warning(
|
| 87 |
+
"span prediction failed", chunk_id=chunk.chunk_id, error=repr(exc)
|
| 88 |
+
)
|
| 89 |
+
continue
|
| 90 |
+
|
| 91 |
+
for entity in found:
|
| 92 |
+
surface = entity.get("text", "")
|
| 93 |
+
if not surface.strip():
|
| 94 |
+
continue
|
| 95 |
+
hit_cap = len(surface.split()) >= SPAN_TOKEN_CAP
|
| 96 |
+
capped += hit_cap
|
| 97 |
+
mentions.append(
|
| 98 |
+
Mention(
|
| 99 |
+
surface=surface,
|
| 100 |
+
chunk_id=chunk.chunk_id,
|
| 101 |
+
char_start=offset + int(entity.get("start", 0)),
|
| 102 |
+
char_end=offset + int(entity.get("end", 0)),
|
| 103 |
+
label=str(entity.get("label", "")),
|
| 104 |
+
score=float(entity.get("score", 0.0)),
|
| 105 |
+
hit_span_cap=hit_cap,
|
| 106 |
+
)
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
deduped = _dedupe(mentions)
|
| 110 |
+
logger.info(
|
| 111 |
+
"span filter complete",
|
| 112 |
+
chunks=len(chunks),
|
| 113 |
+
mentions=len(deduped),
|
| 114 |
+
dropped_overlapping=len(mentions) - len(deduped),
|
| 115 |
+
hit_span_cap=capped,
|
| 116 |
+
seconds=round(time.time() - started, 1),
|
| 117 |
+
)
|
| 118 |
+
return deduped
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _windows(text: str) -> list[tuple[str, int]]:
|
| 122 |
+
"""Overlapping word windows plus each window's character offset.
|
| 123 |
+
|
| 124 |
+
Windowing is on WORDS but offsets must come back in CHARACTERS, so the
|
| 125 |
+
original spacing is walked rather than re-joined β re-joining would shift
|
| 126 |
+
every offset in a chunk containing newlines or double spaces, and every
|
| 127 |
+
span check downstream would then fail.
|
| 128 |
+
"""
|
| 129 |
+
if not text.strip():
|
| 130 |
+
return []
|
| 131 |
+
|
| 132 |
+
positions: list[tuple[int, int]] = []
|
| 133 |
+
cursor = 0
|
| 134 |
+
for word in text.split():
|
| 135 |
+
start = text.index(word, cursor)
|
| 136 |
+
positions.append((start, start + len(word)))
|
| 137 |
+
cursor = start + len(word)
|
| 138 |
+
|
| 139 |
+
if len(positions) <= WINDOW_WORDS:
|
| 140 |
+
return [(text, 0)]
|
| 141 |
+
|
| 142 |
+
step = max(1, WINDOW_WORDS - WINDOW_OVERLAP)
|
| 143 |
+
windows: list[tuple[str, int]] = []
|
| 144 |
+
for begin in range(0, len(positions), step):
|
| 145 |
+
chunk_words = positions[begin : begin + WINDOW_WORDS]
|
| 146 |
+
if not chunk_words:
|
| 147 |
+
break
|
| 148 |
+
lo, hi = chunk_words[0][0], chunk_words[-1][1]
|
| 149 |
+
windows.append((text[lo:hi], lo))
|
| 150 |
+
if begin + WINDOW_WORDS >= len(positions):
|
| 151 |
+
break
|
| 152 |
+
return windows
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _dedupe(mentions: list[Mention]) -> list[Mention]:
|
| 156 |
+
"""Drop duplicates produced by window overlap, keeping the highest score.
|
| 157 |
+
|
| 158 |
+
Overlap is required for recall (a term straddling a window boundary would
|
| 159 |
+
otherwise be missed), and it necessarily re-finds terms in the overlap
|
| 160 |
+
region. Keyed on (chunk, span) so the same surface at a different position
|
| 161 |
+
stays a separate mention β mention COUNT drives the review queue ordering.
|
| 162 |
+
"""
|
| 163 |
+
best: dict[tuple[str, int, int], Mention] = {}
|
| 164 |
+
for mention in mentions:
|
| 165 |
+
key = (mention.chunk_id, mention.char_start, mention.char_end)
|
| 166 |
+
current = best.get(key)
|
| 167 |
+
if current is None or mention.score > current.score:
|
| 168 |
+
best[key] = mention
|
| 169 |
+
return sorted(best.values(), key=lambda m: (m.chunk_id, m.char_start))
|
src/knowledge_extraction/models.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic contracts for the knowledge-extraction pipeline.
|
| 2 |
+
|
| 3 |
+
Three invariants are encoded here rather than described in prose, because every
|
| 4 |
+
one of them is a control that a later change could quietly remove:
|
| 5 |
+
|
| 6 |
+
1. **All content fields are Optional.** A model that cannot answer null will
|
| 7 |
+
fabricate one. Abstention is correct behaviour, never an error.
|
| 8 |
+
2. **`subdomain_tags` is an enum.** Classification, not generation.
|
| 9 |
+
3. **`Provenance.span` is mandatory and verbatim-checked.** It is the primary
|
| 10 |
+
anti-hallucination control and the thing that makes expert review
|
| 11 |
+
finishable β the reviewer checks a quote against a page, not a claim
|
| 12 |
+
against their memory.
|
| 13 |
+
|
| 14 |
+
`Chunk` here is the pipeline's **internal** unit, deliberately narrower than the
|
| 15 |
+
parsed-document artifact being agreed with Sofhia (the seam). Stages depend only
|
| 16 |
+
on this subset; `adapter.py` maps the seam type onto it, so seam churn lands in
|
| 17 |
+
one file instead of seven. See KNOWLEDGE_PIPELINE_TODO.md Β§3.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
from enum import Enum
|
| 23 |
+
from typing import Literal
|
| 24 |
+
|
| 25 |
+
from pydantic import BaseModel, Field
|
| 26 |
+
|
| 27 |
+
Branch = Literal["glossary", "rule", "formula", "summary"]
|
| 28 |
+
ExtractionStatus = Literal["ok", "no_definition_found", "escalated"]
|
| 29 |
+
DiffStatus = Literal["new", "duplicate", "conflicting"]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class SubdomainEnum(str, Enum):
|
| 33 |
+
"""Classification target. Extend deliberately β a new member changes what
|
| 34 |
+
the model is allowed to answer, which is a prompt change, not a data one."""
|
| 35 |
+
|
| 36 |
+
production = "production"
|
| 37 |
+
maintenance = "maintenance"
|
| 38 |
+
hauling = "hauling"
|
| 39 |
+
loading = "loading"
|
| 40 |
+
drilling_blasting = "drilling_blasting"
|
| 41 |
+
equipment = "equipment"
|
| 42 |
+
safety = "safety"
|
| 43 |
+
quality = "quality"
|
| 44 |
+
planning = "planning"
|
| 45 |
+
cost = "cost"
|
| 46 |
+
geology = "geology"
|
| 47 |
+
other = "other"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ββ Stage 1: the chunk (internal view of the seam artifact) βββββββββββββ
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Chunk(BaseModel):
|
| 54 |
+
"""One unit of a parsed document, as the extraction stages need it.
|
| 55 |
+
|
| 56 |
+
`text` must stay **verbatim** from the source document. Span validation
|
| 57 |
+
locates LLM-quoted spans literally inside this text; if it is ever reflowed
|
| 58 |
+
or whitespace-normalised the lookup fails and the field is silently set to
|
| 59 |
+
null. The failure presents as a bad model, but the cause would be here.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
chunk_id: str
|
| 63 |
+
doc_id: str
|
| 64 |
+
text: str
|
| 65 |
+
page_start: int
|
| 66 |
+
page_end: int
|
| 67 |
+
ordinal: int = 0
|
| 68 |
+
|
| 69 |
+
# Structural context. Both Optional β many documents carry no numbering.
|
| 70 |
+
section_no: str | None = None
|
| 71 |
+
heading: str | None = None
|
| 72 |
+
|
| 73 |
+
# Cheap downstream filters / ranking signals
|
| 74 |
+
has_formula: bool = False
|
| 75 |
+
is_tabular: bool = False
|
| 76 |
+
bold_spans: list[str] = Field(default_factory=list)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class ParsedDoc(BaseModel):
|
| 80 |
+
"""A document's chunks plus the identity needed to version and cache them."""
|
| 81 |
+
|
| 82 |
+
doc_id: str
|
| 83 |
+
source_ref: str
|
| 84 |
+
content_hash: str
|
| 85 |
+
n_pages: int
|
| 86 |
+
chunks: list[Chunk]
|
| 87 |
+
parser_name: str = "unknown"
|
| 88 |
+
parser_version: str = ""
|
| 89 |
+
used_heading_split: bool = False
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ββ Stage 2: filters ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class Mention(BaseModel):
|
| 96 |
+
"""One occurrence of a candidate term inside a chunk."""
|
| 97 |
+
|
| 98 |
+
surface: str
|
| 99 |
+
chunk_id: str
|
| 100 |
+
char_start: int
|
| 101 |
+
char_end: int
|
| 102 |
+
label: str = ""
|
| 103 |
+
score: float = 0.0
|
| 104 |
+
hit_span_cap: bool = False
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class RuleCandidate(BaseModel):
|
| 108 |
+
"""A passage a discourse cue marks as possibly stating a rule of thumb."""
|
| 109 |
+
|
| 110 |
+
chunk_id: str
|
| 111 |
+
cue: str
|
| 112 |
+
char_start: int
|
| 113 |
+
char_end: int
|
| 114 |
+
snippet: str
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class AbbrevPair(BaseModel):
|
| 118 |
+
"""`PA` β `Physical Availability`, harvested from a legend block.
|
| 119 |
+
|
| 120 |
+
Legend extraction must run before clustering: without these, an
|
| 121 |
+
abbreviation and its expansion cluster as two unrelated terms.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
abbrev: str
|
| 125 |
+
expansion: str
|
| 126 |
+
chunk_id: str
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class FilterResult(BaseModel):
|
| 130 |
+
doc_id: str
|
| 131 |
+
mentions: list[Mention] = Field(default_factory=list)
|
| 132 |
+
rule_candidates: list[RuleCandidate] = Field(default_factory=list)
|
| 133 |
+
abbrev_pairs: list[AbbrevPair] = Field(default_factory=list)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ββ Stage 3: clusters βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class TermCluster(BaseModel):
|
| 140 |
+
"""All mentions of one term. **The LLM call unit is the cluster**, not the
|
| 141 |
+
chunk and not the mention β that is what cuts expert review burden, and it
|
| 142 |
+
is also the only reason conflicting definitions can be detected at all
|
| 143 |
+
(they must arrive in the same call to be compared)."""
|
| 144 |
+
|
| 145 |
+
cluster_id: str
|
| 146 |
+
canonical: str
|
| 147 |
+
variants: list[str] = Field(default_factory=list)
|
| 148 |
+
mentions: list[Mention] = Field(default_factory=list)
|
| 149 |
+
mention_count: int = 0
|
| 150 |
+
merge_reasons: list[str] = Field(default_factory=list)
|
| 151 |
+
|
| 152 |
+
# Ranked best-first. The FULL list is kept, not just the top K β
|
| 153 |
+
# escalation consumes the tail.
|
| 154 |
+
evidence_chunk_ids: list[str] = Field(default_factory=list)
|
| 155 |
+
evidence_scores: list[float] = Field(default_factory=list)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class ClusterResult(BaseModel):
|
| 159 |
+
doc_id: str
|
| 160 |
+
clusters: list[TermCluster] = Field(default_factory=list)
|
| 161 |
+
n_mentions: int = 0
|
| 162 |
+
n_clusters: int = 0
|
| 163 |
+
compression_ratio: float = 0.0
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ββ Stage 4+: extracted entries βββββββββββββββββββββββββββββββββββββββββ
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class Provenance(BaseModel):
|
| 170 |
+
"""Where a claim came from. `span` is mandatory and must appear verbatim in
|
| 171 |
+
the evidence text; a field whose span cannot be located is rejected, never
|
| 172 |
+
repaired. A repaired span is an unfalsifiable claim."""
|
| 173 |
+
|
| 174 |
+
doc_id: str
|
| 175 |
+
span: str
|
| 176 |
+
page: int | None = None
|
| 177 |
+
section_no: str | None = None
|
| 178 |
+
chunk_id: str | None = None
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class GlossaryEntry(BaseModel):
|
| 182 |
+
term: str
|
| 183 |
+
full_name: str | None = None
|
| 184 |
+
|
| 185 |
+
# The literal wording as the document writes it, un-normalised. The BUMA
|
| 186 |
+
# standard heads its section "Physical of Availability (PA)" while the
|
| 187 |
+
# legend says "Physical Availability"; the discrepancy is surfaced to the
|
| 188 |
+
# expert rather than silently corrected.
|
| 189 |
+
source_wording: str | None = None
|
| 190 |
+
|
| 191 |
+
definition: str | None = None
|
| 192 |
+
formula_latex: str | None = None
|
| 193 |
+
interpretation: str | None = None
|
| 194 |
+
subdomain_tags: list[SubdomainEnum] = Field(default_factory=list)
|
| 195 |
+
domain: str | None = None
|
| 196 |
+
company: str | None = None
|
| 197 |
+
language: str | None = None
|
| 198 |
+
|
| 199 |
+
mention_count: int = 0
|
| 200 |
+
provenance: Provenance
|
| 201 |
+
extraction_status: ExtractionStatus = "ok"
|
| 202 |
+
diff_status: DiffStatus | None = None
|
| 203 |
+
definition_conflict: bool = False
|
| 204 |
+
conflict_variants: list[str] = Field(default_factory=list)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
class RuleEntry(BaseModel):
|
| 208 |
+
"""A rule of thumb / operational convention stated by the document."""
|
| 209 |
+
|
| 210 |
+
rule_id: str
|
| 211 |
+
statement: str | None = None
|
| 212 |
+
condition: str | None = None
|
| 213 |
+
consequence: str | None = None
|
| 214 |
+
applies_to: str | None = None
|
| 215 |
+
subdomain_tags: list[SubdomainEnum] = Field(default_factory=list)
|
| 216 |
+
language: str | None = None
|
| 217 |
+
provenance: Provenance
|
| 218 |
+
extraction_status: ExtractionStatus = "ok"
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class FormulaVariable(BaseModel):
|
| 222 |
+
symbol: str
|
| 223 |
+
meaning: str | None = None
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
class FormulaEntry(BaseModel):
|
| 227 |
+
name: str | None = None
|
| 228 |
+
formula_latex: str | None = None
|
| 229 |
+
variables: list[FormulaVariable] = Field(default_factory=list)
|
| 230 |
+
unit: str | None = None
|
| 231 |
+
provenance: Provenance
|
| 232 |
+
extraction_status: ExtractionStatus = "ok"
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class BriefContext(BaseModel):
|
| 236 |
+
"""Whole-document summary. The only branch that cannot be span-checked β
|
| 237 |
+
a plausible summary is indistinguishable from a correct one, which is why
|
| 238 |
+
it belongs on the larger model tier when one is available."""
|
| 239 |
+
|
| 240 |
+
title: str | None = None
|
| 241 |
+
purpose: str | None = None
|
| 242 |
+
scope: str | None = None
|
| 243 |
+
key_parameters: list[str] = Field(default_factory=list)
|
| 244 |
+
summary_md: str | None = None
|
| 245 |
+
provenance: Provenance
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
class CallUsage(BaseModel):
|
| 249 |
+
"""Per-call accounting. `cached_tokens` comes from the API and is never
|
| 250 |
+
modelled: caching does not engage below 1024 prompt tokens, so assuming it
|
| 251 |
+
would understate cost by ~10x on the input side."""
|
| 252 |
+
|
| 253 |
+
branch: Branch
|
| 254 |
+
deployment: str
|
| 255 |
+
tier: str = "nano"
|
| 256 |
+
prompt_tokens: int = 0
|
| 257 |
+
cached_tokens: int = 0
|
| 258 |
+
completion_tokens: int = 0
|
| 259 |
+
latency_s: float = 0.0
|
| 260 |
+
retries: int = 0
|
| 261 |
+
structured_output_mode: str = ""
|
| 262 |
+
simulated: bool = False
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class RejectedField(BaseModel):
|
| 266 |
+
"""Audit row for a field the span check refused. Kept so a reviewer can see
|
| 267 |
+
what the control caught rather than only what it let through."""
|
| 268 |
+
|
| 269 |
+
entry_term: str
|
| 270 |
+
field: str
|
| 271 |
+
offending_value: str
|
| 272 |
+
reason: str
|
| 273 |
+
branch: Branch
|
src/knowledge_extraction/queue/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .review_queue import build_queue
|
| 2 |
+
|
| 3 |
+
__all__ = ["build_queue"]
|
src/knowledge_extraction/queue/review_queue.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The frequency-sorted review queue β the pipeline's actual product.
|
| 2 |
+
|
| 3 |
+
Ordering is the product decision here, and it targets the bottleneck directly:
|
| 4 |
+
the expert is the scarce resource, so they should hit the terms whose definition
|
| 5 |
+
propagates furthest first. Conflicts are promoted above frequency regardless,
|
| 6 |
+
because a contradiction is a decision only they can make.
|
| 7 |
+
|
| 8 |
+
Each row carries page, section and the verbatim span so review is a matter of
|
| 9 |
+
checking a quote against a page, not a claim against memory. That is what makes
|
| 10 |
+
the queue finishable.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def build_queue(entries: list[dict]) -> list[dict]:
|
| 17 |
+
def sort_key(entry: dict):
|
| 18 |
+
conflicting = (
|
| 19 |
+
entry.get("diff_status") == "conflicting"
|
| 20 |
+
or entry.get("definition_conflict") is True
|
| 21 |
+
)
|
| 22 |
+
return (0 if conflicting else 1, -int(entry.get("mention_count", 0) or 0))
|
| 23 |
+
|
| 24 |
+
queue = []
|
| 25 |
+
for rank, entry in enumerate(sorted(entries, key=sort_key), start=1):
|
| 26 |
+
provenance = entry.get("provenance") or {}
|
| 27 |
+
queue.append(
|
| 28 |
+
{
|
| 29 |
+
"rank": rank,
|
| 30 |
+
"term": entry.get("term"),
|
| 31 |
+
"definition": entry.get("definition"),
|
| 32 |
+
"source_wording": entry.get("source_wording"),
|
| 33 |
+
"mention_count": entry.get("mention_count", 0),
|
| 34 |
+
"extraction_status": entry.get("extraction_status"),
|
| 35 |
+
"diff_status": entry.get("diff_status"),
|
| 36 |
+
"definition_conflict": entry.get("definition_conflict", False),
|
| 37 |
+
"conflict_variants": entry.get("conflict_variants", []),
|
| 38 |
+
"page": provenance.get("page"),
|
| 39 |
+
"section_no": provenance.get("section_no"),
|
| 40 |
+
"span": provenance.get("span"),
|
| 41 |
+
"review_reason": _reason(entry),
|
| 42 |
+
}
|
| 43 |
+
)
|
| 44 |
+
return queue
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _reason(entry: dict) -> str:
|
| 48 |
+
if entry.get("definition_conflict") or entry.get("diff_status") == "conflicting":
|
| 49 |
+
return "conflicting definitions β expert decision required"
|
| 50 |
+
if _wording_differs(entry):
|
| 51 |
+
return "source wording differs from the expanded name β confirm which is correct"
|
| 52 |
+
if entry.get("extraction_status") == "no_definition_found":
|
| 53 |
+
return "term found but no definition in document"
|
| 54 |
+
if not entry.get("definition"):
|
| 55 |
+
return "definition rejected by span check or absent"
|
| 56 |
+
return "routine confirmation"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _wording_differs(entry: dict) -> bool:
|
| 60 |
+
"""The document says "Physical of Availability"; the expansion says
|
| 61 |
+
"Physical Availability". Surfacing that to the expert is a locked
|
| 62 |
+
requirement, so it earns its own review reason."""
|
| 63 |
+
source = (entry.get("source_wording") or "").strip().casefold()
|
| 64 |
+
full = (entry.get("full_name") or "").strip().casefold()
|
| 65 |
+
return bool(source and full) and full not in source
|
src/knowledge_extraction/rank/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .evidence import rank_evidence, rounds_available, top_k
|
| 2 |
+
|
| 3 |
+
__all__ = ["rank_evidence", "rounds_available", "top_k"]
|
src/knowledge_extraction/rank/evidence.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Score and select the top-K evidence chunks per cluster.
|
| 2 |
+
|
| 3 |
+
This is a filter, and it structurally resembles the relevance gate the team
|
| 4 |
+
rejected. It is defensible only because of three properties, all of which must
|
| 5 |
+
survive any future change here:
|
| 6 |
+
|
| 7 |
+
1. **No term is ever dropped** β only its *evidence* is narrowed.
|
| 8 |
+
2. **Every mention stays in the cluster**, so a bad top-3 is visible in
|
| 9 |
+
provenance rather than invisible.
|
| 10 |
+
3. **The full ranked list is retained**, so escalation can take the next K and
|
| 11 |
+
self-correct.
|
| 12 |
+
|
| 13 |
+
The rejected relevance gate had none of the three. If ranking is ever revisited,
|
| 14 |
+
that is the test to apply.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import re
|
| 20 |
+
|
| 21 |
+
from ..cluster.normalize import normalize
|
| 22 |
+
from ..filters.cue_filter import definitional_hits
|
| 23 |
+
from ..filters.legend_filter import find_legend_lines
|
| 24 |
+
from ..models import Chunk, TermCluster
|
| 25 |
+
from ..settings import CUE_PROXIMITY_CHARS, EVIDENCE_K, EVIDENCE_WEIGHTS
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def rank_evidence(
|
| 29 |
+
clusters: list[TermCluster],
|
| 30 |
+
chunks: list[Chunk],
|
| 31 |
+
weights: dict[str, float] | None = None,
|
| 32 |
+
) -> None:
|
| 33 |
+
"""Mutates clusters in place: sets `evidence_chunk_ids` (ranked, best first)
|
| 34 |
+
and `evidence_scores`. Keeps the FULL ranked list, not just the top K."""
|
| 35 |
+
w = {**EVIDENCE_WEIGHTS, **(weights or {})}
|
| 36 |
+
by_id = {c.chunk_id: c for c in chunks}
|
| 37 |
+
# Document order, so "first occurrence" means the term's first appearance in
|
| 38 |
+
# the document β not whichever element a set happened to yield first, which
|
| 39 |
+
# would hand the +1.0 to an arbitrary chunk and make ranking unreproducible.
|
| 40 |
+
doc_order = {c.chunk_id: i for i, c in enumerate(chunks)}
|
| 41 |
+
first_seen: set[str] = set()
|
| 42 |
+
|
| 43 |
+
for cluster in clusters:
|
| 44 |
+
chunk_ids = sorted(
|
| 45 |
+
{m.chunk_id for m in cluster.mentions},
|
| 46 |
+
key=lambda cid: (doc_order.get(cid, len(chunks)), cid),
|
| 47 |
+
)
|
| 48 |
+
scored: list[tuple[float, str]] = []
|
| 49 |
+
for chunk_id in chunk_ids:
|
| 50 |
+
chunk = by_id.get(chunk_id)
|
| 51 |
+
if chunk is None:
|
| 52 |
+
continue
|
| 53 |
+
positions = [m for m in cluster.mentions if m.chunk_id == chunk_id]
|
| 54 |
+
scored.append((_score_chunk(chunk, cluster, positions, w, first_seen), chunk_id))
|
| 55 |
+
# Tie-break on document order so ranking is stable across runs.
|
| 56 |
+
scored.sort(key=lambda t: (-t[0], doc_order.get(t[1], len(chunks)), t[1]))
|
| 57 |
+
cluster.evidence_chunk_ids = [cid for _, cid in scored]
|
| 58 |
+
cluster.evidence_scores = [round(s, 2) for s, _ in scored]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def top_k(
|
| 62 |
+
cluster: TermCluster, k: int = EVIDENCE_K, round_index: int = 0
|
| 63 |
+
) -> list[str]:
|
| 64 |
+
"""Evidence for extraction round `round_index` (0 = best K, 1 = next Kβ¦)."""
|
| 65 |
+
start = round_index * k
|
| 66 |
+
return cluster.evidence_chunk_ids[start : start + k]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def rounds_available(cluster: TermCluster, k: int, max_rounds: int) -> int:
|
| 70 |
+
"""How many escalation rounds this cluster actually has evidence for.
|
| 71 |
+
|
| 72 |
+
Usually 0 on short documents β a cluster with a single evidence chunk has
|
| 73 |
+
nowhere to escalate, which is why an escalation count of zero is not by
|
| 74 |
+
itself a sign the loop is broken.
|
| 75 |
+
"""
|
| 76 |
+
extra = max(0, len(cluster.evidence_chunk_ids) - k)
|
| 77 |
+
return min(max_rounds, -(-extra // k)) # ceil division
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _score_chunk(
|
| 81 |
+
chunk: Chunk,
|
| 82 |
+
cluster: TermCluster,
|
| 83 |
+
positions: list,
|
| 84 |
+
w: dict[str, float],
|
| 85 |
+
first_seen: set[str],
|
| 86 |
+
) -> float:
|
| 87 |
+
score = 0.0
|
| 88 |
+
|
| 89 |
+
# Word-boundary match, NOT substring. "PA" is a substring of "Parameter",
|
| 90 |
+
# "pada" and "composite", so substring matching handed this bonus to almost
|
| 91 |
+
# every Indonesian heading and pushed real definition sections below
|
| 92 |
+
# formula tables.
|
| 93 |
+
heading = normalize(chunk.heading or "")
|
| 94 |
+
term_in_heading = bool(heading) and any(
|
| 95 |
+
_word_match(normalize(v), heading) for v in cluster.variants
|
| 96 |
+
)
|
| 97 |
+
if term_in_heading:
|
| 98 |
+
score += w["term_in_heading"]
|
| 99 |
+
|
| 100 |
+
# Cue proximity. When the heading names the term, the heading counts as a
|
| 101 |
+
# mention at offset 0.
|
| 102 |
+
#
|
| 103 |
+
# Indonesian standards are written "2.1.3 Physical of Availability (PA)" /
|
| 104 |
+
# "Adalah ketersediaan fisik..." β the heading names the term and the body
|
| 105 |
+
# opens with the cue WITHOUT repeating it. Measured on the BUMA standard:
|
| 106 |
+
# without this, the PA definition section scored no cue bonus at all and
|
| 107 |
+
# ranked below a computation section that merely mentions PA often. The
|
| 108 |
+
# section's own definition is the single most valuable piece of evidence
|
| 109 |
+
# there is, so losing the signal there defeats the ranking.
|
| 110 |
+
cues = definitional_hits(chunk.text)
|
| 111 |
+
anchors = [(m.char_start, m.char_end) for m in positions]
|
| 112 |
+
if term_in_heading:
|
| 113 |
+
anchors.append((0, 0))
|
| 114 |
+
if any(
|
| 115 |
+
abs(cs - start) <= CUE_PROXIMITY_CHARS or abs(ce - end) <= CUE_PROXIMITY_CHARS
|
| 116 |
+
for cs, ce in cues
|
| 117 |
+
for start, end in anchors
|
| 118 |
+
):
|
| 119 |
+
score += w["definitional_cue_near"]
|
| 120 |
+
|
| 121 |
+
legend_lines = find_legend_lines(chunk.text)
|
| 122 |
+
if legend_lines:
|
| 123 |
+
line_starts = _line_starts(chunk.text)
|
| 124 |
+
for mention in positions:
|
| 125 |
+
if _line_of(mention.char_start, line_starts) in legend_lines:
|
| 126 |
+
score += w["in_legend_block"]
|
| 127 |
+
break
|
| 128 |
+
|
| 129 |
+
if chunk.has_formula:
|
| 130 |
+
score += w["formula_present"]
|
| 131 |
+
|
| 132 |
+
normalised_variants = {normalize(v) for v in cluster.variants}
|
| 133 |
+
if any(normalize(b) in normalised_variants for b in chunk.bold_spans):
|
| 134 |
+
score += w["bold_or_italic"]
|
| 135 |
+
|
| 136 |
+
if cluster.cluster_id not in first_seen:
|
| 137 |
+
score += w["first_occurrence"]
|
| 138 |
+
first_seen.add(cluster.cluster_id)
|
| 139 |
+
|
| 140 |
+
if chunk.is_tabular:
|
| 141 |
+
score += w["tabular_penalty"]
|
| 142 |
+
|
| 143 |
+
return score
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _word_match(needle: str, haystack: str) -> bool:
|
| 147 |
+
if not needle:
|
| 148 |
+
return False
|
| 149 |
+
return re.search(rf"(?<!\w){re.escape(needle)}(?!\w)", haystack) is not None
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _line_starts(text: str) -> list[int]:
|
| 153 |
+
starts, pos = [0], 0
|
| 154 |
+
for line in text.split("\n")[:-1]:
|
| 155 |
+
pos += len(line) + 1
|
| 156 |
+
starts.append(pos)
|
| 157 |
+
return starts
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _line_of(char_pos: int, line_starts: list[int]) -> int:
|
| 161 |
+
lo, hi = 0, len(line_starts) - 1
|
| 162 |
+
while lo < hi:
|
| 163 |
+
mid = (lo + hi + 1) // 2
|
| 164 |
+
if line_starts[mid] <= char_pos:
|
| 165 |
+
lo = mid
|
| 166 |
+
else:
|
| 167 |
+
hi = mid - 1
|
| 168 |
+
return lo
|
src/knowledge_extraction/service.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pipeline facade: parsed document β candidate entries β review queue.
|
| 2 |
+
|
| 3 |
+
Mirrors the shape of `src/query/service.py` β a deterministic orchestrator over
|
| 4 |
+
stages that each do one thing, with the expensive step isolated and every
|
| 5 |
+
failure degrading rather than aborting.
|
| 6 |
+
|
| 7 |
+
Cost discipline, carried from the prototype and worth keeping: **dry-run, then a
|
| 8 |
+
small pilot, then the full run.** A dry run makes zero API calls and prints the
|
| 9 |
+
token estimate, so the bill is knowable before it is incurred.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import time
|
| 15 |
+
|
| 16 |
+
from ..middlewares.logging import get_logger
|
| 17 |
+
from .cluster import cluster_mentions
|
| 18 |
+
from .diff import diff_glossary
|
| 19 |
+
from .extract import (
|
| 20 |
+
build_glossary_prompt,
|
| 21 |
+
est_tokens,
|
| 22 |
+
extract_formula,
|
| 23 |
+
extract_glossary,
|
| 24 |
+
extract_rule,
|
| 25 |
+
extract_summary,
|
| 26 |
+
)
|
| 27 |
+
from .filters import abbrev_pairs, extract_mentions, rule_candidates
|
| 28 |
+
from .models import (
|
| 29 |
+
CallUsage,
|
| 30 |
+
Chunk,
|
| 31 |
+
ClusterResult,
|
| 32 |
+
FilterResult,
|
| 33 |
+
ParsedDoc,
|
| 34 |
+
RejectedField,
|
| 35 |
+
)
|
| 36 |
+
from .queue import build_queue
|
| 37 |
+
from .rank import rank_evidence, top_k
|
| 38 |
+
from .settings import EVIDENCE_K
|
| 39 |
+
from .validate import evidence_text, find_conflicts, rounds_available, validate_entry
|
| 40 |
+
|
| 41 |
+
logger = get_logger("knowledge_extraction")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ExtractionResult:
|
| 45 |
+
def __init__(self) -> None:
|
| 46 |
+
self.glossary: list[dict] = []
|
| 47 |
+
self.rules: list[dict] = []
|
| 48 |
+
self.formulas: list[dict] = []
|
| 49 |
+
self.brief: dict | None = None
|
| 50 |
+
self.review_queue: list[dict] = []
|
| 51 |
+
self.rejected: list[RejectedField] = []
|
| 52 |
+
self.usages: list[CallUsage] = []
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def total_tokens(self) -> tuple[int, int, int]:
|
| 56 |
+
return (
|
| 57 |
+
sum(u.prompt_tokens for u in self.usages),
|
| 58 |
+
sum(u.cached_tokens for u in self.usages),
|
| 59 |
+
sum(u.completion_tokens for u in self.usages),
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def run_filters(doc: ParsedDoc, use_span_filter: bool = True) -> FilterResult:
|
| 64 |
+
"""All free stages. Zero API calls."""
|
| 65 |
+
pairs = abbrev_pairs(doc.chunks)
|
| 66 |
+
mentions = extract_mentions(doc.chunks) if use_span_filter else []
|
| 67 |
+
return FilterResult(
|
| 68 |
+
doc_id=doc.doc_id,
|
| 69 |
+
mentions=mentions,
|
| 70 |
+
rule_candidates=rule_candidates(doc.chunks),
|
| 71 |
+
abbrev_pairs=pairs,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def build_clusters(doc: ParsedDoc, filtered: FilterResult) -> ClusterResult:
|
| 76 |
+
clustered = cluster_mentions(filtered.mentions, filtered.abbrev_pairs, doc.doc_id)
|
| 77 |
+
rank_evidence(clustered.clusters, doc.chunks)
|
| 78 |
+
return clustered
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def estimate_cost(
|
| 82 |
+
doc: ParsedDoc, clustered: ClusterResult, filtered: FilterResult, limit: int | None = None
|
| 83 |
+
) -> dict:
|
| 84 |
+
"""Dry run: exact prompts are built, nothing is sent."""
|
| 85 |
+
clusters = clustered.clusters[:limit] if limit else clustered.clusters
|
| 86 |
+
prompt_tokens = 0
|
| 87 |
+
for cluster in clusters:
|
| 88 |
+
system, user = build_glossary_prompt(cluster, doc.chunks)
|
| 89 |
+
prompt_tokens += est_tokens(system) + est_tokens(user)
|
| 90 |
+
return {
|
| 91 |
+
"glossary_calls": len(clusters),
|
| 92 |
+
"rule_calls": len(filtered.rule_candidates),
|
| 93 |
+
"formula_calls": sum(1 for c in doc.chunks if c.has_formula),
|
| 94 |
+
"summary_calls": 1,
|
| 95 |
+
"estimated_prompt_tokens": prompt_tokens,
|
| 96 |
+
"note": "estimate only β real counts come from the API usage object",
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def extract_all(
|
| 101 |
+
doc: ParsedDoc,
|
| 102 |
+
clustered: ClusterResult,
|
| 103 |
+
filtered: FilterResult,
|
| 104 |
+
extractor,
|
| 105 |
+
limit: int | None = None,
|
| 106 |
+
active_glossary: list[dict] | None = None,
|
| 107 |
+
branches: tuple[str, ...] = ("glossary", "rule", "formula", "summary"),
|
| 108 |
+
) -> ExtractionResult:
|
| 109 |
+
"""The paid stage plus validation, diff and queue."""
|
| 110 |
+
out = ExtractionResult()
|
| 111 |
+
started = time.time()
|
| 112 |
+
|
| 113 |
+
if "glossary" in branches:
|
| 114 |
+
_run_glossary(doc, clustered, extractor, out, limit)
|
| 115 |
+
if "rule" in branches:
|
| 116 |
+
_run_rules(doc, filtered, extractor, out, limit)
|
| 117 |
+
if "formula" in branches:
|
| 118 |
+
_run_formulas(doc, extractor, out, limit)
|
| 119 |
+
if "summary" in branches:
|
| 120 |
+
_run_summary(doc, extractor, out)
|
| 121 |
+
|
| 122 |
+
out.glossary = diff_glossary(out.glossary, active_glossary or [])
|
| 123 |
+
out.review_queue = build_queue(out.glossary)
|
| 124 |
+
|
| 125 |
+
prompt, cached, completion = out.total_tokens
|
| 126 |
+
logger.info(
|
| 127 |
+
"extraction complete",
|
| 128 |
+
doc_id=doc.doc_id,
|
| 129 |
+
glossary=len(out.glossary),
|
| 130 |
+
rules=len(out.rules),
|
| 131 |
+
formulas=len(out.formulas),
|
| 132 |
+
rejected_fields=len(out.rejected),
|
| 133 |
+
calls=len(out.usages),
|
| 134 |
+
prompt_tokens=prompt,
|
| 135 |
+
cached_tokens=cached,
|
| 136 |
+
completion_tokens=completion,
|
| 137 |
+
seconds=round(time.time() - started, 1),
|
| 138 |
+
)
|
| 139 |
+
return out
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _run_glossary(doc, clustered, extractor, out, limit) -> None:
|
| 143 |
+
clusters = clustered.clusters[:limit] if limit else clustered.clusters
|
| 144 |
+
for cluster in clusters:
|
| 145 |
+
entry = None
|
| 146 |
+
max_round = rounds_available(cluster, EVIDENCE_K)
|
| 147 |
+
|
| 148 |
+
for round_index in range(max_round + 1):
|
| 149 |
+
entry, usage = extract_glossary(
|
| 150 |
+
cluster, doc.chunks, extractor, doc.doc_id, EVIDENCE_K, round_index
|
| 151 |
+
)
|
| 152 |
+
out.usages.append(usage)
|
| 153 |
+
if entry is None:
|
| 154 |
+
continue
|
| 155 |
+
|
| 156 |
+
source = evidence_text(
|
| 157 |
+
top_k(cluster, EVIDENCE_K, round_index), doc.chunks
|
| 158 |
+
)
|
| 159 |
+
entry, rejections = validate_entry(entry, "glossary", source, cluster.canonical)
|
| 160 |
+
out.rejected.extend(rejections)
|
| 161 |
+
|
| 162 |
+
if entry.definition:
|
| 163 |
+
if round_index > 0:
|
| 164 |
+
entry.extraction_status = "escalated"
|
| 165 |
+
break
|
| 166 |
+
# Null definition -> escalate to the next K chunks.
|
| 167 |
+
|
| 168 |
+
if entry is None:
|
| 169 |
+
continue
|
| 170 |
+
if not entry.definition:
|
| 171 |
+
entry.extraction_status = "no_definition_found"
|
| 172 |
+
|
| 173 |
+
conflicting, variants = find_conflicts(
|
| 174 |
+
[entry.definition] if entry.definition else []
|
| 175 |
+
)
|
| 176 |
+
entry.definition_conflict = conflicting
|
| 177 |
+
entry.conflict_variants = variants
|
| 178 |
+
out.glossary.append(entry.model_dump(mode="json"))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _run_rules(doc, filtered, extractor, out, limit) -> None:
|
| 182 |
+
by_id: dict[str, Chunk] = {c.chunk_id: c for c in doc.chunks}
|
| 183 |
+
candidates = filtered.rule_candidates[:limit] if limit else filtered.rule_candidates
|
| 184 |
+
seen: set[str] = set()
|
| 185 |
+
for candidate in candidates:
|
| 186 |
+
chunk = by_id.get(candidate.chunk_id)
|
| 187 |
+
if chunk is None:
|
| 188 |
+
continue
|
| 189 |
+
entry, usage = extract_rule(candidate, chunk, extractor, doc.doc_id)
|
| 190 |
+
out.usages.append(usage)
|
| 191 |
+
if entry is None:
|
| 192 |
+
continue
|
| 193 |
+
entry, rejections = validate_entry(entry, "rule", chunk.text, entry.rule_id)
|
| 194 |
+
out.rejected.extend(rejections)
|
| 195 |
+
key = (entry.statement or "").strip().casefold()
|
| 196 |
+
if key and key in seen:
|
| 197 |
+
continue
|
| 198 |
+
if key:
|
| 199 |
+
seen.add(key)
|
| 200 |
+
out.rules.append(entry.model_dump(mode="json"))
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def _run_formulas(doc, extractor, out, limit) -> None:
|
| 204 |
+
chunks = [c for c in doc.chunks if c.has_formula]
|
| 205 |
+
chunks = chunks[:limit] if limit else chunks
|
| 206 |
+
seen: set[str] = set()
|
| 207 |
+
for chunk in chunks:
|
| 208 |
+
entry, usage = extract_formula(chunk, extractor, doc.doc_id)
|
| 209 |
+
out.usages.append(usage)
|
| 210 |
+
if entry is None:
|
| 211 |
+
continue
|
| 212 |
+
entry, rejections = validate_entry(
|
| 213 |
+
entry, "formula", chunk.text, entry.name or chunk.chunk_id
|
| 214 |
+
)
|
| 215 |
+
out.rejected.extend(rejections)
|
| 216 |
+
key = (entry.formula_latex or "").strip()
|
| 217 |
+
if key and key in seen:
|
| 218 |
+
continue
|
| 219 |
+
if key:
|
| 220 |
+
seen.add(key)
|
| 221 |
+
out.formulas.append(entry.model_dump(mode="json"))
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _run_summary(doc, extractor, out) -> None:
|
| 225 |
+
entry, usage = extract_summary(doc.chunks, extractor, doc.doc_id)
|
| 226 |
+
out.usages.append(usage)
|
| 227 |
+
if entry is None:
|
| 228 |
+
return
|
| 229 |
+
# Summary prose is NOT span-checked β it cannot be. Only its provenance is
|
| 230 |
+
# carried, and the branch belongs on a larger tier for exactly that reason.
|
| 231 |
+
out.brief = entry.model_dump(mode="json")
|
src/knowledge_extraction/settings.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tunables for the knowledge-extraction pipeline.
|
| 2 |
+
|
| 3 |
+
Every value here was calibrated on real documents and each one has a reason
|
| 4 |
+
recorded in KNOWLEDGE_PIPELINE_CALIBRATION.md. Change them deliberately β most
|
| 5 |
+
were arrived at by a measurement, and two of them (`FUZZY_MIN_LEN`,
|
| 6 |
+
`CACHE_MIN_TOKENS`) fix bugs that are silent when reintroduced.
|
| 7 |
+
|
| 8 |
+
Label and cue sets live in `config/*.yaml` so they can be tuned without a code
|
| 9 |
+
change: label phrasing is the main recall lever and the filter is very sensitive
|
| 10 |
+
to it.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from functools import lru_cache
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import yaml
|
| 19 |
+
|
| 20 |
+
CONFIG_DIR = Path(__file__).resolve().parent / "config"
|
| 21 |
+
|
| 22 |
+
# ββ Term filter βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
# Variant C beat both the English-default and Indonesian-phrasing label sets:
|
| 24 |
+
# the other two missed the same class (mining activities and materials).
|
| 25 |
+
LABELS_VARIANT = "broad"
|
| 26 |
+
|
| 27 |
+
# 0.25, not 0.35: measured recall 0.854 @ 0.25 vs 0.658 @ 0.35 on `broad`.
|
| 28 |
+
# Precision falls (0.41 vs 0.50) and that is the intended trade β the filter is
|
| 29 |
+
# deliberately over-inclusive, clustering and ranking absorb the noise, and a
|
| 30 |
+
# term the filter never proposes can never be recovered downstream.
|
| 31 |
+
SPAN_SCORE_THRESHOLD = 0.25
|
| 32 |
+
|
| 33 |
+
# The span NER model truncates past ~384 of its own tokens and *warns rather
|
| 34 |
+
# than failing*, so a long chunk silently loses its tail. Indonesian technical
|
| 35 |
+
# prose subword-tokenises at roughly 2.5x, so 220-word windows still tripped the
|
| 36 |
+
# cap; 130 does not. Chunks are fed as overlapping windows with offsets remapped.
|
| 37 |
+
WINDOW_WORDS = 130
|
| 38 |
+
WINDOW_OVERLAP = 30
|
| 39 |
+
SPAN_TOKEN_CAP = 12
|
| 40 |
+
|
| 41 |
+
# ββ Clustering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
FUZZY_THRESHOLD = 92
|
| 43 |
+
|
| 44 |
+
# Below this length only exact matching is allowed: "PA" and "UA" score highly
|
| 45 |
+
# against each other on token_set_ratio. Over-merging is far worse than
|
| 46 |
+
# under-merging β an under-merge costs one extra call and one extra review row,
|
| 47 |
+
# a wrong merge destroys a distinct term and the expert never sees it.
|
| 48 |
+
FUZZY_MIN_LEN = 5
|
| 49 |
+
|
| 50 |
+
# ββ Evidence ranking ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
+
EVIDENCE_K = 3
|
| 52 |
+
CUE_PROXIMITY_CHARS = 100
|
| 53 |
+
|
| 54 |
+
EVIDENCE_WEIGHTS: dict[str, float] = {
|
| 55 |
+
"definitional_cue_near": 5.0,
|
| 56 |
+
"term_in_heading": 4.0,
|
| 57 |
+
"in_legend_block": 3.5,
|
| 58 |
+
"formula_present": 2.0,
|
| 59 |
+
"bold_or_italic": 1.5,
|
| 60 |
+
"first_occurrence": 1.0,
|
| 61 |
+
"tabular_penalty": -3.0,
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# ββ Chunking ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 65 |
+
MAX_CHUNK_TOKENS = 1500
|
| 66 |
+
MAX_HEADING_LEN = 90
|
| 67 |
+
BOILERPLATE_MIN_FRAC = 0.6
|
| 68 |
+
|
| 69 |
+
# ββ Extraction ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
TEMPERATURE = 0.0
|
| 71 |
+
|
| 72 |
+
# OpenAI-family prompt caching does not engage AT ALL below this many prompt
|
| 73 |
+
# tokens, so a shorter fixed prefix caches nothing and costs ~10x on input. The
|
| 74 |
+
# measured hit rate at/above it was 54%.
|
| 75 |
+
CACHE_MIN_TOKENS = 1024
|
| 76 |
+
|
| 77 |
+
# ββ Validation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
+
MAX_ESCALATION_ROUNDS = 2
|
| 79 |
+
CONFLICT_OVERLAP_THRESHOLD = 0.4
|
| 80 |
+
DUPLICATE_OVERLAP_THRESHOLD = 0.8
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@lru_cache(maxsize=4)
|
| 84 |
+
def load_yaml(name: str) -> dict:
|
| 85 |
+
with open(CONFIG_DIR / name, encoding="utf-8") as fh:
|
| 86 |
+
return yaml.safe_load(fh)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def labels_for(variant: str = LABELS_VARIANT) -> tuple[list[str], float]:
|
| 90 |
+
"""Returns (labels, threshold) for a label variant."""
|
| 91 |
+
cfg = load_yaml("labels.yaml")
|
| 92 |
+
labels = cfg.get(variant) or cfg.get(LABELS_VARIANT) or []
|
| 93 |
+
return list(labels), float(cfg.get("threshold", SPAN_SCORE_THRESHOLD))
|