@axiom-lattice/gateway 2.1.72 → 2.1.74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/gateway",
3
- "version": "2.1.72",
3
+ "version": "2.1.74",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -39,10 +39,11 @@
39
39
  "pg": "^8.11.0",
40
40
  "redis": "^5.0.1",
41
41
  "uuid": "^9.0.1",
42
- "@axiom-lattice/core": "2.1.62",
43
- "@axiom-lattice/pg-stores": "1.0.52",
44
- "@axiom-lattice/protocols": "2.1.32",
45
- "@axiom-lattice/queue-redis": "1.0.31"
42
+ "@axiom-lattice/agent-eval": "2.1.58",
43
+ "@axiom-lattice/core": "2.1.64",
44
+ "@axiom-lattice/pg-stores": "1.0.54",
45
+ "@axiom-lattice/protocols": "2.1.33",
46
+ "@axiom-lattice/queue-redis": "1.0.32"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/jest": "^29.5.14",
@@ -0,0 +1,469 @@
1
+ import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
2
+ import { getStoreLattice } from "@axiom-lattice/core";
3
+ import { EvalStore } from "@axiom-lattice/protocols";
4
+ import { v4 as uuidv4 } from "uuid";
5
+ import { evalRunner, EvalStreamEvent } from "../services/eval-runner";
6
+
7
+ function getTenantId(request: FastifyRequest): string {
8
+ const userTenantId = (request as any).user?.tenantId;
9
+ if (userTenantId) {
10
+ return userTenantId;
11
+ }
12
+ return (request.headers["x-tenant-id"] as string) || "default";
13
+ }
14
+
15
+ function getEvalStore(): EvalStore {
16
+ return getStoreLattice("default", "eval").store as EvalStore;
17
+ }
18
+
19
+ export async function createProject(
20
+ request: FastifyRequest<{ Body: Record<string, unknown> }>,
21
+ reply: FastifyReply
22
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
23
+ try {
24
+ const tenantId = getTenantId(request);
25
+ const store = getEvalStore();
26
+ const id = uuidv4();
27
+ const data = request.body as any;
28
+ const project = await store.createProject(tenantId, id, {
29
+ name: data.name,
30
+ description: data.description,
31
+ version: data.version,
32
+ judgeModelConfig: data.judgeModelConfig ?? {},
33
+ targetServerConfig: data.targetServerConfig ?? {},
34
+ concurrency: data.concurrency ?? 1,
35
+ reportConfig: data.reportConfig,
36
+ });
37
+ return reply.status(201).send({
38
+ success: true,
39
+ message: "Successfully created project",
40
+ data: project,
41
+ });
42
+ } catch (err: unknown) {
43
+ const message = err instanceof Error ? err.message : "Failed to create project";
44
+ return reply.status(500).send({ success: false, message });
45
+ }
46
+ }
47
+
48
+ export async function listProjects(
49
+ request: FastifyRequest,
50
+ reply: FastifyReply
51
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
52
+ try {
53
+ const tenantId = getTenantId(request);
54
+ const store = getEvalStore();
55
+ let projects = await store.getProjectsByTenant(tenantId);
56
+
57
+ // Seed a default project for new tenants
58
+ if (projects.length === 0) {
59
+ try {
60
+ const { modelLatticeManager } = await import("@axiom-lattice/core");
61
+ const models = modelLatticeManager.getAllLattices();
62
+ const first = models[0];
63
+ const judgeModel = first
64
+ ? { modelKey: first.key }
65
+ : { provider: "openai", model: "gpt-4" };
66
+
67
+ const host = request.hostname || "localhost";
68
+ const baseUrl = `http://${host}:${process.env.PORT || 4001}`;
69
+
70
+ await store.createProject(tenantId, uuidv4(), {
71
+ name: "Default",
72
+ description: "Built-in project for testing eval against this server",
73
+ judgeModelConfig: judgeModel,
74
+ targetServerConfig: { base_url: baseUrl, api_key: "" },
75
+ concurrency: 3,
76
+ });
77
+ projects = await store.getProjectsByTenant(tenantId);
78
+ } catch { /* seed failure is non-fatal */ }
79
+ }
80
+
81
+ return {
82
+ success: true,
83
+ message: "Ok",
84
+ data: { records: projects, total: projects.length },
85
+ };
86
+ } catch (err: unknown) {
87
+ const message = err instanceof Error ? err.message : "Failed to list projects";
88
+ return reply.status(500).send({ success: false, message });
89
+ }
90
+ }
91
+
92
+ export async function getProject(
93
+ request: FastifyRequest<{ Params: { pid: string } }>,
94
+ reply: FastifyReply
95
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
96
+ try {
97
+ const tenantId = getTenantId(request);
98
+ const store = getEvalStore();
99
+ const { pid } = request.params;
100
+ const project = await store.getProjectById(tenantId, pid);
101
+ if (!project) {
102
+ return reply.status(404).send({ success: false, message: "Project not found" });
103
+ }
104
+ const suites = await store.getSuitesByProject(tenantId, pid);
105
+ return {
106
+ success: true,
107
+ message: "Ok",
108
+ data: { project, suites },
109
+ };
110
+ } catch (err: unknown) {
111
+ const message = err instanceof Error ? err.message : "Failed to get project";
112
+ return reply.status(500).send({ success: false, message });
113
+ }
114
+ }
115
+
116
+ export async function updateProject(
117
+ request: FastifyRequest<{ Params: { pid: string }; Body: Record<string, unknown> }>,
118
+ reply: FastifyReply
119
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
120
+ try {
121
+ const tenantId = getTenantId(request);
122
+ const store = getEvalStore();
123
+ const { pid } = request.params;
124
+ const existing = await store.getProjectById(tenantId, pid);
125
+ if (!existing) {
126
+ return reply.status(404).send({ success: false, message: "Project not found" });
127
+ }
128
+ const updated = await store.updateProject(tenantId, pid, request.body as any);
129
+ return {
130
+ success: true,
131
+ message: "Successfully updated project",
132
+ data: updated,
133
+ };
134
+ } catch (err: unknown) {
135
+ const message = err instanceof Error ? err.message : "Failed to update project";
136
+ return reply.status(500).send({ success: false, message });
137
+ }
138
+ }
139
+
140
+ export async function deleteProject(
141
+ request: FastifyRequest<{ Params: { pid: string } }>,
142
+ reply: FastifyReply
143
+ ): Promise<{ success: boolean; message: string }> {
144
+ try {
145
+ const tenantId = getTenantId(request);
146
+ const store = getEvalStore();
147
+ const { pid } = request.params;
148
+ const existing = await store.getProjectById(tenantId, pid);
149
+ if (!existing) {
150
+ return reply.status(404).send({ success: false, message: "Project not found" });
151
+ }
152
+ const deleted = await store.deleteProject(tenantId, pid);
153
+ if (!deleted) {
154
+ return reply.status(500).send({ success: false, message: "Failed to delete project" });
155
+ }
156
+ return { success: true, message: "Successfully deleted project" };
157
+ } catch (err: unknown) {
158
+ const message = err instanceof Error ? err.message : "Failed to delete project";
159
+ return reply.status(500).send({ success: false, message });
160
+ }
161
+ }
162
+
163
+ export async function createSuite(
164
+ request: FastifyRequest<{ Params: { pid: string }; Body: Record<string, unknown> }>,
165
+ reply: FastifyReply
166
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
167
+ try {
168
+ const tenantId = getTenantId(request);
169
+ const store = getEvalStore();
170
+ const { pid } = request.params;
171
+ const project = await store.getProjectById(tenantId, pid);
172
+ if (!project) {
173
+ return reply.status(404).send({ success: false, message: "Project not found" });
174
+ }
175
+ const id = uuidv4();
176
+ const data = request.body as any;
177
+ const suite = await store.createSuite(tenantId, pid, id, { name: data.name });
178
+ return reply.status(201).send({
179
+ success: true,
180
+ message: "Successfully created suite",
181
+ data: suite,
182
+ });
183
+ } catch (err: unknown) {
184
+ const message = err instanceof Error ? err.message : "Failed to create suite";
185
+ return reply.status(500).send({ success: false, message });
186
+ }
187
+ }
188
+
189
+ export async function updateSuite(
190
+ request: FastifyRequest<{ Params: { pid: string; sid: string }; Body: Record<string, unknown> }>,
191
+ reply: FastifyReply
192
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
193
+ try {
194
+ const tenantId = getTenantId(request);
195
+ const store = getEvalStore();
196
+ const { sid } = request.params;
197
+ const existing = await store.getSuiteById(tenantId, sid);
198
+ if (!existing) {
199
+ return reply.status(404).send({ success: false, message: "Suite not found" });
200
+ }
201
+ const updated = await store.updateSuite(tenantId, sid, request.body as any);
202
+ return {
203
+ success: true,
204
+ message: "Successfully updated suite",
205
+ data: updated,
206
+ };
207
+ } catch (err: unknown) {
208
+ const message = err instanceof Error ? err.message : "Failed to update suite";
209
+ return reply.status(500).send({ success: false, message });
210
+ }
211
+ }
212
+
213
+ export async function deleteSuite(
214
+ request: FastifyRequest<{ Params: { pid: string; sid: string } }>,
215
+ reply: FastifyReply
216
+ ): Promise<{ success: boolean; message: string }> {
217
+ try {
218
+ const tenantId = getTenantId(request);
219
+ const store = getEvalStore();
220
+ const { sid } = request.params;
221
+ const existing = await store.getSuiteById(tenantId, sid);
222
+ if (!existing) {
223
+ return reply.status(404).send({ success: false, message: "Suite not found" });
224
+ }
225
+ const deleted = await store.deleteSuite(tenantId, sid);
226
+ if (!deleted) {
227
+ return reply.status(500).send({ success: false, message: "Failed to delete suite" });
228
+ }
229
+ return { success: true, message: "Successfully deleted suite" };
230
+ } catch (err: unknown) {
231
+ const message = err instanceof Error ? err.message : "Failed to delete suite";
232
+ return reply.status(500).send({ success: false, message });
233
+ }
234
+ }
235
+
236
+ export async function createCase(
237
+ request: FastifyRequest<{ Params: { pid: string; sid: string }; Body: Record<string, unknown> }>,
238
+ reply: FastifyReply
239
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
240
+ try {
241
+ const tenantId = getTenantId(request);
242
+ const store = getEvalStore();
243
+ const { sid } = request.params;
244
+ const suite = await store.getSuiteById(tenantId, sid);
245
+ if (!suite) {
246
+ return reply.status(404).send({ success: false, message: "Suite not found" });
247
+ }
248
+ const id = uuidv4();
249
+ const data = request.body as any;
250
+ const created = await store.createCase(tenantId, sid, id, {
251
+ inputMessage: data.inputMessage,
252
+ inputFiles: data.inputFiles,
253
+ steps: data.steps ?? [],
254
+ outputType: data.outputType ?? "message_content",
255
+ contentAssertion: data.contentAssertion ?? "",
256
+ rubrics: data.rubrics,
257
+ });
258
+ return reply.status(201).send({
259
+ success: true,
260
+ message: "Successfully created case",
261
+ data: created,
262
+ });
263
+ } catch (err: unknown) {
264
+ const message = err instanceof Error ? err.message : "Failed to create case";
265
+ return reply.status(500).send({ success: false, message });
266
+ }
267
+ }
268
+
269
+ export async function listCasesForSuite(
270
+ request: FastifyRequest<{ Params: { pid: string; sid: string } }>,
271
+ reply: FastifyReply
272
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
273
+ try {
274
+ const tenantId = getTenantId(request);
275
+ const store = getEvalStore();
276
+ const { sid } = request.params;
277
+ const cases = await store.getCasesBySuite(tenantId, sid);
278
+ return {
279
+ success: true,
280
+ message: "Ok",
281
+ data: { records: cases, total: cases.length },
282
+ };
283
+ } catch (err: unknown) {
284
+ const message = err instanceof Error ? err.message : "Failed to list cases";
285
+ return reply.status(500).send({ success: false, message });
286
+ }
287
+ }
288
+
289
+ export async function updateCase(
290
+ request: FastifyRequest<{ Params: { pid: string; sid: string; cid: string }; Body: Record<string, unknown> }>,
291
+ reply: FastifyReply
292
+ ): Promise<{ success: boolean; message: string; data?: unknown }> {
293
+ try {
294
+ const tenantId = getTenantId(request);
295
+ const store = getEvalStore();
296
+ const { cid } = request.params;
297
+ const existing = await store.getCaseById(tenantId, cid);
298
+ if (!existing) {
299
+ return reply.status(404).send({ success: false, message: "Case not found" });
300
+ }
301
+ const updated = await store.updateCase(tenantId, cid, request.body as any);
302
+ return {
303
+ success: true,
304
+ message: "Successfully updated case",
305
+ data: updated,
306
+ };
307
+ } catch (err: unknown) {
308
+ const message = err instanceof Error ? err.message : "Failed to update case";
309
+ return reply.status(500).send({ success: false, message });
310
+ }
311
+ }
312
+
313
+ export async function deleteCase(
314
+ request: FastifyRequest<{ Params: { pid: string; sid: string; cid: string } }>,
315
+ reply: FastifyReply
316
+ ): Promise<{ success: boolean; message: string }> {
317
+ try {
318
+ const tenantId = getTenantId(request);
319
+ const store = getEvalStore();
320
+ const { cid } = request.params;
321
+ const existing = await store.getCaseById(tenantId, cid);
322
+ if (!existing) {
323
+ return reply.status(404).send({ success: false, message: "Case not found" });
324
+ }
325
+ const deleted = await store.deleteCase(tenantId, cid);
326
+ if (!deleted) {
327
+ return reply.status(500).send({ success: false, message: "Failed to delete case" });
328
+ }
329
+ return { success: true, message: "Successfully deleted case" };
330
+ } catch (err: unknown) {
331
+ const message = err instanceof Error ? err.message : "Failed to delete case";
332
+ return reply.status(500).send({ success: false, message });
333
+ }
334
+ }
335
+
336
+ export function registerEvalRoutes(app: FastifyInstance): void {
337
+ app.post("/api/eval/projects", createProject);
338
+ app.get("/api/eval/projects", listProjects);
339
+ app.get("/api/eval/projects/:pid", getProject);
340
+ app.put("/api/eval/projects/:pid", updateProject);
341
+ app.delete("/api/eval/projects/:pid", deleteProject);
342
+
343
+ app.post("/api/eval/projects/:pid/suites", createSuite);
344
+ app.put("/api/eval/projects/:pid/suites/:sid", updateSuite);
345
+ app.delete("/api/eval/projects/:pid/suites/:sid", deleteSuite);
346
+
347
+ app.post("/api/eval/projects/:pid/suites/:sid/cases", createCase);
348
+ app.get("/api/eval/projects/:pid/suites/:sid/cases", listCasesForSuite);
349
+ app.put("/api/eval/projects/:pid/suites/:sid/cases/:cid", updateCase);
350
+ app.delete("/api/eval/projects/:pid/suites/:sid/cases/:cid", deleteCase);
351
+
352
+ // POST /api/eval/projects/:pid/runs — trigger run
353
+ app.post("/api/eval/projects/:pid/runs", async (request, reply) => {
354
+ try {
355
+ const tenantId = getTenantId(request);
356
+ const { pid } = request.params as { pid: string };
357
+ const runId = await evalRunner.startRun(tenantId, pid);
358
+ reply.status(202).send({ success: true, message: "Run started", data: { run_id: runId } });
359
+ } catch (err) {
360
+ const msg = (err as Error).message;
361
+ const code = msg === "Project not found" ? 404 : msg.includes("already in progress") ? 409 : 500;
362
+ reply.status(code).send({ success: false, message: msg });
363
+ }
364
+ });
365
+
366
+ // GET /api/eval/runs — list runs
367
+ app.get("/api/eval/runs", async (request, reply) => {
368
+ try {
369
+ const tenantId = getTenantId(request);
370
+ const query = request.query as { project_id?: string; status?: string };
371
+ const runs = await getEvalStore().getRunsByTenant(tenantId, { projectId: query.project_id, status: query.status });
372
+ reply.send({ success: true, message: "Ok", data: { records: runs, total: runs.length } });
373
+ } catch (err) {
374
+ reply.status(500).send({ success: false, message: (err as Error).message });
375
+ }
376
+ });
377
+
378
+ // GET /api/eval/runs/:rid — get run detail with results
379
+ app.get("/api/eval/runs/:rid", async (request, reply) => {
380
+ try {
381
+ const tenantId = getTenantId(request);
382
+ const { rid } = request.params as { rid: string };
383
+ const run = await getEvalStore().getRunById(tenantId, rid);
384
+ if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
385
+ const results = await getEvalStore().getResultsByRun(tenantId, rid);
386
+ reply.send({ success: true, message: "Ok", data: { ...run, results } });
387
+ } catch (err) {
388
+ reply.status(500).send({ success: false, message: (err as Error).message });
389
+ }
390
+ });
391
+
392
+ // GET /api/eval/runs/:rid/stream — SSE progress stream
393
+ app.get("/api/eval/runs/:rid/stream", async (request, reply) => {
394
+ reply.hijack();
395
+ reply.raw.writeHead(200, {
396
+ "Content-Type": "text/event-stream",
397
+ "Cache-Control": "no-cache",
398
+ Connection: "keep-alive",
399
+ "Access-Control-Allow-Origin": "*",
400
+ });
401
+
402
+ const { rid } = request.params as { rid: string };
403
+ const emitter = evalRunner.getEventEmitter();
404
+ const eventKey = `run:${rid}`;
405
+
406
+ if (!evalRunner.isRunning(rid)) {
407
+ const tenantId = getTenantId(request);
408
+ const run = await getEvalStore().getRunById(tenantId, rid);
409
+ if (run) {
410
+ const eventType = run.status === "completed" ? "completed" : "error";
411
+ reply.raw.write(`event: ${eventType}\ndata: ${JSON.stringify({ passed: run.passedCases, failed: run.failedCases, avgScore: run.avgScore })}\n\n`);
412
+ }
413
+ reply.raw.end();
414
+ return;
415
+ }
416
+
417
+ const handler = (event: EvalStreamEvent): void => {
418
+ reply.raw.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
419
+ if (event.type === "completed" || event.type === "error") {
420
+ emitter.off(eventKey, handler);
421
+ reply.raw.end();
422
+ }
423
+ };
424
+
425
+ emitter.on(eventKey, handler);
426
+
427
+ request.raw.on("close", () => {
428
+ emitter.off(eventKey, handler);
429
+ });
430
+ });
431
+
432
+ // POST /api/eval/runs/:rid/abort — abort run
433
+ app.post("/api/eval/runs/:rid/abort", async (request, reply) => {
434
+ try {
435
+ const { rid } = request.params as { rid: string };
436
+ const aborted = await evalRunner.abortRun(rid);
437
+ if (!aborted) return reply.status(404).send({ success: false, message: "Run not found or not running" });
438
+ reply.send({ success: true, message: "Run aborted" });
439
+ } catch (err) {
440
+ reply.status(500).send({ success: false, message: (err as Error).message });
441
+ }
442
+ });
443
+
444
+ // DELETE /api/eval/runs/:rid — delete run and results
445
+ app.delete("/api/eval/runs/:rid", async (request, reply) => {
446
+ try {
447
+ const tenantId = getTenantId(request);
448
+ const { rid } = request.params as { rid: string };
449
+ const deleted = await getEvalStore().deleteRun(tenantId, rid);
450
+ if (!deleted) return reply.status(404).send({ success: false, message: "Run not found" });
451
+ reply.send({ success: true, message: "Run deleted" });
452
+ } catch (err) {
453
+ reply.status(500).send({ success: false, message: (err as Error).message });
454
+ }
455
+ });
456
+
457
+ // GET /api/eval/reports/projects/:pid — aggregated report
458
+ app.get("/api/eval/reports/projects/:pid", async (request, reply) => {
459
+ try {
460
+ const tenantId = getTenantId(request);
461
+ const { pid } = request.params as { pid: string };
462
+ const report = await getEvalStore().getProjectReport(tenantId, pid);
463
+ if (!report) return reply.status(404).send({ success: false, message: "Project not found" });
464
+ reply.send({ success: true, message: "Ok", data: report });
465
+ } catch (err) {
466
+ reply.status(500).send({ success: false, message: (err as Error).message });
467
+ }
468
+ });
469
+ }
@@ -27,6 +27,7 @@ interface ApiResponse<T = any> {
27
27
  async function getDefinitionsFromAssistants(tenantId: string): Promise<Array<{
28
28
  assistantId: string;
29
29
  assistantName: string;
30
+ description?: string;
30
31
  topologyEdges: { from: string; to: string; purpose: string }[];
31
32
  totalEdges: number;
32
33
  }>> {
@@ -47,10 +48,11 @@ async function getDefinitionsFromAssistants(tenantId: string): Promise<Array<{
47
48
  if (!def || def.type !== "processing") continue;
48
49
  if (!def.middleware) continue;
49
50
  for (const mw of def.middleware) {
50
- if (mw.type === "topology" && mw.enabled && mw.config?.edges?.length > 0) {
51
+ if (mw.type === "topology" && mw.enabled) {
51
52
  results.push({
52
53
  assistantId: a.id,
53
54
  assistantName: a.name,
55
+ description: a.description,
54
56
  topologyEdges: mw.config.edges,
55
57
  totalEdges: mw.config.edges.length,
56
58
  });
@@ -37,6 +37,7 @@ import { registerWorkspaceRoutes } from "../controllers/workspace";
37
37
  import { registerDatabaseConfigRoutes } from "../controllers/database-configs";
38
38
  import { registerMetricsServerConfigRoutes } from "../controllers/metrics-configs";
39
39
  import { registerMcpServerConfigRoutes } from "../controllers/mcp-configs";
40
+ import { registerEvalRoutes } from "../controllers/eval";
40
41
  import { registerUserRoutes } from "../controllers/users";
41
42
  import { registerTenantRoutes } from "../controllers/tenants";
42
43
  import { registerAuthRoutes } from "../controllers/auth";
@@ -320,6 +321,8 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
320
321
 
321
322
  registerSandboxProxyRoutes(app);
322
323
 
324
+ registerEvalRoutes(app);
325
+
323
326
  registerWorkspaceRoutes(app);
324
327
 
325
328
  registerDatabaseConfigRoutes(app);