@guava-ai/guava-sdk 0.18.0 → 0.20.0
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/examples/example.test.d.ts +5 -0
- package/dist/examples/example.test.js +46 -0
- package/dist/examples/example.test.js.map +1 -0
- package/dist/examples/help-desk.d.ts +3 -1
- package/dist/examples/help-desk.js +25 -9
- package/dist/examples/help-desk.js.map +1 -1
- package/dist/examples/property-insurance.js +4 -1
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.js +4 -1
- package/dist/examples/restaurant-waitlist.js.map +1 -1
- package/dist/examples/scheduling-outbound.js +6 -0
- package/dist/examples/scheduling-outbound.js.map +1 -1
- package/dist/src/action-item.d.ts +4 -4
- package/dist/src/agent.d.ts +85 -18
- package/dist/src/agent.js +404 -129
- package/dist/src/agent.js.map +1 -1
- package/dist/src/auth.d.ts +27 -0
- package/dist/src/auth.js +127 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/call.d.ts +1 -1
- package/dist/src/call.js +2 -2
- package/dist/src/call.js.map +1 -1
- package/dist/src/client.d.ts +38 -12
- package/dist/src/client.js +88 -16
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands.d.ts +3 -3
- package/dist/src/events.d.ts +87 -0
- package/dist/src/events.js +25 -6
- package/dist/src/events.js.map +1 -1
- package/dist/src/helpers/llm.d.ts +2 -0
- package/dist/src/helpers/llm.js +17 -0
- package/dist/src/helpers/llm.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/logging.js +16 -11
- package/dist/src/logging.js.map +1 -1
- package/dist/src/sms.d.ts +19 -0
- package/dist/src/sms.js +52 -0
- package/dist/src/sms.js.map +1 -0
- package/dist/src/socket/call-info.d.ts +35 -0
- package/dist/src/socket/call-info.js +59 -0
- package/dist/src/socket/call-info.js.map +1 -0
- package/dist/src/socket/client.d.ts +51 -0
- package/dist/src/socket/client.js +455 -0
- package/dist/src/socket/client.js.map +1 -0
- package/dist/src/socket/listen-inbound.d.ts +83 -0
- package/dist/src/socket/listen-inbound.js +82 -0
- package/dist/src/socket/listen-inbound.js.map +1 -0
- package/dist/src/socket/protocol.d.ts +127 -0
- package/dist/src/socket/protocol.js +69 -0
- package/dist/src/socket/protocol.js.map +1 -0
- package/dist/src/socket/utils.d.ts +8 -0
- package/dist/src/socket/utils.js +26 -0
- package/dist/src/socket/utils.js.map +1 -0
- package/dist/src/telemetry.d.ts +3 -3
- package/dist/src/telemetry.js +9 -7
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/testing/chat.d.ts +2 -0
- package/dist/src/testing/chat.js +181 -0
- package/dist/src/testing/chat.js.map +1 -0
- package/dist/src/testing/mocks.d.ts +6 -0
- package/dist/src/testing/mocks.js +14 -0
- package/dist/src/testing/mocks.js.map +1 -0
- package/dist/src/testing/protocol.d.ts +46 -0
- package/dist/src/testing/protocol.js +61 -0
- package/dist/src/testing/protocol.js.map +1 -0
- package/dist/src/testing/session.d.ts +26 -0
- package/dist/src/testing/session.js +219 -0
- package/dist/src/testing/session.js.map +1 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +19 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/examples/example.test.ts +58 -0
- package/examples/help-desk.ts +14 -3
- package/examples/property-insurance.ts +3 -1
- package/examples/restaurant-waitlist.ts +3 -1
- package/examples/scheduling-outbound.ts +7 -0
- package/package.json +10 -2
- package/src/agent.ts +386 -166
- package/src/auth.ts +109 -0
- package/src/call.ts +3 -3
- package/src/client.ts +119 -18
- package/src/events.ts +52 -10
- package/src/helpers/llm.ts +20 -0
- package/src/index.ts +4 -0
- package/src/logging.ts +21 -13
- package/src/sms.ts +17 -0
- package/src/socket/call-info.ts +30 -0
- package/src/socket/client.ts +433 -0
- package/src/socket/listen-inbound.ts +62 -0
- package/src/socket/protocol.ts +89 -0
- package/src/socket/utils.ts +25 -0
- package/src/telemetry.ts +11 -8
- package/src/testing/chat.ts +196 -0
- package/src/testing/mocks.ts +12 -0
- package/src/testing/protocol.ts +40 -0
- package/src/testing/session.ts +218 -0
- package/src/utils.ts +19 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { format as utilFormat } from "node:util";
|
|
2
|
+
import type { TestSession } from "./session.ts";
|
|
3
|
+
|
|
4
|
+
type SystemLevel = "log" | "info" | "debug" | "warn" | "error";
|
|
5
|
+
type Message =
|
|
6
|
+
| { speaker: "agent" | "you"; text: string }
|
|
7
|
+
| { speaker: "system"; text: string; level: SystemLevel };
|
|
8
|
+
|
|
9
|
+
const CYAN = "\x1b[36m";
|
|
10
|
+
const GREEN = "\x1b[32m";
|
|
11
|
+
const YELLOW = "\x1b[33m";
|
|
12
|
+
const RED_BOLD = "\x1b[1;31m";
|
|
13
|
+
const DIM = "\x1b[2m";
|
|
14
|
+
const RESET = "\x1b[0m";
|
|
15
|
+
|
|
16
|
+
function moveTo(row: number, col: number): string {
|
|
17
|
+
return `\x1b[${row};${col}H`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function wordWrap(text: string, width: number): string[] {
|
|
21
|
+
if (width <= 0 || text.length <= width) return [text];
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
let remaining = text;
|
|
24
|
+
while (remaining.length > width) {
|
|
25
|
+
let breakAt = remaining.lastIndexOf(" ", width);
|
|
26
|
+
if (breakAt <= 0) breakAt = width;
|
|
27
|
+
lines.push(remaining.slice(0, breakAt));
|
|
28
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
29
|
+
}
|
|
30
|
+
if (remaining) lines.push(remaining);
|
|
31
|
+
return lines;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lineColor(speaker: Message["speaker"], level?: SystemLevel): string {
|
|
35
|
+
if (speaker === "agent") return CYAN;
|
|
36
|
+
if (speaker === "you") return GREEN;
|
|
37
|
+
if (level === "warn") return YELLOW;
|
|
38
|
+
if (level === "error") return RED_BOLD;
|
|
39
|
+
return DIM;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function render(messages: Message[], inputBuf: string, sessionEnded: boolean): void {
|
|
43
|
+
const width = process.stdout.columns || 80;
|
|
44
|
+
const height = process.stdout.rows || 24;
|
|
45
|
+
const convHeight = Math.max(1, height - 2);
|
|
46
|
+
|
|
47
|
+
const allLines: { text: string; speaker: Message["speaker"]; level?: SystemLevel }[] = [];
|
|
48
|
+
for (const msg of messages) {
|
|
49
|
+
if (msg.speaker === "system") {
|
|
50
|
+
const wrapped = wordWrap(msg.text, width);
|
|
51
|
+
for (const line of wrapped) {
|
|
52
|
+
allLines.push({ text: line, speaker: "system", level: msg.level });
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
const prefix = msg.speaker === "agent" ? "agent: " : "you: ";
|
|
56
|
+
const continuation = " ".repeat(prefix.length);
|
|
57
|
+
const innerWidth = Math.max(1, width - prefix.length);
|
|
58
|
+
const wrapped = wordWrap(msg.text, innerWidth);
|
|
59
|
+
wrapped.forEach((line, i) => {
|
|
60
|
+
allLines.push({ text: (i === 0 ? prefix : continuation) + line, speaker: msg.speaker });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const visibleLines = allLines.slice(-convHeight);
|
|
66
|
+
|
|
67
|
+
let out = "\x1b[?25l\x1b[2J\x1b[H";
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < convHeight; i++) {
|
|
70
|
+
out += moveTo(i + 1, 1);
|
|
71
|
+
const line = visibleLines[i];
|
|
72
|
+
if (line) {
|
|
73
|
+
const color = lineColor(line.speaker, line.level);
|
|
74
|
+
out += color + line.text.slice(0, width) + RESET;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
out += moveTo(height - 1, 1) + "─".repeat(width);
|
|
79
|
+
|
|
80
|
+
if (sessionEnded) {
|
|
81
|
+
out += `${moveTo(height, 1)}Session ended. Press any key to exit.`;
|
|
82
|
+
} else {
|
|
83
|
+
const label = "you: ";
|
|
84
|
+
const maxLen = Math.max(0, width - label.length);
|
|
85
|
+
out += moveTo(height, 1) + label + inputBuf.slice(-maxLen);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
out += "\x1b[?25h";
|
|
89
|
+
process.stdout.write(out);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hookConsole(
|
|
93
|
+
messages: Message[],
|
|
94
|
+
getState: () => { inputBuf: string; sessionEnded: boolean; cleaned: boolean },
|
|
95
|
+
): () => void {
|
|
96
|
+
const orig = {
|
|
97
|
+
log: console.log,
|
|
98
|
+
info: console.info,
|
|
99
|
+
debug: console.debug,
|
|
100
|
+
warn: console.warn,
|
|
101
|
+
error: console.error,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function makeHook(level: SystemLevel) {
|
|
105
|
+
return (...args: unknown[]) => {
|
|
106
|
+
const { cleaned, inputBuf, sessionEnded } = getState();
|
|
107
|
+
messages.push({
|
|
108
|
+
speaker: "system",
|
|
109
|
+
text: utilFormat(...args),
|
|
110
|
+
level,
|
|
111
|
+
});
|
|
112
|
+
if (!cleaned) render(messages, inputBuf, sessionEnded);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log = makeHook("log");
|
|
117
|
+
console.info = makeHook("info");
|
|
118
|
+
console.debug = makeHook("debug");
|
|
119
|
+
console.warn = makeHook("warn");
|
|
120
|
+
console.error = makeHook("error");
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
console.log = orig.log;
|
|
124
|
+
console.info = orig.info;
|
|
125
|
+
console.debug = orig.debug;
|
|
126
|
+
console.warn = orig.warn;
|
|
127
|
+
console.error = orig.error;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function runChat(session: TestSession): Promise<void> {
|
|
132
|
+
const messages: Message[] = [];
|
|
133
|
+
let inputBuf = "";
|
|
134
|
+
let sessionEnded = false;
|
|
135
|
+
let cleaned = false;
|
|
136
|
+
|
|
137
|
+
const restoreConsole = hookConsole(messages, () => ({ inputBuf, sessionEnded, cleaned }));
|
|
138
|
+
|
|
139
|
+
// Consume agent messages in the background
|
|
140
|
+
void (async () => {
|
|
141
|
+
try {
|
|
142
|
+
while (true) {
|
|
143
|
+
const event = await session.recv();
|
|
144
|
+
if (event.message_type === "bot-tts") {
|
|
145
|
+
messages.push({ speaker: "agent", text: event.transcript });
|
|
146
|
+
if (!cleaned) render(messages, inputBuf, sessionEnded);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
if (!cleaned) {
|
|
151
|
+
sessionEnded = true;
|
|
152
|
+
render(messages, inputBuf, sessionEnded);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
|
|
157
|
+
process.stdin.setRawMode(true);
|
|
158
|
+
process.stdin.resume();
|
|
159
|
+
render(messages, inputBuf, sessionEnded);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await new Promise<void>((resolve) => {
|
|
163
|
+
const onData = (chunk: Buffer) => {
|
|
164
|
+
const ch = chunk[0]!;
|
|
165
|
+
|
|
166
|
+
if (sessionEnded || ch === 0x03 /* Ctrl+C */) {
|
|
167
|
+
process.stdin.off("data", onData);
|
|
168
|
+
resolve();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (ch === 0x0d || ch === 0x0a /* Enter */) {
|
|
173
|
+
const utterance = inputBuf.trim();
|
|
174
|
+
if (utterance) {
|
|
175
|
+
messages.push({ speaker: "you", text: utterance });
|
|
176
|
+
session.say(utterance);
|
|
177
|
+
}
|
|
178
|
+
inputBuf = "";
|
|
179
|
+
} else if (ch === 0x7f || ch === 0x08 /* Backspace */) {
|
|
180
|
+
inputBuf = inputBuf.slice(0, -1);
|
|
181
|
+
} else if (ch >= 0x20 && ch < 0x7f /* Printable ASCII */) {
|
|
182
|
+
inputBuf += String.fromCharCode(ch);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
render(messages, inputBuf, sessionEnded);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
process.stdin.on("data", onData);
|
|
189
|
+
});
|
|
190
|
+
} finally {
|
|
191
|
+
cleaned = true;
|
|
192
|
+
restoreConsole();
|
|
193
|
+
process.stdin.setRawMode(false);
|
|
194
|
+
process.stdout.write("\x1b[?25h\x1b[2J\x1b[H");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Call } from "../call.ts";
|
|
2
|
+
import type { Command } from "../commands.ts";
|
|
3
|
+
|
|
4
|
+
export class MockCall extends Call {
|
|
5
|
+
get commands(): Command[] {
|
|
6
|
+
return this._commandQueue;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
setField(fieldName: string, fieldValue: unknown): void {
|
|
10
|
+
this._fieldValues[fieldName] = fieldValue;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
export const Ping = z.object({ message_type: z.literal("ping") });
|
|
4
|
+
export type Ping = z.infer<typeof Ping>;
|
|
5
|
+
|
|
6
|
+
export const Pong = z.object({ message_type: z.literal("pong") });
|
|
7
|
+
export type Pong = z.infer<typeof Pong>;
|
|
8
|
+
|
|
9
|
+
export const InjectASR = z.object({
|
|
10
|
+
message_type: z.literal("inject-asr"),
|
|
11
|
+
utterance: z.string(),
|
|
12
|
+
});
|
|
13
|
+
export type InjectASR = z.infer<typeof InjectASR>;
|
|
14
|
+
|
|
15
|
+
export const WaitForTurn = z.object({
|
|
16
|
+
message_type: z.literal("wait-for-caller-turn"),
|
|
17
|
+
request_id: z.string(),
|
|
18
|
+
});
|
|
19
|
+
export type WaitForTurn = z.infer<typeof WaitForTurn>;
|
|
20
|
+
|
|
21
|
+
export const BotTTS = z.object({
|
|
22
|
+
message_type: z.literal("bot-tts"),
|
|
23
|
+
transcript: z.string(),
|
|
24
|
+
});
|
|
25
|
+
export type BotTTS = z.infer<typeof BotTTS>;
|
|
26
|
+
|
|
27
|
+
export const TurnStarted = z.object({
|
|
28
|
+
message_type: z.literal("caller-turn-started"),
|
|
29
|
+
request_id: z.string(),
|
|
30
|
+
});
|
|
31
|
+
export type TurnStarted = z.infer<typeof TurnStarted>;
|
|
32
|
+
|
|
33
|
+
export const SessionStarted = z.object({
|
|
34
|
+
message_type: z.literal("session-started"),
|
|
35
|
+
session_id: z.string(),
|
|
36
|
+
});
|
|
37
|
+
export type SessionStarted = z.infer<typeof SessionStarted>;
|
|
38
|
+
|
|
39
|
+
export const TestingEvent = z.discriminatedUnion("message_type", [Ping, Pong, BotTTS, TurnStarted]);
|
|
40
|
+
export type TestingEvent = z.infer<typeof TestingEvent>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type WebSocket from "ws";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { type BotTTS, type InjectASR, TestingEvent } from "./protocol.ts";
|
|
5
|
+
import type { Client } from "../client.ts";
|
|
6
|
+
import { _generate } from "../helpers/llm.ts";
|
|
7
|
+
|
|
8
|
+
type SessionEvent = InjectASR | BotTTS;
|
|
9
|
+
|
|
10
|
+
const _evalResponseSchema = z.object({
|
|
11
|
+
results: z.array(
|
|
12
|
+
z.object({
|
|
13
|
+
passed: z.boolean(),
|
|
14
|
+
reasoning: z.string().optional(),
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export class TestSession {
|
|
20
|
+
private readonly _ws: WebSocket;
|
|
21
|
+
private readonly _client: Client;
|
|
22
|
+
private _sessionEvents: SessionEvent[] = [];
|
|
23
|
+
private _recvBuffer: TestingEvent[] = [];
|
|
24
|
+
private _recvWaiters: Array<{
|
|
25
|
+
resolve: (v: TestingEvent) => void;
|
|
26
|
+
reject: (e: Error) => void;
|
|
27
|
+
}> = [];
|
|
28
|
+
private _closed = false;
|
|
29
|
+
|
|
30
|
+
executedActions: string[] = [];
|
|
31
|
+
terminationReason: string | null = null;
|
|
32
|
+
|
|
33
|
+
/** @internal */
|
|
34
|
+
get _events(): ReadonlyArray<InjectASR | BotTTS> {
|
|
35
|
+
return this._sessionEvents;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor(ws: WebSocket, client: Client) {
|
|
39
|
+
this._ws = ws;
|
|
40
|
+
this._client = client;
|
|
41
|
+
|
|
42
|
+
ws.on("message", (data) => {
|
|
43
|
+
let event: TestingEvent;
|
|
44
|
+
try {
|
|
45
|
+
event = TestingEvent.parse(JSON.parse(data.toString()));
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const waiter = this._recvWaiters.shift();
|
|
50
|
+
if (waiter) {
|
|
51
|
+
waiter.resolve(event);
|
|
52
|
+
} else {
|
|
53
|
+
this._recvBuffer.push(event);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const onClose = () => {
|
|
58
|
+
this._closed = true;
|
|
59
|
+
const err = new Error("Test session WebSocket closed");
|
|
60
|
+
for (const waiter of this._recvWaiters) {
|
|
61
|
+
waiter.reject(err);
|
|
62
|
+
}
|
|
63
|
+
this._recvWaiters = [];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
ws.once("close", onClose);
|
|
67
|
+
ws.once("error", onClose);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private _rawSend(msg: object): void {
|
|
71
|
+
this._ws.send(JSON.stringify(msg));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
say(utterance: string): void {
|
|
75
|
+
const msg: InjectASR = { message_type: "inject-asr", utterance };
|
|
76
|
+
this._sessionEvents.push(msg);
|
|
77
|
+
this._rawSend(msg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async recv(): Promise<TestingEvent> {
|
|
81
|
+
while (true) {
|
|
82
|
+
let event: TestingEvent;
|
|
83
|
+
|
|
84
|
+
const buffered = this._recvBuffer.shift();
|
|
85
|
+
if (buffered !== undefined) {
|
|
86
|
+
event = buffered;
|
|
87
|
+
} else {
|
|
88
|
+
event = await new Promise<TestingEvent>((resolve, reject) => {
|
|
89
|
+
if (this._closed) {
|
|
90
|
+
reject(new Error("Test session WebSocket closed"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this._recvWaiters.push({ resolve, reject });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (event.message_type === "ping") {
|
|
98
|
+
this._rawSend({ message_type: "pong" });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (event.message_type === "bot-tts") {
|
|
103
|
+
this._sessionEvents.push(event);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return event;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getTranscript(): string {
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
for (const event of this._sessionEvents) {
|
|
113
|
+
if (event.message_type === "inject-asr") {
|
|
114
|
+
lines.push(`[caller]: ${event.utterance}`);
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(`[agent]: ${event.transcript}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async waitForTurn(): Promise<void> {
|
|
123
|
+
const requestId = randomBytes(8).toString("hex");
|
|
124
|
+
this._rawSend({ message_type: "wait-for-caller-turn", request_id: requestId });
|
|
125
|
+
while (true) {
|
|
126
|
+
const event = await this.recv();
|
|
127
|
+
if (event.message_type === "caller-turn-started" && event.request_id === requestId) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async waitForEnd(): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
while (true) {
|
|
136
|
+
await this.recv();
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Connection closed — session has ended.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async evaluate({
|
|
144
|
+
passCriteria = [],
|
|
145
|
+
failCriteria = [],
|
|
146
|
+
}: {
|
|
147
|
+
passCriteria?: string[];
|
|
148
|
+
failCriteria?: string[];
|
|
149
|
+
} = {}): Promise<void> {
|
|
150
|
+
const allCriteria = [
|
|
151
|
+
...passCriteria.map((c) => ({ kind: "pass" as const, criterion: c })),
|
|
152
|
+
...failCriteria.map((c) => ({ kind: "fail" as const, criterion: c })),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
if (allCriteria.length === 0) return;
|
|
156
|
+
|
|
157
|
+
const transcript = this.getTranscript();
|
|
158
|
+
const criteriaList = allCriteria.map((c, i) => `${i + 1}. ${c.criterion}`).join("\n");
|
|
159
|
+
|
|
160
|
+
const prompt = `Evaluate whether the following criteria are met based on the conversation transcript below.
|
|
161
|
+
Return one result object per criterion in the same order as listed.
|
|
162
|
+
|
|
163
|
+
Transcript:
|
|
164
|
+
${transcript || "(empty — no conversation occurred)"}
|
|
165
|
+
|
|
166
|
+
Criteria:
|
|
167
|
+
${criteriaList}`;
|
|
168
|
+
|
|
169
|
+
const evalSchema = {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
results: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
passed: { type: "boolean" },
|
|
178
|
+
reasoning: { type: "string" },
|
|
179
|
+
},
|
|
180
|
+
required: ["passed"],
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
required: ["results"],
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const evalResponse = _evalResponseSchema.parse(
|
|
190
|
+
JSON.parse(await _generate(this._client, prompt, evalSchema)),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (evalResponse.results.length !== allCriteria.length) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Evaluation returned ${evalResponse.results.length} results for ${allCriteria.length} criteria.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const failures: string[] = [];
|
|
200
|
+
for (let i = 0; i < allCriteria.length; i++) {
|
|
201
|
+
const { kind, criterion } = allCriteria[i]!;
|
|
202
|
+
const result = evalResponse.results[i]!;
|
|
203
|
+
if (kind === "pass" && !result.passed) {
|
|
204
|
+
let msg = `Pass criterion not met: ${JSON.stringify(criterion)}`;
|
|
205
|
+
if (result.reasoning) msg += ` — ${result.reasoning}`;
|
|
206
|
+
failures.push(msg);
|
|
207
|
+
} else if (kind === "fail" && result.passed) {
|
|
208
|
+
let msg = `Fail criterion triggered: ${JSON.stringify(criterion)}`;
|
|
209
|
+
if (result.reasoning) msg += ` — ${result.reasoning}`;
|
|
210
|
+
failures.push(msg);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (failures.length > 0) {
|
|
215
|
+
throw new Error(`Session evaluation failed:\n${failures.map((f) => ` • ${f}`).join("\n")}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
|
|
@@ -15,8 +16,21 @@ export function getPlatformConfigDir(): string {
|
|
|
15
16
|
return process.env.XDG_CONFIG_HOME ?? path.join(home, ".config");
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export function getCliConfigPath(): string {
|
|
20
|
+
return path.join(getPlatformConfigDir(), "guava", "config.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export function getBaseUrl(): string {
|
|
19
|
-
|
|
24
|
+
if (process.env.GUAVA_BASE_URL) return process.env.GUAVA_BASE_URL;
|
|
25
|
+
|
|
26
|
+
// Try reading from the CLI config.
|
|
27
|
+
const configPath = getCliConfigPath();
|
|
28
|
+
if (fs.existsSync(configPath)) {
|
|
29
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as { base_url?: string };
|
|
30
|
+
if (config.base_url) return config.base_url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return DEFAULT_BASE_URL;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
class HttpStatusError extends Error {
|
|
@@ -31,6 +45,10 @@ class HttpStatusError extends Error {
|
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
|
|
48
|
+
export function sleep(ms: number): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
export async function fetchOrThrow(
|
|
35
53
|
input: RequestInfo | URL,
|
|
36
54
|
init?: RequestInit,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.
|
|
1
|
+
export const SDK_VERSION = "0.20.0";
|