console: chain of thought, from data the console already has
Follows the previous commit but drops everything that would have needed a backend change. The chain is built entirely from the audit trail the console already fetches: the REASONED rung is the employee's own reasoning fields (attribution_reason, ai_recommendation_rationale, and the rest — already in the data), and the DECIDED rung is the activity it committed, the state it moved the lead to, and its confidence (ai_recommendation_confidence, also a real field on the step). The finding stays visible above the chain. No dependency on any endpoint change: the tool-call rung and the tbl_ai_decisions join are gone. What remains is real, self-contained, and ships with a push. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8e86fa7a0a
commit
900f34d11e
@ -54,16 +54,6 @@
|
|||||||
.cot__b { font-size: var(--fs-xs); line-height: var(--lh-normal); color: var(--zk-muted); overflow-wrap: anywhere; }
|
.cot__b { font-size: var(--fs-xs); line-height: var(--lh-normal); color: var(--zk-muted); overflow-wrap: anywhere; }
|
||||||
.cot__b b { color: var(--zk-ink); font-weight: var(--fw-semi); }
|
.cot__b b { color: var(--zk-ink); font-weight: var(--fw-semi); }
|
||||||
|
|
||||||
.cot__tool {
|
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
|
||||||
margin: 3px 6px 0 0; padding: 2px 9px;
|
|
||||||
border-radius: var(--r-pill); background: var(--zk-white); border: 1px solid var(--zk-line);
|
|
||||||
font-family: var(--font-mono); font-size: 11.5px; color: var(--zk-muted);
|
|
||||||
}
|
|
||||||
.cot__tool::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--zk-good, #1f9d63); }
|
|
||||||
.cot__tool.is-empty { color: var(--zk-grey); }
|
|
||||||
.cot__tool.is-empty::before { background: var(--zk-grey); }
|
|
||||||
|
|
||||||
.cot__foot { display: flex; flex-wrap: wrap; gap: 6px; margin-top: var(--sp-2); }
|
.cot__foot { display: flex; flex-wrap: wrap; gap: 6px; margin-top: var(--sp-2); }
|
||||||
.cot__conf { font-size: 11.5px; font-weight: var(--fw-semi); color: var(--zk-blue-dark); background: var(--zk-tint-blue); border: 1px solid var(--zk-blue-light); padding: 1px 9px; border-radius: var(--r-pill); }
|
.cot__conf { font-size: 11.5px; font-weight: var(--fw-semi); color: var(--zk-blue-dark); background: var(--zk-tint-blue); border: 1px solid var(--zk-blue-light); padding: 1px 9px; border-radius: var(--r-pill); }
|
||||||
.cot__model { font-family: var(--font-mono); font-size: 11px; color: var(--zk-grey); padding: 2px 0; }
|
|
||||||
|
|||||||
@ -6,77 +6,34 @@ import './ChainOfThought.css'
|
|||||||
*
|
*
|
||||||
* This is the trust surface. A person signing off on — or overriding — a
|
* This is the trust surface. A person signing off on — or overriding — a
|
||||||
* machine's decision needs the answer to one question first: WHY did it do
|
* machine's decision needs the answer to one question first: WHY did it do
|
||||||
* that. Before this, the reasoning existed (the employees write it, and the
|
* that. Before this, the reasoning existed (the employees write it as workflow
|
||||||
* platform records the tools they called in tbl_ai_decisions) but the console
|
* fields, so it is already in the audit trail) but the console showed only the
|
||||||
* showed only the conclusion. An operator either trusted it blind or opened
|
* conclusion. An operator either trusted it blind or went digging. Neither is
|
||||||
* the raw record. Neither is oversight.
|
* oversight.
|
||||||
*
|
*
|
||||||
* The chain reads the way the employee actually ran: what it CHECKED (the
|
* The chain reads the way the employee's own record reads: what it REASONED and
|
||||||
* tools it called, each marked for whether it came back with anything), what
|
* what it DECIDED. It is assembled from real fields already on the step — the
|
||||||
* it REASONED, and what it DECIDED. It is assembled from real fields, never
|
* employee's own words for the reasoning, the activity it committed and the
|
||||||
* narrated after the fact — the reasoning is the employee's own text, the
|
* state it moved the lead to for the decision — never narrated after the fact.
|
||||||
* tools are the calls it actually made, the decision is the activity it
|
* A step may carry more than one reasoning field (attribution AND eligibility,
|
||||||
* committed and the state it moved the lead to.
|
* say); each is its own rung.
|
||||||
*
|
*
|
||||||
* A tool that returned nothing is drawn with a hollow marker. That is not
|
* The finding — the first sentence, since the employees write the conclusion
|
||||||
* cosmetic: a licence check that came back empty and was read as "unlicensed"
|
* first — is lifted out and stays visible, so the "why" can be read without
|
||||||
* is exactly how a real lead was wrongly dropped, and an operator scanning the
|
* opening the chain.
|
||||||
* chain should see the empty result the machine reasoned from.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** The finding is the first sentence — the employees write the conclusion
|
|
||||||
* first — lifted out so it can be read without opening the chain. */
|
|
||||||
function findingOf(text) {
|
function findingOf(text) {
|
||||||
const m = String(text).match(/^(.{24,200}?[.!?])(\s|$)/)
|
const m = String(text).match(/^(.{24,200}?[.!?])(\s|$)/)
|
||||||
return m ? m[1].trim() : null
|
return m ? m[1].trim() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default function ChainOfThought({ reasoning, decided, confidence }) {
|
||||||
* Tool ids are how the runtime names a call, not how a person reads one.
|
|
||||||
* `retrieve_kb_7f9f…` is a knowledge lookup; `call_agentic_tool_29601` is a
|
|
||||||
* registered tool by number. Give each a name an operator recognises; fall
|
|
||||||
* back to a de-slugged version of whatever it is.
|
|
||||||
*/
|
|
||||||
function toolLabel(name) {
|
|
||||||
const n = String(name)
|
|
||||||
if (n.startsWith('retrieve_kb')) return 'Knowledge base'
|
|
||||||
if (n.startsWith('search_knowledge')) return 'Knowledge base'
|
|
||||||
if (n === 'get_task_context') return 'Task context'
|
|
||||||
if (n === 'lookup_partner') return 'Partner registry'
|
|
||||||
if (n.startsWith('call_agentic_tool')) return 'Agentic tool'
|
|
||||||
return n
|
|
||||||
.replace(/_[0-9a-f-]{8,}$/i, '')
|
|
||||||
.replace(/_\d+$/, '')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/^\w/, (c) => c.toUpperCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One pill per distinct tool. The same knowledge base queried three times is
|
|
||||||
* one thing checked, not three; an operator wants the surfaces consulted, not
|
|
||||||
* the call count. A tool is "empty" only if EVERY call to it came back empty. */
|
|
||||||
function distinctTools(tools) {
|
|
||||||
const seen = new Map()
|
|
||||||
for (const t of tools) {
|
|
||||||
const label = toolLabel(t.name)
|
|
||||||
if (!seen.has(label)) seen.set(label, { name: label, empty: t.empty })
|
|
||||||
else if (!t.empty) seen.get(label).empty = false
|
|
||||||
}
|
|
||||||
return [...seen.values()]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChainOfThought({ reasoning, tools, decided, confidence, model }) {
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const toolPills = tools && tools.length ? distinctTools(tools) : []
|
|
||||||
const primary = reasoning.length ? reasoning[0][1] : ''
|
const primary = reasoning.length ? reasoning[0][1] : ''
|
||||||
const finding = findingOf(primary)
|
const finding = findingOf(primary)
|
||||||
|
|
||||||
// Only the steps that actually have something behind them. A step with no
|
const steps = reasoning.length + 1 // each reasoning rung, plus Decided
|
||||||
// evidence is not drawn — a hollow "Checked" with no tools would be noise.
|
|
||||||
const steps = []
|
|
||||||
if (toolPills.length) steps.push('checked')
|
|
||||||
if (reasoning.length) steps.push('reasoned')
|
|
||||||
steps.push('decided')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cot">
|
<div className="cot">
|
||||||
@ -93,7 +50,7 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence,
|
|||||||
fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
Chain of thought
|
Chain of thought
|
||||||
<span className="cot__ct">· {steps.length} step{steps.length === 1 ? '' : 's'}</span>
|
<span className="cot__ct">· {steps} step{steps === 1 ? '' : 's'}</span>
|
||||||
<svg className={'cot__caret' + (open ? ' is-open' : '')} viewBox="0 0 12 12" aria-hidden="true">
|
<svg className={'cot__caret' + (open ? ' is-open' : '')} viewBox="0 0 12 12" aria-hidden="true">
|
||||||
<path d="M4 2.5 8 6l-4 3.5" fill="none" stroke="currentColor" strokeWidth="1.6"
|
<path d="M4 2.5 8 6l-4 3.5" fill="none" stroke="currentColor" strokeWidth="1.6"
|
||||||
strokeLinecap="round" strokeLinejoin="round" />
|
strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@ -102,19 +59,6 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence,
|
|||||||
|
|
||||||
{open ? (
|
{open ? (
|
||||||
<ol className="cot__steps">
|
<ol className="cot__steps">
|
||||||
{toolPills.length ? (
|
|
||||||
<li className="cot__step">
|
|
||||||
<span className="cot__mark">Checked</span>
|
|
||||||
<div className="cot__b">
|
|
||||||
{toolPills.map((t, i) => (
|
|
||||||
<span key={i} className={'cot__tool' + (t.empty ? ' is-empty' : '')} title={t.empty ? 'returned nothing' : 'returned a result'}>
|
|
||||||
{t.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{reasoning.map(([label, text], i) => (
|
{reasoning.map(([label, text], i) => (
|
||||||
<li className="cot__step" key={label}>
|
<li className="cot__step" key={label}>
|
||||||
<span className="cot__mark">{i === 0 ? 'Reasoned' : label}</span>
|
<span className="cot__mark">{i === 0 ? 'Reasoned' : label}</span>
|
||||||
@ -127,10 +71,11 @@ export default function ChainOfThought({ reasoning, tools, decided, confidence,
|
|||||||
<div className="cot__b">
|
<div className="cot__b">
|
||||||
<b>{decided.what}</b>
|
<b>{decided.what}</b>
|
||||||
{decided.stage ? <> — moved to <b>{decided.stage}</b></> : null}
|
{decided.stage ? <> — moved to <b>{decided.stage}</b></> : null}
|
||||||
{(confidence != null || model) ? (
|
{confidence != null ? (
|
||||||
<div className="cot__foot">
|
<div className="cot__foot">
|
||||||
{confidence != null ? <span className="cot__conf">{Math.round(confidence * (confidence <= 1 ? 100 : 1))}% confidence</span> : null}
|
<span className="cot__conf">
|
||||||
{model ? <span className="cot__model">{model}</span> : null}
|
{Math.round(confidence * (confidence <= 1 ? 100 : 1))}% confidence
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -281,17 +281,14 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
|
|||||||
conversation: CONVERSATION
|
conversation: CONVERSATION
|
||||||
.map(([k, label]) => [label, byBase.get(k)?.value])
|
.map(([k, label]) => [label, byBase.get(k)?.value])
|
||||||
.filter(([, v]) => v !== undefined && v !== null && typeof v !== 'object' && String(v).trim() !== ''),
|
.filter(([, v]) => v !== undefined && v !== null && typeof v !== 'object' && String(v).trim() !== ''),
|
||||||
// The decision's confidence and model, when the step recorded them. The
|
// The decision's confidence, when the step recorded it — ai_recommendation_confidence
|
||||||
// confidence field is written on the advice step; ai_tools/ai_model come
|
// is a real field the advice step writes into the audit data, so it needs
|
||||||
// from the audit row once view-service joins tbl_ai_decisions (until then
|
// no backend change to reach here.
|
||||||
// they are absent and the chain simply shows two rungs instead of three).
|
|
||||||
confidence: (() => {
|
confidence: (() => {
|
||||||
const c = byBase.get('ai_recommendation_confidence')?.value
|
const c = byBase.get('ai_recommendation_confidence')?.value
|
||||||
const n = c == null ? null : Number(c)
|
const n = c == null ? null : Number(c)
|
||||||
return Number.isFinite(n) ? n : null
|
return Number.isFinite(n) ? n : null
|
||||||
})(),
|
})(),
|
||||||
tools: Array.isArray(r.ai_tools) ? r.ai_tools : [],
|
|
||||||
model: r.ai_model || '',
|
|
||||||
figures: [...byBase]
|
figures: [...byBase]
|
||||||
.filter(([base, f]) => MONEY.has(base) && f.value)
|
.filter(([base, f]) => MONEY.has(base) && f.value)
|
||||||
.map(([base, f]) => [base.replace(/_/g, ' '), '₹' + Number(f.value).toLocaleString('en-IN')]),
|
.map(([base, f]) => [base.replace(/_/g, ' '), '₹' + Number(f.value).toLocaleString('en-IN')]),
|
||||||
@ -441,12 +438,10 @@ export default function Timeline({ rows, onOpenAgent, onOpenChat, onOpenCall })
|
|||||||
what it decided. This is the answer to "why did it do that",
|
what it decided. This is the answer to "why did it do that",
|
||||||
which is the first thing a person needs before trusting or
|
which is the first thing a person needs before trusting or
|
||||||
overriding a machine. */}
|
overriding a machine. */}
|
||||||
{it.kind === 'ai' && (it.reasoning.length || it.tools.length) ? (
|
{it.kind === 'ai' && it.reasoning.length ? (
|
||||||
<ChainOfThought
|
<ChainOfThought
|
||||||
reasoning={it.reasoning}
|
reasoning={it.reasoning}
|
||||||
tools={it.tools}
|
|
||||||
confidence={it.confidence}
|
confidence={it.confidence}
|
||||||
model={it.model}
|
|
||||||
decided={{ what: it.what, stage: it.stage ? it.stage.name : null }}
|
decided={{ what: it.what, stage: it.stage ? it.stage.name : null }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user