Skip to content

Commit 7956590

Browse files
Merge pull request #7 from AgentX-ai/feat/tracing-and-evaluations
2 parents 3077646 + a00f15c commit 7956590

33 files changed

Lines changed: 5035 additions & 26 deletions

README.md

Lines changed: 214 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,179 @@ for await (const chunk of stream) {
147147

148148
The workforce chat allows you to leverage multiple specialized agents working together to provide comprehensive responses to your queries.
149149

150+
## Tracing
151+
152+
Send agent runs to AgentX so they show up in Observe / Live Traces. Nested spans link into a
153+
real tree (one row per LLM call, tool call and retrieval), so a multi-step run is inspectable
154+
step by step.
155+
156+
```typescript
157+
import { AgentX } from "@agentx-ai/agentx-js";
158+
159+
const client = new AgentX(); // reads AGENTX_API_KEY
160+
161+
const answer = await client.tracer.withSpan(
162+
"support-agent",
163+
{ input: { query: question }, framework: "openai" },
164+
async (span) => {
165+
// Retrievals and tool calls made inside the block attach to it automatically
166+
const docs = await client.tracer.traceRetrieval("kb_search", { query: question }, async (r) => {
167+
r.docCount = 3;
168+
return knowledgeBase.search(question);
169+
});
170+
171+
const policy = await client.tracer.traceToolCall("policy_lookup", { input: { topic } }, async (t) => {
172+
t.output = await lookupPolicy(topic);
173+
return t.output;
174+
});
175+
176+
const reply = await callLlm(question, docs, policy);
177+
span.output = reply;
178+
return reply;
179+
}
180+
);
181+
182+
await client.tracer.flush(); // traces are queued in the background - flush before exiting
183+
```
184+
185+
Need the trace id back (for example to attach it to an evaluation result)? Open the span with
186+
`sync: true`:
187+
188+
```typescript
189+
const span = client.tracer.trace("support-agent", { sync: true });
190+
span.output = await callLlm(question);
191+
const traceId = await span.end(); // the ingested trace's id
192+
```
193+
194+
Other tracing entry points:
195+
196+
- `client.tracer.wrap("name", fn)` - wrap a function so every call is traced
197+
- `client.tracer.useSpan(span, fn)` - attach work started in another async context to a span
198+
- `span.childSpan(name, { startTime, endTime, ... })` - emit a child row with your own timing
199+
- `span.recordLlmCall({ durationMs, model, inputTokens, outputTokens })` - one LLM-call child row
200+
- `client.tracer.evaluateTrace(traceId, datasetId)` - score an ingested trace, agent not re-run
201+
- `client.ping()` - fail fast at startup on a bad key or base URL (trace delivery is silent)
202+
203+
## Evaluations
204+
205+
Build a dataset, run your own agent against it, and get it scored and analysed.
206+
207+
```typescript
208+
const client = new AgentX();
209+
210+
const dataset = await client.evaluations.datasets
211+
.builder("Support QA", { numberOfRequests: 3, judgeModel: "gpt-5.5" })
212+
.addCase("How do I reset my password?", {
213+
expectedResults: "Point the user at Settings > Security.",
214+
expectedTools: ["kb_search"], // scored as a trajectory match against the linked trace
215+
})
216+
.addCase("What are your support hours?", { expectedResults: "9-5 on weekdays." })
217+
.publish();
218+
219+
const report = await client.evaluations
220+
.run({
221+
datasetId: dataset.id,
222+
subject: { displayName: "Support bot", framework: "openai", runtime: "local" },
223+
})
224+
.execute(async (evaluationCase) => {
225+
const span = client.tracer.trace("support-agent", { sync: true });
226+
span.output = await myAgent(evaluationCase.query);
227+
const traceId = await span.end();
228+
return { output: span.output, traceId }; // links the run's result to the full trace
229+
})
230+
.finalize()
231+
.analyze();
232+
233+
console.log(report.averageRating, report.recommendations);
234+
```
235+
236+
Your `execute` function can return a plain string, an object
237+
(`{ output, traceId, retrievalContext, metadata, inputTokens, outputTokens, error }`), or one of
238+
the bundled adapters:
239+
240+
```typescript
241+
import { HttpEndpointAdapter, PrecomputedAdapter } from "@agentx-ai/agentx-js";
242+
243+
// Call your own service for every case
244+
.execute(new HttpEndpointAdapter({ url: "http://localhost:8080/eval" }))
245+
246+
// Or score answers you already have
247+
.execute(new PrecomputedAdapter({ "case-0": "Go to Settings > Security." }))
248+
```
249+
250+
Live rating stats are available as soon as results are submitted, without waiting for
251+
`.analyze()`:
252+
253+
```typescript
254+
const run = await client.evaluations.run({ datasetId, subject }).execute(myAgent).finalize();
255+
console.log(run.runId, run.averageRating, run.ratedCount);
256+
console.log(await run.fetchResults()); // per-result rows: rating, justification, trace ids
257+
```
258+
259+
Datasets can also be loaded from CSV (`query`, `expected_results`, `expected_capabilities`,
260+
`expected_knowledge_base`, `expected_delegations`; list columns are semicolon-separated):
261+
262+
```typescript
263+
await client.evaluations.datasets.fromCsv("./cases.csv", "Support QA").publish();
264+
```
265+
266+
Reusable grading configs live on `client.evaluations.settings` and can be pointed at any
267+
dataset:
268+
269+
```typescript
270+
const settings = await client.evaluations.settings
271+
.builder("Strict grading", { evaluationCriteria: "Answers must cite a policy.", judgeModel: "gpt-5.5" })
272+
.publish();
273+
274+
await client.evaluations
275+
.run({ datasetId, subject, evaluationSettingsId: settings.id })
276+
.execute(myAgent)
277+
.finalize();
278+
```
279+
280+
## CI/CD gates
281+
282+
Block a merge when quality drops.
283+
284+
```typescript
285+
// Gate a run you just executed (or any finalized run by id)
286+
const gate = await client.evaluations
287+
.run({ datasetId, subject })
288+
.execute(myAgent)
289+
.finalize()
290+
.gate({ failUnder: 7, noRegression: true });
291+
292+
if (!gate.passed) {
293+
process.exit(gate.exitCode);
294+
}
295+
```
296+
297+
For CI-enabled datasets, the whole lifecycle is one call - it creates the run, asks your agent
298+
each question, submits the answers for scoring and returns the gate decision:
299+
300+
```typescript
301+
const result = await client.tracer.runEval(datasetId, (query) => myAgent(query), {
302+
agentName: "support-bot",
303+
concurrency: 4,
304+
failOnGate: true, // throws CIGateFailure when the gate fails
305+
gitContext: { branch: process.env.GITHUB_REF_NAME, commit_sha: process.env.GITHUB_SHA },
306+
});
307+
308+
console.log(result.gate, result.passRate, result.violations);
309+
```
310+
311+
## Self-hosted engines
312+
313+
Point the SDK at a self-hosted AgentX engine with `baseUrl` (or `AGENTX_API_BASE_URL`):
314+
315+
```typescript
316+
const client = new AgentX(process.env.AGENTX_API_KEY, {
317+
baseUrl: "http://localhost:4700/api/v1",
318+
workspaceId: "optional-workspace-id",
319+
});
320+
await client.ping(); // verifies the URL and key before anything is traced
321+
```
322+
150323
## TypeScript Support
151324

152325
This SDK is written in TypeScript and provides full type definitions. All classes, interfaces, and methods are properly typed for better development experience.
@@ -160,13 +333,45 @@ The main client class for interacting with the AgentX API.
160333
#### Constructor
161334

162335
- `new AgentX(apiKey?: string)` - Creates a new AgentX client instance
336+
- `new AgentX(apiKey?: string, options?: AgentXOptions)` - `{ baseUrl, workspaceId, flushTracesOnExit }`
337+
- `AgentX.fromEnv(options?)` - Creates a client from `AGENTX_API_KEY` / `AGENTX_API_BASE_URL`
163338

164339
#### Methods
165340

166341
- `getAgent(id: string): Promise<Agent>` - Get a specific agent by ID
167342
- `listAgents(): Promise<Agent[]>` - List all agents
168343
- `getProfile(): Promise<any>` - Get the current user's profile
169-
- `static listWorkforces(): Promise<Workforce[]>` - List all workforces
344+
- `listWorkforces(): Promise<Workforce[]>` - List all workforces
345+
- `ping(): Promise<{ ok: true; baseUrl: string }>` - Verify the base URL and API key
346+
347+
#### Properties
348+
349+
- `tracer: Tracer` - Tracing (see [Tracing](#tracing))
350+
- `evaluations: EvaluationsRunner` - Evaluations (see [Evaluations](#evaluations))
351+
352+
### Tracer
353+
354+
- `withSpan(name, options?, fn)` - Run `fn` inside a span, closing it automatically
355+
- `trace(name, options?): TraceSpan` - Open a span you close yourself with `span.end()`
356+
- `wrap(name, fn, options?)` - Wrap a function so every call is traced
357+
- `useSpan(span, fn)` - Attach work from another async context to a span
358+
- `traceToolCall(name, options?, fn)` / `recordToolCall(name, options?)` - Record a tool call
359+
- `traceRetrieval(name, options?, fn)` / `recordRetrieval(name, options?)` - Record a retrieval
360+
- `flush(timeoutMs?)` - Wait for queued traces to be delivered
361+
- `evaluateTrace(traceId, datasetId, options?)` - Score an ingested trace against a dataset
362+
- `runEval(datasetId, agentFn, options?)` - Full CI/CD evaluation lifecycle in one call
363+
- `createCiRun` / `submitResult` / `finalizeCiRun` / `getCiRun` - The CI lifecycle, step by step
364+
365+
### EvaluationsRunner (`client.evaluations`)
366+
367+
- `run({ datasetId, subject, evaluationSettingsId? })` - Start a run; chain `.execute(fn)`,
368+
`.finalize()`, `.analyze()`, `.gate()`
369+
- `datasets.builder(name, config?)` / `datasets.fromCsv(path, name, config?)` /
370+
`datasets.fromRows(rows, name, config?)` / `datasets.get(id)` / `datasets.list()`
371+
- `settings.builder(name, config?)` / `settings.get(id)` / `settings.list()`
372+
- `listModels(provider?)` - Model ids valid for judges and portability comparisons
373+
- `getRun(runId)` / `getReport(runId)` / `getAnalysisStatus(runId)` / `gateRun(runId, options?)`
374+
- `listGates()` / `simulateConversation(options)` - self-hosted engines
170375

171376
### Agent
172377

@@ -233,6 +438,11 @@ The SDK throws descriptive errors for various failure scenarios:
233438
- API errors (with status codes)
234439
- Invalid data
235440

441+
Tracing and evaluations calls throw typed errors that all extend `Error`, so existing
442+
`catch (error)` blocks keep working: `AgentXAuthError`, `AgentXConnectionError`,
443+
`AgentXAPIError` (carries `statusCode`), `AgentXValidationError`, `DatasetNotFound`,
444+
`CINotEnabled` and `CIGateFailure`.
445+
236446
```typescript
237447
try {
238448
const agent = await client.getAgent("invalid-id");
@@ -244,6 +454,9 @@ try {
244454
## Environment Variables
245455

246456
- `AGENTX_API_KEY` - Your AgentX API key (optional if passed to constructor)
457+
- `AGENTX_API_BASE_URL` - API base URL, e.g. `http://localhost:4700/api/v1` for a self-hosted
458+
engine (optional; defaults to `https://api.agentx.so/api/v1`)
459+
- `AGENTX_WORKSPACE_ID` - Scope datasets, runs and traces to a workspace (optional)
247460

248461
## Automated Publishing
249462

TESTING.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,19 @@ If the streaming is working correctly, you should see output like:
4949
CoT: null
5050
Bot ID: 6862e8d0414914e72f4f77c2
5151
```
52+
53+
## Tracing + Evaluations (no credentials needed)
54+
55+
`npm test` runs `test-tracing-eval.ts`, which starts a stub AgentX API on localhost and asserts
56+
the exact payloads the tracing and evaluations clients send - span trees, tool calls,
57+
retrievals, result batches, the analyze poll loop and the CI gate.
58+
59+
```bash
60+
npm test
61+
```
62+
63+
To run the same surfaces against a real engine, point the SDK at it and use your own key:
64+
65+
```bash
66+
AGENTX_API_KEY=... AGENTX_API_BASE_URL=http://localhost:4700/api/v1 npx ts-node examples/tracing.ts
67+
```

examples/evaluation.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Evaluation example: publish a dataset, run an agent against it, gate the result.
3+
*
4+
* AGENTX_API_KEY=... npx ts-node examples/evaluation.ts
5+
* # self-hosted: AGENTX_API_BASE_URL=http://localhost:4700/api/v1
6+
*/
7+
import { AgentX, EvaluationCase } from "../src/index";
8+
9+
const client = new AgentX();
10+
11+
/** Your agent. Returning the trace id links each result to its full execution trace. */
12+
async function myAgent(evaluationCase: EvaluationCase): Promise<Record<string, unknown>> {
13+
const span = client.tracer.trace("support-agent", { sync: true });
14+
const answer = `Here is how to handle: ${evaluationCase.query}`;
15+
span.output = answer;
16+
const traceId = await span.end();
17+
return { output: answer, traceId };
18+
}
19+
20+
async function main(): Promise<void> {
21+
const dataset = await client.evaluations.datasets
22+
.builder("Support QA", { description: "Smoke suite", numberOfRequests: 2 })
23+
.addCase("How do I reset my password?", {
24+
expectedResults: "Point the user at Settings > Security.",
25+
judgeGuideline: "Reward concrete, clickable steps.",
26+
})
27+
.addCase("What are your support hours?", { expectedResults: "9-5 on weekdays." })
28+
.publish();
29+
30+
const run = await client.evaluations
31+
.run({
32+
datasetId: dataset.id,
33+
subject: { displayName: "Support bot", framework: "openai", runtime: "local" },
34+
})
35+
.execute(myAgent)
36+
.finalize();
37+
38+
console.log("run:", run.runId, "average rating:", run.averageRating);
39+
40+
const gate = await run.gate({ failUnder: 7, noRegression: true });
41+
if (!gate.passed) {
42+
console.error("Quality gate failed");
43+
process.exitCode = gate.exitCode;
44+
return;
45+
}
46+
47+
await run.analyze(); // prints the full qualitative report
48+
await client.tracer.flush();
49+
}
50+
51+
main().catch((err) => {
52+
console.error(err);
53+
process.exit(1);
54+
});

examples/tracing.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Tracing example: one traced agent run with a retrieval, a tool call and an LLM call.
3+
*
4+
* AGENTX_API_KEY=... npx ts-node examples/tracing.ts
5+
* # self-hosted: AGENTX_API_BASE_URL=http://localhost:4700/api/v1
6+
*/
7+
import { AgentX } from "../src/index";
8+
9+
async function knowledgeBaseSearch(query: string): Promise<string[]> {
10+
return [`doc about ${query}`, "password policy v3"];
11+
}
12+
13+
async function callLlm(question: string, context: string[]): Promise<string> {
14+
return `Based on ${context.length} documents: go to Settings > Security and click "Reset password". (${question})`;
15+
}
16+
17+
async function main(): Promise<void> {
18+
const client = new AgentX();
19+
await client.ping(); // fail fast on a bad key or base URL
20+
21+
const question = "How do I reset my password?";
22+
23+
const span = client.tracer.trace("support-agent", {
24+
input: { query: question },
25+
framework: "openai",
26+
sync: true, // so span.end() gives us the trace id back
27+
});
28+
29+
const docs = await client.tracer.traceRetrieval("kb_search", { query: question }, async (r) => {
30+
const found = await knowledgeBaseSearch(question);
31+
r.docCount = found.length;
32+
return found;
33+
});
34+
35+
await client.tracer.traceToolCall("policy_lookup", { input: { topic: "password" } }, async (t) => {
36+
t.output = { policy: "self-serve reset" };
37+
return t.output;
38+
});
39+
40+
const started = Date.now();
41+
const answer = await callLlm(question, docs);
42+
await span.recordLlmCall({
43+
durationMs: Date.now() - started,
44+
startTime: started / 1000,
45+
endTime: Date.now() / 1000,
46+
model: "gpt-4o-mini",
47+
input: question,
48+
output: answer,
49+
inputTokens: 128,
50+
outputTokens: 42,
51+
});
52+
53+
span.output = answer;
54+
const traceId = await span.end();
55+
await client.tracer.flush();
56+
57+
console.log("answer:", answer);
58+
console.log("trace:", traceId);
59+
}
60+
61+
main().catch((err) => {
62+
console.error(err);
63+
process.exit(1);
64+
});

0 commit comments

Comments
 (0)