thinkless 0.2.0__tar.gz → 0.3.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 (108) hide show
  1. {thinkless-0.2.0 → thinkless-0.3.0}/CHANGELOG.md +35 -0
  2. {thinkless-0.2.0 → thinkless-0.3.0}/PKG-INFO +82 -7
  3. {thinkless-0.2.0 → thinkless-0.3.0}/README.md +61 -5
  4. {thinkless-0.2.0 → thinkless-0.3.0}/pyproject.toml +13 -2
  5. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/__init__.py +3 -0
  6. thinkless-0.3.0/src/thinkless/_version.py +1 -0
  7. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/metrics.py +16 -0
  8. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/cli/main.py +12 -0
  9. thinkless-0.3.0/src/thinkless/cli/serve.py +117 -0
  10. thinkless-0.3.0/src/thinkless/cli/shadow.py +175 -0
  11. thinkless-0.3.0/src/thinkless/cli/traces.py +89 -0
  12. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/engine.py +115 -9
  13. thinkless-0.3.0/src/thinkless/integrations/__init__.py +14 -0
  14. thinkless-0.3.0/src/thinkless/integrations/_messages.py +73 -0
  15. thinkless-0.3.0/src/thinkless/integrations/core.py +205 -0
  16. thinkless-0.3.0/src/thinkless/integrations/langgraph.py +162 -0
  17. thinkless-0.3.0/src/thinkless/integrations/openai_agents.py +185 -0
  18. thinkless-0.3.0/src/thinkless/limits.py +88 -0
  19. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/wire.py +92 -2
  20. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/questions.py +30 -1
  21. thinkless-0.3.0/src/thinkless/server/__init__.py +6 -0
  22. thinkless-0.3.0/src/thinkless/server/app.py +194 -0
  23. thinkless-0.3.0/src/thinkless/server/mcp.py +107 -0
  24. thinkless-0.3.0/src/thinkless/shadow/__init__.py +40 -0
  25. thinkless-0.3.0/src/thinkless/shadow/compare.py +77 -0
  26. thinkless-0.3.0/src/thinkless/shadow/report.py +441 -0
  27. thinkless-0.3.0/src/thinkless/shadow/runner.py +515 -0
  28. thinkless-0.3.0/src/thinkless/tracing/export.py +263 -0
  29. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/summary.py +1 -1
  30. thinkless-0.3.0/tests/unit/test_integrations.py +278 -0
  31. thinkless-0.3.0/tests/unit/test_limits.py +117 -0
  32. thinkless-0.3.0/tests/unit/test_server.py +168 -0
  33. thinkless-0.3.0/tests/unit/test_shadow.py +333 -0
  34. thinkless-0.3.0/tests/unit/test_trace_export.py +84 -0
  35. thinkless-0.2.0/src/thinkless/_version.py +0 -1
  36. {thinkless-0.2.0 → thinkless-0.3.0}/.gitignore +0 -0
  37. {thinkless-0.2.0 → thinkless-0.3.0}/LICENSE +0 -0
  38. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/__main__.py +0 -0
  39. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/_hub.py +0 -0
  40. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/_json.py +0 -0
  41. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/__init__.py +0 -0
  42. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/intents.py +0 -0
  43. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/intents_report.py +0 -0
  44. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/report.py +0 -0
  45. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/support.py +0 -0
  46. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/cli/__init__.py +0 -0
  47. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/confidence.py +0 -0
  48. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/data/pricing.toml +0 -0
  49. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/data/viewer.html +0 -0
  50. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/decision.py +0 -0
  51. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/__init__.py +0 -0
  52. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/__init__.py +0 -0
  53. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/agent.py +0 -0
  54. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/data/calibration.jsonl +0 -0
  55. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/data/scenarios.jsonl +0 -0
  56. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/data/world.json +0 -0
  57. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/questions.py +0 -0
  58. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/stack.py +0 -0
  59. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/world.py +0 -0
  60. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/errors.py +0 -0
  61. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/__init__.py +0 -0
  62. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/anthropic.py +0 -0
  63. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/base.py +0 -0
  64. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/factory.py +0 -0
  65. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/local.py +0 -0
  66. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/openai_compat.py +0 -0
  67. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/openrouter.py +0 -0
  68. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/scripted.py +0 -0
  69. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/logs.py +0 -0
  70. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/pricing.py +0 -0
  71. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/__init__.py +0 -0
  72. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/base.py +0 -0
  73. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/gliner.py +0 -0
  74. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/hf.py +0 -0
  75. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/laya.py +0 -0
  76. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/llm.py +0 -0
  77. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/rules.py +0 -0
  78. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/systemone.py +0 -0
  79. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/py.typed +0 -0
  80. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/settings.py +0 -0
  81. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/__init__.py +0 -0
  82. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/console.py +0 -0
  83. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/otel.py +0 -0
  84. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/sinks.py +0 -0
  85. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/span.py +0 -0
  86. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/tracer.py +0 -0
  87. {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/viewer.py +0 -0
  88. {thinkless-0.2.0 → thinkless-0.3.0}/tests/__init__.py +0 -0
  89. {thinkless-0.2.0 → thinkless-0.3.0}/tests/conftest.py +0 -0
  90. {thinkless-0.2.0 → thinkless-0.3.0}/tests/local/__init__.py +0 -0
  91. {thinkless-0.2.0 → thinkless-0.3.0}/tests/local/test_local_models.py +0 -0
  92. {thinkless-0.2.0 → thinkless-0.3.0}/tests/network/__init__.py +0 -0
  93. {thinkless-0.2.0 → thinkless-0.3.0}/tests/network/test_openrouter_live.py +0 -0
  94. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/__init__.py +0 -0
  95. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_bench.py +0 -0
  96. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_cli_and_viewer.py +0 -0
  97. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_confidence.py +0 -0
  98. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_engine.py +0 -0
  99. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_hf_classifier.py +0 -0
  100. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_llm_decider.py +0 -0
  101. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_openrouter.py +0 -0
  102. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_pricing_and_llm.py +0 -0
  103. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_questions.py +0 -0
  104. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_rules.py +0 -0
  105. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_support_demo.py +0 -0
  106. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_systemone.py +0 -0
  107. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_systemone_sdk_compat.py +0 -0
  108. {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_tracing.py +0 -0
@@ -6,6 +6,41 @@ All notable changes are recorded here. The format follows
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.3.0 - 2026-09-25
10
+
11
+ Shadow mode, framework adapters, limits, a decision server and MCP tools.
12
+
13
+ ### Added
14
+
15
+ - Shadow mode (`thinkless.shadow`): run a candidate engine next to existing
16
+ decision code without changing what it returns, log both answers, and
17
+ report agreement with a 95% interval, the share settled without an LLM,
18
+ cost and latency on both sides, thresholds fitted on production traffic,
19
+ and a verdict per question (`thinkless shadow report`). Disagreements
20
+ export as labeling rows (`thinkless shadow export`). The same class audits
21
+ a live engine against an LLM.
22
+ - Framework adapters (`thinkless.integrations`): a LangGraph router, decision
23
+ node and async decision node; OpenAI Agents SDK input guardrails, tool input
24
+ guardrails and `route_agent`; and framework-neutral `Router`, `route` and
25
+ `gate`, tested with LangChain tools and Pydantic AI. Extras `langgraph` and
26
+ `openai-agents`.
27
+ - Engine deadlines (`deadline_ms` on the engine and per call), spend limits
28
+ (`SpendLimit`, lifetime or rolling window) and per-run caps
29
+ (`engine.run(..., max_cost_usd=...)`). Skipped providers are recorded with
30
+ reason `deadline` or `spend_limit`.
31
+ - A decision server (`thinkless serve`, `thinkless.server.app.create_app`)
32
+ with `/v1/decide` and a System One compatible `/v1/systemone`, so another
33
+ engine's `SystemOne` provider, or TypeSafe's SDK, can use it. Extra
34
+ `server`.
35
+ - MCP tools (`thinkless mcp`, `thinkless.server.mcp.create_mcp_server`): one
36
+ `decide_<question>` tool per registered question. Extra `mcp`.
37
+ - `thinkless trace export` turns traced decisions into labeling rows, and
38
+ `thinkless trace drift` compares two periods and exits with 1 when a
39
+ question drifted.
40
+ - `question_from_spec` rebuilds a question from its spec.
41
+ - Docs: an integrations section, a migration guide, shadow mode, serving,
42
+ a pilot playbook and a FAQ, published to GitHub Pages by a new workflow.
43
+
9
44
  ## 0.2.0 - 2026-09-25
10
45
 
11
46
  First release on PyPI.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thinkless
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: The decision plane for AI agents. Rules and small calibrated models make the routine decisions, the LLM is reserved for the steps that need thought, and every step is traced.
5
5
  Project-URL: Homepage, https://github.com/inboxpraveen/ThinkLess
6
- Project-URL: Documentation, https://github.com/inboxpraveen/ThinkLess/tree/main/docs
6
+ Project-URL: Documentation, https://inboxpraveen.github.io/ThinkLess/
7
7
  Project-URL: Repository, https://github.com/inboxpraveen/ThinkLess
8
8
  Project-URL: Issues, https://github.com/inboxpraveen/ThinkLess/issues
9
9
  Project-URL: Changelog, https://github.com/inboxpraveen/ThinkLess/blob/main/CHANGELOG.md
@@ -34,8 +34,12 @@ Provides-Extra: all
34
34
  Requires-Dist: accelerate>=1.0; extra == 'all'
35
35
  Requires-Dist: anthropic>=1.0; extra == 'all'
36
36
  Requires-Dist: datasets>=3.0; extra == 'all'
37
+ Requires-Dist: fastapi>=0.115; extra == 'all'
37
38
  Requires-Dist: gliner2[local]<3,>=2.0; extra == 'all'
39
+ Requires-Dist: langgraph>=1.0; extra == 'all'
38
40
  Requires-Dist: laya<0.4,>=0.3.20; extra == 'all'
41
+ Requires-Dist: mcp>=1.10; extra == 'all'
42
+ Requires-Dist: openai-agents>=0.22; extra == 'all'
39
43
  Requires-Dist: openai>=2.0; extra == 'all'
40
44
  Requires-Dist: opentelemetry-api>=1.25; extra == 'all'
41
45
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25; extra == 'all'
@@ -44,14 +48,20 @@ Requires-Dist: protobuf>=4.25; extra == 'all'
44
48
  Requires-Dist: sentencepiece>=0.2; extra == 'all'
45
49
  Requires-Dist: torch>=2.1; extra == 'all'
46
50
  Requires-Dist: transformers<5,>=4.48; extra == 'all'
51
+ Requires-Dist: uvicorn>=0.30; extra == 'all'
47
52
  Provides-Extra: anthropic
48
53
  Requires-Dist: anthropic>=1.0; extra == 'anthropic'
49
54
  Provides-Extra: bench
50
55
  Requires-Dist: datasets>=3.0; extra == 'bench'
51
56
  Provides-Extra: dev
57
+ Requires-Dist: fastapi>=0.115; extra == 'dev'
58
+ Requires-Dist: langgraph>=1.0; extra == 'dev'
59
+ Requires-Dist: mcp>=1.10; extra == 'dev'
52
60
  Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
53
61
  Requires-Dist: mkdocstrings[python]>=0.26; extra == 'dev'
54
62
  Requires-Dist: mypy>=1.10; extra == 'dev'
63
+ Requires-Dist: openai-agents>=0.22; extra == 'dev'
64
+ Requires-Dist: pydantic-ai-slim>=1.0; extra == 'dev'
55
65
  Requires-Dist: pytest-cov>=5; extra == 'dev'
56
66
  Requires-Dist: pytest>=8; extra == 'dev'
57
67
  Requires-Dist: ruff>=0.6; extra == 'dev'
@@ -61,6 +71,8 @@ Requires-Dist: gliner2[local]<3,>=2.0; extra == 'gliner'
61
71
  Requires-Dist: protobuf>=4.25; extra == 'gliner'
62
72
  Requires-Dist: sentencepiece>=0.2; extra == 'gliner'
63
73
  Requires-Dist: transformers<5,>=4.48; extra == 'gliner'
74
+ Provides-Extra: langgraph
75
+ Requires-Dist: langgraph>=1.0; extra == 'langgraph'
64
76
  Provides-Extra: laya
65
77
  Requires-Dist: laya<0.4,>=0.3.20; extra == 'laya'
66
78
  Requires-Dist: transformers<5,>=4.48; extra == 'laya'
@@ -76,12 +88,19 @@ Provides-Extra: local-llm
76
88
  Requires-Dist: accelerate>=1.0; extra == 'local-llm'
77
89
  Requires-Dist: torch>=2.1; extra == 'local-llm'
78
90
  Requires-Dist: transformers<5,>=4.48; extra == 'local-llm'
91
+ Provides-Extra: mcp
92
+ Requires-Dist: mcp>=1.10; extra == 'mcp'
79
93
  Provides-Extra: openai
80
94
  Requires-Dist: openai>=2.0; extra == 'openai'
95
+ Provides-Extra: openai-agents
96
+ Requires-Dist: openai-agents>=0.22; extra == 'openai-agents'
81
97
  Provides-Extra: otel
82
98
  Requires-Dist: opentelemetry-api>=1.25; extra == 'otel'
83
99
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25; extra == 'otel'
84
100
  Requires-Dist: opentelemetry-sdk>=1.25; extra == 'otel'
101
+ Provides-Extra: server
102
+ Requires-Dist: fastapi>=0.115; extra == 'server'
103
+ Requires-Dist: uvicorn>=0.30; extra == 'server'
85
104
  Description-Content-Type: text/markdown
86
105
 
87
106
  <h1 align="center">ThinkLess</h1>
@@ -229,6 +248,51 @@ No GPU? `pip install thinkless` and run
229
248
  [`examples/01_rules_only.py`](https://github.com/inboxpraveen/ThinkLess/blob/main/examples/01_rules_only.py): the API, the cascade
230
249
  and the traces with no model downloads.
231
250
 
251
+ ## Use it in your agent
252
+
253
+ ThinkLess sits inside the framework you already use, at the points where the
254
+ agent decides something: which path to take, whether an input is safe,
255
+ whether a tool call is inside policy. Uncertain decisions go to the code you
256
+ run today, so nothing gets worse while the routine ones get cheaper.
257
+
258
+ ```python
259
+ from thinkless.integrations.langgraph import router
260
+
261
+ intent = router(engine, INTENT, {"order_status": "tracking", "refund": "refunds"},
262
+ default="agent") # your existing LLM node
263
+ builder.add_conditional_edges(START, intent, intent.destinations)
264
+ ```
265
+
266
+ - [LangGraph](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/langgraph.md):
267
+ routers, decision nodes and gated tools.
268
+ - [OpenAI Agents SDK](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/openai-agents.md):
269
+ input guardrails, tool guardrails and routing to a specialist agent.
270
+ - [Any framework](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/any-framework.md):
271
+ `route` and `gate` for Pydantic AI, a hand-written loop, or anything else.
272
+
273
+ Before switching anything, measure it. Shadow mode runs ThinkLess next to your
274
+ current code on live traffic and reports, per decision, the agreement with a
275
+ confidence interval and what it would save:
276
+
277
+ ```python
278
+ from thinkless.shadow import Shadow
279
+
280
+ shadow = Shadow(engine, log="shadow/intent.jsonl", sample=0.2)
281
+
282
+ @shadow.watch(INTENT, cost_usd=0.0004) # today's cost per call
283
+ def classify(message: str) -> str:
284
+ ... # unchanged, and still what users get
285
+ ```
286
+
287
+ ```bash
288
+ thinkless shadow report shadow/intent.jsonl --volume 2000000
289
+ ```
290
+
291
+ The [migration guide](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/migration.md)
292
+ walks through it, and the [FAQ](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/faq.md)
293
+ covers the common questions. To share one engine across services, run it as
294
+ a [decision server or MCP tools](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/serving.md).
295
+
232
296
  ## Try the demo agent
233
297
 
234
298
  A complete customer support agent ships with the package: a mock store with
@@ -299,7 +363,15 @@ Writing your own provider is one class. [Providers](https://github.com/inboxprav
299
363
 
300
364
  ## Documentation
301
365
 
302
- - [Getting started](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/getting-started.md)
366
+ - [Getting started](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/getting-started.md),
367
+ [installation](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/installation.md)
368
+ - Use it in your agent: [overview](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/index.md),
369
+ [LangGraph](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/langgraph.md),
370
+ [OpenAI Agents SDK](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/openai-agents.md),
371
+ [any framework](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/any-framework.md),
372
+ [migration](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/migration.md),
373
+ [shadow mode](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/shadow-mode.md),
374
+ [FAQ](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/faq.md)
303
375
  - Concepts: [the four planes](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/concepts/planes.md),
304
376
  [questions](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/concepts/questions.md),
305
377
  [confidence](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/concepts/confidence.md),
@@ -310,16 +382,19 @@ Writing your own provider is one class. [Providers](https://github.com/inboxprav
310
382
  [providers](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/providers.md),
311
383
  [LLM backends](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/llm-backends.md),
312
384
  [production](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/production.md),
385
+ [serving](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/serving.md),
386
+ [running a pilot](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/pilot.md),
313
387
  [troubleshooting](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/troubleshooting.md)
314
388
  - [Benchmarks](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/benchmarks.md), [CLI reference](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/reference/cli.md),
315
389
  [Python API](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/reference/api.md), [roadmap](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/roadmap.md)
316
390
 
317
391
  ## Status
318
392
 
319
- ThinkLess is at 0.1: the core API (questions, the engine, decisions and
320
- traces) is meant to stay stable, and providers and benchmarks will grow.
321
- Next up are shadow mode, calibration from traces, calibrated LLM confidence
322
- from log probabilities, and live Jev runs. See the [roadmap](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/roadmap.md).
393
+ ThinkLess is pre-1.0: the core API (questions, the engine, decisions and
394
+ traces) is meant to stay stable, and providers, adapters and benchmarks will
395
+ grow. Next up are a neutral benchmark for decision models, cross-request
396
+ batching in the server, calibrated LLM confidence from log probabilities, and
397
+ live Jev runs. See the [roadmap](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/roadmap.md).
323
398
 
324
399
  ## Contributing
325
400
 
@@ -143,6 +143,51 @@ No GPU? `pip install thinkless` and run
143
143
  [`examples/01_rules_only.py`](https://github.com/inboxpraveen/ThinkLess/blob/main/examples/01_rules_only.py): the API, the cascade
144
144
  and the traces with no model downloads.
145
145
 
146
+ ## Use it in your agent
147
+
148
+ ThinkLess sits inside the framework you already use, at the points where the
149
+ agent decides something: which path to take, whether an input is safe,
150
+ whether a tool call is inside policy. Uncertain decisions go to the code you
151
+ run today, so nothing gets worse while the routine ones get cheaper.
152
+
153
+ ```python
154
+ from thinkless.integrations.langgraph import router
155
+
156
+ intent = router(engine, INTENT, {"order_status": "tracking", "refund": "refunds"},
157
+ default="agent") # your existing LLM node
158
+ builder.add_conditional_edges(START, intent, intent.destinations)
159
+ ```
160
+
161
+ - [LangGraph](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/langgraph.md):
162
+ routers, decision nodes and gated tools.
163
+ - [OpenAI Agents SDK](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/openai-agents.md):
164
+ input guardrails, tool guardrails and routing to a specialist agent.
165
+ - [Any framework](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/any-framework.md):
166
+ `route` and `gate` for Pydantic AI, a hand-written loop, or anything else.
167
+
168
+ Before switching anything, measure it. Shadow mode runs ThinkLess next to your
169
+ current code on live traffic and reports, per decision, the agreement with a
170
+ confidence interval and what it would save:
171
+
172
+ ```python
173
+ from thinkless.shadow import Shadow
174
+
175
+ shadow = Shadow(engine, log="shadow/intent.jsonl", sample=0.2)
176
+
177
+ @shadow.watch(INTENT, cost_usd=0.0004) # today's cost per call
178
+ def classify(message: str) -> str:
179
+ ... # unchanged, and still what users get
180
+ ```
181
+
182
+ ```bash
183
+ thinkless shadow report shadow/intent.jsonl --volume 2000000
184
+ ```
185
+
186
+ The [migration guide](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/migration.md)
187
+ walks through it, and the [FAQ](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/faq.md)
188
+ covers the common questions. To share one engine across services, run it as
189
+ a [decision server or MCP tools](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/serving.md).
190
+
146
191
  ## Try the demo agent
147
192
 
148
193
  A complete customer support agent ships with the package: a mock store with
@@ -213,7 +258,15 @@ Writing your own provider is one class. [Providers](https://github.com/inboxprav
213
258
 
214
259
  ## Documentation
215
260
 
216
- - [Getting started](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/getting-started.md)
261
+ - [Getting started](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/getting-started.md),
262
+ [installation](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/installation.md)
263
+ - Use it in your agent: [overview](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/index.md),
264
+ [LangGraph](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/langgraph.md),
265
+ [OpenAI Agents SDK](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/openai-agents.md),
266
+ [any framework](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/integrations/any-framework.md),
267
+ [migration](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/migration.md),
268
+ [shadow mode](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/shadow-mode.md),
269
+ [FAQ](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/faq.md)
217
270
  - Concepts: [the four planes](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/concepts/planes.md),
218
271
  [questions](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/concepts/questions.md),
219
272
  [confidence](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/concepts/confidence.md),
@@ -224,16 +277,19 @@ Writing your own provider is one class. [Providers](https://github.com/inboxprav
224
277
  [providers](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/providers.md),
225
278
  [LLM backends](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/llm-backends.md),
226
279
  [production](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/production.md),
280
+ [serving](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/serving.md),
281
+ [running a pilot](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/pilot.md),
227
282
  [troubleshooting](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/guides/troubleshooting.md)
228
283
  - [Benchmarks](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/benchmarks.md), [CLI reference](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/reference/cli.md),
229
284
  [Python API](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/reference/api.md), [roadmap](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/roadmap.md)
230
285
 
231
286
  ## Status
232
287
 
233
- ThinkLess is at 0.1: the core API (questions, the engine, decisions and
234
- traces) is meant to stay stable, and providers and benchmarks will grow.
235
- Next up are shadow mode, calibration from traces, calibrated LLM confidence
236
- from log probabilities, and live Jev runs. See the [roadmap](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/roadmap.md).
288
+ ThinkLess is pre-1.0: the core API (questions, the engine, decisions and
289
+ traces) is meant to stay stable, and providers, adapters and benchmarks will
290
+ grow. Next up are a neutral benchmark for decision models, cross-request
291
+ batching in the server, calibrated LLM confidence from log probabilities, and
292
+ live Jev runs. See the [roadmap](https://github.com/inboxpraveen/ThinkLess/blob/main/docs/roadmap.md).
237
293
 
238
294
  ## Contributing
239
295
 
@@ -70,7 +70,13 @@ otel = [
70
70
  ]
71
71
  # Public dataset benchmarks.
72
72
  bench = ["datasets>=3.0"]
73
- all = ["thinkless[local,openai,anthropic,otel,bench]"]
73
+ # Agent framework adapters (thinkless.integrations).
74
+ langgraph = ["langgraph>=1.0"]
75
+ openai-agents = ["openai-agents>=0.22"]
76
+ # The decision server (thinkless serve) and MCP tools (thinkless mcp).
77
+ server = ["fastapi>=0.115", "uvicorn>=0.30"]
78
+ mcp = ["mcp>=1.10"]
79
+ all = ["thinkless[local,openai,anthropic,otel,bench,langgraph,openai-agents,server,mcp]"]
74
80
  dev = [
75
81
  "pytest>=8",
76
82
  "pytest-cov>=5",
@@ -79,11 +85,16 @@ dev = [
79
85
  "typesafe-sdk>=0.7",
80
86
  "mkdocs-material>=9.5",
81
87
  "mkdocstrings[python]>=0.26",
88
+ "langgraph>=1.0",
89
+ "openai-agents>=0.22",
90
+ "pydantic-ai-slim>=1.0",
91
+ "fastapi>=0.115",
92
+ "mcp>=1.10",
82
93
  ]
83
94
 
84
95
  [project.urls]
85
96
  Homepage = "https://github.com/inboxpraveen/ThinkLess"
86
- Documentation = "https://github.com/inboxpraveen/ThinkLess/tree/main/docs"
97
+ Documentation = "https://inboxpraveen.github.io/ThinkLess/"
87
98
  Repository = "https://github.com/inboxpraveen/ThinkLess"
88
99
  Issues = "https://github.com/inboxpraveen/ThinkLess/issues"
89
100
  Changelog = "https://github.com/inboxpraveen/ThinkLess/blob/main/CHANGELOG.md"
@@ -28,6 +28,7 @@ from ._version import __version__
28
28
  from .decision import Answer, Attempt, Decision, Plane, Status, Usage
29
29
  from .engine import Engine, Run
30
30
  from .errors import ConfigurationError, ThinkLessError
31
+ from .limits import SpendLimit, SpendLimitError
31
32
  from .logs import configure_logging
32
33
  from .questions import Choice, Extract, Kind, Question, Score, YesNo
33
34
  from .settings import load_env
@@ -49,6 +50,8 @@ __all__ = [
49
50
  "Question",
50
51
  "Run",
51
52
  "Score",
53
+ "SpendLimit",
54
+ "SpendLimitError",
52
55
  "Status",
53
56
  "ThinkLessError",
54
57
  "TraceSummary",
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -13,6 +13,7 @@ __all__ = [
13
13
  "percentile",
14
14
  "recommend_threshold",
15
15
  "threshold_sweep",
16
+ "wilson_interval",
16
17
  ]
17
18
 
18
19
 
@@ -113,3 +114,18 @@ def recommend_threshold(
113
114
  if point.accuracy is not None and point.accuracy >= target_accuracy and point.coverage > 0:
114
115
  return point
115
116
  return None
117
+
118
+
119
+ def wilson_interval(successes: int, n: int, z: float = 1.96) -> tuple[float, float]:
120
+ """Wilson score interval for a proportion, 95% by default.
121
+
122
+ It stays inside [0, 1] and behaves at small ``n`` and at rates near 0 or
123
+ 1, which is where agreement numbers usually sit.
124
+ """
125
+ if n <= 0:
126
+ return 0.0, 1.0
127
+ p = successes / n
128
+ denominator = 1 + z * z / n
129
+ centre = (p + z * z / (2 * n)) / denominator
130
+ half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denominator
131
+ return max(0.0, centre - half), min(1.0, centre + half)
@@ -20,6 +20,10 @@ from rich.table import Table
20
20
  from .._version import __version__
21
21
  from ..logs import configure_logging
22
22
  from ..settings import Settings, load_env
23
+ from .serve import mcp as mcp_command
24
+ from .serve import serve as serve_command
25
+ from .shadow import shadow_app
26
+ from .traces import trace_drift, trace_export
23
27
 
24
28
  app = typer.Typer(
25
29
  name="thinkless",
@@ -591,5 +595,13 @@ def trace_view(
591
595
  webbrowser.open(output.resolve().as_uri())
592
596
 
593
597
 
598
+ # Commands defined in their own modules, listed after the ones above.
599
+ trace_app.command("export")(trace_export)
600
+ trace_app.command("drift")(trace_drift)
601
+ app.add_typer(shadow_app, name="shadow")
602
+ app.command("serve")(serve_command)
603
+ app.command("mcp")(mcp_command)
604
+
605
+
594
606
  if __name__ == "__main__": # pragma: no cover
595
607
  app()
@@ -0,0 +1,117 @@
1
+ """``thinkless serve`` and ``thinkless mcp``: run an engine you defined in Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import os
7
+ import sys
8
+ from collections.abc import Iterable, Mapping
9
+ from typing import Annotated, Any
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ err = Console(stderr=True)
15
+
16
+ EngineArg = Annotated[
17
+ str,
18
+ typer.Argument(
19
+ help="Where the engine is, as module:attribute, for example myapp.decisions:engine. "
20
+ "The attribute can also be a function that returns an engine."
21
+ ),
22
+ ]
23
+ QuestionsOption = Annotated[
24
+ str | None,
25
+ typer.Option(
26
+ help="Questions to register, as module:attribute (a list or a mapping of questions). "
27
+ "Default: QUESTIONS in the engine's module, if it exists."
28
+ ),
29
+ ]
30
+
31
+
32
+ def _load(path: str) -> Any:
33
+ module_name, _, attribute = path.partition(":")
34
+ if not module_name or not attribute:
35
+ raise typer.BadParameter(f"expected module:attribute, got {path!r}")
36
+ if os.getcwd() not in sys.path:
37
+ sys.path.insert(0, os.getcwd())
38
+ try:
39
+ module = importlib.import_module(module_name)
40
+ except ImportError as exc:
41
+ raise typer.BadParameter(f"cannot import {module_name!r}: {exc}") from exc
42
+ try:
43
+ return getattr(module, attribute)
44
+ except AttributeError as exc:
45
+ raise typer.BadParameter(f"{module_name!r} has no attribute {attribute!r}") from exc
46
+
47
+
48
+ def load_engine_and_questions(engine_path: str, questions_path: str | None) -> tuple[Any, Any]:
49
+ from ..engine import Engine
50
+ from ..questions import Question
51
+
52
+ engine = _load(engine_path)
53
+ if callable(engine) and not isinstance(engine, Engine):
54
+ engine = engine()
55
+ if not isinstance(engine, Engine):
56
+ raise typer.BadParameter(f"{engine_path} is not a thinkless.Engine")
57
+ if questions_path:
58
+ questions = _load(questions_path)
59
+ else:
60
+ module = sys.modules[engine_path.partition(":")[0]]
61
+ questions = getattr(module, "QUESTIONS", None)
62
+ if questions is not None:
63
+ items = questions.values() if isinstance(questions, Mapping) else questions
64
+ if not isinstance(items, Iterable) or not all(isinstance(q, Question) for q in items):
65
+ raise typer.BadParameter("questions must be a list or a mapping of Question objects")
66
+ return engine, questions
67
+
68
+
69
+ def serve(
70
+ engine: EngineArg,
71
+ questions: QuestionsOption = None,
72
+ host: Annotated[str, typer.Option(help="Interface to bind.")] = "127.0.0.1",
73
+ port: Annotated[int, typer.Option(help="Port to listen on.")] = 8080,
74
+ api_key_env: Annotated[
75
+ str,
76
+ typer.Option(help="Environment variable holding the bearer token clients must send."),
77
+ ] = "THINKLESS_API_KEY",
78
+ registered_only: Annotated[
79
+ bool,
80
+ typer.Option(
81
+ "--registered-only", help="Refuse questions that are not registered on the server."
82
+ ),
83
+ ] = False,
84
+ ) -> None:
85
+ """Serve an engine over HTTP: /v1/decide, /v1/systemone, /v1/questions, /healthz."""
86
+ try:
87
+ import uvicorn
88
+
89
+ from ..server.app import create_app
90
+ except ImportError as exc:
91
+ raise typer.BadParameter('the server needs: pip install "thinkless[server]"') from exc
92
+
93
+ built, registered = load_engine_and_questions(engine, questions)
94
+ api_key = os.environ.get(api_key_env) or None
95
+ if api_key is None and host not in ("127.0.0.1", "localhost", "::1"):
96
+ err.print(
97
+ f"[yellow]Serving on {host} without authentication. Set {api_key_env} to require "
98
+ "a bearer token.[/]"
99
+ )
100
+ app = create_app(built, registered, api_key=api_key, allow_ad_hoc=not registered_only)
101
+ uvicorn.run(app, host=host, port=port)
102
+
103
+
104
+ def mcp(
105
+ engine: EngineArg,
106
+ questions: QuestionsOption = None,
107
+ transport: Annotated[
108
+ str, typer.Option(help="stdio (for desktop and IDE clients) or streamable-http.")
109
+ ] = "stdio",
110
+ ) -> None:
111
+ """Serve an engine's questions as MCP tools, one decide_<question> tool each."""
112
+ from ..server.mcp import create_mcp_server
113
+
114
+ built, registered = load_engine_and_questions(engine, questions)
115
+ if not registered:
116
+ raise typer.BadParameter("MCP needs registered questions: pass --questions")
117
+ create_mcp_server(built, registered).run(transport)