@nirmos/sdk 0.3.2 → 0.3.4
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 +285 -17
- package/dist/cjs/core/transport.js +16 -3
- package/dist/cjs/core/transport.js.map +1 -1
- package/dist/cjs/core/version.js +6 -0
- package/dist/cjs/core/version.js.map +1 -0
- package/dist/cjs/evaluations/types.js +3 -0
- package/dist/cjs/evaluations/types.js.map +1 -0
- package/dist/cjs/gateway/chat-stream.js +10 -0
- package/dist/cjs/gateway/chat-stream.js.map +1 -1
- package/dist/cjs/gateway/chat.js +1 -0
- package/dist/cjs/gateway/chat.js.map +1 -1
- package/dist/cjs/gateway/embeddings.js +1 -0
- package/dist/cjs/gateway/embeddings.js.map +1 -1
- package/dist/cjs/gateway/mappers.js +39 -2
- package/dist/cjs/gateway/mappers.js.map +1 -1
- package/dist/cjs/memory/execution-types.js +3 -0
- package/dist/cjs/memory/execution-types.js.map +1 -0
- package/dist/esm/core/transport.js +15 -2
- package/dist/esm/core/transport.js.map +1 -1
- package/dist/esm/core/version.d.ts +2 -0
- package/dist/esm/core/version.d.ts.map +1 -0
- package/dist/esm/core/version.js +3 -0
- package/dist/esm/core/version.js.map +1 -0
- package/dist/esm/evaluations/types.d.ts +13 -0
- package/dist/esm/evaluations/types.d.ts.map +1 -0
- package/dist/esm/evaluations/types.js +2 -0
- package/dist/esm/evaluations/types.js.map +1 -0
- package/dist/esm/gateway/chat-stream.d.ts.map +1 -1
- package/dist/esm/gateway/chat-stream.js +11 -1
- package/dist/esm/gateway/chat-stream.js.map +1 -1
- package/dist/esm/gateway/chat.d.ts.map +1 -1
- package/dist/esm/gateway/chat.js +1 -0
- package/dist/esm/gateway/chat.js.map +1 -1
- package/dist/esm/gateway/embeddings.d.ts.map +1 -1
- package/dist/esm/gateway/embeddings.js +1 -0
- package/dist/esm/gateway/embeddings.js.map +1 -1
- package/dist/esm/gateway/mappers.d.ts +2 -0
- package/dist/esm/gateway/mappers.d.ts.map +1 -1
- package/dist/esm/gateway/mappers.js +38 -2
- package/dist/esm/gateway/mappers.js.map +1 -1
- package/dist/esm/gateway/types.d.ts +8 -0
- package/dist/esm/gateway/types.d.ts.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/management/contracts.d.ts +4865 -9009
- package/dist/esm/management/contracts.d.ts.map +1 -1
- package/dist/esm/memory/execution-types.d.ts +35 -0
- package/dist/esm/memory/execution-types.d.ts.map +1 -0
- package/dist/esm/memory/execution-types.js +2 -0
- package/dist/esm/memory/execution-types.js.map +1 -0
- package/dist/esm/memory/types.d.ts +9 -23
- package/dist/esm/memory/types.d.ts.map +1 -1
- package/examples/evaluation-offline.mjs +149 -0
- package/examples/evaluation-quickstart.mjs +205 -0
- package/examples/evaluation-quickstart.ts +250 -0
- package/examples/evaluation-trace-import.mjs +81 -0
- package/examples/evaluation-trace-import.ts +105 -0
- package/examples/memory-quickstart.mjs +199 -0
- package/examples/memory-quickstart.ts +240 -0
- package/examples/tsconfig.json +5 -0
- package/package.json +5 -4
- package/src/core/transport.ts +14 -2
- package/src/core/version.ts +2 -0
- package/src/evaluations/types.ts +18 -0
- package/src/gateway/chat-stream.ts +12 -1
- package/src/gateway/chat.ts +1 -0
- package/src/gateway/embeddings.ts +1 -0
- package/src/gateway/mappers.ts +55 -2
- package/src/gateway/types.ts +8 -0
- package/src/index.ts +11 -0
- package/src/management/contracts.ts +42 -79
- package/src/memory/execution-types.ts +2 -0
- package/src/memory/types.ts +10 -22
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Nirmos } from "@nirmos/sdk";
|
|
2
|
+
|
|
3
|
+
declare const process: { env: Record<string, string | undefined> };
|
|
4
|
+
|
|
5
|
+
function requireId(value: unknown, label: string): string {
|
|
6
|
+
if (typeof value !== "string" || !value) throw new Error(`Missing ${label}`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function evaluationQuickstart(
|
|
11
|
+
client: Nirmos,
|
|
12
|
+
options: {
|
|
13
|
+
providerId: string;
|
|
14
|
+
modelId: string;
|
|
15
|
+
cleanup?: boolean;
|
|
16
|
+
pollMs?: number;
|
|
17
|
+
maxPolls?: number;
|
|
18
|
+
},
|
|
19
|
+
) {
|
|
20
|
+
let datasetId: string | undefined;
|
|
21
|
+
let suiteId: string | undefined;
|
|
22
|
+
let evaluatorId: string | undefined;
|
|
23
|
+
let runId: string | undefined;
|
|
24
|
+
let terminal = false;
|
|
25
|
+
let submissionStarted = false;
|
|
26
|
+
try {
|
|
27
|
+
const dataset = await client.evaluations.datasets.create({
|
|
28
|
+
name: "SDK two-case demo",
|
|
29
|
+
});
|
|
30
|
+
datasetId = requireId(dataset.dataset.id, "dataset ID");
|
|
31
|
+
const datasetVersionId = requireId(
|
|
32
|
+
dataset.initialVersion?.id,
|
|
33
|
+
"dataset version ID",
|
|
34
|
+
);
|
|
35
|
+
console.log({ datasetId, datasetVersionId });
|
|
36
|
+
for (const column of [
|
|
37
|
+
{ key: "question", name: "Question", role: "input" as const },
|
|
38
|
+
{ key: "expected", name: "Expected", role: "expected" as const },
|
|
39
|
+
{ key: "topic", name: "Topic", role: "metadata" as const },
|
|
40
|
+
]) {
|
|
41
|
+
await client.evaluations.datasets.columns.create(datasetId, {
|
|
42
|
+
datasetVersionId,
|
|
43
|
+
...column,
|
|
44
|
+
dataType: "text",
|
|
45
|
+
required: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const data of [
|
|
49
|
+
{
|
|
50
|
+
question: "Reply with exactly Paris and nothing else.",
|
|
51
|
+
expected: "Paris",
|
|
52
|
+
topic: "cities",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
question: "Reply with exactly Tokyo and nothing else.",
|
|
56
|
+
expected: "Tokyo",
|
|
57
|
+
topic: "cities",
|
|
58
|
+
},
|
|
59
|
+
]) {
|
|
60
|
+
await client.evaluations.datasets.cases.create(datasetId, {
|
|
61
|
+
datasetVersionId,
|
|
62
|
+
data,
|
|
63
|
+
tags: ["sdk-demo"],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
await client.evaluations.datasets.versions.freeze(
|
|
67
|
+
datasetId,
|
|
68
|
+
datasetVersionId,
|
|
69
|
+
);
|
|
70
|
+
const suite = await client.evaluations.suites.create({
|
|
71
|
+
name: "SDK exact-match demo",
|
|
72
|
+
datasetVersionId,
|
|
73
|
+
});
|
|
74
|
+
suiteId = requireId(suite.id, "suite ID");
|
|
75
|
+
console.log({ suiteId });
|
|
76
|
+
const target = await client.evaluations.columns.create({
|
|
77
|
+
suiteId,
|
|
78
|
+
key: "candidate",
|
|
79
|
+
name: "Candidate",
|
|
80
|
+
category: "target",
|
|
81
|
+
enabled: true,
|
|
82
|
+
config: { role: "candidate", dataType: "output" },
|
|
83
|
+
target: {
|
|
84
|
+
type: "model",
|
|
85
|
+
name: "Demo model",
|
|
86
|
+
providerId: options.providerId,
|
|
87
|
+
modelId: options.modelId,
|
|
88
|
+
inputMapping: { user_message: "dataset.question" },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
// The target's logical ID is distinct from the immutable version used in runs.
|
|
92
|
+
const targetId = requireId(target.target?.id, "target ID");
|
|
93
|
+
const targetVersionId = requireId(
|
|
94
|
+
target.targetVersion?.id,
|
|
95
|
+
"target version ID",
|
|
96
|
+
);
|
|
97
|
+
const evaluator = await client.evaluations.evaluators.create({
|
|
98
|
+
name: "SDK exact match",
|
|
99
|
+
version: {
|
|
100
|
+
type: "exact_match",
|
|
101
|
+
category: "deterministic",
|
|
102
|
+
scope: "output",
|
|
103
|
+
inputSchema: [
|
|
104
|
+
{ key: "actual", type: "any", required: true },
|
|
105
|
+
{ key: "expected", type: "any", required: true },
|
|
106
|
+
],
|
|
107
|
+
config: {
|
|
108
|
+
caseSensitive: true,
|
|
109
|
+
trimWhitespace: true,
|
|
110
|
+
normalizeWhitespace: false,
|
|
111
|
+
},
|
|
112
|
+
outputType: "boolean",
|
|
113
|
+
scoreDirection: "none",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
evaluatorId = requireId(evaluator.evaluator.id, "evaluator ID");
|
|
117
|
+
const evaluatorVersionId = requireId(
|
|
118
|
+
evaluator.version?.id,
|
|
119
|
+
"evaluator version ID",
|
|
120
|
+
);
|
|
121
|
+
console.log({ evaluatorId, targetVersionId, evaluatorVersionId });
|
|
122
|
+
await client.evaluations.columns.create({
|
|
123
|
+
suiteId,
|
|
124
|
+
key: "exact_match",
|
|
125
|
+
name: "Exact match",
|
|
126
|
+
category: "evaluator",
|
|
127
|
+
enabled: true,
|
|
128
|
+
evaluatorVersionId,
|
|
129
|
+
config: {
|
|
130
|
+
inputMapping: {
|
|
131
|
+
actual: { source: "target", path: "output" },
|
|
132
|
+
expected: { source: "dataset", key: "expected" },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const experiment = await client.evaluations.experiments.prepare({
|
|
137
|
+
suiteId,
|
|
138
|
+
datasetVersionId,
|
|
139
|
+
targetVersionIds: [targetVersionId],
|
|
140
|
+
evaluatorVersionIds: [evaluatorVersionId],
|
|
141
|
+
trialCount: 1,
|
|
142
|
+
});
|
|
143
|
+
const experimentId = requireId(experiment.id, "experiment ID");
|
|
144
|
+
// Persist this input and submissionId if implementing replay across process restarts.
|
|
145
|
+
const submissionId = crypto.randomUUID();
|
|
146
|
+
console.log({ experimentId, submissionId });
|
|
147
|
+
submissionStarted = true;
|
|
148
|
+
const created = await client.evaluations.runs.create(
|
|
149
|
+
{
|
|
150
|
+
experimentId,
|
|
151
|
+
cases: "all",
|
|
152
|
+
targetIds: [targetVersionId],
|
|
153
|
+
evaluatorIds: [evaluatorVersionId],
|
|
154
|
+
trialCount: 1,
|
|
155
|
+
},
|
|
156
|
+
{ idempotencyKey: submissionId, maxRetries: 0 },
|
|
157
|
+
);
|
|
158
|
+
runId = created.id;
|
|
159
|
+
console.log({ runId });
|
|
160
|
+
for (let attempt = 0; attempt < (options.maxPolls ?? 60); attempt++) {
|
|
161
|
+
const run = await client.evaluations.runs.get(runId, {
|
|
162
|
+
timeoutMs: 10_000,
|
|
163
|
+
maxRetries: 0,
|
|
164
|
+
});
|
|
165
|
+
if (["COMPLETED", "FAILED", "CANCELLED"].includes(run.status)) {
|
|
166
|
+
terminal = true;
|
|
167
|
+
const results = await client.evaluations.runs.results(runId, {
|
|
168
|
+
page: 1,
|
|
169
|
+
pageSize: 25,
|
|
170
|
+
});
|
|
171
|
+
for (const trial of results.data) {
|
|
172
|
+
console.log({
|
|
173
|
+
caseId: trial.caseId,
|
|
174
|
+
trialStatus: trial.status,
|
|
175
|
+
error: trial.error,
|
|
176
|
+
});
|
|
177
|
+
for (const score of trial.scores) {
|
|
178
|
+
console.log({
|
|
179
|
+
evaluatorVersionId: score.evaluatorVersionId,
|
|
180
|
+
status: score.status,
|
|
181
|
+
value: score.value,
|
|
182
|
+
passed: score.passed,
|
|
183
|
+
error: score.error,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Two cases × one target × one trial fit on one page.
|
|
188
|
+
if (run.status !== "COMPLETED")
|
|
189
|
+
throw new Error(`Run ${runId}: ${run.status}`);
|
|
190
|
+
if (
|
|
191
|
+
results.data.length !== 2 ||
|
|
192
|
+
results.data.some(
|
|
193
|
+
(trial) =>
|
|
194
|
+
trial.status !== "COMPLETED" ||
|
|
195
|
+
trial.scores.length !== 1 ||
|
|
196
|
+
trial.scores.some(
|
|
197
|
+
(score) => score.status !== "COMPLETED" || score.error,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"Run ended with missing or failed trials/scores; inspect results",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return { runId, results }; // passed:false is a valid evaluation outcome.
|
|
206
|
+
}
|
|
207
|
+
await new Promise((resolve) =>
|
|
208
|
+
setTimeout(resolve, options.pollMs ?? 2000),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
await client.evaluations.runs.cancel(runId);
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Polling limit reached; cancellation requested for ${runId}. Confirm terminal state before cleanup.`,
|
|
214
|
+
);
|
|
215
|
+
} finally {
|
|
216
|
+
// Never archive inputs while background execution might still use them.
|
|
217
|
+
if (options.cleanup && (!submissionStarted || terminal)) {
|
|
218
|
+
const cleanup = await Promise.allSettled([
|
|
219
|
+
...(suiteId ? [client.evaluations.suites.archive(suiteId)] : []),
|
|
220
|
+
...(evaluatorId
|
|
221
|
+
? [client.evaluations.evaluators.archive(evaluatorId)]
|
|
222
|
+
: []),
|
|
223
|
+
...(datasetId ? [client.evaluations.datasets.archive(datasetId)] : []),
|
|
224
|
+
]);
|
|
225
|
+
for (const result of cleanup)
|
|
226
|
+
if (result.status === "rejected")
|
|
227
|
+
console.error("Archive failed:", result.reason);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (process.env.NIRMOS_RUN_EVALUATION_EXAMPLE === "1") {
|
|
233
|
+
const key = requireId(process.env.NIRMOS_API_KEY, "NIRMOS_API_KEY");
|
|
234
|
+
await evaluationQuickstart(
|
|
235
|
+
new Nirmos({
|
|
236
|
+
auth: { type: "api-key", key },
|
|
237
|
+
apiBaseUrl: process.env.NIRMOS_API_URL,
|
|
238
|
+
runtimeBaseUrl: process.env.NIRMOS_RUNTIME_URL,
|
|
239
|
+
maxRetries: 0,
|
|
240
|
+
}),
|
|
241
|
+
{
|
|
242
|
+
providerId: requireId(
|
|
243
|
+
process.env.NIRMOS_PROVIDER_ID,
|
|
244
|
+
"NIRMOS_PROVIDER_ID",
|
|
245
|
+
),
|
|
246
|
+
modelId: requireId(process.env.NIRMOS_MODEL_ID, "NIRMOS_MODEL_ID"),
|
|
247
|
+
cleanup: process.env.EVALUATION_CLEANUP === "1",
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Generated from evaluation-trace-import.ts. Do not edit.
|
|
2
|
+
import { APIError, Nirmos } from "@nirmos/sdk";
|
|
3
|
+
/** Destination draft needs required JSON columns named input and output. */
|
|
4
|
+
export async function importGatewayTrace(client, options) {
|
|
5
|
+
const response = await client.gateway.chat.create({
|
|
6
|
+
model: options.model,
|
|
7
|
+
messages: [{ role: "user", content: "What is the capital of France?" }],
|
|
8
|
+
}, { maxRetries: 0 });
|
|
9
|
+
const requestId = response.metadata.requestId;
|
|
10
|
+
if (!requestId)
|
|
11
|
+
throw new Error("Gateway response omitted public request ID");
|
|
12
|
+
const deadline = Date.now() + (options.maxWaitMs ?? 30_000);
|
|
13
|
+
while (true) {
|
|
14
|
+
const remaining = deadline - Date.now();
|
|
15
|
+
if (remaining <= 0)
|
|
16
|
+
throw new Error(`Trace ${requestId} unavailable after bounded wait; check logging and retention`);
|
|
17
|
+
try {
|
|
18
|
+
// Logs API accepts the same public request ID. Existence confirms persistence,
|
|
19
|
+
// not eligibility: import performs the authoritative payload/operation checks.
|
|
20
|
+
await client.logs.get(requestId, {
|
|
21
|
+
timeoutMs: Math.min(5_000, remaining),
|
|
22
|
+
maxRetries: 0,
|
|
23
|
+
});
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!(error instanceof APIError) || error.status !== 404)
|
|
28
|
+
throw error;
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(options.pollMs ?? 1_000, Math.max(0, deadline - Date.now()))));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const imported = await client.evaluations.datasets.importTrace({
|
|
33
|
+
requestLogId: requestId,
|
|
34
|
+
datasetVersionId: options.datasetVersionId,
|
|
35
|
+
mapping: { input: "input", output: "output" },
|
|
36
|
+
tags: ["gateway-regression"],
|
|
37
|
+
}, { maxRetries: 0 });
|
|
38
|
+
const datasetCase = await client.evaluations.datasets.cases.get(imported.case.id);
|
|
39
|
+
return { requestId, imported, datasetCase };
|
|
40
|
+
}
|
|
41
|
+
if (process.env.NIRMOS_RUN_TRACE_EXAMPLE === "1") {
|
|
42
|
+
const apiKey = process.env.NIRMOS_API_KEY;
|
|
43
|
+
const model = process.env.NIRMOS_CHAT_MODEL;
|
|
44
|
+
if (!apiKey || !model)
|
|
45
|
+
throw new Error("Set NIRMOS_API_KEY and NIRMOS_CHAT_MODEL");
|
|
46
|
+
const client = new Nirmos({
|
|
47
|
+
auth: { type: "api-key", key: apiKey },
|
|
48
|
+
apiBaseUrl: process.env.NIRMOS_API_URL,
|
|
49
|
+
runtimeBaseUrl: process.env.NIRMOS_RUNTIME_URL,
|
|
50
|
+
maxRetries: 0,
|
|
51
|
+
});
|
|
52
|
+
const dataset = await client.evaluations.datasets.create({
|
|
53
|
+
name: "Gateway trace demo",
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
for (const key of ["input", "output"]) {
|
|
57
|
+
await client.evaluations.datasets.columns.create(dataset.dataset.id, {
|
|
58
|
+
datasetVersionId: dataset.initialVersion.id,
|
|
59
|
+
key,
|
|
60
|
+
name: key,
|
|
61
|
+
dataType: "json",
|
|
62
|
+
role: key === "input" ? "input" : "expected",
|
|
63
|
+
required: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const result = await importGatewayTrace(client, {
|
|
67
|
+
datasetVersionId: dataset.initialVersion.id,
|
|
68
|
+
model,
|
|
69
|
+
});
|
|
70
|
+
console.log(result.requestId, result.imported.case.id, result.datasetCase.values);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof APIError)
|
|
74
|
+
console.error(error.code, error.message);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (process.env.EVALUATION_CLEANUP === "1")
|
|
79
|
+
await client.evaluations.datasets.archive(dataset.dataset.id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { APIError, Nirmos } from "@nirmos/sdk";
|
|
2
|
+
|
|
3
|
+
declare const process: { env: Record<string, string | undefined> };
|
|
4
|
+
|
|
5
|
+
/** Destination draft needs required JSON columns named input and output. */
|
|
6
|
+
export async function importGatewayTrace(
|
|
7
|
+
client: Nirmos,
|
|
8
|
+
options: {
|
|
9
|
+
datasetVersionId: string;
|
|
10
|
+
model: string;
|
|
11
|
+
maxWaitMs?: number;
|
|
12
|
+
pollMs?: number;
|
|
13
|
+
},
|
|
14
|
+
) {
|
|
15
|
+
const response = await client.gateway.chat.create(
|
|
16
|
+
{
|
|
17
|
+
model: options.model,
|
|
18
|
+
messages: [{ role: "user", content: "What is the capital of France?" }],
|
|
19
|
+
},
|
|
20
|
+
{ maxRetries: 0 },
|
|
21
|
+
);
|
|
22
|
+
const requestId = response.metadata.requestId;
|
|
23
|
+
if (!requestId) throw new Error("Gateway response omitted public request ID");
|
|
24
|
+
const deadline = Date.now() + (options.maxWaitMs ?? 30_000);
|
|
25
|
+
while (true) {
|
|
26
|
+
const remaining = deadline - Date.now();
|
|
27
|
+
if (remaining <= 0)
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Trace ${requestId} unavailable after bounded wait; check logging and retention`,
|
|
30
|
+
);
|
|
31
|
+
try {
|
|
32
|
+
// Logs API accepts the same public request ID. Existence confirms persistence,
|
|
33
|
+
// not eligibility: import performs the authoritative payload/operation checks.
|
|
34
|
+
await client.logs.get(requestId, {
|
|
35
|
+
timeoutMs: Math.min(5_000, remaining),
|
|
36
|
+
maxRetries: 0,
|
|
37
|
+
});
|
|
38
|
+
break;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!(error instanceof APIError) || error.status !== 404) throw error;
|
|
41
|
+
await new Promise((resolve) =>
|
|
42
|
+
setTimeout(
|
|
43
|
+
resolve,
|
|
44
|
+
Math.min(options.pollMs ?? 1_000, Math.max(0, deadline - Date.now())),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const imported = await client.evaluations.datasets.importTrace(
|
|
50
|
+
{
|
|
51
|
+
requestLogId: requestId,
|
|
52
|
+
datasetVersionId: options.datasetVersionId,
|
|
53
|
+
mapping: { input: "input", output: "output" },
|
|
54
|
+
tags: ["gateway-regression"],
|
|
55
|
+
},
|
|
56
|
+
{ maxRetries: 0 },
|
|
57
|
+
);
|
|
58
|
+
const datasetCase = await client.evaluations.datasets.cases.get(
|
|
59
|
+
imported.case.id,
|
|
60
|
+
);
|
|
61
|
+
return { requestId, imported, datasetCase };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (process.env.NIRMOS_RUN_TRACE_EXAMPLE === "1") {
|
|
65
|
+
const apiKey = process.env.NIRMOS_API_KEY;
|
|
66
|
+
const model = process.env.NIRMOS_CHAT_MODEL;
|
|
67
|
+
if (!apiKey || !model)
|
|
68
|
+
throw new Error("Set NIRMOS_API_KEY and NIRMOS_CHAT_MODEL");
|
|
69
|
+
const client = new Nirmos({
|
|
70
|
+
auth: { type: "api-key", key: apiKey },
|
|
71
|
+
apiBaseUrl: process.env.NIRMOS_API_URL,
|
|
72
|
+
runtimeBaseUrl: process.env.NIRMOS_RUNTIME_URL,
|
|
73
|
+
maxRetries: 0,
|
|
74
|
+
});
|
|
75
|
+
const dataset = await client.evaluations.datasets.create({
|
|
76
|
+
name: "Gateway trace demo",
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
for (const key of ["input", "output"]) {
|
|
80
|
+
await client.evaluations.datasets.columns.create(dataset.dataset.id, {
|
|
81
|
+
datasetVersionId: dataset.initialVersion.id,
|
|
82
|
+
key,
|
|
83
|
+
name: key,
|
|
84
|
+
dataType: "json",
|
|
85
|
+
role: key === "input" ? "input" : "expected",
|
|
86
|
+
required: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const result = await importGatewayTrace(client, {
|
|
90
|
+
datasetVersionId: dataset.initialVersion.id,
|
|
91
|
+
model,
|
|
92
|
+
});
|
|
93
|
+
console.log(
|
|
94
|
+
result.requestId,
|
|
95
|
+
result.imported.case.id,
|
|
96
|
+
result.datasetCase.values,
|
|
97
|
+
);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof APIError) console.error(error.code, error.message);
|
|
100
|
+
throw error;
|
|
101
|
+
} finally {
|
|
102
|
+
if (process.env.EVALUATION_CLEANUP === "1")
|
|
103
|
+
await client.evaluations.datasets.archive(dataset.dataset.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Generated from memory-quickstart.ts. Do not edit.
|
|
2
|
+
import { APIError, Nirmos, } from "@nirmos/sdk";
|
|
3
|
+
async function all(load) {
|
|
4
|
+
const items = [];
|
|
5
|
+
let cursor;
|
|
6
|
+
do {
|
|
7
|
+
const page = await load(cursor);
|
|
8
|
+
items.push(...page.items);
|
|
9
|
+
cursor = page.nextCursor ?? undefined;
|
|
10
|
+
} while (cursor);
|
|
11
|
+
return items;
|
|
12
|
+
}
|
|
13
|
+
/** Server-side only. Persist eventKey with the source event before first ingestion. */
|
|
14
|
+
export async function memoryQuickstart(client, options) {
|
|
15
|
+
const pause = () => new Promise((resolve) => setTimeout(resolve, options.paceMs ?? 20000));
|
|
16
|
+
const provider = (await all((cursor) => client.providers.list({ cursor, limit: 100 }))).find((p) => p.slug === "google");
|
|
17
|
+
if (!provider)
|
|
18
|
+
throw new Error("Google provider missing from this deployment's catalog.");
|
|
19
|
+
// Optional SDK setup; dashboard Gateway Credentials → Add credential → Google is equivalent.
|
|
20
|
+
if (options.googleApiKey)
|
|
21
|
+
await client.providerCredentials.create({
|
|
22
|
+
name: "Memory quickstart Google",
|
|
23
|
+
providerId: provider.id,
|
|
24
|
+
credentialType: "api_key",
|
|
25
|
+
secretData: { apiKey: options.googleApiKey },
|
|
26
|
+
isDefault: true,
|
|
27
|
+
});
|
|
28
|
+
const models = await all((cursor) => client.models.list({ providerId: provider.id, limit: 100, cursor }));
|
|
29
|
+
const text = models.find((m) => m.slug === "google-gemini-2-5-flash-lite" && m.availability);
|
|
30
|
+
const embedding = models.find((m) => m.slug === "google-gemini-embedding-001" && m.availability);
|
|
31
|
+
if (!text || !embedding)
|
|
32
|
+
throw new Error("Configure an active Google credential and verify both models in models.list(). Availability does not guarantee provider quota.");
|
|
33
|
+
if (!embedding.knowledgeSupportedDimensions?.includes(768))
|
|
34
|
+
throw new Error("Selected embedding model does not support 768 dimensions in this deployment.");
|
|
35
|
+
const textModel = `${provider.slug}/${text.id}`;
|
|
36
|
+
const policyInput = {
|
|
37
|
+
policyKey: `${options.eventKey}-policy`,
|
|
38
|
+
name: "Memory quickstart",
|
|
39
|
+
version: 1,
|
|
40
|
+
status: "active",
|
|
41
|
+
captureTypes: [
|
|
42
|
+
"preference",
|
|
43
|
+
"fact",
|
|
44
|
+
"project_fact",
|
|
45
|
+
"decision",
|
|
46
|
+
"constraint",
|
|
47
|
+
"commitment",
|
|
48
|
+
"event",
|
|
49
|
+
"temporary_state",
|
|
50
|
+
],
|
|
51
|
+
allowInferredMemories: false,
|
|
52
|
+
minimumConfidence: 0.7,
|
|
53
|
+
minimumFutureUtility: 0.5,
|
|
54
|
+
defaultMemoryTTLSeconds: 86400,
|
|
55
|
+
extractionConfig: {
|
|
56
|
+
model: textModel,
|
|
57
|
+
reconciliationModel: textModel,
|
|
58
|
+
profileModel: textModel,
|
|
59
|
+
observationModel: textModel,
|
|
60
|
+
indexing: {
|
|
61
|
+
model: `${provider.slug}/${embedding.id}`,
|
|
62
|
+
dimensions: 768,
|
|
63
|
+
distanceMetric: "cosine",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
privacyConfig: {
|
|
67
|
+
allowPII: false,
|
|
68
|
+
allowedSensitivities: ["unclassified", "public", "internal"],
|
|
69
|
+
},
|
|
70
|
+
retentionConfig: {
|
|
71
|
+
episodeContent: "full",
|
|
72
|
+
evidenceExcerpts: true,
|
|
73
|
+
candidateContent: false,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const existing = await client.memory.policies.list({
|
|
77
|
+
policyKey: policyInput.policyKey,
|
|
78
|
+
limit: 100,
|
|
79
|
+
});
|
|
80
|
+
const policy = existing.items.find((p) => p.version === 1) ??
|
|
81
|
+
(await client.memory.policies.create(policyInput));
|
|
82
|
+
const spaceKey = `${options.eventKey}/customer-123`;
|
|
83
|
+
const spaces = await all((cursor) => client.memory.spaces.list({
|
|
84
|
+
externalSubjectId: "customer-123",
|
|
85
|
+
cursor,
|
|
86
|
+
limit: 100,
|
|
87
|
+
}));
|
|
88
|
+
const space = spaces.find((s) => s.spaceKey === spaceKey) ??
|
|
89
|
+
(await client.memory.spaces.create({
|
|
90
|
+
spaceKey,
|
|
91
|
+
name: "Disposable customer memory",
|
|
92
|
+
scopeType: "user",
|
|
93
|
+
scopeId: "customer-123",
|
|
94
|
+
externalSubjectId: "customer-123",
|
|
95
|
+
policyId: policy.id,
|
|
96
|
+
}));
|
|
97
|
+
const input = {
|
|
98
|
+
memorySpaceId: space.id,
|
|
99
|
+
conversationId: options.eventKey,
|
|
100
|
+
idempotencyKey: options.eventKey,
|
|
101
|
+
messages: [
|
|
102
|
+
{
|
|
103
|
+
id: `${options.eventKey}-message`,
|
|
104
|
+
role: "user",
|
|
105
|
+
content: "I prefer important notifications through email rather than WhatsApp.",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
result = await client.memory.ingest(input, {
|
|
112
|
+
idempotencyKey: options.eventKey,
|
|
113
|
+
maxRetries: 0,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof APIError) {
|
|
118
|
+
// Safe identifiers/categories only; never print secrets, prompts or memory content.
|
|
119
|
+
console.error({
|
|
120
|
+
code: error.code,
|
|
121
|
+
status: error.status,
|
|
122
|
+
retryable: error.retryable,
|
|
123
|
+
retryAfterMs: error.retryAfterMs,
|
|
124
|
+
requestId: error.requestId,
|
|
125
|
+
diagnostics: error.details,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// After correcting configuration/quota, call ingest(input) with the SAME event identity.
|
|
129
|
+
// Do not blindly retry, mutate the input, or generate a fresh key.
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
console.log({
|
|
133
|
+
runId: result.memoryRunId,
|
|
134
|
+
accepted: result.acceptedCount,
|
|
135
|
+
rejected: result.rejectedCount,
|
|
136
|
+
indexing: result.indexing,
|
|
137
|
+
});
|
|
138
|
+
const run = await client.memory.runs.get(result.memoryRunId);
|
|
139
|
+
if (result.indexing.failed)
|
|
140
|
+
throw new Error(`Indexing incomplete for run ${run.id}; inspect diagnostics and repair through memory.reindex().`);
|
|
141
|
+
// A completed event replays without model execution or another memory write.
|
|
142
|
+
const replay = await client.memory.ingest(input, {
|
|
143
|
+
idempotencyKey: options.eventKey,
|
|
144
|
+
maxRetries: 0,
|
|
145
|
+
});
|
|
146
|
+
if (replay.memoryRunId !== result.memoryRunId || !replay.replayed)
|
|
147
|
+
throw new Error("Expected the original completed event to replay.");
|
|
148
|
+
console.log({ replayed: replay.replayed, runId: replay.memoryRunId });
|
|
149
|
+
await pause();
|
|
150
|
+
const search = await client.memory.search({
|
|
151
|
+
memorySpaceId: space.id,
|
|
152
|
+
query: "How should I send important notifications?",
|
|
153
|
+
relevance: "relevant",
|
|
154
|
+
});
|
|
155
|
+
console.log({ relevantMatches: search.memories.length });
|
|
156
|
+
await pause();
|
|
157
|
+
const context = await client.memory.context({
|
|
158
|
+
memorySpaceId: space.id,
|
|
159
|
+
messages: [{ role: "user", content: "How should you notify me?" }],
|
|
160
|
+
});
|
|
161
|
+
console.log({ contextMemories: context.memories.length });
|
|
162
|
+
await pause();
|
|
163
|
+
await client.memory.profiles.generate({ memorySpaceId: space.id });
|
|
164
|
+
const profile = await client.memory.profiles.get(space.id);
|
|
165
|
+
const items = await client.memory.items.list({
|
|
166
|
+
memorySpaceId: space.id,
|
|
167
|
+
limit: 25,
|
|
168
|
+
});
|
|
169
|
+
const first = items.items[0];
|
|
170
|
+
if (first) {
|
|
171
|
+
await client.memory.items.update(first.id, { importance: 0.8 });
|
|
172
|
+
await client.memory.items.evidence(first.id, { limit: 25 });
|
|
173
|
+
// Explicit cleanup only for this disposable example's space.
|
|
174
|
+
if (options.cleanup)
|
|
175
|
+
await client.memory.items.delete(first.id);
|
|
176
|
+
}
|
|
177
|
+
if (options.cleanup) {
|
|
178
|
+
await client.memory.spaces.delete(space.id);
|
|
179
|
+
await client.memory.erase(space.id);
|
|
180
|
+
await client.memory.policies.delete(policy.id);
|
|
181
|
+
}
|
|
182
|
+
return { space, policy, result, search, context, profile };
|
|
183
|
+
}
|
|
184
|
+
if (process.env.NIRMOS_RUN_MEMORY_EXAMPLE === "1") {
|
|
185
|
+
const token = process.env.NIRMOS_API_KEY;
|
|
186
|
+
const eventKey = process.env.MEMORY_EVENT_KEY;
|
|
187
|
+
if (!token || !eventKey)
|
|
188
|
+
throw new Error("Set NIRMOS_API_KEY and a persisted MEMORY_EVENT_KEY.");
|
|
189
|
+
await memoryQuickstart(new Nirmos({
|
|
190
|
+
auth: { type: "api-key", key: token },
|
|
191
|
+
apiBaseUrl: process.env.NIRMOS_API_BASE_URL ?? "http://localhost:4001",
|
|
192
|
+
runtimeBaseUrl: process.env.NIRMOS_RUNTIME_BASE_URL ?? "http://localhost:4000",
|
|
193
|
+
maxRetries: 0,
|
|
194
|
+
}), {
|
|
195
|
+
eventKey,
|
|
196
|
+
googleApiKey: process.env.GOOGLE_API_KEY,
|
|
197
|
+
cleanup: process.env.MEMORY_CLEANUP === "1",
|
|
198
|
+
});
|
|
199
|
+
}
|