@iris-eval/mcp-server 0.8.2 → 0.10.0
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.
- package/README.md +9 -2
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +65 -0
- package/dist/config/defaults.js +17 -0
- package/dist/custom-rule-store.d.ts +4 -0
- package/dist/custom-rule-store.js +8 -3
- package/dist/dashboard/assets/{index-CyzO6OC7.js → index-CeJbaq6m.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/routes/capabilities.d.ts +3 -0
- package/dist/dashboard/routes/capabilities.js +11 -0
- package/dist/dashboard/routes/health.d.ts +5 -1
- package/dist/dashboard/routes/health.js +15 -3
- package/dist/dashboard/routes/rules.js +4 -1
- package/dist/dashboard/routes/traces.d.ts +3 -0
- package/dist/dashboard/routes/traces.js +11 -30
- package/dist/dashboard/seed-demo-data.js +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +6 -2
- package/dist/eval/accuracy.d.ts +41 -0
- package/dist/eval/accuracy.js +97 -0
- package/dist/eval/citation-verify/verifier.d.ts +16 -1
- package/dist/eval/citation-verify/verifier.js +14 -4
- package/dist/eval/compose.d.ts +57 -0
- package/dist/eval/compose.js +179 -0
- package/dist/eval/criticality.d.ts +15 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/decision-moment.js +33 -4
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +6 -2
- package/dist/eval/engine.js +126 -12
- package/dist/eval/failure-classes.d.ts +8 -0
- package/dist/eval/failure-classes.js +18 -0
- package/dist/eval/llm-judge/evaluator.d.ts +30 -0
- package/dist/eval/llm-judge/evaluator.js +26 -2
- package/dist/eval/published-accuracy.d.ts +230 -0
- package/dist/eval/published-accuracy.js +86 -0
- package/dist/eval/questions.d.ts +12 -0
- package/dist/eval/questions.js +14 -0
- package/dist/eval/response-schema.d.ts +652 -0
- package/dist/eval/response-schema.js +130 -0
- package/dist/eval/response.d.ts +12 -0
- package/dist/eval/response.js +30 -0
- package/dist/eval/risk.d.ts +60 -0
- package/dist/eval/risk.js +187 -0
- package/dist/eval/rules/completeness.js +36 -1
- package/dist/eval/rules/cost.d.ts +2 -2
- package/dist/eval/rules/cost.js +50 -6
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +22 -0
- package/dist/eval/rules/relevance.js +23 -2
- package/dist/eval/rules/safety.d.ts +6 -2
- package/dist/eval/rules/safety.js +224 -51
- package/dist/eval/seeded-random.d.ts +4 -0
- package/dist/eval/seeded-random.js +36 -0
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +89 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -0
- package/dist/eval/text/checksums.d.ts +23 -0
- package/dist/eval/text/checksums.js +97 -0
- package/dist/eval/text/normalise.d.ts +30 -0
- package/dist/eval/text/normalise.js +265 -0
- package/dist/eval/text/sentences.d.ts +15 -0
- package/dist/eval/text/sentences.js +149 -0
- package/dist/eval/verdict.d.ts +34 -0
- package/dist/eval/verdict.js +131 -0
- package/dist/index.js +5 -28
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +53 -0
- package/dist/judge-enablement.d.ts +34 -0
- package/dist/judge-enablement.js +78 -0
- package/dist/judge-enablement.json +10 -0
- package/dist/preferences.d.ts +1 -1
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +29 -0
- package/dist/resources/index.d.ts +5 -2
- package/dist/resources/index.js +65 -5
- package/dist/resources/uris.d.ts +12 -0
- package/dist/resources/uris.js +24 -0
- package/dist/retention.d.ts +20 -0
- package/dist/retention.js +44 -0
- package/dist/self-test.d.ts +1 -0
- package/dist/self-test.js +17 -3
- package/dist/server.d.ts +10 -1
- package/dist/server.js +34 -7
- package/dist/storage/index.js +1 -1
- package/dist/storage/migrations/007-eval-provenance.d.ts +3 -0
- package/dist/storage/migrations/007-eval-provenance.js +30 -0
- package/dist/storage/migrations/index.js +24 -4
- package/dist/storage/sqlite-adapter.d.ts +26 -1
- package/dist/storage/sqlite-adapter.js +149 -15
- package/dist/tools/delete-rule.d.ts +8 -0
- package/dist/tools/delete-rule.js +30 -38
- package/dist/tools/delete-trace.d.ts +5 -0
- package/dist/tools/delete-trace.js +24 -27
- package/dist/tools/deploy-rule.d.ts +13 -1
- package/dist/tools/deploy-rule.js +37 -34
- package/dist/tools/describe.d.ts +20 -0
- package/dist/tools/describe.js +36 -0
- package/dist/tools/errors.d.ts +36 -0
- package/dist/tools/errors.js +134 -0
- package/dist/tools/evaluate-output.d.ts +8 -1
- package/dist/tools/evaluate-output.js +39 -60
- package/dist/tools/evaluate-with-llm-judge.d.ts +34 -0
- package/dist/tools/evaluate-with-llm-judge.js +124 -69
- package/dist/tools/get-traces.d.ts +9 -0
- package/dist/tools/get-traces.js +29 -28
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +22 -1
- package/dist/tools/list-rules.d.ts +13 -0
- package/dist/tools/list-rules.js +43 -46
- package/dist/tools/log-trace.d.ts +4 -0
- package/dist/tools/log-trace.js +31 -29
- package/dist/tools/respond.d.ts +42 -0
- package/dist/tools/respond.js +90 -0
- package/dist/tools/strict-input.js +1 -1
- package/dist/tools/trace-link.d.ts +2 -0
- package/dist/tools/trace-link.js +13 -2
- package/dist/tools/verify-citations.d.ts +18 -2
- package/dist/tools/verify-citations.js +122 -96
- package/dist/types/config.d.ts +44 -0
- package/dist/types/eval.d.ts +309 -0
- package/dist/types/eval.js +2 -1
- package/dist/types/query.d.ts +2 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/resources/dashboard-summary.d.ts +0 -3
- package/dist/resources/dashboard-summary.js +0 -16
- package/dist/resources/trace-detail.d.ts +0 -3
- package/dist/resources/trace-detail.js +0 -30
package/dist/tools/log-trace.js
CHANGED
|
@@ -3,6 +3,9 @@ import { generateTraceId, generateSpanId } from '../utils/ids.js';
|
|
|
3
3
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
4
4
|
import { bestEffortExport } from '../otel/lazy.js';
|
|
5
5
|
import { strictInput, strictNested } from './strict-input.js';
|
|
6
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
7
|
+
import { guarded, respond } from './respond.js';
|
|
8
|
+
import { traceUri } from '../resources/uris.js';
|
|
6
9
|
/*
|
|
7
10
|
* The tool-call record — one entry of `tool_calls[]`.
|
|
8
11
|
*
|
|
@@ -58,47 +61,51 @@ export const logTraceInputShape = {
|
|
|
58
61
|
framework: z.string().optional().describe('Agent framework identifier (e.g., langchain, autogen, custom)'),
|
|
59
62
|
input: z.string().optional().describe('Agent input text — the user prompt or upstream input that produced this output'),
|
|
60
63
|
output: z.string().optional().describe('Agent output text — what the agent produced (pass to evaluate_output for scoring)'),
|
|
61
|
-
tool_calls: z.array(toolCallSchema).optional().describe('Tool calls made during execution
|
|
64
|
+
tool_calls: z.array(toolCallSchema).optional().describe('Tool calls made during execution, in order, each { tool_name, input?, output?, latency_ms?, error? } — what the trajectory rules judge; evaluate_output reuses them when given this trace_id'),
|
|
62
65
|
latency_ms: z.number().optional().describe('Total execution time in milliseconds (end-to-end agent latency)'),
|
|
63
66
|
token_usage: TokenUsageSchema.optional().describe('Token usage breakdown (prompt/completion/total — used for cost analysis)'),
|
|
64
67
|
cost_usd: z.number().optional().describe('Total cost in USD — overrides per-span aggregation when provided (treated as authoritative)'),
|
|
65
68
|
metadata: z.record(z.string(), z.unknown()).optional().describe('Opaque key-value tags (e.g. {requestId, userId, env}) — queryable in dashboard, not via get_traces filters'),
|
|
66
|
-
spans: z.array(SpanSchema).optional().describe('Detailed execution spans (hierarchical span tree with timings, attributes, events)'),
|
|
69
|
+
spans: z.array(SpanSchema).optional().describe('Detailed execution spans (hierarchical span tree with timings, attributes, events); a span without start_time takes the trace timestamp'),
|
|
67
70
|
timestamp: z.string().optional().describe('Trace timestamp (ISO 8601); defaults to now() when omitted'),
|
|
68
71
|
};
|
|
72
|
+
export const logTraceOutputSchema = z.looseObject({
|
|
73
|
+
trace_id: z.string().describe('the stored trace id, 32 hex — pass it to evaluate_output, get_traces or delete_trace'),
|
|
74
|
+
status: z.literal('stored').describe('always "stored" on success'),
|
|
75
|
+
});
|
|
69
76
|
export function registerLogTraceTool(server, storage) {
|
|
70
77
|
server.registerTool('log_trace', {
|
|
71
78
|
title: 'Log Trace',
|
|
72
|
-
description:
|
|
73
|
-
'
|
|
74
|
-
''
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'',
|
|
79
|
-
|
|
80
|
-
''
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
].join('\n'),
|
|
79
|
+
description: describeTool({
|
|
80
|
+
summary: 'Store one agent execution — input, output, tool calls, spans, cost, latency, token usage — and get the trace_id every later call keys on.',
|
|
81
|
+
does: 'Writes one trace row to local SQLite and mints a fresh trace_id; nothing is deduplicated, so resubmitting the same payload stores a second trace. ' +
|
|
82
|
+
'Only agent_name is required. Store what you have: tool_calls so the trajectory rules can later judge what the agent did, cost_usd and token_usage so the cost rules can, input and output so everything else can. ' +
|
|
83
|
+
'When IRIS_OTEL_ENDPOINT is set the trace is also exported to that collector, best-effort and asynchronous; the local write never waits on it. ' +
|
|
84
|
+
'Traces are immutable: there is no update path. In stdio mode nothing authenticates the caller; over HTTP a Bearer token is required only when an API key is configured.',
|
|
85
|
+
whenNot: 'For a transient log line (use your logger). To score an output: log first, then call evaluate_output with the trace_id, which also lets it reuse the stored tool_calls. To change a stored trace: delete_trace and log again.',
|
|
86
|
+
returns: logTraceOutputSchema,
|
|
87
|
+
errors: 'IRIS_STORAGE_ERROR when the database cannot be written. An unknown argument or a malformed span or tool_calls entry is refused before the handler runs, naming the valid keys. ' +
|
|
88
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
89
|
+
siblings: {
|
|
90
|
+
evaluate_output: 'score the stored output',
|
|
91
|
+
get_traces: 'query what was logged',
|
|
92
|
+
delete_trace: 'remove one trace',
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
89
95
|
// Strict at the MCP boundary (unknown args rejected, not stripped).
|
|
90
96
|
// The dashboard's HTTP ingest builds its own — equally strict —
|
|
91
97
|
// schema FROM this shape (dashboard/validation.ts): a client-supplied
|
|
92
98
|
// trace_id is rejected there with a 400 whose message says the server
|
|
93
99
|
// mints it, exactly as this tool mints its own in the handler below.
|
|
94
100
|
inputSchema: strictInput(logTraceInputShape),
|
|
101
|
+
outputSchema: logTraceOutputSchema,
|
|
95
102
|
annotations: {
|
|
96
103
|
readOnlyHint: false, // Writes a row to storage
|
|
97
104
|
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
98
105
|
idempotentHint: false, // Each call mints a fresh trace_id; duplicate payloads produce distinct traces
|
|
99
106
|
openWorldHint: false, // Local storage first. When IRIS_OTEL_ENDPOINT is set a best-effort async OTel export runs but is non-blocking (tool succeeds even if export fails).
|
|
100
107
|
},
|
|
101
|
-
}, async (args) => {
|
|
108
|
+
}, guarded(async (args) => {
|
|
102
109
|
const traceId = generateTraceId();
|
|
103
110
|
const timestamp = args.timestamp ?? new Date().toISOString();
|
|
104
111
|
const trace = {
|
|
@@ -128,13 +135,8 @@ export function registerLogTraceTool(server, storage) {
|
|
|
128
135
|
// eslint-disable-next-line no-console
|
|
129
136
|
console.warn(`[iris.otel] ${err.message}`);
|
|
130
137
|
});
|
|
131
|
-
return {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
text: JSON.stringify({ trace_id: traceId, status: 'stored' }),
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
};
|
|
139
|
-
});
|
|
138
|
+
return respond(logTraceOutputSchema, { trace_id: traceId, status: 'stored' }, [
|
|
139
|
+
{ uri: traceUri(traceId), name: `trace ${traceId}`, description: 'The stored trace with its spans and, later, its evaluations' },
|
|
140
|
+
]);
|
|
141
|
+
}));
|
|
140
142
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { IrisError } from './errors.js';
|
|
4
|
+
export declare const CAPABILITIES_URI = "iris://capabilities";
|
|
5
|
+
export interface ResourceLink {
|
|
6
|
+
uri: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const CAPABILITIES_LINK: ResourceLink;
|
|
11
|
+
export declare function respond<S extends z.ZodType>(schema: S, payload: object, links?: ResourceLink[]): CallToolResult;
|
|
12
|
+
export declare const errorEnvelopeSchema: z.ZodObject<{
|
|
13
|
+
error: z.ZodObject<{
|
|
14
|
+
code: z.ZodEnum<{
|
|
15
|
+
IRIS_INVALID_ARGUMENT: "IRIS_INVALID_ARGUMENT";
|
|
16
|
+
IRIS_UNKNOWN_TRACE: "IRIS_UNKNOWN_TRACE";
|
|
17
|
+
IRIS_DUPLICATE_RULE: "IRIS_DUPLICATE_RULE";
|
|
18
|
+
IRIS_INVALID_RULE_CONFIG: "IRIS_INVALID_RULE_CONFIG";
|
|
19
|
+
IRIS_JUDGE_NOT_ENABLED: "IRIS_JUDGE_NOT_ENABLED";
|
|
20
|
+
IRIS_JUDGE_UNKNOWN_MODEL: "IRIS_JUDGE_UNKNOWN_MODEL";
|
|
21
|
+
IRIS_BUDGET_EXCEEDED: "IRIS_BUDGET_EXCEEDED";
|
|
22
|
+
IRIS_PROVIDER_ERROR: "IRIS_PROVIDER_ERROR";
|
|
23
|
+
IRIS_JUDGE_FAILED: "IRIS_JUDGE_FAILED";
|
|
24
|
+
IRIS_STORAGE_ERROR: "IRIS_STORAGE_ERROR";
|
|
25
|
+
IRIS_INTERNAL_ERROR: "IRIS_INTERNAL_ERROR";
|
|
26
|
+
}>;
|
|
27
|
+
message: z.ZodString;
|
|
28
|
+
recovery: z.ZodArray<z.ZodString>;
|
|
29
|
+
retryable: z.ZodBoolean;
|
|
30
|
+
field: z.ZodOptional<z.ZodString>;
|
|
31
|
+
valid: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
32
|
+
see: z.ZodOptional<z.ZodString>;
|
|
33
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
34
|
+
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
}, z.core.$loose>;
|
|
36
|
+
}, z.core.$loose>;
|
|
37
|
+
export type ErrorEnvelope = z.infer<typeof errorEnvelopeSchema>;
|
|
38
|
+
export declare function errorResult(err: IrisError): CallToolResult;
|
|
39
|
+
/** Wrap a handler so every failure returns an envelope instead of a flattened line. */
|
|
40
|
+
export declare function guarded<A extends unknown[]>(fn: (...args: A) => Promise<CallToolResult> | CallToolResult): (...args: A) => Promise<CallToolResult>;
|
|
41
|
+
/** Links for what an evaluation created: the evaluation, and the trace when linked. */
|
|
42
|
+
export declare function evaluationLinks(evalId: string, traceId?: string): ResourceLink[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One way to answer a tool call.
|
|
3
|
+
*
|
|
4
|
+
* `respond` parses the payload through the tool's own output schema
|
|
5
|
+
* BEFORE serialising it — a field the schema does not describe fails a
|
|
6
|
+
* test, not a user — and emits the same object twice: as the text a
|
|
7
|
+
* client without structured-content support reads, and as
|
|
8
|
+
* `structuredContent` for one that has it. Beside the payload go
|
|
9
|
+
* `resource_link` items for what the call created and what explains its
|
|
10
|
+
* limits, so a client can follow them with resources/read instead of
|
|
11
|
+
* guessing a URI.
|
|
12
|
+
*
|
|
13
|
+
* `errorResult` is the failure shape: the IrisError envelope as the text
|
|
14
|
+
* and as structuredContent, `isError: true`, and a link to
|
|
15
|
+
* iris://capabilities. `guarded` wraps a handler so nothing thrown inside
|
|
16
|
+
* it reaches the SDK's flattener.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { ERROR_CODE_CATALOGUE, toIrisError } from './errors.js';
|
|
20
|
+
import { CAPABILITIES_RESOURCE_URI, evaluationUri, traceUri } from '../resources/uris.js';
|
|
21
|
+
export const CAPABILITIES_URI = CAPABILITIES_RESOURCE_URI;
|
|
22
|
+
export const CAPABILITIES_LINK = {
|
|
23
|
+
uri: CAPABILITIES_URI,
|
|
24
|
+
name: 'capabilities',
|
|
25
|
+
description: 'What this server can judge, what each rule needs, judge state, limits, tools and resources',
|
|
26
|
+
};
|
|
27
|
+
const linkItem = (l) => ({
|
|
28
|
+
type: 'resource_link',
|
|
29
|
+
uri: l.uri,
|
|
30
|
+
name: l.name,
|
|
31
|
+
...(l.description ? { description: l.description } : {}),
|
|
32
|
+
mimeType: 'application/json',
|
|
33
|
+
});
|
|
34
|
+
/** JSON round-trip: what the text carries is exactly what structuredContent carries. */
|
|
35
|
+
function normalise(payload) {
|
|
36
|
+
return JSON.parse(JSON.stringify(payload));
|
|
37
|
+
}
|
|
38
|
+
export function respond(schema, payload, links = []) {
|
|
39
|
+
const body = normalise(payload);
|
|
40
|
+
const parsed = schema.safeParse(body);
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
// A programming error: the tool built a response its own schema does
|
|
43
|
+
// not describe. Loud on purpose — the drift-lock tests catch it.
|
|
44
|
+
const issues = parsed.error.issues.slice(0, 3).map((i) => `${i.path.map(String).join('.') || '(root)'}: ${i.message}`);
|
|
45
|
+
throw new Error(`response does not match the tool's output schema: ${issues.join('; ')}`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: 'text', text: JSON.stringify(body) }, ...links.map(linkItem)],
|
|
49
|
+
structuredContent: body,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export const errorEnvelopeSchema = z.looseObject({
|
|
53
|
+
error: z.looseObject({
|
|
54
|
+
code: z.enum(ERROR_CODE_CATALOGUE),
|
|
55
|
+
message: z.string(),
|
|
56
|
+
recovery: z.array(z.string()),
|
|
57
|
+
retryable: z.boolean(),
|
|
58
|
+
field: z.string().optional(),
|
|
59
|
+
valid: z.array(z.string()).optional(),
|
|
60
|
+
see: z.string().optional(),
|
|
61
|
+
kind: z.string().optional(),
|
|
62
|
+
retryAfterMs: z.number().optional(),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
export function errorResult(err) {
|
|
66
|
+
const body = normalise({ error: err.envelope });
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: JSON.stringify(body) }, linkItem(CAPABILITIES_LINK)],
|
|
69
|
+
structuredContent: body,
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Wrap a handler so every failure returns an envelope instead of a flattened line. */
|
|
74
|
+
export function guarded(fn) {
|
|
75
|
+
return async (...args) => {
|
|
76
|
+
try {
|
|
77
|
+
return await fn(...args);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return errorResult(toIrisError(err));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Links for what an evaluation created: the evaluation, and the trace when linked. */
|
|
85
|
+
export function evaluationLinks(evalId, traceId) {
|
|
86
|
+
const links = [{ uri: evaluationUri(evalId), name: `evaluation ${evalId}`, description: 'The stored evaluation, as every reader sees it' }];
|
|
87
|
+
if (traceId)
|
|
88
|
+
links.push({ uri: traceUri(traceId), name: `trace ${traceId}`, description: 'The trace this evaluation is linked to, with its spans and every evaluation' });
|
|
89
|
+
return links;
|
|
90
|
+
}
|
|
@@ -29,7 +29,7 @@ export function strictInput(shape) {
|
|
|
29
29
|
`Valid arguments: ${validKeys}. ` +
|
|
30
30
|
'Unknown arguments are rejected rather than silently ignored, so a misspelled ' +
|
|
31
31
|
'argument name cannot change what gets evaluated — check the spelling against ' +
|
|
32
|
-
"the tool's input schema and retry."
|
|
32
|
+
"the tool's input schema and retry. Code IRIS_INVALID_ARGUMENT."
|
|
33
33
|
: undefined,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
@@ -11,6 +11,8 @@ export declare function unknownTraceMessage(traceId: string): string;
|
|
|
11
11
|
* existence check had to load the row anyway. Fetching it twice would be
|
|
12
12
|
* two reads for one fact — and two chances for them to disagree.
|
|
13
13
|
*/
|
|
14
|
+
/** The IRIS_UNKNOWN_TRACE error, built once for both the pre-check and the insert race. */
|
|
15
|
+
export declare function unknownTraceError(traceId: string): import("./errors.js").IrisError;
|
|
14
16
|
export declare function getTraceOrThrow(storage: IStorageAdapter, tenantId: TenantId, traceId: string): Promise<Trace>;
|
|
15
17
|
export declare function assertTraceExists(storage: IStorageAdapter, tenantId: TenantId, traceId: string): Promise<void>;
|
|
16
18
|
/** insertEvalResult with the foreign-key race translated into the same clear message. */
|
package/dist/tools/trace-link.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { irisError } from './errors.js';
|
|
1
2
|
/*
|
|
2
3
|
* Linking an evaluation to a trace that does not exist.
|
|
3
4
|
*
|
|
@@ -26,10 +27,20 @@ export function unknownTraceMessage(traceId) {
|
|
|
26
27
|
* existence check had to load the row anyway. Fetching it twice would be
|
|
27
28
|
* two reads for one fact — and two chances for them to disagree.
|
|
28
29
|
*/
|
|
30
|
+
/** The IRIS_UNKNOWN_TRACE error, built once for both the pre-check and the insert race. */
|
|
31
|
+
export function unknownTraceError(traceId) {
|
|
32
|
+
return irisError('IRIS_UNKNOWN_TRACE', unknownTraceMessage(traceId), {
|
|
33
|
+
field: 'trace_id',
|
|
34
|
+
recovery: [
|
|
35
|
+
'Pass the trace_id that log_trace returned, or one listed by get_traces.',
|
|
36
|
+
'Or omit trace_id to store an unlinked evaluation.',
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
29
40
|
export async function getTraceOrThrow(storage, tenantId, traceId) {
|
|
30
41
|
const trace = await storage.getTrace(tenantId, traceId);
|
|
31
42
|
if (!trace)
|
|
32
|
-
throw
|
|
43
|
+
throw unknownTraceError(traceId);
|
|
33
44
|
return trace;
|
|
34
45
|
}
|
|
35
46
|
export async function assertTraceExists(storage, tenantId, traceId) {
|
|
@@ -44,7 +55,7 @@ export async function insertLinkedEvalResult(storage, tenantId, result) {
|
|
|
44
55
|
const code = err.code;
|
|
45
56
|
const message = err instanceof Error ? err.message : String(err);
|
|
46
57
|
if (result.trace_id && (code === 'SQLITE_CONSTRAINT_FOREIGNKEY' || /FOREIGN KEY constraint failed/i.test(message))) {
|
|
47
|
-
throw
|
|
58
|
+
throw unknownTraceError(result.trace_id);
|
|
48
59
|
}
|
|
49
60
|
throw err;
|
|
50
61
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { IStorageAdapter } from '../types/query.js';
|
|
3
4
|
/**
|
|
@@ -5,8 +6,8 @@ import type { IStorageAdapter } from '../types/query.js';
|
|
|
5
6
|
* was nothing to judge (no citations, none resolved). It is NOT the honest
|
|
6
7
|
* answer when citations resolved and the judge then failed on every one —
|
|
7
8
|
* a wrong API key, a model the provider refused, a parse failure — because
|
|
8
|
-
* the caller reads "passed" and ships. That case is
|
|
9
|
-
* cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
9
|
+
* the caller reads "passed" and ships. That case is IRIS_JUDGE_FAILED
|
|
10
|
+
* naming the cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
10
11
|
*/
|
|
11
12
|
export declare function assertJudgeRan(result: {
|
|
12
13
|
totalResolved: number;
|
|
@@ -19,4 +20,19 @@ export declare function assertJudgeRan(result: {
|
|
|
19
20
|
};
|
|
20
21
|
}>;
|
|
21
22
|
}): void;
|
|
23
|
+
export declare const verifyCitationsOutputSchema: z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
trace_id: z.ZodOptional<z.ZodString>;
|
|
26
|
+
overall_score: z.ZodNullable<z.ZodNumber>;
|
|
27
|
+
passed: z.ZodNullable<z.ZodBoolean>;
|
|
28
|
+
total_unsupported: z.ZodNumber;
|
|
29
|
+
total_citations_found: z.ZodNumber;
|
|
30
|
+
total_resolved: z.ZodNumber;
|
|
31
|
+
total_judged: z.ZodNumber;
|
|
32
|
+
total_supported: z.ZodNumber;
|
|
33
|
+
total_cost_usd: z.ZodNumber;
|
|
34
|
+
citations: z.ZodArray<z.ZodObject<{
|
|
35
|
+
resolve_status: z.ZodString;
|
|
36
|
+
}, z.core.$loose>>;
|
|
37
|
+
}, z.core.$loose>;
|
|
22
38
|
export declare function registerVerifyCitationsTool(server: McpServer, storage: IStorageAdapter): void;
|
|
@@ -1,41 +1,31 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
3
3
|
import { verifyCitations } from '../eval/citation-verify/verifier.js';
|
|
4
|
-
import { findPricing } from '../eval/llm-judge/pricing.js';
|
|
5
4
|
import { generateEvalId } from '../utils/ids.js';
|
|
5
|
+
import { JUDGE_KEY_VARS } from '../judge-enablement.js';
|
|
6
6
|
import { strictInput } from './strict-input.js';
|
|
7
7
|
import { assertTraceExists, insertLinkedEvalResult } from './trace-link.js';
|
|
8
|
+
import { inferProvider, resolveApiKey } from './evaluate-with-llm-judge.js';
|
|
9
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
10
|
+
import { irisError } from './errors.js';
|
|
11
|
+
import { evaluationLinks, guarded, respond } from './respond.js';
|
|
8
12
|
const inputSchema = {
|
|
9
13
|
output: z.string().min(1).describe('The agent output containing citations to verify'),
|
|
10
14
|
model: z
|
|
11
15
|
.string()
|
|
12
|
-
.describe('Judge model for per-citation verification. Supported: anthropic = claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5-20251001; openai = gpt-4o | gpt-4o-mini | o1-mini.'),
|
|
16
|
+
.describe('Judge model for per-citation verification. Supported: anthropic = claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5 | claude-haiku-4-5-20251001; openai = gpt-4o | gpt-4o-mini | o1-mini.'),
|
|
13
17
|
provider: z.enum(['anthropic', 'openai']).optional().describe('Auto-detected from model when omitted'),
|
|
14
18
|
allow_fetch: z.boolean().optional().describe('Permit outbound HTTP to resolve URLs/DOIs. Defaults to IRIS_CITATION_ALLOW_FETCH=1; false otherwise. SSRF-guarded regardless.'),
|
|
15
19
|
domain_allowlist: z
|
|
16
20
|
.array(z.string())
|
|
17
21
|
.optional()
|
|
18
22
|
.describe('Restrict fetches to hostnames in this list (suffix match allowed). Merged with IRIS_CITATION_DOMAINS env.'),
|
|
19
|
-
max_cost_usd_total: z.number().positive().optional().describe('Cap TOTAL judge cost across all citations in this call; default
|
|
20
|
-
max_citations: z.number().int().positive().max(50).optional().describe('Max citations to verify (extras skipped); default 20'),
|
|
23
|
+
max_cost_usd_total: z.number().positive().optional().describe('Cap TOTAL judge cost across all citations in this call; default 1.00 USD — the pipeline stops when the next call would exceed it'),
|
|
24
|
+
max_citations: z.number().int().positive().max(50).optional().describe('Max citations to verify (extras skipped, not errored); default 20, at most 50'),
|
|
21
25
|
per_source_timeout_ms: z.number().int().positive().optional().describe('Per-URL fetch timeout; default 10_000'),
|
|
22
26
|
per_source_max_bytes: z.number().int().positive().optional().describe('Per-URL body cap; default 5MB'),
|
|
23
27
|
trace_id: z.string().optional().describe('Link verification result to a stored trace (id from log_trace / get_traces); an unknown id is rejected before any fetch or judge call'),
|
|
24
28
|
};
|
|
25
|
-
function inferProvider(model) {
|
|
26
|
-
const pricing = findPricing(model);
|
|
27
|
-
if (!pricing) {
|
|
28
|
-
throw new Error(`Unknown model "${model}". Provider cannot be inferred. Supported models: src/eval/llm-judge/pricing.ts.`);
|
|
29
|
-
}
|
|
30
|
-
return pricing.provider;
|
|
31
|
-
}
|
|
32
|
-
function resolveApiKey(provider) {
|
|
33
|
-
const key = provider === 'anthropic' ? process.env.IRIS_ANTHROPIC_API_KEY : process.env.IRIS_OPENAI_API_KEY;
|
|
34
|
-
if (!key) {
|
|
35
|
-
throw new Error(`${provider === 'anthropic' ? 'Anthropic' : 'OpenAI'} judge requires IRIS_${provider === 'anthropic' ? 'ANTHROPIC' : 'OPENAI'}_API_KEY for verify_citations.`);
|
|
36
|
-
}
|
|
37
|
-
return key;
|
|
38
|
-
}
|
|
39
29
|
function resolveAllowFetch(paramValue) {
|
|
40
30
|
if (paramValue !== undefined)
|
|
41
31
|
return paramValue;
|
|
@@ -54,8 +44,8 @@ function resolveDomainAllowlist(paramValue) {
|
|
|
54
44
|
* was nothing to judge (no citations, none resolved). It is NOT the honest
|
|
55
45
|
* answer when citations resolved and the judge then failed on every one —
|
|
56
46
|
* a wrong API key, a model the provider refused, a parse failure — because
|
|
57
|
-
* the caller reads "passed" and ships. That case is
|
|
58
|
-
* cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
47
|
+
* the caller reads "passed" and ships. That case is IRIS_JUDGE_FAILED
|
|
48
|
+
* naming the cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
59
49
|
*/
|
|
60
50
|
export function assertJudgeRan(result) {
|
|
61
51
|
if (result.totalResolved === 0 || result.totalJudged > 0)
|
|
@@ -65,39 +55,63 @@ export function assertJudgeRan(result) {
|
|
|
65
55
|
return;
|
|
66
56
|
const kinds = [...new Set(judgeFailures.map((c) => c.resolveError.kind))].join(', ');
|
|
67
57
|
const first = judgeFailures[0].resolveError.message;
|
|
68
|
-
throw
|
|
69
|
-
`Nothing was verified and nothing was stored, so there is no verdict. First error: ${first}
|
|
58
|
+
throw irisError('IRIS_JUDGE_FAILED', `verify_citations could not judge any of the ${result.totalResolved} resolved citation(s): the judge failed on every one (${kinds}). ` +
|
|
59
|
+
`Nothing was verified and nothing was stored, so there is no verdict. First error: ${first}`, {
|
|
60
|
+
retryable: /timeout|rate_limit|server_error/.test(kinds),
|
|
61
|
+
recovery: [
|
|
62
|
+
'Check the key and the model: a refused key or an unknown model fails every citation the same way.',
|
|
63
|
+
'Retry when the kind is a timeout, a rate limit or a provider server error.',
|
|
64
|
+
'Raise max_cost_usd_total when the kind is cost_cap_reached.',
|
|
65
|
+
],
|
|
66
|
+
});
|
|
70
67
|
}
|
|
68
|
+
export const verifyCitationsOutputSchema = z.looseObject({
|
|
69
|
+
id: z.string().describe('the evaluation id; read it back at iris://evaluations/{id}'),
|
|
70
|
+
trace_id: z.string().optional().describe('the linked trace, when one was named'),
|
|
71
|
+
overall_score: z.number().nullable().describe('supported / judged; null when nothing was judged'),
|
|
72
|
+
passed: z
|
|
73
|
+
.boolean()
|
|
74
|
+
.nullable()
|
|
75
|
+
.describe('true when every judged citation was supported; false when any judged citation was not; NULL when nothing was judged — no verdict, because nothing was verified. Until 0.10.0 that last case returned true.'),
|
|
76
|
+
total_unsupported: z.number().int().describe('judged citations the judge ruled unsupported — the number the verdict turns on'),
|
|
77
|
+
total_citations_found: z.number().int().describe('citations extracted from the output'),
|
|
78
|
+
total_resolved: z.number().int().describe('citations whose source was fetched'),
|
|
79
|
+
total_judged: z.number().int().describe('citations the judge ruled on'),
|
|
80
|
+
total_supported: z.number().int().describe('citations the judge found supported'),
|
|
81
|
+
total_cost_usd: z.number().describe('the spend across every judge call'),
|
|
82
|
+
citations: z.array(z.looseObject({ resolve_status: z.string() })).describe('per citation: the citation (raw, kind, identifier, offsets), resolve_status ok | skipped | error, resolve_error, source (url, status, content_type, bytes_fetched, truncated), judge (supported, confidence, rationale, cost_usd, latency_ms, tokens)'),
|
|
83
|
+
});
|
|
71
84
|
export function registerVerifyCitationsTool(server, storage) {
|
|
72
85
|
server.registerTool('verify_citations', {
|
|
73
86
|
title: 'Verify Citations',
|
|
74
|
-
description:
|
|
75
|
-
'Extract citations
|
|
76
|
-
''
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
description: describeTool({
|
|
88
|
+
summary: 'Extract the citations in an output, fetch the sources (opt-in, SSRF-guarded) and ask an LLM judge on your key whether each source supports its claim.',
|
|
89
|
+
does: 'Three phases. Extraction, no network: [N] references, (Author, Year), bare URLs and DOIs. Fetch of URL and DOI citations only when allow_fetch is true or IRIS_CITATION_ALLOW_FETCH=1, through a scheme allowlist, private and cloud-metadata address blocking, an optional hostname allowlist (domain_allowlist, merged with IRIS_CITATION_DOMAINS), a per-source timeout and byte cap, and at most three re-checked redirects. ' +
|
|
90
|
+
'Then one judge call per resolved citation on your own key, reading the first part of each source, capped in total by max_cost_usd_total. Up to max_citations are verified; extras are skipped, not errored. ' +
|
|
91
|
+
'overall_score is supported / judged and null when nothing was judged. Per-citation failures (bad scheme, blocked address, timeout, too large, cost cap, fetch disabled) are reported on the citation, never scored as unsupported. One evaluation row is stored.',
|
|
92
|
+
whenNot: "When the output has no citations: the score is null, and evaluate_output's hallucination signals are the cheap check. " +
|
|
93
|
+
`Without a key (${JUDGE_KEY_VARS.anthropic} or ${JUDGE_KEY_VARS.openai}): the call returns IRIS_JUDGE_NOT_ENABLED with the enable steps. ` +
|
|
94
|
+
'With fetch enabled and an open allowlist on untrusted output: you are running a user-directed fetcher — set IRIS_CITATION_DOMAINS.',
|
|
95
|
+
returns: verifyCitationsOutputSchema,
|
|
96
|
+
errors: 'IRIS_JUDGE_NOT_ENABLED, IRIS_JUDGE_UNKNOWN_MODEL and IRIS_UNKNOWN_TRACE before any fetch or spend. IRIS_JUDGE_FAILED when citations resolved but the judge failed on every one — an error, not a passing verdict; nothing is stored. ' +
|
|
97
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
98
|
+
siblings: {
|
|
99
|
+
evaluate_with_llm_judge: 'general semantic scoring',
|
|
100
|
+
evaluate_output: 'the free deterministic path, including the hallucination signals',
|
|
101
|
+
log_trace: 'record the execution first',
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
91
104
|
inputSchema: strictInput(inputSchema),
|
|
105
|
+
outputSchema: verifyCitationsOutputSchema,
|
|
92
106
|
annotations: {
|
|
93
107
|
readOnlyHint: false, // Writes eval_result + spends money
|
|
94
108
|
destructiveHint: false, // Creates data; doesn't overwrite/delete
|
|
95
109
|
idempotentHint: false, // External fetches + provider non-determinism
|
|
96
110
|
openWorldHint: true, // Outbound HTTP to citation URLs + LLM provider API
|
|
97
111
|
},
|
|
98
|
-
}, async (args) => {
|
|
112
|
+
}, guarded(async (args) => {
|
|
99
113
|
const provider = args.provider ?? inferProvider(args.model);
|
|
100
|
-
const apiKey = resolveApiKey(provider);
|
|
114
|
+
const apiKey = resolveApiKey(provider, 'verify_citations');
|
|
101
115
|
const allowFetch = resolveAllowFetch(args.allow_fetch);
|
|
102
116
|
const domainAllowlist = resolveDomainAllowlist(args.domain_allowlist);
|
|
103
117
|
// Refused before any fetch or judge call spends anything (#376).
|
|
@@ -128,73 +142,85 @@ export function registerVerifyCitationsTool(server, storage) {
|
|
|
128
142
|
eval_type: 'custom',
|
|
129
143
|
output_text: args.output,
|
|
130
144
|
score,
|
|
131
|
-
passed: result.passed,
|
|
145
|
+
passed: result.passed === true,
|
|
132
146
|
rule_results: [
|
|
133
147
|
{
|
|
134
148
|
ruleName: `semantic_citation_verify:${provider}/${args.model}`,
|
|
135
|
-
|
|
149
|
+
/*
|
|
150
|
+
* Null means nothing was judged, which is not a pass and not a
|
|
151
|
+
* failure — it is a check that did not run. Stored as a SKIP so
|
|
152
|
+
* the composer treats it as coverage rather than silently
|
|
153
|
+
* reading a paid-for "nothing verified" as clean.
|
|
154
|
+
*/
|
|
155
|
+
passed: result.passed === null ? false : result.passed,
|
|
156
|
+
...(result.passed === null
|
|
157
|
+
? { skipped: true, skipReason: `no citation was judged (found ${result.totalCitationsFound}, resolved ${result.totalResolved})` }
|
|
158
|
+
: {}),
|
|
159
|
+
kind: 'judgment',
|
|
136
160
|
score,
|
|
137
161
|
message: result.overallScore === null
|
|
138
162
|
? `No citations judged (found ${result.totalCitationsFound}, resolved ${result.totalResolved}, judged 0)`
|
|
139
163
|
: `${result.totalSupported}/${result.totalJudged} judged sources supported the output`,
|
|
140
164
|
},
|
|
141
165
|
],
|
|
142
|
-
suggestions: result.passed
|
|
166
|
+
suggestions: result.passed === null
|
|
167
|
+
? ['No citation was judged, so nothing about the sources was verified. This is not a pass.']
|
|
168
|
+
: result.passed
|
|
169
|
+
? []
|
|
170
|
+
: [`${result.totalUnsupported} of ${result.totalJudged} judged sources did not support the claim.`],
|
|
143
171
|
rules_evaluated: 1,
|
|
144
172
|
rules_skipped: 0,
|
|
145
173
|
insufficient_data: result.overallScore === null,
|
|
174
|
+
eval_cost_usd: result.totalCostUsd,
|
|
146
175
|
});
|
|
147
|
-
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
},
|
|
168
|
-
resolve_status: c.resolveStatus,
|
|
169
|
-
resolve_error: c.resolveError,
|
|
170
|
-
// Mapped to the documented snake_case keys. The verifier's
|
|
171
|
-
// internal shape is camelCase (contentType, bytesFetched) and
|
|
172
|
-
// used to be passed through verbatim, so a client parsing
|
|
173
|
-
// `source.content_type` per the description read undefined.
|
|
174
|
-
source: c.source
|
|
175
|
-
? {
|
|
176
|
-
url: c.source.url,
|
|
177
|
-
status: c.source.status,
|
|
178
|
-
content_type: c.source.contentType,
|
|
179
|
-
bytes_fetched: c.source.bytesFetched,
|
|
180
|
-
truncated: c.source.truncated,
|
|
181
|
-
}
|
|
182
|
-
: undefined,
|
|
183
|
-
judge: c.judge
|
|
184
|
-
? {
|
|
185
|
-
supported: c.judge.supported,
|
|
186
|
-
confidence: c.judge.confidence,
|
|
187
|
-
rationale: c.judge.rationale,
|
|
188
|
-
cost_usd: c.judge.costUsd,
|
|
189
|
-
latency_ms: c.judge.latencyMs,
|
|
190
|
-
input_tokens: c.judge.inputTokens,
|
|
191
|
-
output_tokens: c.judge.outputTokens,
|
|
192
|
-
}
|
|
193
|
-
: undefined,
|
|
194
|
-
})),
|
|
195
|
-
}),
|
|
176
|
+
return respond(verifyCitationsOutputSchema, {
|
|
177
|
+
id: evalId,
|
|
178
|
+
...(args.trace_id ? { trace_id: args.trace_id } : {}),
|
|
179
|
+
overall_score: result.overallScore,
|
|
180
|
+
passed: result.passed,
|
|
181
|
+
// Derived rather than read: the verifier reports it, but the tool
|
|
182
|
+
// must not break if a caller hands it an older shape.
|
|
183
|
+
total_unsupported: result.totalUnsupported ?? Math.max(0, result.totalJudged - result.totalSupported),
|
|
184
|
+
total_citations_found: result.totalCitationsFound,
|
|
185
|
+
total_resolved: result.totalResolved,
|
|
186
|
+
total_judged: result.totalJudged,
|
|
187
|
+
total_supported: result.totalSupported,
|
|
188
|
+
total_cost_usd: result.totalCostUsd,
|
|
189
|
+
citations: result.citations.map((c) => ({
|
|
190
|
+
citation: {
|
|
191
|
+
raw: c.citation.raw,
|
|
192
|
+
kind: c.citation.kind,
|
|
193
|
+
identifier: c.citation.identifier,
|
|
194
|
+
offset_start: c.citation.offsetStart,
|
|
195
|
+
offset_end: c.citation.offsetEnd,
|
|
196
196
|
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
resolve_status: c.resolveStatus,
|
|
198
|
+
resolve_error: c.resolveError,
|
|
199
|
+
// Mapped to the documented snake_case keys. The verifier's
|
|
200
|
+
// internal shape is camelCase (contentType, bytesFetched) and
|
|
201
|
+
// used to be passed through verbatim, so a client parsing
|
|
202
|
+
// `source.content_type` per the description read undefined.
|
|
203
|
+
source: c.source
|
|
204
|
+
? {
|
|
205
|
+
url: c.source.url,
|
|
206
|
+
status: c.source.status,
|
|
207
|
+
content_type: c.source.contentType,
|
|
208
|
+
bytes_fetched: c.source.bytesFetched,
|
|
209
|
+
truncated: c.source.truncated,
|
|
210
|
+
}
|
|
211
|
+
: undefined,
|
|
212
|
+
judge: c.judge
|
|
213
|
+
? {
|
|
214
|
+
supported: c.judge.supported,
|
|
215
|
+
confidence: c.judge.confidence,
|
|
216
|
+
rationale: c.judge.rationale,
|
|
217
|
+
cost_usd: c.judge.costUsd,
|
|
218
|
+
latency_ms: c.judge.latencyMs,
|
|
219
|
+
input_tokens: c.judge.inputTokens,
|
|
220
|
+
output_tokens: c.judge.outputTokens,
|
|
221
|
+
}
|
|
222
|
+
: undefined,
|
|
223
|
+
})),
|
|
224
|
+
}, evaluationLinks(evalId, args.trace_id));
|
|
225
|
+
}));
|
|
200
226
|
}
|