fast-mode-kit 0.2.0__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.
Files changed (55) hide show
  1. fast_mode_kit-0.2.0/LICENSE +21 -0
  2. fast_mode_kit-0.2.0/PKG-INFO +106 -0
  3. fast_mode_kit-0.2.0/README.md +87 -0
  4. fast_mode_kit-0.2.0/fast_mode_kit.egg-info/PKG-INFO +106 -0
  5. fast_mode_kit-0.2.0/fast_mode_kit.egg-info/SOURCES.txt +53 -0
  6. fast_mode_kit-0.2.0/fast_mode_kit.egg-info/dependency_links.txt +1 -0
  7. fast_mode_kit-0.2.0/fast_mode_kit.egg-info/entry_points.txt +2 -0
  8. fast_mode_kit-0.2.0/fast_mode_kit.egg-info/requires.txt +6 -0
  9. fast_mode_kit-0.2.0/fast_mode_kit.egg-info/top_level.txt +1 -0
  10. fast_mode_kit-0.2.0/fastkit/__init__.py +0 -0
  11. fast_mode_kit-0.2.0/fastkit/__main__.py +5 -0
  12. fast_mode_kit-0.2.0/fastkit/checks.py +131 -0
  13. fast_mode_kit-0.2.0/fastkit/cli.py +190 -0
  14. fast_mode_kit-0.2.0/fastkit/client.py +77 -0
  15. fast_mode_kit-0.2.0/fastkit/evals/__init__.py +0 -0
  16. fast_mode_kit-0.2.0/fastkit/evals/report.py +48 -0
  17. fast_mode_kit-0.2.0/fastkit/evals/run.py +96 -0
  18. fast_mode_kit-0.2.0/fastkit/evals/schema.py +50 -0
  19. fast_mode_kit-0.2.0/fastkit/evals/shop_gold.jsonl +24 -0
  20. fast_mode_kit-0.2.0/fastkit/examples/__init__.py +0 -0
  21. fast_mode_kit-0.2.0/fastkit/examples/shop/__init__.py +0 -0
  22. fast_mode_kit-0.2.0/fastkit/examples/shop/agent.py +104 -0
  23. fast_mode_kit-0.2.0/fastkit/examples/shop/data.py +32 -0
  24. fast_mode_kit-0.2.0/fastkit/examples/shop/demo_model.py +69 -0
  25. fast_mode_kit-0.2.0/fastkit/examples/shop/tools.py +73 -0
  26. fast_mode_kit-0.2.0/fastkit/hedge.py +84 -0
  27. fast_mode_kit-0.2.0/fastkit/loop.py +149 -0
  28. fast_mode_kit-0.2.0/fastkit/mock.py +32 -0
  29. fast_mode_kit-0.2.0/fastkit/modes.py +20 -0
  30. fast_mode_kit-0.2.0/fastkit/pipeline.py +56 -0
  31. fast_mode_kit-0.2.0/fastkit/prompts.py +21 -0
  32. fast_mode_kit-0.2.0/fastkit/py.typed +0 -0
  33. fast_mode_kit-0.2.0/fastkit/scaffold/README.md +11 -0
  34. fast_mode_kit-0.2.0/fastkit/scaffold/agent.py +84 -0
  35. fast_mode_kit-0.2.0/fastkit/scaffold/gold.jsonl +3 -0
  36. fast_mode_kit-0.2.0/fastkit/scaffold/tools.py +37 -0
  37. fast_mode_kit-0.2.0/fastkit/templates/fast_system.md +21 -0
  38. fast_mode_kit-0.2.0/fastkit/templates/notes.md +9 -0
  39. fast_mode_kit-0.2.0/fastkit/templates/reader.md +7 -0
  40. fast_mode_kit-0.2.0/fastkit/templates/writer.md +5 -0
  41. fast_mode_kit-0.2.0/fastkit/tools.py +65 -0
  42. fast_mode_kit-0.2.0/fastkit/types.py +47 -0
  43. fast_mode_kit-0.2.0/pyproject.toml +31 -0
  44. fast_mode_kit-0.2.0/setup.cfg +4 -0
  45. fast_mode_kit-0.2.0/tests/test_checks.py +55 -0
  46. fast_mode_kit-0.2.0/tests/test_cli.py +64 -0
  47. fast_mode_kit-0.2.0/tests/test_client.py +51 -0
  48. fast_mode_kit-0.2.0/tests/test_evals.py +41 -0
  49. fast_mode_kit-0.2.0/tests/test_example_agent.py +39 -0
  50. fast_mode_kit-0.2.0/tests/test_hedge.py +34 -0
  51. fast_mode_kit-0.2.0/tests/test_loop.py +138 -0
  52. fast_mode_kit-0.2.0/tests/test_mock.py +22 -0
  53. fast_mode_kit-0.2.0/tests/test_modes.py +13 -0
  54. fast_mode_kit-0.2.0/tests/test_prompts.py +22 -0
  55. fast_mode_kit-0.2.0/tests/test_tools.py +14 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fast-mode-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: fast-mode-kit
3
+ Version: 0.2.0
4
+ Summary: Starter kit for adding a fast, data-only answer mode to any chat agent
5
+ License: MIT
6
+ Keywords: llm,agents,latency,tool-calling,evals
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: pydantic>=2.6
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # fast-mode-kit
21
+
22
+ A starter kit for adding a **Fast mode** to any chat agent. Fast mode answers exactly what was asked, from your data, right the first time, in a few seconds.
23
+
24
+ It comes from shipping Fast mode in a live product. Every rule here traces back to a measured result or a real failure, and the [failure catalogue](docs/08-failure-catalogue.md) lists them.
25
+
26
+ It works with **frontier APIs** (any OpenAI-compatible endpoint) and with **models you host yourself** (mlx, vLLM, Ollama, llama.cpp).
27
+
28
+ ## Quick start (offline, no key, no cost)
29
+
30
+ ```bash
31
+ pip install fast-mode-kit
32
+
33
+ fastkit demo "rank products by revenue" # the example shop agent, offline
34
+ fastkit eval # accuracy, latency, cost and fallback report
35
+ fastkit init my-agent # your own Fast mode project
36
+ cd my-agent && python agent.py "how many widgets in stock?"
37
+ ```
38
+
39
+ The offline stand-in model picks tools by word overlap and has no understanding, so the example scores about 40% on `fastkit eval`. That is expected: it shows what keyword routing gets you. A real model fills the gap.
40
+
41
+ ## With a real model
42
+
43
+ ```bash
44
+ export FASTKIT_API_KEY=... # leave unset for local servers (Ollama, vLLM, mlx, llama.cpp)
45
+
46
+ # 1. One tiny request: is this model fast enough, and does it call tools?
47
+ fastkit check --base-url https://openrouter.ai/api/v1 --model google/gemini-2.5-flash
48
+
49
+ # 2. Measure your agent on your questions, with a spend cap
50
+ fastkit eval --agent agent.py --gold gold.jsonl \
51
+ --base-url https://openrouter.ai/api/v1 --model google/gemini-2.5-flash \
52
+ --price-in 0.30 --price-out 2.50 --spend-cap 0.50
53
+ ```
54
+
55
+ `fastkit check` prints PASS, SLOW or FAIL against the 4.5 s first-words budget and tells you whether the model called the tool. A model that answers without calling it can't run the one-call loop.
56
+
57
+ `fastkit.checks.safe_filter` runs after the answer has finished. Use it in evals and logs, or buffer the output if you want to filter before the user sees it.
58
+
59
+ ## Develop
60
+
61
+ ```bash
62
+ git clone <this repo> && cd fast-mode-kit
63
+ pip install -e ".[dev]" && pytest -q # every test runs against a mock model
64
+ ```
65
+
66
+ ## What's inside
67
+
68
+ | Path | What it is |
69
+ |---|---|
70
+ | `docs/` | the guide, chapters 1-9: what Fast is, architecture, deadlines, prompts, checks, model choice, self-hosting, failure catalogue, measuring |
71
+ | `fastkit/templates/` | prompt templates: Fast system, reader, writer, server-owned lines |
72
+ | `fastkit/client.py` | streaming client for any OpenAI-compatible server |
73
+ | `fastkit/loop.py` | the one-call loop: read, call tools and write in one conversation, with a fallback |
74
+ | `fastkit/pipeline.py` | the fallback: a reader plans, the server fetches, a writer answers |
75
+ | `fastkit/hedge.py` | hedged routes; a stream that has started is never cut |
76
+ | `fastkit/tools.py` | tool registry, `find_tools`, `note_turn`, propose/confirm writes |
77
+ | `fastkit/checks.py` | do-no-harm figure checks |
78
+ | `fastkit/modes.py` | Fast-only gate |
79
+ | `fastkit/mock.py` | scripted model for offline tests |
80
+ | `fastkit/examples/shop/` | a complete agent over a toy shop database |
81
+ | `fastkit/evals/` | gold-set runner, report and spend cap |
82
+ | `fastkit/cli.py` | the `fastkit` command: demo, init, check, eval |
83
+ | `fastkit/scaffold/` | the starter project `fastkit init` writes |
84
+
85
+ ## Adapt it to your domain in 5 steps
86
+
87
+ 1. **Tools:** write read tools that return rows with every derived figure already computed (totals, ranks, percentages), plus propose tools for writes. See `fastkit/examples/shop/tools.py`.
88
+ 2. **Prompt:** render `fastkit/templates/fast_system.md` with your domain and tool list.
89
+ 3. **Loop:** call `fast_answer(...)` with your model and a pipeline fallback. See `fastkit/examples/shop/agent.py`.
90
+ 4. **Gold set:** write 40-60 real questions with their expected figures, across the languages your users write in.
91
+ 5. **Measure:** run `fastkit eval` on two or three models and pick by tail latency and per-language accuracy, not by the median.
92
+
93
+ ## The rules in one screen
94
+
95
+ - No second chance in Fast, so the first answer must be right.
96
+ - One line per asked part, figure first, data only.
97
+ - The server computes every figure. The model copies.
98
+ - Fall back to another model path, never to keyword guesses.
99
+ - A deadline never cuts an answer that has started streaming.
100
+ - A check that edits a correct line is a bug in the check.
101
+ - Keep Fast changes behind one gate, and prove the other modes unchanged.
102
+ - Never judge a reading model with thinking switched off.
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,87 @@
1
+ # fast-mode-kit
2
+
3
+ A starter kit for adding a **Fast mode** to any chat agent. Fast mode answers exactly what was asked, from your data, right the first time, in a few seconds.
4
+
5
+ It comes from shipping Fast mode in a live product. Every rule here traces back to a measured result or a real failure, and the [failure catalogue](docs/08-failure-catalogue.md) lists them.
6
+
7
+ It works with **frontier APIs** (any OpenAI-compatible endpoint) and with **models you host yourself** (mlx, vLLM, Ollama, llama.cpp).
8
+
9
+ ## Quick start (offline, no key, no cost)
10
+
11
+ ```bash
12
+ pip install fast-mode-kit
13
+
14
+ fastkit demo "rank products by revenue" # the example shop agent, offline
15
+ fastkit eval # accuracy, latency, cost and fallback report
16
+ fastkit init my-agent # your own Fast mode project
17
+ cd my-agent && python agent.py "how many widgets in stock?"
18
+ ```
19
+
20
+ The offline stand-in model picks tools by word overlap and has no understanding, so the example scores about 40% on `fastkit eval`. That is expected: it shows what keyword routing gets you. A real model fills the gap.
21
+
22
+ ## With a real model
23
+
24
+ ```bash
25
+ export FASTKIT_API_KEY=... # leave unset for local servers (Ollama, vLLM, mlx, llama.cpp)
26
+
27
+ # 1. One tiny request: is this model fast enough, and does it call tools?
28
+ fastkit check --base-url https://openrouter.ai/api/v1 --model google/gemini-2.5-flash
29
+
30
+ # 2. Measure your agent on your questions, with a spend cap
31
+ fastkit eval --agent agent.py --gold gold.jsonl \
32
+ --base-url https://openrouter.ai/api/v1 --model google/gemini-2.5-flash \
33
+ --price-in 0.30 --price-out 2.50 --spend-cap 0.50
34
+ ```
35
+
36
+ `fastkit check` prints PASS, SLOW or FAIL against the 4.5 s first-words budget and tells you whether the model called the tool. A model that answers without calling it can't run the one-call loop.
37
+
38
+ `fastkit.checks.safe_filter` runs after the answer has finished. Use it in evals and logs, or buffer the output if you want to filter before the user sees it.
39
+
40
+ ## Develop
41
+
42
+ ```bash
43
+ git clone <this repo> && cd fast-mode-kit
44
+ pip install -e ".[dev]" && pytest -q # every test runs against a mock model
45
+ ```
46
+
47
+ ## What's inside
48
+
49
+ | Path | What it is |
50
+ |---|---|
51
+ | `docs/` | the guide, chapters 1-9: what Fast is, architecture, deadlines, prompts, checks, model choice, self-hosting, failure catalogue, measuring |
52
+ | `fastkit/templates/` | prompt templates: Fast system, reader, writer, server-owned lines |
53
+ | `fastkit/client.py` | streaming client for any OpenAI-compatible server |
54
+ | `fastkit/loop.py` | the one-call loop: read, call tools and write in one conversation, with a fallback |
55
+ | `fastkit/pipeline.py` | the fallback: a reader plans, the server fetches, a writer answers |
56
+ | `fastkit/hedge.py` | hedged routes; a stream that has started is never cut |
57
+ | `fastkit/tools.py` | tool registry, `find_tools`, `note_turn`, propose/confirm writes |
58
+ | `fastkit/checks.py` | do-no-harm figure checks |
59
+ | `fastkit/modes.py` | Fast-only gate |
60
+ | `fastkit/mock.py` | scripted model for offline tests |
61
+ | `fastkit/examples/shop/` | a complete agent over a toy shop database |
62
+ | `fastkit/evals/` | gold-set runner, report and spend cap |
63
+ | `fastkit/cli.py` | the `fastkit` command: demo, init, check, eval |
64
+ | `fastkit/scaffold/` | the starter project `fastkit init` writes |
65
+
66
+ ## Adapt it to your domain in 5 steps
67
+
68
+ 1. **Tools:** write read tools that return rows with every derived figure already computed (totals, ranks, percentages), plus propose tools for writes. See `fastkit/examples/shop/tools.py`.
69
+ 2. **Prompt:** render `fastkit/templates/fast_system.md` with your domain and tool list.
70
+ 3. **Loop:** call `fast_answer(...)` with your model and a pipeline fallback. See `fastkit/examples/shop/agent.py`.
71
+ 4. **Gold set:** write 40-60 real questions with their expected figures, across the languages your users write in.
72
+ 5. **Measure:** run `fastkit eval` on two or three models and pick by tail latency and per-language accuracy, not by the median.
73
+
74
+ ## The rules in one screen
75
+
76
+ - No second chance in Fast, so the first answer must be right.
77
+ - One line per asked part, figure first, data only.
78
+ - The server computes every figure. The model copies.
79
+ - Fall back to another model path, never to keyword guesses.
80
+ - A deadline never cuts an answer that has started streaming.
81
+ - A check that edits a correct line is a bug in the check.
82
+ - Keep Fast changes behind one gate, and prove the other modes unchanged.
83
+ - Never judge a reading model with thinking switched off.
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: fast-mode-kit
3
+ Version: 0.2.0
4
+ Summary: Starter kit for adding a fast, data-only answer mode to any chat agent
5
+ License: MIT
6
+ Keywords: llm,agents,latency,tool-calling,evals
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: pydantic>=2.6
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # fast-mode-kit
21
+
22
+ A starter kit for adding a **Fast mode** to any chat agent. Fast mode answers exactly what was asked, from your data, right the first time, in a few seconds.
23
+
24
+ It comes from shipping Fast mode in a live product. Every rule here traces back to a measured result or a real failure, and the [failure catalogue](docs/08-failure-catalogue.md) lists them.
25
+
26
+ It works with **frontier APIs** (any OpenAI-compatible endpoint) and with **models you host yourself** (mlx, vLLM, Ollama, llama.cpp).
27
+
28
+ ## Quick start (offline, no key, no cost)
29
+
30
+ ```bash
31
+ pip install fast-mode-kit
32
+
33
+ fastkit demo "rank products by revenue" # the example shop agent, offline
34
+ fastkit eval # accuracy, latency, cost and fallback report
35
+ fastkit init my-agent # your own Fast mode project
36
+ cd my-agent && python agent.py "how many widgets in stock?"
37
+ ```
38
+
39
+ The offline stand-in model picks tools by word overlap and has no understanding, so the example scores about 40% on `fastkit eval`. That is expected: it shows what keyword routing gets you. A real model fills the gap.
40
+
41
+ ## With a real model
42
+
43
+ ```bash
44
+ export FASTKIT_API_KEY=... # leave unset for local servers (Ollama, vLLM, mlx, llama.cpp)
45
+
46
+ # 1. One tiny request: is this model fast enough, and does it call tools?
47
+ fastkit check --base-url https://openrouter.ai/api/v1 --model google/gemini-2.5-flash
48
+
49
+ # 2. Measure your agent on your questions, with a spend cap
50
+ fastkit eval --agent agent.py --gold gold.jsonl \
51
+ --base-url https://openrouter.ai/api/v1 --model google/gemini-2.5-flash \
52
+ --price-in 0.30 --price-out 2.50 --spend-cap 0.50
53
+ ```
54
+
55
+ `fastkit check` prints PASS, SLOW or FAIL against the 4.5 s first-words budget and tells you whether the model called the tool. A model that answers without calling it can't run the one-call loop.
56
+
57
+ `fastkit.checks.safe_filter` runs after the answer has finished. Use it in evals and logs, or buffer the output if you want to filter before the user sees it.
58
+
59
+ ## Develop
60
+
61
+ ```bash
62
+ git clone <this repo> && cd fast-mode-kit
63
+ pip install -e ".[dev]" && pytest -q # every test runs against a mock model
64
+ ```
65
+
66
+ ## What's inside
67
+
68
+ | Path | What it is |
69
+ |---|---|
70
+ | `docs/` | the guide, chapters 1-9: what Fast is, architecture, deadlines, prompts, checks, model choice, self-hosting, failure catalogue, measuring |
71
+ | `fastkit/templates/` | prompt templates: Fast system, reader, writer, server-owned lines |
72
+ | `fastkit/client.py` | streaming client for any OpenAI-compatible server |
73
+ | `fastkit/loop.py` | the one-call loop: read, call tools and write in one conversation, with a fallback |
74
+ | `fastkit/pipeline.py` | the fallback: a reader plans, the server fetches, a writer answers |
75
+ | `fastkit/hedge.py` | hedged routes; a stream that has started is never cut |
76
+ | `fastkit/tools.py` | tool registry, `find_tools`, `note_turn`, propose/confirm writes |
77
+ | `fastkit/checks.py` | do-no-harm figure checks |
78
+ | `fastkit/modes.py` | Fast-only gate |
79
+ | `fastkit/mock.py` | scripted model for offline tests |
80
+ | `fastkit/examples/shop/` | a complete agent over a toy shop database |
81
+ | `fastkit/evals/` | gold-set runner, report and spend cap |
82
+ | `fastkit/cli.py` | the `fastkit` command: demo, init, check, eval |
83
+ | `fastkit/scaffold/` | the starter project `fastkit init` writes |
84
+
85
+ ## Adapt it to your domain in 5 steps
86
+
87
+ 1. **Tools:** write read tools that return rows with every derived figure already computed (totals, ranks, percentages), plus propose tools for writes. See `fastkit/examples/shop/tools.py`.
88
+ 2. **Prompt:** render `fastkit/templates/fast_system.md` with your domain and tool list.
89
+ 3. **Loop:** call `fast_answer(...)` with your model and a pipeline fallback. See `fastkit/examples/shop/agent.py`.
90
+ 4. **Gold set:** write 40-60 real questions with their expected figures, across the languages your users write in.
91
+ 5. **Measure:** run `fastkit eval` on two or three models and pick by tail latency and per-language accuracy, not by the median.
92
+
93
+ ## The rules in one screen
94
+
95
+ - No second chance in Fast, so the first answer must be right.
96
+ - One line per asked part, figure first, data only.
97
+ - The server computes every figure. The model copies.
98
+ - Fall back to another model path, never to keyword guesses.
99
+ - A deadline never cuts an answer that has started streaming.
100
+ - A check that edits a correct line is a bug in the check.
101
+ - Keep Fast changes behind one gate, and prove the other modes unchanged.
102
+ - Never judge a reading model with thinking switched off.
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,53 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ fast_mode_kit.egg-info/PKG-INFO
5
+ fast_mode_kit.egg-info/SOURCES.txt
6
+ fast_mode_kit.egg-info/dependency_links.txt
7
+ fast_mode_kit.egg-info/entry_points.txt
8
+ fast_mode_kit.egg-info/requires.txt
9
+ fast_mode_kit.egg-info/top_level.txt
10
+ fastkit/__init__.py
11
+ fastkit/__main__.py
12
+ fastkit/checks.py
13
+ fastkit/cli.py
14
+ fastkit/client.py
15
+ fastkit/hedge.py
16
+ fastkit/loop.py
17
+ fastkit/mock.py
18
+ fastkit/modes.py
19
+ fastkit/pipeline.py
20
+ fastkit/prompts.py
21
+ fastkit/py.typed
22
+ fastkit/tools.py
23
+ fastkit/types.py
24
+ fastkit/evals/__init__.py
25
+ fastkit/evals/report.py
26
+ fastkit/evals/run.py
27
+ fastkit/evals/schema.py
28
+ fastkit/evals/shop_gold.jsonl
29
+ fastkit/examples/__init__.py
30
+ fastkit/examples/shop/__init__.py
31
+ fastkit/examples/shop/agent.py
32
+ fastkit/examples/shop/data.py
33
+ fastkit/examples/shop/demo_model.py
34
+ fastkit/examples/shop/tools.py
35
+ fastkit/scaffold/README.md
36
+ fastkit/scaffold/agent.py
37
+ fastkit/scaffold/gold.jsonl
38
+ fastkit/scaffold/tools.py
39
+ fastkit/templates/fast_system.md
40
+ fastkit/templates/notes.md
41
+ fastkit/templates/reader.md
42
+ fastkit/templates/writer.md
43
+ tests/test_checks.py
44
+ tests/test_cli.py
45
+ tests/test_client.py
46
+ tests/test_evals.py
47
+ tests/test_example_agent.py
48
+ tests/test_hedge.py
49
+ tests/test_loop.py
50
+ tests/test_mock.py
51
+ tests/test_modes.py
52
+ tests/test_prompts.py
53
+ tests/test_tools.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fastkit = fastkit.cli:main
@@ -0,0 +1,6 @@
1
+ httpx>=0.27
2
+ pydantic>=2.6
3
+
4
+ [dev]
5
+ pytest>=8
6
+ pytest-asyncio>=0.23
File without changes
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,131 @@
1
+ """Do-no-harm figure checks.
2
+
3
+ Rules learned the hard way:
4
+ - an identifier that appears in the rows is sourced, even if it has digits;
5
+ - a zero or negative that answers the question is said, never trimmed;
6
+ - a sentence is dropped only when it has an unsourced figure AND no sourced
7
+ one; a check never leaves an empty answer or a fragment.
8
+ Every edit a check makes to a correct line is a bug in the check.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import re
14
+ from typing import Any, Iterable
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+ _NUM = re.compile(r"(?<![\w.])-?\d[\d,]*(?:\.\d+)?%?")
19
+ _RANGE_DASH = re.compile(r"(?<=\d)-(?=\d)")
20
+ _LIST_MARK = re.compile(r"^\s*\d+[.)]\s+")
21
+ _TOKEN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_\-./]*[A-Za-z0-9]|[A-Za-z0-9]")
22
+ _SENT = re.compile(r"(?<=[.!?])\s+")
23
+
24
+
25
+ def _walk(obj: Any) -> Iterable[Any]:
26
+ if isinstance(obj, dict):
27
+ for v in obj.values():
28
+ yield from _walk(v)
29
+ elif isinstance(obj, (list, tuple)):
30
+ for v in obj:
31
+ yield from _walk(v)
32
+ else:
33
+ yield obj
34
+
35
+
36
+ def _row_facts(rows: list[dict], question: str) -> tuple[list[float], set[str]]:
37
+ nums: list[float] = []
38
+ strings: set[str] = set()
39
+ for v in _walk(rows):
40
+ if isinstance(v, bool):
41
+ continue
42
+ if isinstance(v, (int, float)):
43
+ nums.append(float(v))
44
+ elif isinstance(v, str):
45
+ strings.add(v.lower())
46
+ try:
47
+ nums.append(float(v.replace(",", "")))
48
+ except ValueError:
49
+ pass
50
+ for m in _NUM.finditer(question):
51
+ nums.append(float(m.group(0).rstrip("%").replace(",", "")))
52
+ strings.add(question.lower())
53
+ return nums, strings
54
+
55
+
56
+ def _matches(text: str, nums: list[float]) -> bool:
57
+ pct = text.endswith("%")
58
+ raw = text.rstrip("%").replace(",", "")
59
+ try:
60
+ val = float(raw)
61
+ except ValueError:
62
+ return False
63
+ decimals = len(raw.split(".")[1]) if "." in raw else 0
64
+ for n in nums:
65
+ for cand in ((n, n * 100) if pct else (n,)):
66
+ for c in (cand, abs(cand)) if val >= 0 else (cand,):
67
+ # "fell 3.2%" states -3.2 without its sign
68
+ if round(c, decimals) == val or c == val:
69
+ return True
70
+ if decimals == 0 and abs(c) >= 1 and round(c) == val:
71
+ return True
72
+ return False
73
+
74
+
75
+ def _figures(answer: str) -> list[tuple[str, bool]]:
76
+ """(figure, is_identifier) pairs found in the answer."""
77
+ out = []
78
+ spans = []
79
+ for tok in _TOKEN.finditer(answer):
80
+ t = tok.group(0)
81
+ if not any(c.isdigit() for c in t):
82
+ continue
83
+ # codes (SKU-1042), dates (2026-11-01), versions: identifiers, not quantities
84
+ if re.fullmatch(r"\d+(?:\.\d+)?-\d+(?:\.\d+)?", t):
85
+ continue # a range like 10-20: two plain numbers, handled below
86
+ if any(c.isalpha() for c in t) or "-" in t.lstrip("-") or t.count(".") > 1 or "/" in t:
87
+ out.append((t, True))
88
+ spans.append(tok.span())
89
+ for m in _NUM.finditer(_RANGE_DASH.sub(" ", answer)):
90
+ if any(a <= m.start() < b for a, b in spans):
91
+ continue
92
+ out.append((m.group(0).rstrip(".,"), False))
93
+ return out
94
+
95
+
96
+ def ungrounded_figures(answer: str, rows: list[dict], question: str = "") -> list[str]:
97
+ nums, strings = _row_facts(rows, question)
98
+ bad = []
99
+ for fig, ident in _figures(answer):
100
+ if ident:
101
+ if not any(fig.lower() in s for s in strings):
102
+ bad.append(fig)
103
+ elif not _matches(fig, nums):
104
+ bad.append(fig)
105
+ return bad
106
+
107
+
108
+ def safe_filter(answer: str, rows: list[dict], question: str = "") -> str:
109
+ """Drop a sentence only when every figure in it is unsourced. Lines and
110
+ list markers are kept as written; never returns an empty answer."""
111
+ out_lines = []
112
+ dropped = False
113
+ for line in answer.split("\n"):
114
+ mark = _LIST_MARK.match(line)
115
+ prefix, body = (line[:mark.end()], line[mark.end():]) if mark else ("", line)
116
+ kept = []
117
+ for sent in _SENT.split(body):
118
+ figs = [f for f, _ in _figures(sent)]
119
+ bad = ungrounded_figures(sent, rows, question)
120
+ if bad and len(bad) == len(figs):
121
+ log.info("dropped unsourced sentence: %r", sent)
122
+ dropped = True
123
+ continue
124
+ kept.append(sent)
125
+ if kept or not body.strip():
126
+ out_lines.append(prefix + " ".join(kept))
127
+ result = "\n".join(out_lines).strip()
128
+ if not result:
129
+ log.warning("check would empty the answer; keeping original")
130
+ return answer
131
+ return result if dropped else answer