@cartanova/qgrid-ai-sdk 0.1.0 → 2.0.2
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/dist/index.d.ts +3 -2
- package/dist/index.js +17 -15
- package/package.json +20 -13
- package/e2e/e2e-logger.ts +0 -112
- package/e2e/e2e.ts +0 -217
- package/src/index.test.ts +0 -338
- package/src/index.ts +0 -396
- package/src/index.types.ts +0 -131
- package/src/logger.test.ts +0 -563
- package/src/logger.ts +0 -364
- package/src/utils.ts +0 -305
- package/tsconfig.json +0 -15
- package/tsdown.config.ts +0 -9
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,8 @@ type QgridProviderOptions = {
|
|
|
30
30
|
};
|
|
31
31
|
type QgridSupportedModel = "openai/gpt-5.5" | "openai/gpt-5.4" | "openai/gpt-5.2" | "openai/gpt-5.4-mini" | "openai/gpt-5.3-codex" | "openai/gpt-5.3-codex-spark" | "anthropic/claude-haiku-4-5" | "anthropic/claude-sonnet-4" | "anthropic/claude-sonnet-4-5" | "anthropic/claude-sonnet-4-6" | "anthropic/claude-sonnet-4-7" | "anthropic/claude-opus-4" | "anthropic/claude-opus-4-1" | "anthropic/claude-opus-4-5" | "anthropic/claude-opus-4-6" | "anthropic/claude-opus-4-7";
|
|
32
32
|
type QgridLoggerConfig = {
|
|
33
|
-
|
|
33
|
+
/** qgrid 서버 주소. 기본값: QGRID_URL 환경변수 또는 http://localhost:44900 */
|
|
34
|
+
serverUrl?: string;
|
|
34
35
|
projectName?: string;
|
|
35
36
|
tokenName?: string;
|
|
36
37
|
/**
|
|
@@ -51,7 +52,7 @@ type QgridLoggerConfig = {
|
|
|
51
52
|
};
|
|
52
53
|
//#endregion
|
|
53
54
|
//#region src/logger.d.ts
|
|
54
|
-
declare function createQgridLogger(config
|
|
55
|
+
declare function createQgridLogger(config?: QgridLoggerConfig): TelemetrySettings;
|
|
55
56
|
//#endregion
|
|
56
57
|
//#region src/index.d.ts
|
|
57
58
|
declare function qgrid(modelId: QgridSupportedModel, config?: QgridProviderConfig): LanguageModelV3;
|
package/dist/index.js
CHANGED
|
@@ -237,7 +237,9 @@ function timedKeySet() {
|
|
|
237
237
|
}
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
|
-
function createQgridLogger(config) {
|
|
240
|
+
function createQgridLogger(config = {}) {
|
|
241
|
+
const serverUrl = config.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900";
|
|
242
|
+
const onLogError = config.onLogError ?? ((e) => console.warn(`[qgrid-logger] ${e.message}`));
|
|
241
243
|
const runs = /* @__PURE__ */ new Map();
|
|
242
244
|
const keyTtl = typeof config.staleRunTimeoutMs === "number" && config.staleRunTimeoutMs > 0 ? config.staleRunTimeoutMs : DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
243
245
|
const suppressedQgrid = timedKeySet();
|
|
@@ -249,7 +251,7 @@ function createQgridLogger(config) {
|
|
|
249
251
|
runs.delete(runKey);
|
|
250
252
|
if (run.watchdog) clearTimeout(run.watchdog);
|
|
251
253
|
run.cleanupAbortListener?.();
|
|
252
|
-
for (const pending of run.pendingToolCalls) run.pendingSteps.push(appendStep(
|
|
254
|
+
for (const pending of run.pendingToolCalls) run.pendingSteps.push(appendStep(serverUrl, {
|
|
253
255
|
requestLogId: run.requestLogId,
|
|
254
256
|
stepIndex: pending.stepIndex,
|
|
255
257
|
type: "tool_call",
|
|
@@ -258,10 +260,10 @@ function createQgridLogger(config) {
|
|
|
258
260
|
toolName: pending.toolName,
|
|
259
261
|
toolArgs: pending.toolArgs,
|
|
260
262
|
toolDurationMs: run.toolDurations.get(pending.toolCallId)
|
|
261
|
-
}).catch((e) =>
|
|
263
|
+
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
262
264
|
run.pendingToolCalls = [];
|
|
263
265
|
await Promise.allSettled(run.pendingSteps);
|
|
264
|
-
await finishRun(
|
|
266
|
+
await finishRun(serverUrl, {
|
|
265
267
|
requestLogId: run.requestLogId,
|
|
266
268
|
status: result.status,
|
|
267
269
|
response: result.response,
|
|
@@ -273,7 +275,7 @@ function createQgridLogger(config) {
|
|
|
273
275
|
totalDurationMs: Date.now() - run.startTime,
|
|
274
276
|
history: run.history,
|
|
275
277
|
...result.errorMessage ? { errorMessage: result.errorMessage } : {}
|
|
276
|
-
}).catch((e) =>
|
|
278
|
+
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e))));
|
|
277
279
|
};
|
|
278
280
|
let autoRunIdCounter = 0;
|
|
279
281
|
const resolveRunKey = (event) => {
|
|
@@ -289,7 +291,7 @@ function createQgridLogger(config) {
|
|
|
289
291
|
if (!event.metadata?.qgridRunId && !event.functionId && event.metadata) event.metadata.qgridRunId = `auto-${++autoRunIdCounter}`;
|
|
290
292
|
const runKey = resolveRunKey(event);
|
|
291
293
|
if (quarantined.has(runKey)) {
|
|
292
|
-
|
|
294
|
+
onLogError(/* @__PURE__ */ new Error("createQgridLogger: telemetry key is quarantined after overlap"));
|
|
293
295
|
return;
|
|
294
296
|
}
|
|
295
297
|
if (event.model.provider === "qgrid") {
|
|
@@ -303,12 +305,12 @@ function createQgridLogger(config) {
|
|
|
303
305
|
errorMessage: msg
|
|
304
306
|
});
|
|
305
307
|
quarantined.add(runKey, keyTtl);
|
|
306
|
-
|
|
308
|
+
onLogError(new Error(msg));
|
|
307
309
|
return;
|
|
308
310
|
}
|
|
309
311
|
try {
|
|
310
312
|
const messages = event.messages ?? (Array.isArray(event.prompt) ? event.prompt : void 0);
|
|
311
|
-
const result = await createRun(
|
|
313
|
+
const result = await createRun(serverUrl, {
|
|
312
314
|
userPrompt: extractUserPrompt(event.prompt, messages),
|
|
313
315
|
systemPrompt: extractSystemPrompt(event.system),
|
|
314
316
|
modelName: event.model.modelId,
|
|
@@ -358,7 +360,7 @@ function createQgridLogger(config) {
|
|
|
358
360
|
finishing: false
|
|
359
361
|
});
|
|
360
362
|
} catch (e) {
|
|
361
|
-
|
|
363
|
+
onLogError(e instanceof Error ? e : new Error(String(e)));
|
|
362
364
|
}
|
|
363
365
|
},
|
|
364
366
|
onToolCallFinish(event) {
|
|
@@ -379,7 +381,7 @@ function createQgridLogger(config) {
|
|
|
379
381
|
const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === pending.toolCallId);
|
|
380
382
|
const te = content.find((p) => p.type === "tool-error" && p.toolCallId === pending.toolCallId);
|
|
381
383
|
if (tr || te) {
|
|
382
|
-
run.pendingSteps.push(appendStep(
|
|
384
|
+
run.pendingSteps.push(appendStep(serverUrl, {
|
|
383
385
|
requestLogId: run.requestLogId,
|
|
384
386
|
stepIndex: pending.stepIndex,
|
|
385
387
|
type: "tool_call",
|
|
@@ -390,12 +392,12 @@ function createQgridLogger(config) {
|
|
|
390
392
|
toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
|
|
391
393
|
toolDurationMs: run.toolDurations.get(pending.toolCallId),
|
|
392
394
|
error: te && "error" in te ? safeStringify(te.error) : void 0
|
|
393
|
-
}).catch((e) =>
|
|
395
|
+
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
394
396
|
run.toolDurations.delete(pending.toolCallId);
|
|
395
397
|
} else remainingPending.push(pending);
|
|
396
398
|
}
|
|
397
399
|
run.pendingToolCalls = remainingPending;
|
|
398
|
-
run.pendingSteps.push(appendStep(
|
|
400
|
+
run.pendingSteps.push(appendStep(serverUrl, {
|
|
399
401
|
requestLogId: run.requestLogId,
|
|
400
402
|
stepIndex: stepNumber,
|
|
401
403
|
type: "generate",
|
|
@@ -406,13 +408,13 @@ function createQgridLogger(config) {
|
|
|
406
408
|
finishReason,
|
|
407
409
|
reasoningText: typeof reasoningText === "string" && reasoningText.length > 0 ? reasoningText : void 0,
|
|
408
410
|
reasoningTokens: usage.outputTokenDetails?.reasoningTokens
|
|
409
|
-
}).catch((e) =>
|
|
411
|
+
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
410
412
|
const toolCalls = content.filter((p) => p.type === "tool-call");
|
|
411
413
|
for (const [i, tc] of toolCalls.entries()) {
|
|
412
414
|
const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === tc.toolCallId);
|
|
413
415
|
const te = content.find((p) => p.type === "tool-error" && p.toolCallId === tc.toolCallId);
|
|
414
416
|
if (tr || te) {
|
|
415
|
-
run.pendingSteps.push(appendStep(
|
|
417
|
+
run.pendingSteps.push(appendStep(serverUrl, {
|
|
416
418
|
requestLogId: run.requestLogId,
|
|
417
419
|
stepIndex: stepNumber,
|
|
418
420
|
type: "tool_call",
|
|
@@ -423,7 +425,7 @@ function createQgridLogger(config) {
|
|
|
423
425
|
toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
|
|
424
426
|
toolDurationMs: run.toolDurations.get(tc.toolCallId),
|
|
425
427
|
error: te && "error" in te ? safeStringify(te.error) : void 0
|
|
426
|
-
}).catch((e) =>
|
|
428
|
+
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
427
429
|
run.toolDurations.delete(tc.toolCallId);
|
|
428
430
|
} else run.pendingToolCalls.push({
|
|
429
431
|
stepIndex: stepNumber,
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartanova/qgrid-ai-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "AI SDK LanguageModelV3 provider for qgrid",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/cartanova-ai/qgrid"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
5
12
|
"type": "module",
|
|
6
13
|
"main": "dist/index.js",
|
|
7
14
|
"types": "dist/index.d.ts",
|
|
@@ -11,21 +18,21 @@
|
|
|
11
18
|
"types": "./dist/index.d.ts"
|
|
12
19
|
}
|
|
13
20
|
},
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsdown && mv dist/index-*.d.ts dist/index.d.ts",
|
|
16
|
-
"test": "vitest run",
|
|
17
|
-
"e2e": "tsx e2e/e2e.ts",
|
|
18
|
-
"e2e:logger": "tsx e2e/e2e-logger.ts"
|
|
19
|
-
},
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"@ai-sdk/provider": "
|
|
22
|
-
"ai": "
|
|
23
|
-
"tsdown": "
|
|
24
|
-
"typescript": "
|
|
25
|
-
"vitest": "
|
|
22
|
+
"@ai-sdk/provider": "^3.0.0",
|
|
23
|
+
"ai": "^6.0.190",
|
|
24
|
+
"tsdown": "^0.12.9",
|
|
25
|
+
"typescript": "^5.9.3",
|
|
26
|
+
"vitest": "^4.0.10"
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
28
29
|
"@ai-sdk/provider": "^3.0.0",
|
|
29
30
|
"ai": "^6.0.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown && mv dist/index-*.d.ts dist/index.d.ts",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"e2e": "tsx e2e/e2e.ts",
|
|
36
|
+
"e2e:logger": "tsx e2e/e2e-logger.ts"
|
|
30
37
|
}
|
|
31
|
-
}
|
|
38
|
+
}
|
package/e2e/e2e-logger.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartanova/qgrid-ai-sdk Logger E2E Test
|
|
3
|
-
*
|
|
4
|
-
* TelemetryIntegration 기반 logger가 실서버에 run lifecycle을 기록하는지 검증.
|
|
5
|
-
* qgrid 서버(localhost:44900)가 떠 있어야 합니다.
|
|
6
|
-
*
|
|
7
|
-
* 사용법: pnpm --filter @cartanova/qgrid-ai-sdk e2e:logger
|
|
8
|
-
*/
|
|
9
|
-
import { generateText, stepCountIs, tool } from "ai";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
|
|
12
|
-
import { createQgridLogger, qgrid } from "../src/index";
|
|
13
|
-
|
|
14
|
-
const SERVER = process.env.QGRID_URL ?? "http://localhost:44900";
|
|
15
|
-
const MODEL = (process.env.QGRID_MODEL ?? "openai/gpt-5.5") as Parameters<typeof qgrid>[0];
|
|
16
|
-
|
|
17
|
-
let passed = 0;
|
|
18
|
-
let failed = 0;
|
|
19
|
-
|
|
20
|
-
async function test(name: string, fn: () => Promise<void>) {
|
|
21
|
-
try {
|
|
22
|
-
await fn();
|
|
23
|
-
passed++;
|
|
24
|
-
console.log(` ✓ ${name}`);
|
|
25
|
-
} catch (e) {
|
|
26
|
-
failed++;
|
|
27
|
-
console.error(` ✗ ${name}`);
|
|
28
|
-
console.error(` ${(e as Error).message}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function assert(condition: unknown, message: string): asserts condition {
|
|
33
|
-
if (!condition) throw new Error(message);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const WEATHER_DB: Record<string, { temperature: number; condition: string }> = {
|
|
37
|
-
Seoul: { temperature: 22, condition: "sunny" },
|
|
38
|
-
Busan: { temperature: 26, condition: "partly cloudy" },
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
async function main() {
|
|
42
|
-
console.log(`\n@cartanova/qgrid-ai-sdk Logger E2E (server: ${SERVER}, model: ${MODEL})\n`);
|
|
43
|
-
|
|
44
|
-
// 1. 단순 텍스트 + logger
|
|
45
|
-
await test("logger: simple text generation", async () => {
|
|
46
|
-
const logger = createQgridLogger({
|
|
47
|
-
serverUrl: SERVER,
|
|
48
|
-
projectName: "e2e-logger",
|
|
49
|
-
tokenName: "e2e-test",
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const result = await generateText({
|
|
53
|
-
model: qgrid(MODEL),
|
|
54
|
-
prompt: 'Say exactly: "logger-ok"',
|
|
55
|
-
experimental_telemetry: { integrations: [logger] },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
assert(result.text.length > 0, "empty response");
|
|
59
|
-
console.log(` text="${result.text.slice(0, 50)}"`);
|
|
60
|
-
|
|
61
|
-
// 대기 후 DB 검증
|
|
62
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
63
|
-
|
|
64
|
-
const logRes = await fetch(
|
|
65
|
-
`${SERVER}/api/requestLog/findMany?subset=A&rawParams%5Bnum%5D=1&rawParams%5Bpage%5D=1&rawParams%5BorderBy%5D=id-desc`,
|
|
66
|
-
);
|
|
67
|
-
const logData = (await logRes.json()) as { rows: Array<Record<string, unknown>> };
|
|
68
|
-
const log = logData.rows[0];
|
|
69
|
-
|
|
70
|
-
// qgrid provider가 이미 lifecycle을 관리하므로 logger는 skip해야 함
|
|
71
|
-
// 하지만 단순 호출이라 qgrid provider의 runState도 생성 안 됨 (tools 없음)
|
|
72
|
-
// 이 경우 둘 다 기록하지 않거나, qgrid provider만 기록
|
|
73
|
-
console.log(` latest log id=${log?.id}, status=${log?.status}`);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// 2. tool calling + logger (qgrid provider 사용 — logger가 skip하는지 검증)
|
|
77
|
-
await test("logger: skips when qgrid provider is used", async () => {
|
|
78
|
-
const errors: Error[] = [];
|
|
79
|
-
const logger = createQgridLogger({
|
|
80
|
-
serverUrl: SERVER,
|
|
81
|
-
projectName: "e2e-logger",
|
|
82
|
-
onLogError: (err) => errors.push(err),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const result = await generateText({
|
|
86
|
-
model: qgrid(MODEL),
|
|
87
|
-
prompt: "What is the weather in Seoul? Use the getWeather tool.",
|
|
88
|
-
tools: {
|
|
89
|
-
getWeather: tool({
|
|
90
|
-
description: "Get weather for a city",
|
|
91
|
-
inputSchema: z.object({ city: z.string() }),
|
|
92
|
-
execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
|
|
93
|
-
}),
|
|
94
|
-
},
|
|
95
|
-
stopWhen: stepCountIs(3),
|
|
96
|
-
experimental_telemetry: { integrations: [logger] },
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
assert(result.text.length > 0, "empty response");
|
|
100
|
-
assert(errors.length === 0, `logger errors: ${errors.map((e) => e.message).join(", ")}`);
|
|
101
|
-
console.log(` text="${result.text.slice(0, 80)}"`);
|
|
102
|
-
console.log(` logger skipped (qgrid provider detected)`);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
|
106
|
-
if (failed > 0) process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
main().catch((e) => {
|
|
110
|
-
console.error("E2E failed:", e);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
});
|
package/e2e/e2e.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartanova/qgrid-ai-sdk E2E Test Suite
|
|
3
|
-
*
|
|
4
|
-
* 실제 qgrid 서버(localhost:44900)에 요청을 보내는 통합 테스트.
|
|
5
|
-
* 서버가 떠 있어야 합니다.
|
|
6
|
-
*
|
|
7
|
-
* 사용법: pnpm --filter @cartanova/qgrid-ai-sdk e2e
|
|
8
|
-
*/
|
|
9
|
-
import { generateText, stepCountIs, tool } from "ai";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
|
|
12
|
-
import { qgrid } from "../src/index";
|
|
13
|
-
|
|
14
|
-
const SERVER = process.env.QGRID_URL ?? "http://localhost:44900";
|
|
15
|
-
const MODEL = (process.env.QGRID_MODEL ?? "openai/gpt-5.5") as Parameters<typeof qgrid>[0];
|
|
16
|
-
|
|
17
|
-
// ── Test runner ────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
let passed = 0;
|
|
20
|
-
let failed = 0;
|
|
21
|
-
|
|
22
|
-
async function test(name: string, fn: () => Promise<void>) {
|
|
23
|
-
try {
|
|
24
|
-
await fn();
|
|
25
|
-
passed++;
|
|
26
|
-
console.log(` ✓ ${name}`);
|
|
27
|
-
} catch (e) {
|
|
28
|
-
failed++;
|
|
29
|
-
console.error(` ✗ ${name}`);
|
|
30
|
-
console.error(` ${(e as Error).message}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function assert(condition: unknown, message: string): asserts condition {
|
|
35
|
-
if (!condition) throw new Error(message);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── Mock data ──────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
const WEATHER_DB: Record<string, { temperature: number; condition: string }> = {
|
|
41
|
-
Seoul: { temperature: 22, condition: "sunny" },
|
|
42
|
-
Busan: { temperature: 26, condition: "partly cloudy" },
|
|
43
|
-
Daegu: { temperature: 28, condition: "hot" },
|
|
44
|
-
Jeju: { temperature: 24, condition: "rainy" },
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const RESTAURANT_DB: Record<
|
|
48
|
-
string,
|
|
49
|
-
Array<{ id: string; name: string; cuisine: string; rating: number }>
|
|
50
|
-
> = {
|
|
51
|
-
Daegu: [
|
|
52
|
-
{ id: "r6", name: "대구 막창", cuisine: "BBQ", rating: 4.4 },
|
|
53
|
-
{ id: "r7", name: "안지랑 곱창", cuisine: "Korean", rating: 4.7 },
|
|
54
|
-
],
|
|
55
|
-
Busan: [
|
|
56
|
-
{ id: "r3", name: "해운대 회센터", cuisine: "Seafood", rating: 4.7 },
|
|
57
|
-
{ id: "r4", name: "서면 돼지국밥", cuisine: "Korean", rating: 4.9 },
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// ── Tests ──────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
async function main() {
|
|
64
|
-
console.log(`\n@cartanova/qgrid-ai-sdk E2E (server: ${SERVER}, model: ${MODEL})\n`);
|
|
65
|
-
|
|
66
|
-
// 1. 단순 텍스트
|
|
67
|
-
await test("simple text generation", async () => {
|
|
68
|
-
const result = await generateText({
|
|
69
|
-
model: qgrid(MODEL),
|
|
70
|
-
prompt: 'Respond with exactly: "hello-e2e"',
|
|
71
|
-
});
|
|
72
|
-
assert(result.text.length > 0, "empty response");
|
|
73
|
-
assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
|
|
74
|
-
assert(result.usage?.inputTokens, "missing input token usage");
|
|
75
|
-
console.log(` text="${result.text.slice(0, 80)}"`);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// 2. system prompt
|
|
79
|
-
await test("system prompt", async () => {
|
|
80
|
-
const result = await generateText({
|
|
81
|
-
model: qgrid(MODEL),
|
|
82
|
-
system: "You must reply in exactly one word.",
|
|
83
|
-
prompt: "What color is the sky?",
|
|
84
|
-
});
|
|
85
|
-
assert(result.text.length > 0, "empty response");
|
|
86
|
-
console.log(` text="${result.text.slice(0, 80)}"`);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// 3. single tool call
|
|
90
|
-
await test("single tool call", async () => {
|
|
91
|
-
const result = await generateText({
|
|
92
|
-
model: qgrid(MODEL),
|
|
93
|
-
prompt: "What is the weather in Seoul? Use the getWeather tool.",
|
|
94
|
-
tools: {
|
|
95
|
-
getWeather: tool({
|
|
96
|
-
description: "Get current weather for a city",
|
|
97
|
-
inputSchema: z.object({ city: z.string() }),
|
|
98
|
-
execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
|
|
99
|
-
}),
|
|
100
|
-
},
|
|
101
|
-
stopWhen: stepCountIs(3),
|
|
102
|
-
});
|
|
103
|
-
assert(result.steps.length >= 2, `expected at least 2 steps, got ${result.steps.length}`);
|
|
104
|
-
assert(result.steps[0].toolCalls.length > 0, "step 0 should have tool calls");
|
|
105
|
-
assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
|
|
106
|
-
assert(result.text.length > 0, "empty final text");
|
|
107
|
-
console.log(` steps=${result.steps.length}, text="${result.text.slice(0, 80)}"`);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// 4. multi-tool, multi-step
|
|
111
|
-
await test("multi-tool multi-step", async () => {
|
|
112
|
-
const result = await generateText({
|
|
113
|
-
model: qgrid(MODEL),
|
|
114
|
-
prompt:
|
|
115
|
-
"Step 1: Use the getWeather tool for Seoul, Busan, and Daegu. Step 2: Identify the warmest city. Step 3: Use the searchRestaurants tool for that warmest city. Then give a final summary.",
|
|
116
|
-
tools: {
|
|
117
|
-
getWeather: tool({
|
|
118
|
-
description: "Get weather for a city (English name: Seoul, Busan, Daegu, Jeju)",
|
|
119
|
-
inputSchema: z.object({ city: z.string() }),
|
|
120
|
-
execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
|
|
121
|
-
}),
|
|
122
|
-
searchRestaurants: tool({
|
|
123
|
-
description: "Search restaurants in a city",
|
|
124
|
-
inputSchema: z.object({ city: z.string() }),
|
|
125
|
-
execute: async ({ city }) => RESTAURANT_DB[city] ?? [],
|
|
126
|
-
}),
|
|
127
|
-
},
|
|
128
|
-
stopWhen: stepCountIs(5),
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const allToolCalls = result.steps.flatMap((s) => s.toolCalls);
|
|
132
|
-
const weatherCalls = allToolCalls.filter((tc) => tc.toolName === "getWeather");
|
|
133
|
-
const restaurantCalls = allToolCalls.filter((tc) => tc.toolName === "searchRestaurants");
|
|
134
|
-
const toolNames = [...new Set(allToolCalls.map((tc) => tc.toolName))];
|
|
135
|
-
|
|
136
|
-
assert(
|
|
137
|
-
allToolCalls.length >= 2,
|
|
138
|
-
`expected at least 2 total tool calls, got ${allToolCalls.length}`,
|
|
139
|
-
);
|
|
140
|
-
assert(
|
|
141
|
-
weatherCalls.length >= 2,
|
|
142
|
-
`expected at least 2 getWeather calls, got ${weatherCalls.length}`,
|
|
143
|
-
);
|
|
144
|
-
assert(result.steps.length >= 2, `expected at least 2 steps, got ${result.steps.length}`);
|
|
145
|
-
assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
|
|
146
|
-
console.log(
|
|
147
|
-
` steps=${result.steps.length}, tools=${toolNames.join(",")}, weatherCalls=${weatherCalls.length}, restaurantCalls=${restaurantCalls.length}`,
|
|
148
|
-
);
|
|
149
|
-
console.log(` text="${result.text.slice(0, 100)}"`);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// 5. auto lifecycle — run이 DB에 기록되었는지 검증
|
|
153
|
-
await test("auto lifecycle (createRun → appendStep → finishRun)", async () => {
|
|
154
|
-
const result = await generateText({
|
|
155
|
-
model: qgrid(MODEL),
|
|
156
|
-
prompt: "What is the weather in Busan? Use the getWeather tool.",
|
|
157
|
-
tools: {
|
|
158
|
-
getWeather: tool({
|
|
159
|
-
description: "Get weather for a city",
|
|
160
|
-
inputSchema: z.object({ city: z.string() }),
|
|
161
|
-
execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
|
|
162
|
-
}),
|
|
163
|
-
},
|
|
164
|
-
stopWhen: stepCountIs(3),
|
|
165
|
-
});
|
|
166
|
-
assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
|
|
167
|
-
|
|
168
|
-
// fire-and-forget appendStep 완료 대기
|
|
169
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
170
|
-
|
|
171
|
-
// 가장 최근 request_log 조회
|
|
172
|
-
const logRes = await fetch(
|
|
173
|
-
`${SERVER}/api/requestLog/findMany?subset=A&rawParams%5Bnum%5D=1&rawParams%5Bpage%5D=1&rawParams%5BorderBy%5D=id-desc`,
|
|
174
|
-
);
|
|
175
|
-
const logData = (await logRes.json()) as { rows: Array<Record<string, unknown>> };
|
|
176
|
-
const log = logData.rows[0];
|
|
177
|
-
assert(log, "no request_log found");
|
|
178
|
-
assert(log.status === "succeeded", `expected status=succeeded, got ${log.status}`);
|
|
179
|
-
assert((log.input_tokens as number) > 0, "input_tokens should be > 0");
|
|
180
|
-
assert(
|
|
181
|
-
(log.tool_call_count as number) >= 1,
|
|
182
|
-
`expected tool_call_count >= 1, got ${log.tool_call_count}`,
|
|
183
|
-
);
|
|
184
|
-
assert((log.response as string)?.length > 0, "response should not be empty");
|
|
185
|
-
|
|
186
|
-
// steps 검증
|
|
187
|
-
const stepsRes = await fetch(
|
|
188
|
-
`${SERVER}/api/requestLogStep/findMany?subset=A&rawParams%5Bnum%5D=50&rawParams%5Bpage%5D=1&rawParams%5Brequest_log_id%5D=${log.id}&rawParams%5BorderBy%5D=id-asc`,
|
|
189
|
-
);
|
|
190
|
-
const stepsData = (await stepsRes.json()) as {
|
|
191
|
-
rows: Array<Record<string, unknown>>;
|
|
192
|
-
total: number;
|
|
193
|
-
};
|
|
194
|
-
const generateSteps = stepsData.rows.filter((s) => s.type === "generate");
|
|
195
|
-
const toolSteps = stepsData.rows.filter((s) => s.type === "tool_call");
|
|
196
|
-
|
|
197
|
-
assert(
|
|
198
|
-
generateSteps.length >= 2,
|
|
199
|
-
`expected at least 2 generate steps, got ${generateSteps.length}`,
|
|
200
|
-
);
|
|
201
|
-
assert(toolSteps.length >= 1, `expected at least 1 tool_call step, got ${toolSteps.length}`);
|
|
202
|
-
|
|
203
|
-
console.log(
|
|
204
|
-
` requestLogId=${log.id}, status=${log.status}, tool_call_count=${log.tool_call_count}, steps=${stepsData.total}`,
|
|
205
|
-
);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// ── Summary ──────────────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
|
211
|
-
if (failed > 0) process.exit(1);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
main().catch((e) => {
|
|
215
|
-
console.error("E2E failed:", e);
|
|
216
|
-
process.exit(1);
|
|
217
|
-
});
|