@guava-ai/guava-sdk 0.23.0 → 0.24.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.
Files changed (57) hide show
  1. package/dist/examples/help-desk.js +22 -8
  2. package/dist/examples/help-desk.js.map +1 -1
  3. package/dist/examples/property-insurance.js +14 -3
  4. package/dist/examples/property-insurance.js.map +1 -1
  5. package/dist/examples/restaurant-waitlist.js +9 -1
  6. package/dist/examples/restaurant-waitlist.js.map +1 -1
  7. package/dist/examples/scheduling-outbound.js +2 -4
  8. package/dist/examples/scheduling-outbound.js.map +1 -1
  9. package/dist/src/action-item.d.ts +4 -4
  10. package/dist/src/action-item.js +1 -1
  11. package/dist/src/action-item.js.map +1 -1
  12. package/dist/src/agent.d.ts +18 -6
  13. package/dist/src/agent.js +269 -121
  14. package/dist/src/agent.js.map +1 -1
  15. package/dist/src/call.d.ts +9 -1
  16. package/dist/src/call.js +24 -1
  17. package/dist/src/call.js.map +1 -1
  18. package/dist/src/commands.d.ts +37 -9
  19. package/dist/src/commands.js +13 -3
  20. package/dist/src/commands.js.map +1 -1
  21. package/dist/src/events.d.ts +28 -1
  22. package/dist/src/events.js +10 -2
  23. package/dist/src/events.js.map +1 -1
  24. package/dist/src/health.d.ts +35 -0
  25. package/dist/src/health.js +99 -0
  26. package/dist/src/health.js.map +1 -0
  27. package/dist/src/helpers/index.d.ts +1 -0
  28. package/dist/src/helpers/index.js +9 -0
  29. package/dist/src/helpers/index.js.map +1 -0
  30. package/dist/src/helpers/llm.d.ts +33 -1
  31. package/dist/src/helpers/llm.js +220 -0
  32. package/dist/src/helpers/llm.js.map +1 -1
  33. package/dist/src/helpers/server-rag.d.ts +25 -0
  34. package/dist/src/helpers/server-rag.js +119 -0
  35. package/dist/src/helpers/server-rag.js.map +1 -0
  36. package/dist/src/socket/protocol.d.ts +4 -4
  37. package/dist/src/testing/mocks.d.ts +6 -0
  38. package/dist/src/testing/mocks.js +12 -0
  39. package/dist/src/testing/mocks.js.map +1 -1
  40. package/dist/src/version.d.ts +1 -1
  41. package/dist/src/version.js +1 -1
  42. package/examples/help-desk.ts +21 -12
  43. package/examples/property-insurance.ts +15 -3
  44. package/examples/restaurant-waitlist.ts +10 -1
  45. package/examples/scheduling-outbound.ts +2 -4
  46. package/package.json +5 -1
  47. package/src/action-item.ts +1 -1
  48. package/src/agent.ts +261 -127
  49. package/src/call.ts +37 -1
  50. package/src/commands.ts +16 -2
  51. package/src/events.ts +11 -1
  52. package/src/health.ts +120 -0
  53. package/src/helpers/index.ts +2 -0
  54. package/src/helpers/llm.ts +178 -1
  55. package/src/helpers/server-rag.ts +132 -0
  56. package/src/testing/mocks.ts +17 -0
  57. package/src/version.ts +1 -1
package/src/call.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  TransferCommand,
10
10
  ReadScriptCommand,
11
11
  RetryTaskCommand,
12
+ SetAgentDTMFCommand,
12
13
  } from "./commands.ts";
13
14
  import type * as z from "zod";
14
15
  import type {
@@ -20,6 +21,7 @@ import type {
20
21
  } from "./action-item.ts";
21
22
  import { Say } from "./action-item.ts";
22
23
  import { telemetryClient } from "./telemetry.ts";
24
+ import type { CallInfo } from "./socket/call-info.ts";
23
25
 
24
26
  export type TaskObjective =
25
27
  | { objective: string }
@@ -33,6 +35,8 @@ export type ReachPersonOutcome = {
33
35
 
34
36
  @telemetryClient.trackClass()
35
37
  export class Call {
38
+ private _callId: string;
39
+ private _callInfo: CallInfo;
36
40
  protected _commandQueue: Command[] = [];
37
41
  private _variables: Record<string, any> = {};
38
42
  protected logger: Logger;
@@ -42,8 +46,17 @@ export class Call {
42
46
  // input (mutating it) (i.e. _drain should use Array.splice)
43
47
  private _drain?: (_: Command[]) => Promise<void>;
44
48
  _fieldValues: Record<string, unknown> = {};
49
+ _fieldKeysByTaskId: Record<string, string[]> = {};
50
+
51
+ constructor(
52
+ callId: string,
53
+ callInfo: CallInfo,
54
+ variables: Record<string, any> = {},
55
+ logger: Logger = getDefaultLogger(),
56
+ ) {
57
+ this._callId = callId;
58
+ this._callInfo = callInfo;
45
59
 
46
- constructor(variables: Record<string, any> = {}, logger: Logger = getDefaultLogger()) {
47
60
  // Set initial variables.
48
61
  this._variables = { ...variables };
49
62
 
@@ -51,6 +64,14 @@ export class Call {
51
64
  this.logger = logger;
52
65
  }
53
66
 
67
+ get id(): string {
68
+ return this._callId;
69
+ }
70
+
71
+ get callInfo(): CallInfo {
72
+ return this._callInfo;
73
+ }
74
+
54
75
  /**
55
76
  * @description Supply a function used to consume commands from the internal command queue.
56
77
  *
@@ -75,6 +96,10 @@ export class Call {
75
96
  }
76
97
  }
77
98
 
99
+ hasField(key: string): boolean {
100
+ return key in this._fieldValues;
101
+ }
102
+
78
103
  async sendCommand<C extends Command, Schema extends z.ZodType<C>>(
79
104
  schema: Schema,
80
105
  data: z.input<Schema>,
@@ -84,6 +109,13 @@ export class Call {
84
109
  await this.flush();
85
110
  }
86
111
 
112
+ async setAgentDtmf(enabled: boolean) {
113
+ await this.sendCommand(SetAgentDTMFCommand, {
114
+ command_type: "set-agent-dtmf",
115
+ enabled,
116
+ });
117
+ }
118
+
87
119
  async setLanguageMode(args: { primary?: Language; secondary?: Language[] }) {
88
120
  await this.sendCommand(SetLanguageModeCommand, {
89
121
  command_type: "set-language-mode",
@@ -128,6 +160,7 @@ export class Call {
128
160
  throw new Error("At least one of ['objective', 'checklist'] must be provided.");
129
161
  }
130
162
 
163
+ const fieldKeys: string[] = [];
131
164
  const action_items = checklist.map((item): ActionItem => {
132
165
  if (typeof item === "string") {
133
166
  return { item_type: "todo", description: item } satisfies TodoItem;
@@ -138,12 +171,15 @@ export class Call {
138
171
  "choiceGenerator is not compatible with the Agent / Call API. Use searchable=true and register a handler.",
139
172
  );
140
173
  }
174
+ fieldKeys.push(item.key);
141
175
  const { choiceGenerator: _, ...fieldData } = item;
142
176
  return { ...fieldData, is_search_field: item.searchable } satisfies SerializableFieldItem;
143
177
  }
144
178
  return item;
145
179
  });
146
180
 
181
+ this._fieldKeysByTaskId[taskId] = fieldKeys;
182
+
147
183
  await this.sendCommand(SetTaskCommand, {
148
184
  command_type: "set-task",
149
185
  task_id: taskId,
package/src/commands.ts CHANGED
@@ -94,14 +94,21 @@ export const RegisteredHooksCommand = z.strictObject({
94
94
  has_on_question: z.boolean(),
95
95
  has_on_intent: z.boolean(),
96
96
  has_on_action_requested: z.boolean().optional().default(false),
97
+ has_on_escalate: z.boolean().optional().default(false),
98
+ accept_dtmf_for_numbers: z.boolean().default(true),
97
99
  });
98
100
  export type RegisteredHooksCommand = z.input<typeof RegisteredHooksCommand>;
99
101
 
102
+ export const ActionCandidate = z.strictObject({
103
+ key: z.string(),
104
+ description: z.string().default(""),
105
+ });
106
+ export type ActionCandidate = z.input<typeof ActionCandidate>;
107
+
100
108
  export const ActionSuggestionCommand = z.strictObject({
101
109
  command_type: z.literal("action-suggestion"),
102
110
  intent_id: z.string(),
103
- action_key: z.string().nullable(),
104
- action_description: z.string().default(""),
111
+ actions: z.array(ActionCandidate),
105
112
  });
106
113
  export type ActionSuggestionCommand = z.input<typeof ActionSuggestionCommand>;
107
114
 
@@ -120,6 +127,12 @@ export const RetryTaskCommand = z.strictObject({
120
127
  });
121
128
  export type RetryTaskCommand = z.input<typeof RetryTaskCommand>;
122
129
 
130
+ export const SetAgentDTMFCommand = z.strictObject({
131
+ command_type: z.literal("set-agent-dtmf"),
132
+ enabled: z.boolean(),
133
+ });
134
+ export type SetAgentDTMFCommand = z.input<typeof SetAgentDTMFCommand>;
135
+
123
136
  export const AnyCommand = z.union([
124
137
  StartOutboundCallCommand,
125
138
  ListenInboundCommand,
@@ -136,6 +149,7 @@ export const AnyCommand = z.union([
136
149
  ChoiceResultCommand,
137
150
  ActionSuggestionCommand,
138
151
  RetryTaskCommand,
152
+ SetAgentDTMFCommand,
139
153
  ]);
140
154
  export type Command = z.input<typeof AnyCommand>;
141
155
 
package/src/events.ts CHANGED
@@ -14,12 +14,14 @@ export const InboundCallEvent = z.object({
14
14
  export type InboundCallEvent = z.infer<typeof InboundCallEvent>;
15
15
 
16
16
  /**
17
- * @description The caller has said something.
17
+ * @description The caller has said something. For utterances with the same id,
18
+ * all but the latest is out of date.
18
19
  */
19
20
  export const CallerSpeechEvent = z.object({
20
21
  event_type: z.literal("caller-speech"),
21
22
 
22
23
  utterance: z.string(),
24
+ utterance_id: z.string().optional(),
23
25
  });
24
26
  export type CallerSpeechEvent = z.infer<typeof CallerSpeechEvent>;
25
27
 
@@ -145,9 +147,16 @@ export type DTMFDigit =
145
147
  export const DTMFPressedEvent = z.object({
146
148
  event_type: z.literal("dtmf"),
147
149
  digit: z.enum(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#", "A", "B", "C", "D"]),
150
+ recent_digits: z.string().default(""),
148
151
  });
149
152
  export type DTMFPressedEvent = z.infer<typeof DTMFPressedEvent>;
150
153
 
154
+ export const EscalateEvent = z.object({
155
+ event_type: z.literal("escalate"),
156
+ requested_by: z.enum(["human", "agent"]).default("human"),
157
+ });
158
+ export type EscalateEvent = z.infer<typeof EscalateEvent>;
159
+
151
160
  export const GuavaEvent = z.discriminatedUnion("event_type", [
152
161
  SessionStartedEvent,
153
162
  InboundCallEvent,
@@ -165,6 +174,7 @@ export const GuavaEvent = z.discriminatedUnion("event_type", [
165
174
  ChoiceQueryEvent,
166
175
  ActionRequestEvent,
167
176
  ExecuteActionEvent,
177
+ EscalateEvent,
168
178
  DTMFPressedEvent,
169
179
  ]);
170
180
  export type GuavaEvent = z.infer<typeof GuavaEvent>;
package/src/health.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { type Server, createServer } from "node:http";
2
+ import { getDefaultLogger, type Logger } from "./logging.ts";
3
+
4
+ export type HealthState = "starting" | "live" | "stopped";
5
+
6
+ export class HealthContext {
7
+ private _state: HealthState = "starting";
8
+
9
+ setState(newState: HealthState): void {
10
+ this._state = newState;
11
+ }
12
+
13
+ ready(): void {
14
+ this.setState("live");
15
+ }
16
+
17
+ stopped(): void {
18
+ this.setState("stopped");
19
+ }
20
+
21
+ isLive(): boolean {
22
+ return this._state === "live";
23
+ }
24
+ }
25
+
26
+ export class MultiHealthContext {
27
+ private _ctxs: HealthContext[] = [];
28
+
29
+ createCtx(): HealthContext {
30
+ const ctx = new HealthContext();
31
+ this._ctxs.push(ctx);
32
+ return ctx;
33
+ }
34
+
35
+ isLive(): boolean {
36
+ return this._ctxs.length > 0 && this._ctxs.every((ctx) => ctx.isLive());
37
+ }
38
+ }
39
+
40
+ export interface HealthServerOptions {
41
+ host?: string;
42
+ port?: number;
43
+ logger?: Logger;
44
+ }
45
+
46
+ export class HealthServer implements AsyncDisposable {
47
+ private readonly _server: Server;
48
+ private readonly _logger: Logger;
49
+ private _stopping: Promise<void> | null = null;
50
+
51
+ private constructor(server: Server, logger: Logger) {
52
+ this._server = server;
53
+ this._logger = logger;
54
+ }
55
+
56
+ static async start(
57
+ healthCtx: HealthContext | MultiHealthContext,
58
+ options: HealthServerOptions = {},
59
+ ): Promise<HealthServer> {
60
+ const host = options.host ?? "0.0.0.0";
61
+ const port = options.port ?? 4828;
62
+ const logger = options.logger ?? getDefaultLogger();
63
+
64
+ const server = createServer((req, res) => {
65
+ if (req.method === "GET" && req.url === "/live") {
66
+ res.statusCode = healthCtx.isLive() ? 200 : 503;
67
+ } else {
68
+ res.statusCode = 404;
69
+ }
70
+ res.end();
71
+ });
72
+
73
+ await new Promise<void>((resolve, reject) => {
74
+ server.once("error", reject);
75
+ server.listen(port, host, () => {
76
+ server.off("error", reject);
77
+ resolve();
78
+ });
79
+ });
80
+
81
+ logger.info("Health check server listening on http://%s:%d/live", host, port);
82
+ return new HealthServer(server, logger);
83
+ }
84
+
85
+ stop(): Promise<void> {
86
+ if (this._stopping) return this._stopping;
87
+ this._logger.info("Health check server shutting down.");
88
+ this._stopping = new Promise<void>((resolve, reject) => {
89
+ this._server.closeAllConnections();
90
+ this._server.close((err) => (err ? reject(err) : resolve()));
91
+ });
92
+ return this._stopping;
93
+ }
94
+
95
+ async [Symbol.asyncDispose](): Promise<void> {
96
+ await this.stop();
97
+ }
98
+ }
99
+
100
+ const NOOP_DISPOSABLE: AsyncDisposable = {
101
+ async [Symbol.asyncDispose]() {},
102
+ };
103
+
104
+ function isEnabled(): boolean {
105
+ const value = (process.env.GUAVA_HEALTH_SERVER ?? "false").toLowerCase().trim();
106
+ return value === "yes" || value === "true" || value === "on";
107
+ }
108
+
109
+ /**
110
+ * Returns an AsyncDisposable that, when the GUAVA_HEALTH_SERVER env var is
111
+ * truthy ("yes", "true", "on"), starts an HTTP health check server exposing
112
+ * `/live` on port 4828. Otherwise returns a no-op disposable. Intended for use
113
+ * with `await using`.
114
+ */
115
+ export async function getHealthServer(
116
+ healthCtx: HealthContext | MultiHealthContext,
117
+ ): Promise<AsyncDisposable> {
118
+ if (!isEnabled()) return NOOP_DISPOSABLE;
119
+ return HealthServer.start(healthCtx);
120
+ }
@@ -0,0 +1,2 @@
1
+ /* This file must not include the OpenAI helpers. */
2
+ export { DatetimeFilter, DocumentQA, IntentRecognizer } from "./llm.ts";
@@ -1,5 +1,8 @@
1
- import type { Client } from "../client.ts";
1
+ import type { SuggestedAction } from "../agent.ts";
2
+ import { Client } from "../client.ts";
3
+ import { telemetryClient } from "../telemetry.ts";
2
4
  import { fetchOrThrow } from "../utils.ts";
5
+ import { ServerRAG } from "./server-rag.ts";
3
6
 
4
7
  export async function _generate(
5
8
  client: Client,
@@ -14,7 +17,181 @@ export async function _generate(
14
17
  method: "POST",
15
18
  headers: { ...(await client.headers()), "Content-Type": "application/json" },
16
19
  body: JSON.stringify(body),
20
+ signal: AbortSignal.timeout(60_000),
17
21
  });
18
22
 
19
23
  return ((await response.json()) as { text: string }).text;
20
24
  }
25
+
26
+ @telemetryClient.trackClass()
27
+ export class DocumentQA {
28
+ private readonly _rag: ServerRAG;
29
+ private readonly _instructions: string | undefined;
30
+ private readonly _initialized: Promise<void>;
31
+
32
+ constructor({
33
+ documents,
34
+ ids,
35
+ instructions,
36
+ namespace,
37
+ }: {
38
+ documents?: string | string[];
39
+ ids?: string[];
40
+ instructions?: string;
41
+ namespace?: string;
42
+ } = {}) {
43
+ this._rag = new ServerRAG(new Client(), { namespace });
44
+ this._instructions = instructions;
45
+
46
+ if (documents !== undefined) {
47
+ const docs = typeof documents === "string" ? [documents] : documents;
48
+ this._initialized = this._rag.reconcile(docs, ids);
49
+ } else {
50
+ this._initialized = Promise.resolve();
51
+ }
52
+ }
53
+
54
+ async upsertDocument(key: string, text: string): Promise<void> {
55
+ await this._initialized;
56
+ await this._rag.upsertDocument(key, text);
57
+ }
58
+
59
+ async addDocument(text: string): Promise<void> {
60
+ await this._initialized;
61
+ await this._rag.addDocument(text);
62
+ }
63
+
64
+ async deleteDocument(key: string): Promise<void> {
65
+ await this._initialized;
66
+ await this._rag.removeDocument(key);
67
+ }
68
+
69
+ async clear(): Promise<void> {
70
+ await this._initialized;
71
+ await this._rag.clear();
72
+ }
73
+
74
+ async ask(question: string): Promise<string> {
75
+ await this._initialized;
76
+ const trackedKeys = this._rag.trackedKeys;
77
+ return this._rag.ask(question, {
78
+ documentKeys: trackedKeys.size > 0 ? [...trackedKeys] : undefined,
79
+ instructions: this._instructions,
80
+ });
81
+ }
82
+ }
83
+
84
+ const _filterSchema = {
85
+ type: "object",
86
+ properties: {
87
+ matching_appointments: { type: "array", items: { type: "string" } },
88
+ other_appointments: { type: "array", items: { type: "string" } },
89
+ },
90
+ required: ["matching_appointments", "other_appointments"],
91
+ additionalProperties: false,
92
+ };
93
+
94
+ @telemetryClient.trackClass()
95
+ export class DatetimeFilter {
96
+ private readonly _client: Client;
97
+ private readonly _slotsStr: string;
98
+
99
+ constructor(sourceList: string[]) {
100
+ this._client = new Client();
101
+ this._slotsStr = sourceList.join("\n");
102
+ }
103
+
104
+ async filter(
105
+ query: string,
106
+ { maxResults = 5 }: { maxResults?: number } = {},
107
+ ): Promise<[string[], string[]]> {
108
+ const today = new Date().toLocaleDateString("en-US", {
109
+ month: "long",
110
+ day: "numeric",
111
+ year: "numeric",
112
+ });
113
+
114
+ const prompt = `Return datetime slots from the list that match the query.
115
+ If none match, return close alternatives in other_appointments instead.
116
+ Never return datetimes that are not in the list.
117
+
118
+ Query: <query>${query}</query>
119
+ Today's Date: ${today}
120
+ Available slots:
121
+ ${this._slotsStr}
122
+
123
+ Return at most ${maxResults} items per list.`;
124
+
125
+ const text = await _generate(this._client, prompt, _filterSchema);
126
+ const result = JSON.parse(text) as {
127
+ matching_appointments: string[];
128
+ other_appointments: string[];
129
+ };
130
+ return [
131
+ result.matching_appointments.slice(0, maxResults),
132
+ result.other_appointments.slice(0, maxResults),
133
+ ];
134
+ }
135
+ }
136
+
137
+ @telemetryClient.trackClass()
138
+ export class IntentRecognizer {
139
+ private readonly _client: Client;
140
+ private readonly _intentChoices: string[] | Record<string, string>;
141
+ private readonly _schema: object;
142
+
143
+ constructor(intentChoices: string[] | Record<string, string>) {
144
+ this._client = new Client();
145
+ this._intentChoices = intentChoices;
146
+ const choiceList = Array.isArray(intentChoices) ? intentChoices : Object.keys(intentChoices);
147
+ this._schema = {
148
+ type: "object",
149
+ title: "ChoiceModel",
150
+ properties: {
151
+ possible_matches: {
152
+ type: "array",
153
+ items: { type: "string", enum: choiceList },
154
+ description:
155
+ "Choices that could match the caller's intent, ordered by likelihood. Include all plausible matches.",
156
+ },
157
+ },
158
+ required: ["possible_matches"],
159
+ additionalProperties: false,
160
+ };
161
+ }
162
+
163
+ async classify(intent: string): Promise<SuggestedAction[] | null> {
164
+ const choiceList = Array.isArray(this._intentChoices)
165
+ ? this._intentChoices
166
+ : Object.keys(this._intentChoices);
167
+
168
+ let prompt = `Classify the intent below into the most appropriate choice(s) from the list.
169
+
170
+ Intent: <intent>${intent}</intent>
171
+ Available Choices: ${JSON.stringify(choiceList)}
172
+
173
+ Rules:
174
+ - Default to returning a single choice — the one that best matches the intent.
175
+ - Only return additional choices when the intent is genuinely ambiguous: a reasonable person reading it would be unable to decide which category it belongs to. Thematic overlap or partial relevance is NOT enough — do not include weakly or tangentially related choices.
176
+ - Order matches by likelihood (most likely first).
177
+ - If no choice plausibly matches, return an empty list.`;
178
+
179
+ if (!Array.isArray(this._intentChoices)) {
180
+ const descriptionString = Object.entries(this._intentChoices)
181
+ .map(([key, val]) => `${key}: ${val}`)
182
+ .join("\n ");
183
+ prompt += `\n\nDetailed descriptions of each choice:\n ${descriptionString}`;
184
+ }
185
+
186
+ const text = await _generate(this._client, prompt, this._schema);
187
+ const keys = (JSON.parse(text) as { possible_matches: string[] }).possible_matches;
188
+
189
+ if (!keys.length) return null;
190
+
191
+ if (!Array.isArray(this._intentChoices)) {
192
+ const choices = this._intentChoices;
193
+ return keys.map((k) => ({ key: k, description: choices[k] }));
194
+ }
195
+ return keys.map((k) => ({ key: k }));
196
+ }
197
+ }
@@ -0,0 +1,132 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Client } from "../client.ts";
3
+ import { fetchOrThrow } from "../utils.ts";
4
+
5
+ export function _contentKey(text: string): string {
6
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
7
+ }
8
+
9
+ export function _prefixedKey(namespace: string | undefined, key: string): string {
10
+ return namespace ? `${namespace}.${key}` : key;
11
+ }
12
+
13
+ export class ServerRAG {
14
+ private readonly _client: Client;
15
+ private readonly _namespace: string | undefined;
16
+ readonly trackedKeys = new Set<string>();
17
+
18
+ constructor(client: Client, { namespace }: { namespace?: string } = {}) {
19
+ this._client = client;
20
+ this._namespace = namespace;
21
+ }
22
+
23
+ async uploadDocument(key: string, text: string): Promise<void> {
24
+ const url = new URL("v1/rag/documents", this._client.getHttpBase());
25
+ await fetchOrThrow(url, {
26
+ method: "POST",
27
+ headers: { ...(await this._client.headers()), "Content-Type": "application/json" },
28
+ body: JSON.stringify({ key, text }),
29
+ signal: AbortSignal.timeout(60_000),
30
+ });
31
+ }
32
+
33
+ async deleteDocument(key: string): Promise<void> {
34
+ const url = new URL(`v1/rag/documents/${key}`, this._client.getHttpBase());
35
+ await fetchOrThrow(url, {
36
+ method: "DELETE",
37
+ headers: await this._client.headers(),
38
+ signal: AbortSignal.timeout(30_000),
39
+ });
40
+ }
41
+
42
+ async listDocuments(): Promise<{ key: string }[]> {
43
+ const url = new URL("v1/rag/documents", this._client.getHttpBase());
44
+ const response = await fetchOrThrow(url, {
45
+ headers: await this._client.headers(),
46
+ signal: AbortSignal.timeout(30_000),
47
+ });
48
+ return (await response.json()) as { key: string }[];
49
+ }
50
+
51
+ async ask(
52
+ question: string,
53
+ { documentKeys, instructions }: { documentKeys?: string[]; instructions?: string } = {},
54
+ ): Promise<string> {
55
+ const url = new URL("v1/rag/ask", this._client.getHttpBase());
56
+ const payload: Record<string, unknown> = { question };
57
+ if (documentKeys !== undefined) payload.document_keys = documentKeys;
58
+ if (instructions !== undefined) payload.instructions = instructions;
59
+
60
+ const response = await fetchOrThrow(url, {
61
+ method: "POST",
62
+ headers: { ...(await this._client.headers()), "Content-Type": "application/json" },
63
+ body: JSON.stringify(payload),
64
+ signal: AbortSignal.timeout(120_000),
65
+ });
66
+ const result = (await response.json()) as { answer: string; warning?: string };
67
+ if (result.warning) console.warn(`Guava RAG: ${result.warning}`);
68
+ return result.answer;
69
+ }
70
+
71
+ async reconcile(documents: string[], ids?: string[]): Promise<void> {
72
+ if (ids !== undefined && ids.length !== documents.length) {
73
+ throw new Error(
74
+ `ids length (${ids.length}) must match documents length (${documents.length})`,
75
+ );
76
+ }
77
+
78
+ const desired = new Map<string, string>();
79
+ if (ids !== undefined) {
80
+ for (let i = 0; i < documents.length; i++) {
81
+ desired.set(_prefixedKey(this._namespace, ids[i]!), documents[i]!);
82
+ }
83
+ } else {
84
+ for (const doc of documents) {
85
+ desired.set(_prefixedKey(this._namespace, _contentKey(doc)), doc);
86
+ }
87
+ }
88
+
89
+ const existing = await this.listDocuments();
90
+ const existingKeys = new Set(existing.map((d) => d.key));
91
+ const scopedKeys = this._namespace
92
+ ? new Set([...existingKeys].filter((k) => k.startsWith(`${this._namespace}.`)))
93
+ : existingKeys;
94
+
95
+ for (const [key, doc] of desired) {
96
+ if (ids !== undefined || !existingKeys.has(key)) {
97
+ await this.uploadDocument(key, doc);
98
+ }
99
+ this.trackedKeys.add(key);
100
+ }
101
+
102
+ const stale = [...scopedKeys].filter((k) => !desired.has(k));
103
+ for (const key of stale) {
104
+ await this.deleteDocument(key);
105
+ }
106
+ }
107
+
108
+ async upsertDocument(key: string, text: string): Promise<void> {
109
+ const fullKey = _prefixedKey(this._namespace, key);
110
+ await this.uploadDocument(fullKey, text);
111
+ this.trackedKeys.add(fullKey);
112
+ }
113
+
114
+ async addDocument(text: string): Promise<void> {
115
+ const fullKey = _prefixedKey(this._namespace, _contentKey(text));
116
+ await this.uploadDocument(fullKey, text);
117
+ this.trackedKeys.add(fullKey);
118
+ }
119
+
120
+ async removeDocument(key: string): Promise<void> {
121
+ const fullKey = _prefixedKey(this._namespace, key);
122
+ await this.deleteDocument(fullKey);
123
+ this.trackedKeys.delete(fullKey);
124
+ }
125
+
126
+ async clear(): Promise<void> {
127
+ for (const key of this.trackedKeys) {
128
+ await this.deleteDocument(key);
129
+ }
130
+ this.trackedKeys.clear();
131
+ }
132
+ }
@@ -1,7 +1,24 @@
1
+ import crypto from "node:crypto";
1
2
  import { Call } from "../call.ts";
3
+ import type { CallInfo } from "../socket/call-info.ts";
2
4
  import type { Command } from "../commands.ts";
3
5
 
4
6
  export class MockCall extends Call {
7
+ constructor(
8
+ args: { callId?: string; callInfo?: CallInfo; variables?: Record<string, any> } = {},
9
+ ) {
10
+ super(
11
+ args.callId ?? `mock-${crypto.randomBytes(6).toString("hex")}`,
12
+ args.callInfo ?? {
13
+ call_type: "pstn",
14
+ from_number: null,
15
+ to_number: "+15555555555",
16
+ caller_id: null,
17
+ },
18
+ args.variables ?? {},
19
+ );
20
+ }
21
+
5
22
  get commands(): Command[] {
6
23
  return this._commandQueue;
7
24
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.23.0";
1
+ export const SDK_VERSION = "0.24.0";