score: 0
label: "not actionable"
explanation: "The report summarizes revenue
and margins but never says whether
to buy, hold, or sell..."
import { register } from "@arizeai/phoenix-otel";
import { ClaudeAgentSDKInstrumentation } from
"@arizeai/openinference-instrumentation-claude-agent-sdk";
import * as ClaudeAgentSDKModule from "@anthropic-ai/claude-agent-sdk";
register({ projectName: "phoenix-101" });
const instrumentation = new ClaudeAgentSDKInstrumentation();
export const ClaudeAgentSDK =
instrumentation.manuallyInstrument(ClaudeAgentSDKModule);
const options = {
model: "claude-haiku-4-5",
allowedTools: ["WebSearch"],
permissionMode: "dontAsk",
settingSources: [],
skills: [],
env: withoutClaudeCodeVariables(process.env),
};
async function runTurn(prompt: string, resume?: string) {
let text = "", sessionId = resume;
for await (const msg of ClaudeAgentSDK.query({
prompt, options: { ...options, resume },
})) {
if (msg.type === "result" && msg.subtype === "success") {
text = msg.result;
sessionId = msg.session_id;
}
}
return { text, sessionId };
}
export const financialReport = traceAgent(
async ({ tickers, focus }: Query) => {
const research = await runTurn(prompts.research(tickers, focus));
const report = await runTurn(prompts.write, research.sessionId);
return { report: report.text, research: research.text };
},
{ name: "financial_report",
processOutput: (r) => ({ "output.value": r.report }) },
);
npm run agent -- TSLA "growth outlook"
span kind: AGENT
input.value: {"tickers":"TSLA","focus":"growth outlook"}
output.value: "I'll provide the financial report here instead…"
tokens, latency, llm.cost.total
npm run batch
npx @arizeai/phoenix-cli setup
# Claude Code:
claude plugin marketplace add Arize-ai/phoenix
claude plugin install arize-phoenix@arize-phoenix
# Anything else:
npx skills add Arize-ai/phoenix --skill phoenix-error-analysis
const { spans } = await getSpans({
project: { projectName: "phoenix-101" },
parentId: null,
limit: 200,
});
export const mentionsTicker = createEvaluator(
({ input, output }: { input: Query; output: string }) => {
const tickers = input.tickers.split(",").map((t) => t.trim());
const missing = tickers.filter((t) => !output.includes(t));
return missing.length === 0
? { label: "pass", score: 1 }
: { label: "fail", score: 0,
explanation: `Missing: ${missing.join(", ")}` };
},
{ name: "mentions_ticker", kind: "CODE" },
);
await logSpanAnnotations({
spanAnnotations: results.map((r) => ({
spanId: r.spanId, name: "mentions_ticker",
label: r.label, score: r.score,
explanation: r.explanation, annotatorKind: "CODE",
})),
sync: true,
});
npm run eval:code
import { anthropic } from "@ai-sdk/anthropic";
const judge = anthropic("claude-sonnet-5-5");
const telemetry = { isEnabled: false };
import { createCorrectnessEvaluator } from "@arizeai/phoenix-evals";
const correctness =
createCorrectnessEvaluator({
model: judge,
telemetry
});
const result = await correctness.evaluate({ input, output });
const faithfulness =
createFaithfulnessEvaluator({ model: judge, telemetry });
await faithfulness.evaluate({
input: formatQuery(run.input),
output: run.output,
context: `${run.searchResults}\n\nResearch notes:\n${run.research}`,
});
await toolSelection.evaluate({
input: turn.prompt,
availableTools: AVAILABLE_TOOLS,
toolSelection: turn.tools.join("\n"),
});
npm run eval:tools
<data>
<request>
{{input}}
</request>
<report>
{{output}}
</report>
</data>
export const actionability = createClassificationEvaluator({
name: "actionability",
model: judge,
telemetry,
promptTemplate: ACTIONABILITY_TEMPLATE,
choices: { actionable: 1, "not actionable": 0 },
});
npm run eval:custom
labeled.sort((a, b) => a.spanId.localeCompare(b.spanId));
await createDataset({
name: "actionability-golden",
examples: labeled.map((run, i) => ({
input: { input: run.input, output: run.output },
output: { label: run.humanLabel },
splits: i % 4 === 0 ? "test" : "dev",
spanId: run.spanId,
})),
});
await runExperiment({
dataset: { datasetName: "actionability-golden", splits: ["dev"] },
task: async (example) => {
const { label, explanation } = await actionability.evaluate(example.input);
return { label, explanation };
},
evaluators: [agreesWithHuman],
});
npm run meta
Judge vs. human on the dev split (12 reports)
judge: fail judge: pass
human: fail 9 0
human: pass 0 3
Agreement: 100%
await runExperiment({
experimentName: "improved-prompts",
dataset: { datasetName: "financial-agent-regressions" },
task: async (example) => financialReport(example.input),
evaluators: [
asExperimentEvaluator({ name: "actionability", kind: "LLM",
evaluate: ({ input, output }) => actionability.evaluate({
input: formatQuery(input), output: output.report }) }),
// ...and the same for mentions_ticker and faithfulness
],
repetitions: 1,
concurrency: 5,
});
npm run experiment
export function recommendationOf(report: string) {
const lines = report.split("\n");
const headline =
lines.find((l) => /recommendation|rating|verdict/i.test(l) && CALL.test(l)) ??
lines.find((l) => CALL.test(l));
return headline?.match(CALL)?.[1] ?? null;
}
export const consistentCall = createEvaluator(({ calls }) => ({
label: calls.every((c) => c === calls[0]) ? "consistent" : "inconsistent",
explanation: calls.join(", "),
}), { name: "consistent_call", kind: "CODE" });
npm run eval:consistency
✘ RIVN: financial health and cash runway HOLD, HOLD, SELL
✘ MSFT: cloud computing growth BUY, HOLD, BUY
✘ TSLA: yo is it a buy rn HOLD, SELL, HOLD
✔ KO: dividend yield and stability BUY, BUY, BUY
8 of 16 questions got the same call every time.
import * as px from "@arizeai/phoenix-client/vitest";
px.describe("financial agent", () => {
px.test.each(regressionQueries)(
(row) => `report for ${row.input.tickers}`,
async ({ input }) => {
const { report } = await financialReport(input);
px.logOutput(report);
await px.evaluate(mentionsTicker);
await px.evaluate(actionability, { input: formatQuery(input) });
});
}, { acceptanceCriteria: [...] });
acceptanceCriteria: [
{ annotationName: "mentions_ticker", metric: "passRate",
passFn: (a) => a.score === 1, minPassRate: 1 },
{ annotationName: "actionability", metric: "average",
threshold: 0.8 },
]
financial agent 4/4 passed · 1 miss
mentions_ticker 1.00 actionability 0.75
✗ report for RIVN
actionability 0 · The report is data-rich and forward-looking: …
✗ acceptance actionability average 0.750 (need mean >= 0.800; 4 samples)
services:
phoenix:
image: arizephoenix/phoenix:version-20.19.0
ports: ["6006:6006"]
steps:
- run: npm ci
- run: npm test
env:
PHOENIX_ENDPOINT: http://localhost:6006
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}