thinkless 0.2.0__py3-none-any.whl
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/__init__.py +63 -0
- thinkless/__main__.py +3 -0
- thinkless/_hub.py +84 -0
- thinkless/_json.py +33 -0
- thinkless/_version.py +1 -0
- thinkless/bench/__init__.py +22 -0
- thinkless/bench/intents.py +406 -0
- thinkless/bench/intents_report.py +87 -0
- thinkless/bench/metrics.py +115 -0
- thinkless/bench/report.py +123 -0
- thinkless/bench/support.py +342 -0
- thinkless/cli/__init__.py +1 -0
- thinkless/cli/main.py +595 -0
- thinkless/confidence.py +62 -0
- thinkless/data/pricing.toml +56 -0
- thinkless/data/viewer.html +411 -0
- thinkless/decision.py +185 -0
- thinkless/demo/__init__.py +1 -0
- thinkless/demo/support/__init__.py +19 -0
- thinkless/demo/support/agent.py +379 -0
- thinkless/demo/support/data/calibration.jsonl +48 -0
- thinkless/demo/support/data/scenarios.jsonl +53 -0
- thinkless/demo/support/data/world.json +78 -0
- thinkless/demo/support/questions.py +118 -0
- thinkless/demo/support/stack.py +93 -0
- thinkless/demo/support/world.py +165 -0
- thinkless/engine.py +605 -0
- thinkless/errors.py +13 -0
- thinkless/llm/__init__.py +47 -0
- thinkless/llm/anthropic.py +126 -0
- thinkless/llm/base.py +82 -0
- thinkless/llm/factory.py +91 -0
- thinkless/llm/local.py +168 -0
- thinkless/llm/openai_compat.py +165 -0
- thinkless/llm/openrouter.py +78 -0
- thinkless/llm/scripted.py +75 -0
- thinkless/logs.py +68 -0
- thinkless/pricing.py +100 -0
- thinkless/providers/__init__.py +43 -0
- thinkless/providers/base.py +108 -0
- thinkless/providers/gliner.py +194 -0
- thinkless/providers/hf.py +161 -0
- thinkless/providers/laya.py +121 -0
- thinkless/providers/llm.py +305 -0
- thinkless/providers/rules.py +181 -0
- thinkless/providers/systemone.py +147 -0
- thinkless/providers/wire.py +108 -0
- thinkless/py.typed +0 -0
- thinkless/questions.py +246 -0
- thinkless/settings.py +93 -0
- thinkless/tracing/__init__.py +23 -0
- thinkless/tracing/console.py +169 -0
- thinkless/tracing/otel.py +139 -0
- thinkless/tracing/sinks.py +142 -0
- thinkless/tracing/span.py +98 -0
- thinkless/tracing/summary.py +143 -0
- thinkless/tracing/tracer.py +159 -0
- thinkless/tracing/viewer.py +87 -0
- thinkless-0.2.0.dist-info/METADATA +357 -0
- thinkless-0.2.0.dist-info/RECORD +63 -0
- thinkless-0.2.0.dist-info/WHEEL +4 -0
- thinkless-0.2.0.dist-info/entry_points.txt +2 -0
- thinkless-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Reference prices used to estimate cost in traces and benchmark reports.
|
|
2
|
+
#
|
|
3
|
+
# Prices are USD per million tokens. They change; check the source before
|
|
4
|
+
# relying on a number, and override this file with your negotiated rates:
|
|
5
|
+
#
|
|
6
|
+
# THINKLESS_PRICING=/path/to/pricing.toml
|
|
7
|
+
#
|
|
8
|
+
# Matching: an entry applies when the provider matches and the model id equals
|
|
9
|
+
# `model`, or starts with it when `match = "prefix"`. Local models are free at
|
|
10
|
+
# the API level and need no entry; unknown hosted models cost 0 and are
|
|
11
|
+
# flagged in the trace as `cost_known = false`.
|
|
12
|
+
|
|
13
|
+
as_of = "2026-09-25"
|
|
14
|
+
|
|
15
|
+
[[price]]
|
|
16
|
+
provider = "anthropic"
|
|
17
|
+
model = "claude-fable-5-1"
|
|
18
|
+
input = 10.00
|
|
19
|
+
output = 50.00
|
|
20
|
+
source = "https://platform.claude.com/docs/en/about-claude/pricing"
|
|
21
|
+
|
|
22
|
+
[[price]]
|
|
23
|
+
provider = "anthropic"
|
|
24
|
+
model = "claude-opus-5-5"
|
|
25
|
+
input = 4.00
|
|
26
|
+
output = 20.00
|
|
27
|
+
source = "https://platform.claude.com/docs/en/about-claude/pricing"
|
|
28
|
+
|
|
29
|
+
[[price]]
|
|
30
|
+
provider = "anthropic"
|
|
31
|
+
model = "claude-opus-5"
|
|
32
|
+
input = 5.00
|
|
33
|
+
output = 25.00
|
|
34
|
+
source = "https://platform.claude.com/docs/en/about-claude/pricing"
|
|
35
|
+
|
|
36
|
+
[[price]]
|
|
37
|
+
provider = "anthropic"
|
|
38
|
+
model = "claude-sonnet-5"
|
|
39
|
+
input = 2.00
|
|
40
|
+
output = 10.00
|
|
41
|
+
source = "https://platform.claude.com/docs/en/about-claude/pricing"
|
|
42
|
+
|
|
43
|
+
[[price]]
|
|
44
|
+
provider = "anthropic"
|
|
45
|
+
model = "claude-haiku-4-5"
|
|
46
|
+
input = 1.00
|
|
47
|
+
output = 5.00
|
|
48
|
+
source = "https://platform.claude.com/docs/en/about-claude/pricing"
|
|
49
|
+
|
|
50
|
+
[[price]]
|
|
51
|
+
provider = "typesafe"
|
|
52
|
+
model = "jev"
|
|
53
|
+
match = "prefix"
|
|
54
|
+
input = 0.042
|
|
55
|
+
output = 0.0
|
|
56
|
+
source = "https://docs.typesafe.ai/models"
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ThinkLess Traces</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f6f6f4; --surface: #ffffff; --surface-2: #f1f1ee; --text: #1c1c21; --muted: #676772;
|
|
10
|
+
--border: #e2e2dd; --accent: #1f5eff; --warn: #b45309; --bad: #c2362b; --good: #15803d;
|
|
11
|
+
--rule: #0e8f84; --model: #2563eb; --llm: #7c3aed; --tool: #15803d; --code: #7b7b86;
|
|
12
|
+
--radius: 10px; --shadow: 0 1px 2px rgba(20, 20, 30, .06), 0 4px 16px rgba(20, 20, 30, .04);
|
|
13
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
14
|
+
color-scheme: light;
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root:not([data-theme="light"]) {
|
|
18
|
+
--bg: #0f1013; --surface: #17191e; --surface-2: #1d2027; --text: #e8e8ec; --muted: #9b9ba8;
|
|
19
|
+
--border: #2a2d36; --accent: #6d9bff; --warn: #f59e0b; --bad: #f87171; --good: #4ade80;
|
|
20
|
+
--rule: #2dd4bf; --model: #60a5fa; --llm: #a78bfa; --tool: #4ade80; --code: #a1a1aa;
|
|
21
|
+
--shadow: none; color-scheme: dark;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
:root[data-theme="dark"] {
|
|
25
|
+
--bg: #0f1013; --surface: #17191e; --surface-2: #1d2027; --text: #e8e8ec; --muted: #9b9ba8;
|
|
26
|
+
--border: #2a2d36; --accent: #6d9bff; --warn: #f59e0b; --bad: #f87171; --good: #4ade80;
|
|
27
|
+
--rule: #2dd4bf; --model: #60a5fa; --llm: #a78bfa; --tool: #4ade80; --code: #a1a1aa;
|
|
28
|
+
--shadow: none; color-scheme: dark;
|
|
29
|
+
}
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
html, body { margin: 0; background: var(--bg); color: var(--text); }
|
|
32
|
+
body { font: 14px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
|
33
|
+
header.top { display: flex; align-items: center; gap: 16px; padding: 14px 20px; border-bottom: 1px solid var(--border); background: var(--surface); position: sticky; top: 0; z-index: 5; }
|
|
34
|
+
.brand { font-weight: 700; letter-spacing: .2px; font-size: 16px; }
|
|
35
|
+
.brand span { color: var(--accent); }
|
|
36
|
+
.subtitle { color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
37
|
+
.spacer { flex: 1; }
|
|
38
|
+
button, select, input { font: inherit; color: inherit; }
|
|
39
|
+
.btn { border: 1px solid var(--border); background: var(--surface-2); border-radius: 8px; padding: 6px 10px; cursor: pointer; }
|
|
40
|
+
.btn:hover { border-color: var(--accent); }
|
|
41
|
+
main { display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: 16px; padding: 16px 20px 40px; max-width: 1600px; margin: 0 auto; }
|
|
42
|
+
@media (max-width: 900px) { main { grid-template-columns: minmax(0, 1fr); padding: 12px 16px 32px; } }
|
|
43
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
|
|
44
|
+
.section-title { font-size: 12px; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); margin: 0 0 8px; font-weight: 600; }
|
|
45
|
+
aside .card { padding: 12px; }
|
|
46
|
+
.filters { display: grid; gap: 8px; margin-bottom: 10px; }
|
|
47
|
+
.filters input, .filters select { width: 100%; padding: 7px 9px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); }
|
|
48
|
+
.runs { list-style: none; margin: 0; padding: 0; max-height: calc(100vh - 260px); overflow: auto; }
|
|
49
|
+
@media (max-width: 900px) { .runs { max-height: 320px; } }
|
|
50
|
+
.run { padding: 8px 10px; border-radius: 8px; cursor: pointer; display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; border: 1px solid transparent; }
|
|
51
|
+
.run:hover { background: var(--surface-2); }
|
|
52
|
+
.run.active { border-color: var(--accent); background: var(--surface-2); }
|
|
53
|
+
.run .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
54
|
+
.run .meta { color: var(--muted); font-size: 12px; grid-column: 1 / -1; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
55
|
+
.run .dur { font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
56
|
+
.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 1px 8px; font-size: 11.5px; border: 1px solid var(--border); background: var(--surface-2); white-space: nowrap; }
|
|
57
|
+
.pill.bad { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, var(--border)); }
|
|
58
|
+
.pill.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, var(--border)); }
|
|
59
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
60
|
+
.overview { padding: 14px; margin-bottom: 16px; overflow-x: auto; }
|
|
61
|
+
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
62
|
+
th, td { text-align: right; padding: 6px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
63
|
+
th:first-child, td:first-child { text-align: left; }
|
|
64
|
+
th { color: var(--muted); font-weight: 600; font-size: 12px; }
|
|
65
|
+
.detail { padding: 16px; min-width: 0; }
|
|
66
|
+
.detail h2 { margin: 0 0 4px; font-size: 18px; }
|
|
67
|
+
.detail .sub { color: var(--muted); margin-bottom: 14px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
68
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 16px; }
|
|
69
|
+
.metric { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; background: var(--surface-2); min-width: 0; }
|
|
70
|
+
.metric .label { color: var(--muted); font-size: 12px; }
|
|
71
|
+
.metric .value { font-size: 18px; font-weight: 650; font-variant-numeric: tabular-nums; margin-top: 2px; }
|
|
72
|
+
.metric .hint { color: var(--muted); font-size: 11.5px; }
|
|
73
|
+
.stack { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--border); margin-top: 6px; }
|
|
74
|
+
.stack > div { height: 100%; }
|
|
75
|
+
.legend { display: flex; gap: 12px; flex-wrap: wrap; color: var(--muted); font-size: 12px; margin: 4px 0 10px; }
|
|
76
|
+
.legend span { display: inline-flex; align-items: center; gap: 5px; }
|
|
77
|
+
.waterfall { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
78
|
+
.row { display: grid; grid-template-columns: minmax(180px, 38%) minmax(0, 1fr) 76px; align-items: center; gap: 10px; padding: 5px 10px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
79
|
+
.row:last-child { border-bottom: 0; }
|
|
80
|
+
.row:hover { background: var(--surface-2); }
|
|
81
|
+
.row.selected { background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
|
|
82
|
+
.row .label { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
83
|
+
.row .label .text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
84
|
+
.kind { font-family: var(--mono); font-size: 10.5px; padding: 0 5px; border-radius: 4px; color: #fff; flex: none; }
|
|
85
|
+
.track { position: relative; height: 16px; background: repeating-linear-gradient(90deg, transparent 0 calc(25% - 1px), var(--border) calc(25% - 1px) 25%); border-radius: 3px; }
|
|
86
|
+
.bar { position: absolute; top: 2px; height: 12px; border-radius: 3px; min-width: 2px; opacity: .92; }
|
|
87
|
+
.row .ms { font-family: var(--mono); font-size: 12px; color: var(--muted); text-align: right; }
|
|
88
|
+
.row.error .text { color: var(--bad); }
|
|
89
|
+
.chips { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 0 4px 22px; }
|
|
90
|
+
.chip { font-size: 12px; border: 1px solid var(--border); border-radius: 6px; padding: 2px 7px; background: var(--surface); font-family: var(--mono); }
|
|
91
|
+
.chip b { font-family: inherit; }
|
|
92
|
+
.chip.uncertain { border-color: var(--warn); }
|
|
93
|
+
.inspector { margin-top: 16px; border: 1px solid var(--border); border-radius: 8px; padding: 12px; background: var(--surface-2); }
|
|
94
|
+
.inspector h3 { margin: 0 0 8px; font-size: 14px; }
|
|
95
|
+
pre { margin: 0; font-family: var(--mono); font-size: 12px; white-space: pre-wrap; word-break: break-word; max-height: 420px; overflow: auto; }
|
|
96
|
+
.text-block { margin-top: 10px; }
|
|
97
|
+
.text-block .section-title { margin-bottom: 4px; }
|
|
98
|
+
.text-block pre { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px; }
|
|
99
|
+
.empty { color: var(--muted); padding: 40px; text-align: center; }
|
|
100
|
+
.tabs { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
101
|
+
.tab { padding: 5px 10px; border-radius: 8px; border: 1px solid var(--border); cursor: pointer; background: var(--surface-2); }
|
|
102
|
+
.tab.active { border-color: var(--accent); color: var(--accent); }
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<header class="top">
|
|
107
|
+
<div class="brand">Think<span>Less</span></div>
|
|
108
|
+
<div class="subtitle" id="subtitle"></div>
|
|
109
|
+
<div class="spacer"></div>
|
|
110
|
+
<button class="btn" id="theme" type="button" aria-label="Toggle color theme">Theme</button>
|
|
111
|
+
</header>
|
|
112
|
+
<main>
|
|
113
|
+
<aside>
|
|
114
|
+
<div class="card">
|
|
115
|
+
<p class="section-title">Runs</p>
|
|
116
|
+
<div class="filters">
|
|
117
|
+
<input id="search" type="search" placeholder="Filter by name, ticket, action" aria-label="Filter runs">
|
|
118
|
+
<select id="mode" aria-label="Filter by mode"></select>
|
|
119
|
+
</div>
|
|
120
|
+
<ul class="runs" id="runs"></ul>
|
|
121
|
+
</div>
|
|
122
|
+
</aside>
|
|
123
|
+
<section>
|
|
124
|
+
<div class="card overview" id="overview" hidden></div>
|
|
125
|
+
<div class="card detail" id="detail"><div class="empty">Select a run</div></div>
|
|
126
|
+
</section>
|
|
127
|
+
</main>
|
|
128
|
+
<script id="thinkless-data" type="application/json">__THINKLESS_DATA__</script>
|
|
129
|
+
<script>
|
|
130
|
+
(function () {
|
|
131
|
+
"use strict";
|
|
132
|
+
const data = JSON.parse(document.getElementById("thinkless-data").textContent);
|
|
133
|
+
const PLANES = ["rule", "model", "llm", "tool", "code"];
|
|
134
|
+
const KIND_PLANE = { run: "code", step: "code", decide: "code" };
|
|
135
|
+
const state = { selected: null, span: null, mode: "all", query: "" };
|
|
136
|
+
|
|
137
|
+
const el = (tag, attrs, ...children) => {
|
|
138
|
+
const node = document.createElement(tag);
|
|
139
|
+
for (const [k, v] of Object.entries(attrs || {})) {
|
|
140
|
+
if (k === "class") node.className = v;
|
|
141
|
+
else if (k === "style") node.setAttribute("style", v);
|
|
142
|
+
else if (k.startsWith("on")) node.addEventListener(k.slice(2), v);
|
|
143
|
+
else if (v !== undefined && v !== null) node.setAttribute(k, v);
|
|
144
|
+
}
|
|
145
|
+
for (const child of children.flat()) {
|
|
146
|
+
if (child === null || child === undefined || child === false) continue;
|
|
147
|
+
node.append(child instanceof Node ? child : document.createTextNode(String(child)));
|
|
148
|
+
}
|
|
149
|
+
return node;
|
|
150
|
+
};
|
|
151
|
+
const cssVar = (name) => `var(--${name})`;
|
|
152
|
+
const planeOf = (span) => span.plane || KIND_PLANE[span.kind] || "code";
|
|
153
|
+
const fmtMs = (ms) => ms == null ? "?" : ms >= 1000 ? (ms / 1000).toFixed(2) + " s" : ms >= 10 ? ms.toFixed(0) + " ms" : ms.toFixed(1) + " ms";
|
|
154
|
+
const fmtCost = (usd) => usd ? "$" + (usd < 0.01 ? usd.toFixed(6) : usd.toFixed(4)) : "$0";
|
|
155
|
+
const short = (value, n) => {
|
|
156
|
+
const s = typeof value === "string" ? value : JSON.stringify(value);
|
|
157
|
+
return s && s.length > n ? s.slice(0, n - 3) + "..." : s;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Theme
|
|
161
|
+
const root = document.documentElement;
|
|
162
|
+
try { const saved = localStorage.getItem("thinkless-theme"); if (saved) root.dataset.theme = saved; } catch (e) {}
|
|
163
|
+
document.getElementById("theme").addEventListener("click", () => {
|
|
164
|
+
const dark = root.dataset.theme ? root.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
165
|
+
root.dataset.theme = dark ? "light" : "dark";
|
|
166
|
+
try { localStorage.setItem("thinkless-theme", root.dataset.theme); } catch (e) {}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const traces = data.traces
|
|
170
|
+
.map((t) => {
|
|
171
|
+
const rootSpan = t.spans.find((s) => !s.parent_id) || t.spans[0];
|
|
172
|
+
const attrs = (rootSpan && rootSpan.attributes) || {};
|
|
173
|
+
return { ...t, root: rootSpan, attrs, mode: attrs.mode || "default" };
|
|
174
|
+
})
|
|
175
|
+
.sort((a, b) => (a.root.start_ms || 0) - (b.root.start_ms || 0))
|
|
176
|
+
.map((t, index) => ({ ...t, index }));
|
|
177
|
+
const modes = [...new Set(traces.map((t) => t.mode))];
|
|
178
|
+
const hasExpected = traces.some((t) => t.attrs.expected_action !== undefined);
|
|
179
|
+
|
|
180
|
+
document.getElementById("subtitle").textContent =
|
|
181
|
+
`${data.title || "Traces"} · ${traces.length} run${traces.length === 1 ? "" : "s"}` +
|
|
182
|
+
(data.generated_at ? ` · generated ${data.generated_at}` : "");
|
|
183
|
+
|
|
184
|
+
// Mode filter
|
|
185
|
+
const modeSelect = document.getElementById("mode");
|
|
186
|
+
modeSelect.append(el("option", { value: "all" }, "All modes"));
|
|
187
|
+
for (const m of modes) modeSelect.append(el("option", { value: m }, m));
|
|
188
|
+
modeSelect.hidden = modes.length < 2;
|
|
189
|
+
modeSelect.addEventListener("change", () => { state.mode = modeSelect.value; renderRuns(); });
|
|
190
|
+
document.getElementById("search").addEventListener("input", (e) => { state.query = e.target.value.toLowerCase(); renderRuns(); });
|
|
191
|
+
|
|
192
|
+
function planeStack(counts) {
|
|
193
|
+
const total = PLANES.reduce((a, p) => a + (counts[p] || 0), 0) + (counts.unresolved || 0);
|
|
194
|
+
const bar = el("div", { class: "stack", title: "decisions by plane" });
|
|
195
|
+
if (!total) return bar;
|
|
196
|
+
for (const p of [...PLANES, "unresolved"]) {
|
|
197
|
+
const n = counts[p] || 0;
|
|
198
|
+
if (n) bar.append(el("div", { style: `width:${(n / total) * 100}%;background:${p === "unresolved" ? cssVar("bad") : cssVar(p)}`, title: `${p}: ${n}` }));
|
|
199
|
+
}
|
|
200
|
+
return bar;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderOverview() {
|
|
204
|
+
const box = document.getElementById("overview");
|
|
205
|
+
if (modes.length < 2) { box.hidden = true; return; }
|
|
206
|
+
box.hidden = false;
|
|
207
|
+
const rows = modes.map((m) => {
|
|
208
|
+
const ts = traces.filter((t) => t.mode === m);
|
|
209
|
+
const n = ts.length || 1;
|
|
210
|
+
const sum = (f) => ts.reduce((a, t) => a + (f(t.summary) || 0), 0);
|
|
211
|
+
const planes = {};
|
|
212
|
+
for (const t of ts) for (const [k, v] of Object.entries(t.summary.decisions_by_plane || {})) planes[k] = (planes[k] || 0) + v;
|
|
213
|
+
const labeled = ts.filter((t) => t.attrs.expected_action !== undefined);
|
|
214
|
+
const correct = labeled.filter((t) => t.attrs.expected_action === t.attrs.action).length;
|
|
215
|
+
const lat = ts.map((t) => t.summary.duration_ms).sort((a, b) => a - b);
|
|
216
|
+
const p50 = lat.length ? lat[Math.floor((lat.length - 1) / 2)] : 0;
|
|
217
|
+
return el("tr", {},
|
|
218
|
+
el("td", {}, el("b", {}, m)),
|
|
219
|
+
el("td", {}, String(ts.length)),
|
|
220
|
+
hasExpected ? el("td", {}, labeled.length ? ((correct / labeled.length) * 100).toFixed(1) + "%" : "n/a") : null,
|
|
221
|
+
el("td", {}, (sum((s) => s.llm_calls) / n).toFixed(2)),
|
|
222
|
+
el("td", {}, (sum((s) => s.decisions) / n).toFixed(1)),
|
|
223
|
+
el("td", { style: "min-width:140px" }, planeStack(planes)),
|
|
224
|
+
el("td", {}, (sum((s) => s.escalations) / n).toFixed(2)),
|
|
225
|
+
el("td", {}, fmtMs(p50)),
|
|
226
|
+
el("td", {}, Math.round(sum((s) => s.llm_input_tokens + s.llm_output_tokens) / n).toString()),
|
|
227
|
+
el("td", {}, fmtCost(sum((s) => s.cost_usd) / n)));
|
|
228
|
+
});
|
|
229
|
+
box.replaceChildren(
|
|
230
|
+
el("p", { class: "section-title" }, "Modes compared (means per run)"),
|
|
231
|
+
el("table", {},
|
|
232
|
+
el("thead", {}, el("tr", {}, ["Mode", "Runs", hasExpected ? "Correct" : null, "LLM calls", "Decisions", "By plane", "Escalations", "Latency p50", "LLM tokens", "Cost"].filter(Boolean).map((h) => el("th", {}, h)))),
|
|
233
|
+
el("tbody", {}, rows)),
|
|
234
|
+
legend());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function legend() {
|
|
238
|
+
return el("div", { class: "legend" }, PLANES.map((p) => el("span", {}, el("i", { class: "dot", style: `background:${cssVar(p)}` }), p)));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function matches(t) {
|
|
242
|
+
if (state.mode !== "all" && t.mode !== state.mode) return false;
|
|
243
|
+
if (!state.query) return true;
|
|
244
|
+
const hay = [t.root && t.root.name, t.attrs.ticket_id, t.attrs.action, t.mode, t.file].join(" ").toLowerCase();
|
|
245
|
+
return hay.includes(state.query);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function renderRuns() {
|
|
249
|
+
const list = document.getElementById("runs");
|
|
250
|
+
const items = traces.filter(matches).map((t) => {
|
|
251
|
+
const s = t.summary;
|
|
252
|
+
const label = t.attrs.ticket_id ? `${t.attrs.ticket_id} · ${t.root.name}` : t.root.name;
|
|
253
|
+
const meta = [el("span", {}, `${s.llm_calls} LLM`), el("span", {}, `${s.decisions} decisions`)];
|
|
254
|
+
if (modes.length > 1) meta.unshift(el("span", { class: "pill" }, t.mode));
|
|
255
|
+
if (t.attrs.action) meta.push(el("span", {}, t.attrs.action));
|
|
256
|
+
if (s.uncertain) meta.push(el("span", { class: "pill warn" }, `${s.uncertain} uncertain`));
|
|
257
|
+
if (s.errors) meta.push(el("span", { class: "pill bad" }, `${s.errors} errors`));
|
|
258
|
+
return el("li", { class: "run" + (state.selected === t.index ? " active" : ""), onclick: () => select(t.index) },
|
|
259
|
+
el("div", { class: "name", title: label }, label), el("div", { class: "dur" }, fmtMs(s.duration_ms)), el("div", { class: "meta" }, meta));
|
|
260
|
+
});
|
|
261
|
+
list.replaceChildren(...(items.length ? items : [el("li", { class: "empty" }, "No runs match")]));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function select(index) {
|
|
265
|
+
state.selected = index;
|
|
266
|
+
state.span = null;
|
|
267
|
+
renderRuns();
|
|
268
|
+
renderDetail();
|
|
269
|
+
const t = traces[index];
|
|
270
|
+
if (t) {
|
|
271
|
+
const params = new URLSearchParams();
|
|
272
|
+
if (modes.length > 1) params.set("mode", t.mode);
|
|
273
|
+
if (t.attrs.ticket_id) params.set("ticket", t.attrs.ticket_id); else params.set("run", String(index));
|
|
274
|
+
if (root.dataset.theme) params.set("theme", root.dataset.theme);
|
|
275
|
+
try { history.replaceState(null, "", "#" + params.toString()); } catch (e) {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function initialRun() {
|
|
280
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
281
|
+
if (params.get("theme")) root.dataset.theme = params.get("theme");
|
|
282
|
+
if (params.get("run")) return Number(params.get("run")) || 0;
|
|
283
|
+
const ticket = params.get("ticket");
|
|
284
|
+
const mode = params.get("mode");
|
|
285
|
+
if (ticket || mode) {
|
|
286
|
+
const found = traces.find((t) => (!ticket || t.attrs.ticket_id === ticket) && (!mode || t.mode === mode));
|
|
287
|
+
if (found) {
|
|
288
|
+
if (mode) { state.mode = mode; modeSelect.value = mode; }
|
|
289
|
+
return found.index;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function orderSpans(spans) {
|
|
296
|
+
const children = new Map();
|
|
297
|
+
const ids = new Set(spans.map((s) => s.span_id));
|
|
298
|
+
for (const s of spans) {
|
|
299
|
+
const parent = ids.has(s.parent_id) ? s.parent_id : null;
|
|
300
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
301
|
+
children.get(parent).push(s);
|
|
302
|
+
}
|
|
303
|
+
for (const list of children.values()) list.sort((a, b) => a.start_ms - b.start_ms);
|
|
304
|
+
const out = [];
|
|
305
|
+
const walk = (parent, depth) => { for (const s of children.get(parent) || []) { out.push([s, depth]); walk(s.span_id, depth + 1); } };
|
|
306
|
+
walk(null, 0);
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function spanLabel(span) {
|
|
311
|
+
const a = span.attributes || {};
|
|
312
|
+
if (span.kind === "attempt") return `${a.provider || span.name}${a.model ? " [" + a.model + "]" : ""} ${(a.accepted || []).length}/${(a.questions || []).length} accepted`;
|
|
313
|
+
if (span.kind === "llm") { const u = a.usage || {}; return `${span.name}${a.model ? " [" + a.model + "]" : ""} ${u.input_tokens || 0} in / ${u.output_tokens || 0} out`; }
|
|
314
|
+
if (span.kind === "rule") return `${span.name} = ${short(a.value, 40)}`;
|
|
315
|
+
if (span.kind === "run" && a.mode) return `${span.name} (${a.mode})`;
|
|
316
|
+
return span.name;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function metric(label, value, hint, extra) {
|
|
320
|
+
return el("div", { class: "metric" }, el("div", { class: "label" }, label), el("div", { class: "value" }, value), hint ? el("div", { class: "hint" }, hint) : null, extra || null);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderDetail() {
|
|
324
|
+
const box = document.getElementById("detail");
|
|
325
|
+
const t = traces[state.selected];
|
|
326
|
+
if (!t) { box.replaceChildren(el("div", { class: "empty" }, "Select a run")); return; }
|
|
327
|
+
const s = t.summary;
|
|
328
|
+
const planes = s.decisions_by_plane || {};
|
|
329
|
+
const byPlane = (s.time_by_plane_ms || {});
|
|
330
|
+
const sub = [el("span", { class: "pill" }, t.mode), el("span", {}, s.started_at)];
|
|
331
|
+
if (t.attrs.action) sub.push(el("span", { class: "pill" }, "action: " + t.attrs.action));
|
|
332
|
+
if (t.attrs.expected_action) sub.push(el("span", { class: "pill" + (t.attrs.expected_action === t.attrs.action ? "" : " bad") }, "expected: " + t.attrs.expected_action));
|
|
333
|
+
if (t.file) sub.push(el("span", {}, t.file));
|
|
334
|
+
|
|
335
|
+
const cards = el("div", { class: "cards" },
|
|
336
|
+
metric("Total time", fmtMs(s.duration_ms), Object.entries(byPlane).map(([k, v]) => `${k} ${fmtMs(v)}`).join(", ")),
|
|
337
|
+
metric("LLM calls", String(s.llm_calls), `${s.generation_calls} generation, ${s.llm_calls - s.generation_calls} decision`),
|
|
338
|
+
metric("Decisions", String(s.decisions), PLANES.filter((p) => planes[p]).map((p) => `${p} ${planes[p]}`).join(", ") + (planes.unresolved ? `, unresolved ${planes.unresolved}` : ""), planeStack(planes)),
|
|
339
|
+
metric("Escalations", String(s.escalations), `${s.uncertain} uncertain`),
|
|
340
|
+
metric("LLM tokens", String(s.llm_input_tokens + s.llm_output_tokens), `${s.llm_input_tokens} in / ${s.llm_output_tokens} out`),
|
|
341
|
+
metric("Cost", fmtCost(s.cost_usd), "from the price table"));
|
|
342
|
+
|
|
343
|
+
const ordered = orderSpans(t.spans);
|
|
344
|
+
const start = Math.min(...t.spans.map((x) => x.start_ms));
|
|
345
|
+
const end = Math.max(...t.spans.map((x) => x.start_ms + (x.duration_ms || 0)));
|
|
346
|
+
const total = Math.max(end - start, 0.001);
|
|
347
|
+
const rows = [];
|
|
348
|
+
for (const [span, depth] of ordered) {
|
|
349
|
+
const plane = planeOf(span);
|
|
350
|
+
const left = ((span.start_ms - start) / total) * 100;
|
|
351
|
+
const width = Math.max(((span.duration_ms || 0) / total) * 100, 0.3);
|
|
352
|
+
const row = el("div", { class: "row" + (span.status === "error" ? " error" : "") + (state.span === span.span_id ? " selected" : ""), onclick: () => { state.span = span.span_id; renderDetail(); } },
|
|
353
|
+
el("div", { class: "label", style: `padding-left:${depth * 14}px` },
|
|
354
|
+
el("span", { class: "kind", style: `background:${cssVar(plane)}` }, span.kind),
|
|
355
|
+
el("span", { class: "text", title: spanLabel(span) }, spanLabel(span))),
|
|
356
|
+
el("div", { class: "track" }, el("div", { class: "bar", style: `left:${left}%;width:${width}%;background:${cssVar(plane)}` })),
|
|
357
|
+
el("div", { class: "ms" }, fmtMs(span.duration_ms)));
|
|
358
|
+
rows.push(row);
|
|
359
|
+
if (span.kind === "decide") {
|
|
360
|
+
const decisions = (span.attributes || {}).decisions || [];
|
|
361
|
+
if (decisions.length) {
|
|
362
|
+
rows.push(el("div", { class: "row", style: "cursor:default", onclick: null },
|
|
363
|
+
el("div", { class: "chips" }, decisions.map((d) => {
|
|
364
|
+
const conf = d.confidence == null ? "n/a" : d.confidence.toFixed(2);
|
|
365
|
+
const color = d.plane ? cssVar(d.plane) : cssVar("bad");
|
|
366
|
+
const value = d.level || d.value;
|
|
367
|
+
return el("span", { class: "chip" + (d.status !== "accepted" ? " uncertain" : ""), title: `status ${d.status}, threshold ${d.threshold}, tried ${(d.attempts || []).join(" > ")}` },
|
|
368
|
+
el("i", { class: "dot", style: `background:${color};margin-right:5px` }),
|
|
369
|
+
el("b", {}, d.name), " = ", short(value, 36), ` ${conf} ${d.provider || "none"}`,
|
|
370
|
+
(d.attempts || []).length > 1 ? ` (${d.attempts.join(" > ")})` : "");
|
|
371
|
+
}))));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const inspector = renderInspector(t);
|
|
377
|
+
box.replaceChildren(
|
|
378
|
+
el("h2", {}, t.attrs.ticket_id ? `${t.attrs.ticket_id} · ${t.root.name}` : t.root.name),
|
|
379
|
+
el("div", { class: "sub" }, sub),
|
|
380
|
+
cards,
|
|
381
|
+
legend(),
|
|
382
|
+
el("div", { class: "waterfall" }, rows),
|
|
383
|
+
inspector);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function renderInspector(t) {
|
|
387
|
+
const span = t.spans.find((x) => x.span_id === state.span) || t.root;
|
|
388
|
+
const attrs = { ...(span.attributes || {}) };
|
|
389
|
+
const blocks = [];
|
|
390
|
+
for (const key of ["prompt", "system", "completion", "messages", "state", "input", "arguments", "result"]) {
|
|
391
|
+
if (attrs[key] === undefined) continue;
|
|
392
|
+
const value = attrs[key];
|
|
393
|
+
delete attrs[key];
|
|
394
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
395
|
+
blocks.push(el("div", { class: "text-block" }, el("p", { class: "section-title" }, key), el("pre", {}, text)));
|
|
396
|
+
}
|
|
397
|
+
const header = { kind: span.kind, name: span.name, plane: span.plane, status: span.status, duration_ms: span.duration_ms, span_id: span.span_id, parent_id: span.parent_id, error: span.error || undefined };
|
|
398
|
+
return el("div", { class: "inspector" },
|
|
399
|
+
el("h3", {}, `${span.kind} · ${span.name}`),
|
|
400
|
+
el("pre", {}, JSON.stringify({ ...header, attributes: attrs }, null, 2)),
|
|
401
|
+
blocks);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const first = initialRun();
|
|
405
|
+
renderOverview();
|
|
406
|
+
renderRuns();
|
|
407
|
+
if (traces.length) select(first);
|
|
408
|
+
})();
|
|
409
|
+
</script>
|
|
410
|
+
</body>
|
|
411
|
+
</html>
|
thinkless/decision.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Results returned by the decision plane."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
from .questions import Kind
|
|
11
|
+
|
|
12
|
+
__all__ = ["Answer", "Attempt", "Decision", "Plane", "Status", "Usage"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Plane(str, Enum):
|
|
16
|
+
"""Where a piece of work ran.
|
|
17
|
+
|
|
18
|
+
``rule`` is deterministic code, ``model`` is a small decision model,
|
|
19
|
+
``llm`` is a generative model, ``tool`` is an action with side effects and
|
|
20
|
+
``code`` is application logic recorded for the trace.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
RULE = "rule"
|
|
24
|
+
MODEL = "model"
|
|
25
|
+
LLM = "llm"
|
|
26
|
+
TOOL = "tool"
|
|
27
|
+
CODE = "code"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Status(str, Enum):
|
|
31
|
+
"""Outcome of a decision after the cascade finished.
|
|
32
|
+
|
|
33
|
+
``accepted`` means an answer met its threshold (or came from a provider the
|
|
34
|
+
policy trusts without calibration). ``uncertain`` means every provider
|
|
35
|
+
answered below threshold; the best answer is returned and the application
|
|
36
|
+
decides what to do. ``abstained`` means no provider produced an answer.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
ACCEPTED = "accepted"
|
|
40
|
+
UNCERTAIN = "uncertain"
|
|
41
|
+
ABSTAINED = "abstained"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Usage(BaseModel):
|
|
45
|
+
"""Token accounting for one call."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(extra="forbid")
|
|
48
|
+
|
|
49
|
+
input_tokens: int = 0
|
|
50
|
+
output_tokens: int = 0
|
|
51
|
+
|
|
52
|
+
def __add__(self, other: Usage) -> Usage:
|
|
53
|
+
return Usage(
|
|
54
|
+
input_tokens=self.input_tokens + other.input_tokens,
|
|
55
|
+
output_tokens=self.output_tokens + other.output_tokens,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def total_tokens(self) -> int:
|
|
60
|
+
return self.input_tokens + self.output_tokens
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Answer(BaseModel):
|
|
64
|
+
"""What a provider returns for one question, before policy is applied.
|
|
65
|
+
|
|
66
|
+
Providers fill ``probabilities`` whenever they have a distribution; the
|
|
67
|
+
engine derives the normalized confidence from it. ``confidence`` is only
|
|
68
|
+
used when no distribution is available (extraction, for example).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="forbid")
|
|
72
|
+
|
|
73
|
+
value: Any
|
|
74
|
+
probabilities: dict[str, float] | None = None
|
|
75
|
+
confidence: float | None = None
|
|
76
|
+
fields: dict[str, float | None] | None = None
|
|
77
|
+
raw: dict[str, Any] | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Attempt(BaseModel):
|
|
81
|
+
"""One provider's try at one question."""
|
|
82
|
+
|
|
83
|
+
model_config = ConfigDict(extra="forbid")
|
|
84
|
+
|
|
85
|
+
provider: str
|
|
86
|
+
plane: Plane
|
|
87
|
+
value: Any = None
|
|
88
|
+
confidence: float | None = None
|
|
89
|
+
latency_ms: float = 0.0
|
|
90
|
+
accepted: bool = False
|
|
91
|
+
reason: str = ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Decision(BaseModel):
|
|
95
|
+
"""The resolved answer to a question.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
name: The question key.
|
|
99
|
+
kind: The question kind.
|
|
100
|
+
value: The answer. A label for ``Choice``, a float for ``Score``, a bool
|
|
101
|
+
for ``YesNo`` and a dict of field values for ``Extract``. ``None``
|
|
102
|
+
when the decision abstained.
|
|
103
|
+
status: ``accepted``, ``uncertain`` or ``abstained``.
|
|
104
|
+
confidence: Normalized confidence in [0, 1]. ``None`` when the
|
|
105
|
+
answering provider is not calibrated (a prompted LLM, for example).
|
|
106
|
+
probability: Probability of the chosen answer, when known.
|
|
107
|
+
probabilities: Full distribution over answers, when known.
|
|
108
|
+
level: For ``Score`` questions, the most likely level label.
|
|
109
|
+
threshold: The threshold that was applied.
|
|
110
|
+
provider: Name of the provider whose answer was used.
|
|
111
|
+
plane: Plane of that provider.
|
|
112
|
+
model: Model identifier reported by that provider.
|
|
113
|
+
latency_ms: Wall time spent on this question across all attempts. When
|
|
114
|
+
several questions share a provider call, the call time is counted
|
|
115
|
+
once per question.
|
|
116
|
+
attempts: Every provider that was tried, in order.
|
|
117
|
+
usage: Tokens consumed by the answering provider's call.
|
|
118
|
+
cost_usd: Estimated cost of the answering provider's call.
|
|
119
|
+
raw: The provider's native payload for this question.
|
|
120
|
+
span_id: Trace span that recorded this decision.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
model_config = ConfigDict(extra="forbid")
|
|
124
|
+
|
|
125
|
+
name: str
|
|
126
|
+
kind: Kind
|
|
127
|
+
value: Any = None
|
|
128
|
+
status: Status
|
|
129
|
+
confidence: float | None = None
|
|
130
|
+
probability: float | None = None
|
|
131
|
+
probabilities: dict[str, float] | None = None
|
|
132
|
+
level: str | None = None
|
|
133
|
+
fields: dict[str, float | None] | None = None
|
|
134
|
+
threshold: float
|
|
135
|
+
provider: str | None = None
|
|
136
|
+
plane: Plane | None = None
|
|
137
|
+
model: str | None = None
|
|
138
|
+
latency_ms: float = 0.0
|
|
139
|
+
attempts: list[Attempt] = Field(default_factory=list)
|
|
140
|
+
usage: Usage = Field(default_factory=Usage)
|
|
141
|
+
cost_usd: float = 0.0
|
|
142
|
+
raw: dict[str, Any] | None = None
|
|
143
|
+
span_id: str | None = None
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def accepted(self) -> bool:
|
|
147
|
+
return self.status is Status.ACCEPTED
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def escalated(self) -> bool:
|
|
151
|
+
"""True when an earlier provider answered (or failed) and was passed over.
|
|
152
|
+
|
|
153
|
+
A provider that abstains, such as a rule that does not fire, hands the
|
|
154
|
+
question on without an escalation: that is the cascade working as
|
|
155
|
+
designed, at no cost.
|
|
156
|
+
"""
|
|
157
|
+
return any(
|
|
158
|
+
attempt.reason in ("below_threshold", "no_confidence")
|
|
159
|
+
or attempt.reason.startswith("error")
|
|
160
|
+
for attempt in self.attempts[:-1]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def is_(self, value: Any) -> bool:
|
|
164
|
+
"""True when the decision was accepted and equals ``value``.
|
|
165
|
+
|
|
166
|
+
Convenient for routing code: ``if intent.is_("refund"): ...`` never
|
|
167
|
+
fires on an uncertain answer.
|
|
168
|
+
"""
|
|
169
|
+
return self.accepted and self.value == value
|
|
170
|
+
|
|
171
|
+
def summary(self) -> dict[str, Any]:
|
|
172
|
+
"""Compact form used in trace attributes."""
|
|
173
|
+
return {
|
|
174
|
+
"name": self.name,
|
|
175
|
+
"kind": self.kind.value,
|
|
176
|
+
"value": self.value,
|
|
177
|
+
"status": self.status.value,
|
|
178
|
+
"confidence": None if self.confidence is None else round(self.confidence, 4),
|
|
179
|
+
"threshold": self.threshold,
|
|
180
|
+
"provider": self.provider,
|
|
181
|
+
"plane": None if self.plane is None else self.plane.value,
|
|
182
|
+
"attempts": [a.provider for a in self.attempts],
|
|
183
|
+
"escalated": self.escalated,
|
|
184
|
+
"level": self.level,
|
|
185
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runnable demos that ship with the package."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""The customer support demo: a realistic agent, a mock store and 53 labeled tickets."""
|
|
2
|
+
|
|
3
|
+
from .agent import ACTIONS, Outcome, SupportAgent
|
|
4
|
+
from .questions import TRIAGE, support_rules
|
|
5
|
+
from .stack import MODES, SupportStack
|
|
6
|
+
from .world import World, find_duplicate_payment, load_scenarios
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ACTIONS",
|
|
10
|
+
"MODES",
|
|
11
|
+
"TRIAGE",
|
|
12
|
+
"Outcome",
|
|
13
|
+
"SupportAgent",
|
|
14
|
+
"SupportStack",
|
|
15
|
+
"World",
|
|
16
|
+
"find_duplicate_payment",
|
|
17
|
+
"load_scenarios",
|
|
18
|
+
"support_rules",
|
|
19
|
+
]
|