sooth 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sooth-0.2.1 → sooth-0.2.2}/PKG-INFO +61 -12
- sooth-0.2.2/README.md +136 -0
- {sooth-0.2.1 → sooth-0.2.2}/pyproject.toml +1 -1
- {sooth-0.2.1 → sooth-0.2.2}/src/sooth/cli.py +28 -3
- sooth-0.2.2/src/sooth/demo.json +1 -0
- {sooth-0.2.1 → sooth-0.2.2}/src/sooth/report.py +24 -0
- {sooth-0.2.1 → sooth-0.2.2}/tests/test_core.py +20 -1
- sooth-0.2.1/README.md +0 -87
- {sooth-0.2.1 → sooth-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/.github/workflows/ci.yml +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/.gitignore +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/LICENSE +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/docs/DESIGN.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/docs/DOGFOOD.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/docs/PRD.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/docs/TESTING.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/calibration.json +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/calibration.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/draft.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/news-1-evil.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/news-1-output.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/news-1-summ.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/news-1.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/policy.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/pricing.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/examples/source.md +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/src/sooth/__init__.py +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/src/sooth/claims.py +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/src/sooth/verify.py +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/tests/calibrate.py +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/tests/conftest.py +0 -0
- {sooth-0.2.1 → sooth-0.2.2}/tests/smoke.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sooth
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Verify AI-generated text against source material. Claim-by-claim trust reports powered by Jev (TypeSafe System One).
|
|
5
5
|
Project-URL: Homepage, https://github.com/naufalhilmiaji/sooth
|
|
6
6
|
Project-URL: Documentation, https://github.com/naufalhilmiaji/sooth/tree/main/docs
|
|
@@ -23,15 +23,23 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
# Sooth
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
**LLMs generate. Sooth verifies.**
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Claim-by-claim fact-checking for AI output, designed for CI. Sooth checks every sentence in a draft against your source material and returns `PASS` / `FAIL` / `REVIEW` with calibrated probabilities and the exact source line behind each verdict.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
sooth
|
|
30
|
+
[](https://pypi.org/project/sooth/)
|
|
31
|
+
[](https://pypi.org/project/sooth/)
|
|
32
|
+
[](https://github.com/naufalhilmiaji/sooth/blob/main/LICENSE)
|
|
33
|
+
[](https://github.com/naufalhilmiaji/sooth/actions/workflows/ci.yml)
|
|
34
|
+
|
|
35
|
+
## See it catch a hallucination
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install sooth
|
|
39
|
+
sooth demo # no API key needed
|
|
32
40
|
```
|
|
33
41
|
|
|
34
|
-
Real output
|
|
42
|
+
Real output — seven claims in an AI-written news summary, three numbers quietly wrong:
|
|
35
43
|
|
|
36
44
|
```
|
|
37
45
|
# Sooth
|
|
@@ -46,14 +54,18 @@ Real output (planted errors in the draft vs a news source):
|
|
|
46
54
|
| 7 | Para investor ritel sangat senang dengan aturan baru ini. | ➖ UNCHECKABLE | ██░░░░░░ 0.24 | P(checkable)=0.24 | |
|
|
47
55
|
```
|
|
48
56
|
|
|
57
|
+
Every verdict carries its probability distribution and the source span it was judged against. When Sooth is unsure, it says `REVIEW` instead of guessing.
|
|
58
|
+
|
|
49
59
|
## Why
|
|
50
60
|
|
|
51
|
-
AI writes fast, nobody checks. Claims ship wrong
|
|
61
|
+
AI writes fast, nobody checks. Claims ship wrong — usually a number, a hedge, or a name that drifted. Asking another LLM "is this right?" just produces more prose.
|
|
62
|
+
|
|
63
|
+
Sooth takes a different bet: **the model makes typed judgments, code makes the decision.** Every rule that turns a probability into a verdict is a readable line of Python, not a prompt.
|
|
52
64
|
|
|
53
65
|
## Install
|
|
54
66
|
|
|
55
67
|
```bash
|
|
56
|
-
pip install sooth
|
|
68
|
+
pip install sooth
|
|
57
69
|
export TYPESAFE_API_KEY=... # get one at console.typesafe.ai
|
|
58
70
|
```
|
|
59
71
|
|
|
@@ -73,14 +85,49 @@ sooth --source policy.md --text draft.md --format plain
|
|
|
73
85
|
# --log FILE append full judgment trace (one JSONL line per run)
|
|
74
86
|
```
|
|
75
87
|
|
|
88
|
+
Wire it into CI as a quality gate for generated content:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
- name: Verify AI output
|
|
92
|
+
run: sooth --source docs/policy.md --text generated-reply.md
|
|
93
|
+
env:
|
|
94
|
+
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
|
|
95
|
+
```
|
|
96
|
+
|
|
76
97
|
## How it works
|
|
77
98
|
|
|
78
99
|
1. Draft is split into claims (one sentence each).
|
|
79
|
-
2. Each claim gets
|
|
100
|
+
2. Each claim gets four questions to Jev, fanned out in parallel batches: *is this checkable?*, *does the source support it?* (`supports` / `contradicts` / `not_found`), *do all details match exactly?*, and *which source span is the evidence?*
|
|
80
101
|
3. Verdicts are mapped in code: uncheckable → `UNCHECKABLE`; low confidence → `REVIEW`; then `PASS` / `FAIL`. Safeguards demote `PASS` to `REVIEW` when details drift or claim numbers are absent from the source (checked in plain code).
|
|
81
102
|
4. The report shows the full probability distribution per claim — not just a label.
|
|
82
103
|
|
|
83
|
-
The decision logic lives in [`src/sooth/verify.py`](src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
|
|
104
|
+
The decision logic lives in [`src/sooth/verify.py`](https://github.com/naufalhilmiaji/sooth/blob/main/src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
|
|
105
|
+
|
|
106
|
+
Judgments come from [Jev](https://docs.typesafe.ai) (TypeSafe System One), which returns typed distributions instead of generated prose.
|
|
107
|
+
|
|
108
|
+
## Why not just use an LLM judge?
|
|
109
|
+
|
|
110
|
+
Not claiming Sooth is universally more accurate. Claiming it gives you **architectural guarantees** a prompt cannot:
|
|
111
|
+
|
|
112
|
+
| | Sooth | Generic LLM judge |
|
|
113
|
+
|---|-------|-------------------|
|
|
114
|
+
| Claim-level verification | ✅ | Sometimes |
|
|
115
|
+
| Source span behind each verdict | ✅ | Sometimes |
|
|
116
|
+
| `PASS` / `FAIL` / `REVIEW` | ✅ fixed vocabulary | Generated labels |
|
|
117
|
+
| Probability distribution | ✅ | Usually none |
|
|
118
|
+
| Verdict rules in readable code | ✅ | Prompt-dependent |
|
|
119
|
+
| CI exit codes | ✅ | ❌ |
|
|
120
|
+
| Audit log (`--log` JSONL) | ✅ | DIY |
|
|
121
|
+
|
|
122
|
+
## Who it's for
|
|
123
|
+
|
|
124
|
+
Developers building systems that generate text from trusted material:
|
|
125
|
+
|
|
126
|
+
- **RAG pipelines** — retrieved the right document, still wrote the wrong number
|
|
127
|
+
- **AI agents** — before an agent's reply reaches a human
|
|
128
|
+
- **Support / ops copilots** — policy-backed answers, checked against the policy
|
|
129
|
+
- **Report & summary generation** — figures that must match the source
|
|
130
|
+
- **Compliance workflows** — evidence trail for every claim, not vibes
|
|
84
131
|
|
|
85
132
|
## Known limits (alpha)
|
|
86
133
|
|
|
@@ -98,11 +145,13 @@ bash tests/smoke.sh # live smoke (needs TYPESAFE_API_KEY)
|
|
|
98
145
|
PYTHONPATH=src python3 tests/calibrate.py # live calibration, 30 labeled claims
|
|
99
146
|
```
|
|
100
147
|
|
|
101
|
-
Docs: [PRD](docs/PRD.md) · [Design](docs/DESIGN.md) · [Testing](docs/TESTING.md)
|
|
148
|
+
Docs: [PRD](https://github.com/naufalhilmiaji/sooth/blob/main/docs/PRD.md) · [Design](https://github.com/naufalhilmiaji/sooth/blob/main/docs/DESIGN.md) · [Testing](https://github.com/naufalhilmiaji/sooth/blob/main/docs/TESTING.md)
|
|
102
149
|
|
|
103
150
|
## Roadmap
|
|
104
151
|
|
|
105
|
-
- v0.2 shipped:
|
|
152
|
+
- v0.2.2 shipped: `sooth demo` (offline, no API key)
|
|
153
|
+
- v0.2.1 shipped: source-span evidence, published on PyPI
|
|
154
|
+
- GitHub Action
|
|
106
155
|
- later: hosted web app — paste UI, history, team review queues
|
|
107
156
|
|
|
108
157
|
## License
|
sooth-0.2.2/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Sooth
|
|
2
|
+
|
|
3
|
+
**LLMs generate. Sooth verifies.**
|
|
4
|
+
|
|
5
|
+
Claim-by-claim fact-checking for AI output, designed for CI. Sooth checks every sentence in a draft against your source material and returns `PASS` / `FAIL` / `REVIEW` with calibrated probabilities and the exact source line behind each verdict.
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/sooth/)
|
|
8
|
+
[](https://pypi.org/project/sooth/)
|
|
9
|
+
[](https://github.com/naufalhilmiaji/sooth/blob/main/LICENSE)
|
|
10
|
+
[](https://github.com/naufalhilmiaji/sooth/actions/workflows/ci.yml)
|
|
11
|
+
|
|
12
|
+
## See it catch a hallucination
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install sooth
|
|
16
|
+
sooth demo # no API key needed
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Real output — seven claims in an AI-written news summary, three numbers quietly wrong:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
# Sooth
|
|
23
|
+
|
|
24
|
+
**PASS 2 · FAIL 3 · REVIEW 1 · UNCHECKABLE 1** — threshold 0.70
|
|
25
|
+
|
|
26
|
+
| # | Claim | Verdict | P | Why (P distribution) | Evidence |
|
|
27
|
+
|---|-------|---------|---|----------------------|----------|
|
|
28
|
+
| 3 | BNBR baru menuntaskan rights issue bernilai besar di harga Rp 33. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 33 | `examples/news-1.md:25` "Saham ini juga baru menyelesaikan rights issue dalam juml…" |
|
|
29
|
+
| 5 | BUMI hanya perlu turun sekitar 10% untuk menyentuh level Rp 50. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 10 | `examples/news-1.md:23` "Adapun PT Bumi Resources Tbk (BUMI) di sekitar Rp192 haru…" |
|
|
30
|
+
| 6 | BEI juga menetapkan batas atas harga saham Rp 5.000 per saham. | ⚠️ REVIEW | ████████ 1.00 | contradicts 0.00 / not_found 1.00 / supports 0.00 · details 0.02 · missing #s: 5.000 | |
|
|
31
|
+
| 7 | Para investor ritel sangat senang dengan aturan baru ini. | ➖ UNCHECKABLE | ██░░░░░░ 0.24 | P(checkable)=0.24 | |
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Every verdict carries its probability distribution and the source span it was judged against. When Sooth is unsure, it says `REVIEW` instead of guessing.
|
|
35
|
+
|
|
36
|
+
## Why
|
|
37
|
+
|
|
38
|
+
AI writes fast, nobody checks. Claims ship wrong — usually a number, a hedge, or a name that drifted. Asking another LLM "is this right?" just produces more prose.
|
|
39
|
+
|
|
40
|
+
Sooth takes a different bet: **the model makes typed judgments, code makes the decision.** Every rule that turns a probability into a verdict is a readable line of Python, not a prompt.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install sooth
|
|
46
|
+
export TYPESAFE_API_KEY=... # get one at console.typesafe.ai
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
sooth --source docs/policy.md --source tickets/t123.md --text draft-reply.md
|
|
53
|
+
|
|
54
|
+
# CI-friendly exit codes
|
|
55
|
+
sooth --source policy.md --text draft.md --format plain
|
|
56
|
+
# 0 = clean · 1 = any FAIL · 2 = any REVIEW · 3 = usage/config error
|
|
57
|
+
|
|
58
|
+
# Options
|
|
59
|
+
# --confidence T REVIEW below this confidence (default 0.7)
|
|
60
|
+
# --format md|plain
|
|
61
|
+
# -o FILE write report to file
|
|
62
|
+
# --log FILE append full judgment trace (one JSONL line per run)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Wire it into CI as a quality gate for generated content:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
- name: Verify AI output
|
|
69
|
+
run: sooth --source docs/policy.md --text generated-reply.md
|
|
70
|
+
env:
|
|
71
|
+
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
1. Draft is split into claims (one sentence each).
|
|
77
|
+
2. Each claim gets four questions to Jev, fanned out in parallel batches: *is this checkable?*, *does the source support it?* (`supports` / `contradicts` / `not_found`), *do all details match exactly?*, and *which source span is the evidence?*
|
|
78
|
+
3. Verdicts are mapped in code: uncheckable → `UNCHECKABLE`; low confidence → `REVIEW`; then `PASS` / `FAIL`. Safeguards demote `PASS` to `REVIEW` when details drift or claim numbers are absent from the source (checked in plain code).
|
|
79
|
+
4. The report shows the full probability distribution per claim — not just a label.
|
|
80
|
+
|
|
81
|
+
The decision logic lives in [`src/sooth/verify.py`](https://github.com/naufalhilmiaji/sooth/blob/main/src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
|
|
82
|
+
|
|
83
|
+
Judgments come from [Jev](https://docs.typesafe.ai) (TypeSafe System One), which returns typed distributions instead of generated prose.
|
|
84
|
+
|
|
85
|
+
## Why not just use an LLM judge?
|
|
86
|
+
|
|
87
|
+
Not claiming Sooth is universally more accurate. Claiming it gives you **architectural guarantees** a prompt cannot:
|
|
88
|
+
|
|
89
|
+
| | Sooth | Generic LLM judge |
|
|
90
|
+
|---|-------|-------------------|
|
|
91
|
+
| Claim-level verification | ✅ | Sometimes |
|
|
92
|
+
| Source span behind each verdict | ✅ | Sometimes |
|
|
93
|
+
| `PASS` / `FAIL` / `REVIEW` | ✅ fixed vocabulary | Generated labels |
|
|
94
|
+
| Probability distribution | ✅ | Usually none |
|
|
95
|
+
| Verdict rules in readable code | ✅ | Prompt-dependent |
|
|
96
|
+
| CI exit codes | ✅ | ❌ |
|
|
97
|
+
| Audit log (`--log` JSONL) | ✅ | DIY |
|
|
98
|
+
|
|
99
|
+
## Who it's for
|
|
100
|
+
|
|
101
|
+
Developers building systems that generate text from trusted material:
|
|
102
|
+
|
|
103
|
+
- **RAG pipelines** — retrieved the right document, still wrote the wrong number
|
|
104
|
+
- **AI agents** — before an agent's reply reaches a human
|
|
105
|
+
- **Support / ops copilots** — policy-backed answers, checked against the policy
|
|
106
|
+
- **Report & summary generation** — figures that must match the source
|
|
107
|
+
- **Compliance workflows** — evidence trail for every claim, not vibes
|
|
108
|
+
|
|
109
|
+
## Known limits (alpha)
|
|
110
|
+
|
|
111
|
+
- Safeguards catch most drift and smuggling: claim numbers absent from the source demote `PASS` to `REVIEW`, and a detail-match gate flags altered hedges ("about 74%" → "over 74%"). Not perfect — read the `Why` column before trusting a verdict.
|
|
112
|
+
- Derived numbers (totals, values computed outside the source) look "missing" and land in `REVIEW`.
|
|
113
|
+
- Source + questions must fit ~64k tokens — split long documents yourself.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install -e ".[dev]"
|
|
119
|
+
python3 tests/test_core.py # pure checks, no network
|
|
120
|
+
pytest # same suite
|
|
121
|
+
bash tests/smoke.sh # live smoke (needs TYPESAFE_API_KEY)
|
|
122
|
+
PYTHONPATH=src python3 tests/calibrate.py # live calibration, 30 labeled claims
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Docs: [PRD](https://github.com/naufalhilmiaji/sooth/blob/main/docs/PRD.md) · [Design](https://github.com/naufalhilmiaji/sooth/blob/main/docs/DESIGN.md) · [Testing](https://github.com/naufalhilmiaji/sooth/blob/main/docs/TESTING.md)
|
|
126
|
+
|
|
127
|
+
## Roadmap
|
|
128
|
+
|
|
129
|
+
- v0.2.2 shipped: `sooth demo` (offline, no API key)
|
|
130
|
+
- v0.2.1 shipped: source-span evidence, published on PyPI
|
|
131
|
+
- GitHub Action
|
|
132
|
+
- later: hosted web app — paste UI, history, team review queues
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT © Naufal Hilmiaji. Powered by [TypeSafe](https://typesafe.ai) / Jev.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sooth"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Verify AI-generated text against source material. Claim-by-claim trust reports powered by Jev (TypeSafe System One)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -9,9 +9,11 @@ from datetime import UTC, datetime
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
from sooth.claims import split_claims
|
|
12
|
-
from sooth.report import exit_code, log_record, render_markdown, render_plain
|
|
12
|
+
from sooth.report import exit_code, log_record, render_markdown, render_plain, verdicts_from_record
|
|
13
13
|
from sooth.verify import DEFAULT_THRESHOLD, VerifyError, verify_claims
|
|
14
14
|
|
|
15
|
+
DEMO_RECORD = Path(__file__).with_name("demo.json")
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
17
19
|
p = argparse.ArgumentParser(
|
|
@@ -19,9 +21,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
19
21
|
description="Verify AI-generated text against source material. "
|
|
20
22
|
"Each claim gets PASS / FAIL / REVIEW with calibrated probabilities.",
|
|
21
23
|
)
|
|
22
|
-
p.add_argument("
|
|
24
|
+
p.add_argument("command", nargs="?", choices=["demo"],
|
|
25
|
+
help="run the offline demo against a bundled recorded run (no API key)")
|
|
26
|
+
p.add_argument("--source", action="append", metavar="FILE",
|
|
23
27
|
help="ground-truth source file (repeatable)")
|
|
24
|
-
p.add_argument("--text",
|
|
28
|
+
p.add_argument("--text", metavar="FILE", help="AI-generated draft to check")
|
|
25
29
|
p.add_argument("--confidence", type=float, default=DEFAULT_THRESHOLD, metavar="T",
|
|
26
30
|
help=f"REVIEW below this confidence (default {DEFAULT_THRESHOLD})")
|
|
27
31
|
p.add_argument("--format", choices=["md", "plain"], default="md", help="report format")
|
|
@@ -30,8 +34,29 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
30
34
|
return p.parse_args(argv)
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
def run_demo(fmt: str) -> int:
|
|
38
|
+
"""Replay a bundled judgment trace through the real report code. No network."""
|
|
39
|
+
record = json.loads(DEMO_RECORD.read_text(encoding="utf-8"))
|
|
40
|
+
print(
|
|
41
|
+
"demo: replaying a recorded run "
|
|
42
|
+
f"({record['draft']} vs {', '.join(record['sources'])}) — no API key needed",
|
|
43
|
+
file=sys.stderr,
|
|
44
|
+
)
|
|
45
|
+
verdicts = verdicts_from_record(record)
|
|
46
|
+
report = (render_markdown if fmt == "md" else render_plain)(
|
|
47
|
+
verdicts, threshold=record["threshold"]
|
|
48
|
+
)
|
|
49
|
+
print(report, end="")
|
|
50
|
+
return exit_code(verdicts)
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
def main(argv: list[str] | None = None) -> int:
|
|
34
54
|
args = parse_args(argv)
|
|
55
|
+
if args.command == "demo":
|
|
56
|
+
return run_demo(args.format)
|
|
57
|
+
if not args.source or not args.text:
|
|
58
|
+
print("error: --source and --text are required (or try: sooth demo)", file=sys.stderr)
|
|
59
|
+
return 3
|
|
35
60
|
try:
|
|
36
61
|
sources = [(path, Path(path).read_text(encoding="utf-8")) for path in args.source]
|
|
37
62
|
draft = Path(args.text).read_text(encoding="utf-8")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"model": "jev-1.13.0", "threshold": 0.7, "sources": ["examples/news-1.md"], "draft": "examples/news-1-evil.md", "usage": {"input_tokens": 11111, "output_tokens": 1107}, "results": [{"id": "c1", "text": "Aturan baru BEI menurunkan batas bawah harga saham menjadi Rp 1 per saham mulai 28 September 2026.", "line": 3, "kind": "PASS", "p_checkable": 0.98, "choice": "supports", "probabilities": {"supports": 1.0, "not_found": 0.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.98, "missing_numbers": [], "evidence": {"id": "s1", "text": "Saham-saham Grup Bakrie kembali menjadi sorotan menjelang pemberlakuan aturan baru Bursa Efek Indonesia (BEI) yang menurunkan batas bawah harga saham menjadi Rp 1 per saham mulai 28 September 2026.", "line": 3, "source": "examples/news-1.md"}}, {"id": "c2", "text": "Menurut analis Bumiputera Sekuritas, aturan ini berlaku untuk seluruh emiten di BEI.", "line": 5, "kind": "PASS", "p_checkable": 0.96, "choice": "supports", "probabilities": {"supports": 1.0, "not_found": 0.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.82, "missing_numbers": [], "evidence": {"id": "s9", "text": "Ia menyebut aturan ini berlaku untuk seluruh saham di BEI, bukan khusus Grup Bakrie.", "line": 7, "source": "examples/news-1.md"}}, {"id": "c3", "text": "BNBR baru menuntaskan rights issue bernilai besar di harga Rp 33.", "line": 7, "kind": "FAIL", "p_checkable": 0.93, "choice": "contradicts", "probabilities": {"supports": 0.0, "not_found": 0.0, "contradicts": 1.0}, "confidence": 1.0, "details_p": 0.01, "missing_numbers": ["33"], "evidence": {"id": "s47", "text": "Saham ini juga baru menyelesaikan rights issue dalam jumlah besar di harga Rp 53.", "line": 25, "source": "examples/news-1.md"}}, {"id": "c4", "text": "Saham VIVA disuspensi di harga Rp 50.", "line": 9, "kind": "FAIL", "p_checkable": 0.92, "choice": "contradicts", "probabilities": {"supports": 0.0, "not_found": 0.0, "contradicts": 1.0}, "confidence": 1.0, "details_p": 0.02, "missing_numbers": [], "evidence": null}, {"id": "c5", "text": "BUMI hanya perlu turun sekitar 10% untuk menyentuh level Rp 50.", "line": 11, "kind": "FAIL", "p_checkable": 0.92, "choice": "contradicts", "probabilities": {"supports": 0.0, "not_found": 0.0, "contradicts": 1.0}, "confidence": 1.0, "details_p": 0.01, "missing_numbers": ["10"], "evidence": {"id": "s40", "text": "Adapun PT Bumi Resources Tbk (BUMI) di sekitar Rp192 harus turun sekitar 74%, PT Darma Henwa Tbk (DEWA) sekitar 86%, PT Bumi Resources Minerals Tbk (BRMS) sekitar 93%, dan PT Energi Mega Persada Tbk (ENRG) lebih dari 96% untuk turun ke level Rp50.", "line": 23, "source": "examples/news-1.md"}}, {"id": "c6", "text": "BEI juga menetapkan batas atas harga saham Rp 5.000 per saham di pasar reguler.", "line": 13, "kind": "REVIEW", "p_checkable": 0.57, "choice": "not_found", "probabilities": {"supports": 0.0, "not_found": 1.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.02, "missing_numbers": ["5.000"], "evidence": null}, {"id": "c7", "text": "Para investor ritel sangat senang dengan aturan baru ini.", "line": 15, "kind": "UNCHECKABLE", "p_checkable": 0.25, "choice": "not_found", "probabilities": {"supports": 0.0, "not_found": 1.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.02, "missing_numbers": [], "evidence": null}], "ts": "2026-09-25T09:59:57+00:00"}
|
|
@@ -123,3 +123,27 @@ def log_record(result: VerifyResult, threshold: float, sources: list[str], draft
|
|
|
123
123
|
for v in result.verdicts
|
|
124
124
|
],
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def verdicts_from_record(record: dict) -> list[Verdict]:
|
|
129
|
+
"""Inverse of log_record's `results` — replay a recorded run without the API."""
|
|
130
|
+
out = []
|
|
131
|
+
for r in record["results"]:
|
|
132
|
+
ev = r.get("evidence")
|
|
133
|
+
out.append(Verdict(
|
|
134
|
+
claim_id=r["id"],
|
|
135
|
+
claim_text=r["text"],
|
|
136
|
+
line=r["line"],
|
|
137
|
+
kind=r["kind"],
|
|
138
|
+
p_checkable=r.get("p_checkable"),
|
|
139
|
+
choice=r.get("choice"),
|
|
140
|
+
probabilities=r.get("probabilities") or {},
|
|
141
|
+
confidence=r.get("confidence"),
|
|
142
|
+
details_p=r.get("details_p"),
|
|
143
|
+
missing_numbers=tuple(r.get("missing_numbers") or ()),
|
|
144
|
+
evidence_id=ev.get("id") if ev else None,
|
|
145
|
+
evidence_text=ev.get("text") if ev else None,
|
|
146
|
+
evidence_line=ev.get("line") if ev else None,
|
|
147
|
+
evidence_source=ev.get("source") if ev else None,
|
|
148
|
+
))
|
|
149
|
+
return out
|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) # direct-run without install
|
|
13
13
|
|
|
14
14
|
from sooth.claims import split_claims, split_segments
|
|
15
|
-
from sooth.report import bar, exit_code, log_record, render_markdown
|
|
15
|
+
from sooth.report import bar, exit_code, log_record, render_markdown, verdicts_from_record
|
|
16
16
|
from sooth.verify import (
|
|
17
17
|
FAIL,
|
|
18
18
|
PASS,
|
|
@@ -229,6 +229,25 @@ def test_log_record_roundtrip_no_secret():
|
|
|
229
229
|
assert "api_key" not in json.dumps(parsed).lower()
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
def test_verdicts_from_record_roundtrip():
|
|
233
|
+
"""log_record → JSON → verdicts_from_record is lossless (demo replay path)."""
|
|
234
|
+
result = VerifyResult(
|
|
235
|
+
verdicts=[
|
|
236
|
+
v(PASS, confidence=0.9, probabilities={"supports": 0.9}, details_p=0.9,
|
|
237
|
+
evidence_id="s1", evidence_text="Source line.", evidence_line=3,
|
|
238
|
+
evidence_source="a.md"),
|
|
239
|
+
v(FAIL, claim_id="c2", confidence=1.0, probabilities={"contradicts": 1.0},
|
|
240
|
+
missing_numbers=("33",)),
|
|
241
|
+
],
|
|
242
|
+
model="jev-1.13.0",
|
|
243
|
+
usage={"input_tokens": 10, "output_tokens": 1},
|
|
244
|
+
)
|
|
245
|
+
back = verdicts_from_record(json.loads(json.dumps(log_record(result, 0.7, ["a.md"], "d.md"))))
|
|
246
|
+
assert [(x.claim_id, x.kind, x.confidence) for x in back] == [("c1", PASS, 0.9), ("c2", FAIL, 1.0)]
|
|
247
|
+
assert back[0].evidence_text == "Source line." and back[0].evidence_line == 3
|
|
248
|
+
assert back[1].missing_numbers == ("33",) and back[1].evidence_text is None
|
|
249
|
+
|
|
250
|
+
|
|
232
251
|
if __name__ == "__main__":
|
|
233
252
|
fns = [f for name, f in sorted(globals().items()) if name.startswith("test_") and callable(f)]
|
|
234
253
|
for fn in fns:
|
sooth-0.2.1/README.md
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Sooth
|
|
2
|
-
|
|
3
|
-
Verify AI-generated text against source material. Claim-by-claim **PASS / FAIL / REVIEW**, with calibrated probabilities you can act on in CI.
|
|
4
|
-
|
|
5
|
-
Built on [Jev](https://docs.typesafe.ai) (TypeSafe System One) — typed judgments and probabilities instead of generated prose. Every verdict shows its probability distribution, and the combination rules are plain code you can read. No black box.
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
sooth --source policy.md --text draft-reply.md
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Real output (planted errors in the draft vs a news source):
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
# Sooth
|
|
15
|
-
|
|
16
|
-
**PASS 2 · FAIL 3 · REVIEW 1 · UNCHECKABLE 1** — threshold 0.70
|
|
17
|
-
|
|
18
|
-
| # | Claim | Verdict | P | Why (P distribution) | Evidence |
|
|
19
|
-
|---|-------|---------|---|----------------------|----------|
|
|
20
|
-
| 3 | BNBR baru menuntaskan rights issue bernilai besar di harga Rp 33. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 33 | `examples/news-1.md:25` "Saham ini juga baru menyelesaikan rights issue dalam juml…" |
|
|
21
|
-
| 5 | BUMI hanya perlu turun sekitar 10% untuk menyentuh level Rp 50. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 10 | `examples/news-1.md:23` "Adapun PT Bumi Resources Tbk (BUMI) di sekitar Rp192 haru…" |
|
|
22
|
-
| 6 | BEI juga menetapkan batas atas harga saham Rp 5.000 per saham. | ⚠️ REVIEW | ████████ 1.00 | contradicts 0.00 / not_found 1.00 / supports 0.00 · details 0.02 · missing #s: 5.000 | |
|
|
23
|
-
| 7 | Para investor ritel sangat senang dengan aturan baru ini. | ➖ UNCHECKABLE | ██░░░░░░ 0.24 | P(checkable)=0.24 | |
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Why
|
|
27
|
-
|
|
28
|
-
AI writes fast, nobody checks. Claims ship wrong. Sooth checks each claim against the evidence you give it — and when it is unsure, it says `REVIEW` instead of guessing.
|
|
29
|
-
|
|
30
|
-
## Install
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pip install sooth # or: pip install -e ".[dev]" from source
|
|
34
|
-
export TYPESAFE_API_KEY=... # get one at console.typesafe.ai
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Usage
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
sooth --source docs/policy.md --source tickets/t123.md --text draft-reply.md
|
|
41
|
-
|
|
42
|
-
# CI-friendly exit codes
|
|
43
|
-
sooth --source policy.md --text draft.md --format plain
|
|
44
|
-
# 0 = clean · 1 = any FAIL · 2 = any REVIEW · 3 = usage/config error
|
|
45
|
-
|
|
46
|
-
# Options
|
|
47
|
-
# --confidence T REVIEW below this confidence (default 0.7)
|
|
48
|
-
# --format md|plain
|
|
49
|
-
# -o FILE write report to file
|
|
50
|
-
# --log FILE append full judgment trace (one JSONL line per run)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## How it works
|
|
54
|
-
|
|
55
|
-
1. Draft is split into claims (one sentence each).
|
|
56
|
-
2. Each claim gets three questions to Jev, all fanned out in parallel batches: *is this checkable?*, *does the source support it?* (`supports` / `contradicts` / `not_found`), and *do all details match exactly?*
|
|
57
|
-
3. Verdicts are mapped in code: uncheckable → `UNCHECKABLE`; low confidence → `REVIEW`; then `PASS` / `FAIL`. Safeguards demote `PASS` to `REVIEW` when details drift or claim numbers are absent from the source (checked in plain code).
|
|
58
|
-
4. The report shows the full probability distribution per claim — not just a label.
|
|
59
|
-
|
|
60
|
-
The decision logic lives in [`src/sooth/verify.py`](src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
|
|
61
|
-
|
|
62
|
-
## Known limits (alpha)
|
|
63
|
-
|
|
64
|
-
- Safeguards catch most drift and smuggling: claim numbers absent from the source demote `PASS` to `REVIEW`, and a detail-match gate flags altered hedges ("about 74%" → "over 74%"). Not perfect — read the `Why` column before trusting a verdict.
|
|
65
|
-
- Derived numbers (totals, values computed outside the source) look "missing" and land in `REVIEW`.
|
|
66
|
-
- Source + questions must fit ~64k tokens — split long documents yourself.
|
|
67
|
-
|
|
68
|
-
## Development
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
pip install -e ".[dev]"
|
|
72
|
-
python3 tests/test_core.py # pure checks, no network
|
|
73
|
-
pytest # same suite
|
|
74
|
-
bash tests/smoke.sh # live smoke (needs TYPESAFE_API_KEY)
|
|
75
|
-
PYTHONPATH=src python3 tests/calibrate.py # live calibration, 30 labeled claims
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Docs: [PRD](docs/PRD.md) · [Design](docs/DESIGN.md) · [Testing](docs/TESTING.md)
|
|
79
|
-
|
|
80
|
-
## Roadmap
|
|
81
|
-
|
|
82
|
-
- v0.2 shipped: source-span evidence (the exact source line behind each verdict, shown in the report)
|
|
83
|
-
- later: hosted web app — paste UI, history, team review queues
|
|
84
|
-
|
|
85
|
-
## License
|
|
86
|
-
|
|
87
|
-
MIT © Naufal Hilmiaji. Powered by [TypeSafe](https://typesafe.ai) / Jev.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|