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.
- {thinkless-0.2.0 → thinkless-0.3.0}/CHANGELOG.md +35 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/PKG-INFO +82 -7
- {thinkless-0.2.0 → thinkless-0.3.0}/README.md +61 -5
- {thinkless-0.2.0 → thinkless-0.3.0}/pyproject.toml +13 -2
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/__init__.py +3 -0
- thinkless-0.3.0/src/thinkless/_version.py +1 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/metrics.py +16 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/cli/main.py +12 -0
- thinkless-0.3.0/src/thinkless/cli/serve.py +117 -0
- thinkless-0.3.0/src/thinkless/cli/shadow.py +175 -0
- thinkless-0.3.0/src/thinkless/cli/traces.py +89 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/engine.py +115 -9
- thinkless-0.3.0/src/thinkless/integrations/__init__.py +14 -0
- thinkless-0.3.0/src/thinkless/integrations/_messages.py +73 -0
- thinkless-0.3.0/src/thinkless/integrations/core.py +205 -0
- thinkless-0.3.0/src/thinkless/integrations/langgraph.py +162 -0
- thinkless-0.3.0/src/thinkless/integrations/openai_agents.py +185 -0
- thinkless-0.3.0/src/thinkless/limits.py +88 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/wire.py +92 -2
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/questions.py +30 -1
- thinkless-0.3.0/src/thinkless/server/__init__.py +6 -0
- thinkless-0.3.0/src/thinkless/server/app.py +194 -0
- thinkless-0.3.0/src/thinkless/server/mcp.py +107 -0
- thinkless-0.3.0/src/thinkless/shadow/__init__.py +40 -0
- thinkless-0.3.0/src/thinkless/shadow/compare.py +77 -0
- thinkless-0.3.0/src/thinkless/shadow/report.py +441 -0
- thinkless-0.3.0/src/thinkless/shadow/runner.py +515 -0
- thinkless-0.3.0/src/thinkless/tracing/export.py +263 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/summary.py +1 -1
- thinkless-0.3.0/tests/unit/test_integrations.py +278 -0
- thinkless-0.3.0/tests/unit/test_limits.py +117 -0
- thinkless-0.3.0/tests/unit/test_server.py +168 -0
- thinkless-0.3.0/tests/unit/test_shadow.py +333 -0
- thinkless-0.3.0/tests/unit/test_trace_export.py +84 -0
- thinkless-0.2.0/src/thinkless/_version.py +0 -1
- {thinkless-0.2.0 → thinkless-0.3.0}/.gitignore +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/LICENSE +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/__main__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/_hub.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/_json.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/intents.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/intents_report.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/report.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/bench/support.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/cli/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/confidence.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/data/pricing.toml +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/data/viewer.html +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/decision.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/agent.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/data/calibration.jsonl +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/data/scenarios.jsonl +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/data/world.json +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/questions.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/stack.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/demo/support/world.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/errors.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/anthropic.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/base.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/factory.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/local.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/openai_compat.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/openrouter.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/llm/scripted.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/logs.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/pricing.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/base.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/gliner.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/hf.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/laya.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/llm.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/rules.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/providers/systemone.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/py.typed +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/settings.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/console.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/otel.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/sinks.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/span.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/tracer.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/src/thinkless/tracing/viewer.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/conftest.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/local/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/local/test_local_models.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/network/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/network/test_openrouter_live.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/__init__.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_bench.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_cli_and_viewer.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_confidence.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_engine.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_hf_classifier.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_llm_decider.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_openrouter.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_pricing_and_llm.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_questions.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_rules.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_support_demo.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_systemone.py +0 -0
- {thinkless-0.2.0 → thinkless-0.3.0}/tests/unit/test_systemone_sdk_compat.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
320
|
-
traces) is meant to stay stable, and providers and benchmarks will
|
|
321
|
-
Next up are
|
|
322
|
-
|
|
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
|
|
234
|
-
traces) is meant to stay stable, and providers and benchmarks will
|
|
235
|
-
Next up are
|
|
236
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|