small-model-harness 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sinply-ehis
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,426 @@
1
+ Metadata-Version: 2.4
2
+ Name: small-model-harness
3
+ Version: 0.1.0
4
+ Summary: Session-level intelligence for small LLMs — failure tracking, adaptive steering, loop detection, response compaction.
5
+ Author: sinply-ehis
6
+ License-Expression: MIT
7
+ Keywords: llm,harness,small-model,tool-calling,agent
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pydantic>=2.0
19
+ Provides-Extra: pydantic-deep
20
+ Requires-Dist: pydantic-deep>=0.3; extra == "pydantic-deep"
21
+ Requires-Dist: pydantic-ai[duckduckgo]>=0.0.50; extra == "pydantic-deep"
22
+ Provides-Extra: pdf
23
+ Requires-Dist: pymupdf>=1.23; extra == "pdf"
24
+ Provides-Extra: epub
25
+ Requires-Dist: ebooklib>=0.18; extra == "epub"
26
+ Requires-Dist: beautifulsoup4>=4.12; extra == "epub"
27
+ Provides-Extra: full
28
+ Requires-Dist: pymupdf>=1.23; extra == "full"
29
+ Requires-Dist: ebooklib>=0.18; extra == "full"
30
+ Requires-Dist: beautifulsoup4>=4.12; extra == "full"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ <p align="center">
37
+ <img src="logo.svg" width="120" alt="small-model-harness logo">
38
+ </p>
39
+
40
+ <h1 align="center">small-model-harness</h1>
41
+
42
+ <p align="center">
43
+ <strong>Session-level intelligence for small LLMs (2B–4B parameters)</strong>
44
+ </p>
45
+
46
+ <p align="center">
47
+ <a href="#install">Install</a> ·
48
+ <a href="#quick-start">Quick Start</a> ·
49
+ <a href="#features">Features</a> ·
50
+ <a href="#api">API</a> ·
51
+ <a href="#architecture">Architecture</a> ·
52
+ <a href="#research">Research</a>
53
+ </p>
54
+
55
+ ---
56
+
57
+ <p align="center">
58
+ <img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python 3.10+">
59
+ <img src="https://img.shields.io/badge/pydantic-v2-ff6b6b.svg" alt="Pydantic v2">
60
+ <img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License MIT">
61
+ <img src="https://img.shields.io/badge/142-tests-passing-brightgreen.svg" alt="142 Tests">
62
+ <img src="https://img.shields.io/badge/size-<10KB-lightgrey.svg" alt="Size">
63
+ </p>
64
+
65
+ <p align="center">
66
+ Small models fail on tool calling in predictable ways. This harness<em> compensates</em> for those failures
67
+ deterministically — no extra LLM calls, no retries, no cloud dependency.
68
+ </p>
69
+
70
+ ---
71
+
72
+ ## Why this exists
73
+
74
+ A 2B–4B parameter model will:
75
+
76
+ 1. **Skip tool calls entirely** — acknowledge uncertainty, then confabulate an answer instead of calling the tool
77
+ 2. **Produce malformed JSON** — trailing commas, wrong types, camelCase instead of snake_case
78
+ 3. **Select the wrong tool** — when given 20+ options, small models pick randomly
79
+ 4. **Get stuck in loops** — call the same tool repeatedly hoping for a different result
80
+ 5. **Run out of context** — verbose tool responses fill the window before the task completes
81
+
82
+ This harness fixes all five — deterministically, at the session level, without modifying the model.
83
+
84
+ ## Install
85
+
86
+ ```bash
87
+ pip install small-model-harness
88
+ ```
89
+
90
+ With optional extras:
91
+
92
+ ```bash
93
+ pip install "small-model-harness[full]" # PDF + EPUB support
94
+ pip install "small-model-harness[pydantic-deep]" # Full agent framework
95
+ pip install "small-model-harness[dev]" # Testing tools
96
+ ```
97
+
98
+ ## Quick start
99
+
100
+ ```python
101
+ from small_model_harness import (
102
+ create_harness_session,
103
+ ExecutionRecord,
104
+ repair_tool_call,
105
+ score_tool_call_confidence,
106
+ rank_tools,
107
+ compact_tool_response,
108
+ )
109
+
110
+ # 1. Create a session
111
+ session = create_harness_session(n_ctx=4096)
112
+
113
+ # 2. Before showing tools, rank by relevance (show top 5, not all 211)
114
+ ranked = rank_tools("play some music", tool_schemas, top_n=5)
115
+ prompt = build_compact_tool_prompt("play some music", tool_schemas, top_n=5)
116
+
117
+ # 3. Model outputs malformed JSON — repair it deterministically
118
+ args, name, fixes = repair_tool_call(
119
+ '{"tool": "play_audio", "arguments": {"filePath": "/a/b.wav", "volume": "0.8"}}',
120
+ tool_schemas,
121
+ )
122
+ # name = "play_audio"
123
+ # args = {"file_path": "/a/b.wav", "volume": 0.8} # renamed + coerced
124
+ # fixes = ["Renamed 'filePath' → 'file_path'", "Converted 'volume' from string to number"]
125
+
126
+ # 4. Score confidence — should we escalate to cloud?
127
+ score = score_tool_call_confidence(name, args, tool_schemas[name], harness=session)
128
+ if score.should_escalate:
129
+ escalate_to_cloud()
130
+
131
+ # 5. Record the execution
132
+ session.record_execution(ExecutionRecord(
133
+ timestamp="2026-08-31T12:00:00Z",
134
+ tool_name=name,
135
+ arguments=args,
136
+ status="completed",
137
+ result="Playing audio...",
138
+ duration_ms=340.0,
139
+ ))
140
+
141
+ # 6. Get steering hints for the next system prompt
142
+ steering = session.get_steering_prompt()
143
+ # → "Avoid: play_audio (failed 3 times this session)"
144
+
145
+ # 7. Compact verbose responses to save context
146
+ compacted, was_compacted = compact_tool_response("list_voices", huge_json, max_tokens=200)
147
+ ```
148
+
149
+ ## Features
150
+
151
+ ### Deterministic tool repair
152
+
153
+ Fixes malformed output without re-prompting. The model's *intent* is usually correct — only the serialization drifted.
154
+
155
+ | What breaks | How it's fixed |
156
+ |---|---|
157
+ | Trailing commas: `{"a": 1,}` | JSON repair strips them |
158
+ | Wrong types: `"5"` instead of `5` | Type coercion from schema |
159
+ | Key naming: `maxResults` vs `max_results` | Key renaming (camelCase/snake_case) |
160
+ | Missing defaults: model forgets optional args | Default injection from schema |
161
+ | Markdown fences: ` ```json ... ``` ` | Stripped before parsing |
162
+
163
+ ```python
164
+ from small_model_harness import repair_tool_call
165
+
166
+ args, name, fixes = repair_tool_call(malformed_output, tool_schemas)
167
+ # Always returns (args, tool_name, fixes_applied) or (None, None, error)
168
+ ```
169
+
170
+ ### Confidence scoring
171
+
172
+ Multi-signal scoring to know when to trust the model vs escalate to cloud.
173
+
174
+ | Signal | Weight | What it measures |
175
+ |---|---|---|
176
+ | Schema match | 35% | Are args valid against the tool schema? |
177
+ | History | 25% | Has this tool succeeded before in this session? |
178
+ | Completeness | 25% | Are required arguments present? |
179
+ | Repair cost | 15% | How much did we have to fix? |
180
+
181
+ ```python
182
+ from small_model_harness import score_tool_call_confidence
183
+
184
+ score = score_tool_call_confidence(
185
+ "search_web", args, schema, harness=session, repairs_applied=fixes
186
+ )
187
+ print(score.overall) # 0.85
188
+ print(score.should_escalate) # False
189
+ print(format_confidence_summary(score))
190
+ # Confidence: 85% (HIGH)
191
+ # Schema: 100%
192
+ # History: 90%
193
+ # Completeness: 100%
194
+ # Repair cost: 90%
195
+ ```
196
+
197
+ ### Progressive tool disclosure
198
+
199
+ Don't show the model all 211 tools — show the 5 most relevant ones. Reduces selection errors dramatically.
200
+
201
+ ```python
202
+ from small_model_harness import rank_tools, build_compact_tool_prompt
203
+
204
+ # Rank tools by query relevance
205
+ ranked = rank_tools("play some music", tool_schemas, top_n=5)
206
+ # Returns: [ToolScore(name="play_audio", score=0.85, ...), ...]
207
+
208
+ # Build a compact prompt with only relevant tools
209
+ prompt = build_compact_tool_prompt("play some music", tool_schemas, top_n=5)
210
+ # "You have access to the following tools:
211
+ # - play_audio: Play an audio file | params: file_path: string*, volume: number
212
+ # - speak_text: Convert text to speech | params: text: string*, voice: string
213
+ # ..."
214
+ ```
215
+
216
+ ### Adaptive steering
217
+
218
+ After repeated failures, inject "avoid this" hints into the system prompt.
219
+
220
+ ```python
221
+ session.record_execution(ExecutionRecord(
222
+ timestamp="...", tool_name="play_audio", status="failed", ...
223
+ ))
224
+ session.record_execution(ExecutionRecord(
225
+ timestamp="...", tool_name="play_audio", status="failed", ...
226
+ ))
227
+
228
+ steering = session.get_steering_prompt()
229
+ # → "Avoid: play_audio (failed 2 times this session)"
230
+ ```
231
+
232
+ ### Loop detection
233
+
234
+ Catch when the model calls the same tool repeatedly.
235
+
236
+ ```python
237
+ from small_model_harness import detect_failure_loop
238
+
239
+ calls = ["search", "search", "search"]
240
+ tool = detect_failure_loop(calls)
241
+ # → "search" (loop detected)
242
+ ```
243
+
244
+ ### Context pressure monitoring
245
+
246
+ Track token usage and trigger compaction before overflow.
247
+
248
+ ```python
249
+ session.update_context_pressure(estimated_tokens=3500, n_ctx=4096)
250
+ print(session.context_pressure) # 0.85
251
+ print(session.is_context_warned) # True (≥75%)
252
+ print(session.is_context_critical) # False (<90%)
253
+ ```
254
+
255
+ ### Response compaction
256
+
257
+ Shrink verbose tool responses to save context space.
258
+
259
+ ```python
260
+ from small_model_harness import compact_tool_response
261
+
262
+ compacted, was_compacted = compact_tool_response(
263
+ "play_audio",
264
+ '{"audio_file": "/long/path/file.wav", "duration": 300, "sample_rate": 44100, ...}',
265
+ max_tokens=50,
266
+ )
267
+ # → '{"audio_file": "/long/path/file.wav", "duration": 300}' (compacted)
268
+ ```
269
+
270
+ ## API
271
+
272
+ ### Core functions
273
+
274
+ | Function | Purpose |
275
+ |---|---|
276
+ | `create_harness_session(session_id, budget, n_ctx)` | Create a new session |
277
+ | `repair_tool_call(raw_output, tool_schemas)` | Fix malformed model output |
278
+ | `score_tool_call_confidence(name, args, schema, harness, repairs)` | Score tool call confidence |
279
+ | `rank_tools(query, tool_schemas, harness, top_n)` | Rank tools by relevance |
280
+ | `build_compact_tool_prompt(query, tool_schemas, harness, top_n)` | Build compact tool prompt |
281
+ | `compact_tool_response(tool_name, response, max_tokens)` | Shrink verbose responses |
282
+ | `detect_failure_loop(recent_calls, window)` | Detect same-tool loops |
283
+ | `estimate_tokens(text)` | Rough token estimate |
284
+ | `tool_schema(name, description, properties)` | Generate OpenAI tool schema |
285
+ | `pydantic_tool_schema(model, name, description)` | Generate schema from pydantic model |
286
+
287
+ ### HarnessState
288
+
289
+ | Method | Purpose |
290
+ |---|---|
291
+ | `record_execution(record)` | Record a tool call, update stats |
292
+ | `update_context_pressure(tokens, n_ctx)` | Update pressure estimate |
293
+ | `get_steering_prompt()` | Get hints for system prompt |
294
+ | `get_avoided_tools_prompt()` | List tools to avoid |
295
+ | `format_audit_trail()` | Human-readable execution log |
296
+ | `generate_session_summary()` | One-line session summary |
297
+
298
+ | Property | Type | Meaning |
299
+ |---|---|---|
300
+ | `success_rate` | float | 0.0–1.0 |
301
+ | `is_context_warned` | bool | pressure ≥ 0.75 |
302
+ | `is_context_critical` | bool | pressure ≥ 0.90 |
303
+ | `is_budget_exhausted` | bool | no calls remaining |
304
+ | `steering_hints` | list[str] | Current steering hints |
305
+ | `avoided_tools` | set[str] | Tools marked as avoided |
306
+
307
+ ### Models (pydantic v2)
308
+
309
+ | Model | Fields |
310
+ |---|---|
311
+ | `HarnessState` | session_id, started_at, budget, execution_history, tool stats, steering, pressure |
312
+ | `ExecutionRecord` | timestamp, tool_name, arguments, status, result, error, duration_ms, confidence |
313
+ | `FileInput` | path, content, mime_type, size_bytes |
314
+ | `ConfidenceScore` | overall, schema_match, history_score, completeness, repair_cost, should_escalate |
315
+ | `ToolScore` | name, score, reason, schema |
316
+
317
+ ## Architecture
318
+
319
+ ```
320
+ ┌──────────────────────────────────────────────────────────────┐
321
+ │ Your application │
322
+ │ (streaming loop, agent, CLI, whatever) │
323
+ ├──────────────────────────────────────────────────────────────┤
324
+ │ small-model-harness │
325
+ │ │
326
+ │ ┌─────────────┐ ┌──────────────┐ ┌────────────────────┐ │
327
+ │ │ Tool Repair │ │ Confidence │ │ Tool Disclosure │ │
328
+ │ │ JSON fix │ │ Schema │ │ Rank by relevance │ │
329
+ │ │ Type coerce │ │ History │ │ Top-N selection │ │
330
+ │ │ Key rename │ │ Completeness│ │ Compact prompts │ │
331
+ │ │ Defaults │ │ Repair cost │ │ │ │
332
+ │ └─────────────┘ └──────────────┘ └────────────────────┘ │
333
+ │ │
334
+ │ ┌──────────────────────────────────────────────────────┐ │
335
+ │ │ Session Intelligence │ │
336
+ │ │ Failure tracking · Adaptive steering · Loop detection │ │
337
+ │ │ Context pressure · Response compaction · Audit trail │ │
338
+ │ └──────────────────────────────────────────────────────┘ │
339
+ ├──────────────────────────────────────────────────────────────┤
340
+ │ llama.cpp / vLLM / OpenAI-compatible endpoint │
341
+ └──────────────────────────────────────────────────────────────┘
342
+ ```
343
+
344
+ ### How the pieces fit together
345
+
346
+ ```python
347
+ # The full pipeline — from raw model output to repaired, scored, tracked call
348
+
349
+ # 1. Model outputs something broken
350
+ raw = model.generate(prompt)
351
+
352
+ # 2. Repair it
353
+ args, name, fixes = repair_tool_call(raw, tool_schemas)
354
+
355
+ # 3. Score confidence
356
+ score = score_tool_call_confidence(name, args, tool_schemas[name], session, fixes)
357
+
358
+ # 4. If confidence is low, escalate
359
+ if score.should_escalate:
360
+ raw = cloud_model.generate(prompt)
361
+ args, name, fixes = repair_tool_call(raw, tool_schemas)
362
+
363
+ # 5. Execute the tool
364
+ result = execute_tool(name, args)
365
+
366
+ # 6. Compact the response
367
+ compacted, _ = compact_tool_response(name, result, max_tokens=200)
368
+
369
+ # 7. Record execution
370
+ session.record_execution(ExecutionRecord(
371
+ timestamp=now(), tool_name=name, arguments=args,
372
+ status="completed", result=compacted, confidence=score.overall,
373
+ ))
374
+
375
+ # 8. Feed back to model
376
+ messages.append({"role": "tool", "content": compacted})
377
+ ```
378
+
379
+ ## Research
380
+
381
+ This harness is informed by:
382
+
383
+ - **Cho et al. (2026)** — "It's Not the Size: Harness Design Determines Operational Stability in Small Language Models" (arXiv:2605.12129). Found that a 4-stage scaffold takes 2B models from 58% to 95% task success.
384
+ - **ManiFreeBird (2026)** — Deterministic tool repair eliminates retry loops. Key insight: "structural failures are not reasoning failures."
385
+ - **Gorilla (Patil et al., 2023)** — Tool retrieval reduces selection hallucination. Show 3–5 relevant tools, not all.
386
+ - **NVIDIA (2025)** — SLM-first routing with confidence-based cloud escalation keeps 80–90% of steps local.
387
+
388
+ ## Pydantic-deep integration (optional)
389
+
390
+ For full agent capabilities (context compaction, subagents, skills, memory, planning):
391
+
392
+ ```bash
393
+ pip install "small-model-harness[pydantic-deep]"
394
+ ```
395
+
396
+ ```python
397
+ from small_model_harness.pydantic_deep_integration import (
398
+ build_small_model_agent,
399
+ run_with_harness,
400
+ )
401
+
402
+ result = build_small_model_agent(
403
+ model_url="http://localhost:8080/v1",
404
+ model_name="qwen3.5-4b",
405
+ n_ctx=4096,
406
+ )
407
+
408
+ run_result = await run_with_harness(result, "Plan a 3-step audio mixing task")
409
+ ```
410
+
411
+ ## Running tests
412
+
413
+ ```bash
414
+ pip install "small-model-harness[dev]"
415
+ pytest # Run all 142 tests
416
+ pytest tests/test_harness.py # Core harness tests only
417
+ pytest -v # Verbose output
418
+ ```
419
+
420
+ ## Contributing
421
+
422
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
423
+
424
+ ## License
425
+
426
+ MIT — see [LICENSE](LICENSE).