@guava-ai/guava-sdk 0.3.0 → 0.4.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/bin/example-runner.js +16 -7
  2. package/dist/examples/credit-card-activation.js +94 -112
  3. package/dist/examples/credit-card-activation.js.map +1 -1
  4. package/dist/examples/property-insurance.js +12 -26
  5. package/dist/examples/property-insurance.js.map +1 -1
  6. package/dist/examples/scheduling-outbound.d.ts +1 -0
  7. package/dist/examples/scheduling-outbound.js +62 -0
  8. package/dist/examples/scheduling-outbound.js.map +1 -0
  9. package/dist/examples/thai-palace.js +25 -43
  10. package/dist/examples/thai-palace.js.map +1 -1
  11. package/dist/package.json +7 -5
  12. package/dist/src/action_item.d.ts +34 -12
  13. package/dist/src/action_item.js +34 -7
  14. package/dist/src/action_item.js.map +1 -1
  15. package/dist/src/call-controller.d.ts +137 -0
  16. package/dist/src/call-controller.js +433 -0
  17. package/dist/src/call-controller.js.map +1 -0
  18. package/dist/src/commands.d.ts +67 -27
  19. package/dist/src/commands.js +41 -27
  20. package/dist/src/commands.js.map +1 -1
  21. package/dist/src/events.d.ts +47 -30
  22. package/dist/src/events.js +42 -36
  23. package/dist/src/events.js.map +1 -1
  24. package/dist/src/example_data.d.ts +1 -0
  25. package/dist/src/example_data.js +33 -0
  26. package/dist/src/example_data.js.map +1 -1
  27. package/dist/src/helpers/openai.d.ts +12 -1
  28. package/dist/src/helpers/openai.js +168 -68
  29. package/dist/src/helpers/openai.js.map +1 -1
  30. package/dist/src/index.d.ts +6 -121
  31. package/dist/src/index.js +249 -483
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/logging.d.ts +2 -1
  34. package/dist/src/logging.js +32 -7
  35. package/dist/src/logging.js.map +1 -1
  36. package/dist/src/telemetry.d.ts +23 -0
  37. package/dist/src/telemetry.js +98 -0
  38. package/dist/src/telemetry.js.map +1 -0
  39. package/dist/src/utils.d.ts +3 -0
  40. package/dist/src/utils.js +28 -0
  41. package/dist/src/utils.js.map +1 -0
  42. package/examples/biome.json +5 -0
  43. package/examples/credit-card-activation.ts +20 -26
  44. package/examples/property-insurance.ts +6 -16
  45. package/examples/scheduling-outbound.ts +80 -0
  46. package/examples/thai-palace.ts +10 -13
  47. package/package.json +7 -5
  48. package/src/action_item.ts +53 -13
  49. package/src/call-controller.ts +451 -0
  50. package/src/commands.ts +58 -42
  51. package/src/events.ts +66 -51
  52. package/src/example_data.ts +42 -0
  53. package/src/helpers/openai.ts +73 -18
  54. package/src/index.ts +81 -403
  55. package/src/logging.ts +39 -7
  56. package/src/telemetry.ts +125 -0
  57. package/src/utils.ts +32 -0
package/src/commands.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import * as z from "zod";
2
- import { actionItem } from "./action_item.ts";
2
+ import { ActionItem } from "./action_item.ts";
3
3
 
4
- export const startOutboundCallCommand = z.strictObject({
4
+ export const StartOutboundCallCommand = z.strictObject({
5
5
  command_type: z.literal("start-outbound"),
6
6
 
7
7
  from_number: z.e164().optional(),
8
8
  to_number: z.e164(),
9
9
  });
10
- export type StartOutboundCallCommand = z.input<typeof startOutboundCallCommand>;
10
+ export type StartOutboundCallCommand = z.input<typeof StartOutboundCallCommand>;
11
11
 
12
- export const listenInboundCommand = z
12
+ export const ListenInboundCommand = z
13
13
  .strictObject({
14
14
  command_type: z.literal("listen-inbound"),
15
15
 
@@ -18,83 +18,99 @@ export const listenInboundCommand = z
18
18
  })
19
19
  .refine(
20
20
  (obj) => {
21
- return (
22
- typeof obj.agent_number == "string" ||
23
- typeof obj.webrtc_code == "string"
24
- );
21
+ return typeof obj.agent_number === "string" || typeof obj.webrtc_code === "string";
25
22
  },
26
23
  { error: "one of ['agent_number', 'webrtc_code'] must be set" },
27
24
  );
28
- export type ListenInboundCommand = z.input<typeof listenInboundCommand>;
25
+ export type ListenInboundCommand = z.input<typeof ListenInboundCommand>;
29
26
 
30
- export const rejectInboundCallCommand = z.strictObject({
27
+ export const RejectInboundCallCommand = z.strictObject({
31
28
  command_type: z.literal("reject-inbound"),
32
29
  });
33
- export type RejectInboundCallCommand = z.input<typeof rejectInboundCallCommand>;
30
+ export type RejectInboundCallCommand = z.input<typeof RejectInboundCallCommand>;
34
31
 
35
- export const acceptInboundCallCommand = z.strictObject({
32
+ export const AcceptInboundCallCommand = z.strictObject({
36
33
  command_type: z.literal("accept-inbound"),
37
34
  });
38
- export type AcceptInboundCallCommand = z.input<typeof acceptInboundCallCommand>;
35
+ export type AcceptInboundCallCommand = z.input<typeof AcceptInboundCallCommand>;
39
36
 
40
- export const setTaskCommand = z.strictObject({
37
+ export const SetTaskCommand = z.strictObject({
41
38
  command_type: z.literal("set-task"),
42
39
  task_id: z.string(),
43
40
  objective: z.string(),
44
- action_items: z.array(actionItem),
41
+ action_items: z.array(ActionItem),
45
42
  });
46
- export type SetTaskCommand = z.input<typeof setTaskCommand>;
43
+ export type SetTaskCommand = z.input<typeof SetTaskCommand>;
47
44
 
48
- export const readScriptCommand = z.strictObject({
45
+ export const ReadScriptCommand = z.strictObject({
49
46
  command_type: z.literal("read-script"),
50
47
  script: z.string(),
51
48
  });
52
- export type ReadScriptCommand = z.input<typeof readScriptCommand>;
49
+ export type ReadScriptCommand = z.input<typeof ReadScriptCommand>;
53
50
 
54
- export const answerQuestionCommand = z.strictObject({
51
+ export const AnswerQuestionCommand = z.strictObject({
55
52
  command_type: z.literal("answer-question"),
56
53
  question_id: z.string(),
57
54
  answer: z.string(),
58
55
  });
59
- export type AnswerQuestionCommand = z.input<typeof answerQuestionCommand>;
56
+ export type AnswerQuestionCommand = z.input<typeof AnswerQuestionCommand>;
60
57
 
61
- export const setPersona = z.strictObject({
58
+ export const SetPersona = z.strictObject({
62
59
  command_type: z.literal("set-persona"),
63
60
  agent_name: z.string().optional(),
64
61
  organization_name: z.string().optional(),
65
62
  agent_purpose: z.string().optional(),
63
+ voice: z.string().optional(),
66
64
  });
67
- export type SetPersona = z.input<typeof setPersona>;
65
+ export type SetPersona = z.input<typeof SetPersona>;
68
66
 
69
- export const sendInstructionCommand = z.strictObject({
67
+ export const SendInstructionCommand = z.strictObject({
70
68
  command_type: z.literal("send-instruction"),
71
69
  instruction: z.string(),
72
70
  });
73
- export type SendInstructionCommand = z.input<typeof sendInstructionCommand>;
71
+ export type SendInstructionCommand = z.input<typeof SendInstructionCommand>;
74
72
 
75
- export const transferCommand = z.strictObject({
73
+ export const TransferCommand = z.strictObject({
76
74
  command_type: z.literal("transfer-call"),
77
75
  transfer_message: z.string(),
78
76
  to_number: z.string(),
79
77
  });
80
- export type TransferCommand = z.input<typeof transferCommand>;
81
-
82
- export const anyCommand = z.union([
83
- startOutboundCallCommand,
84
- listenInboundCommand,
85
- rejectInboundCallCommand,
86
- acceptInboundCallCommand,
87
- setTaskCommand,
88
- readScriptCommand,
89
- answerQuestionCommand,
90
- setPersona,
91
- sendInstructionCommand,
92
- transferCommand,
78
+ export type TransferCommand = z.input<typeof TransferCommand>;
79
+
80
+ export const RegisteredHooksCommand = z.strictObject({
81
+ command_type: z.literal("registered-hooks"),
82
+ has_on_question: z.boolean(),
83
+ has_on_intent: z.boolean(),
84
+ });
85
+ export type RegisteredHooksCommand = z.input<typeof RegisteredHooksCommand>;
86
+
87
+ export const ChoiceResultCommand = z.strictObject({
88
+ command_type: z.literal("choice-query-result"),
89
+ field_key: z.string(),
90
+ query_id: z.string(),
91
+ matched_choices: z.array(z.string()),
92
+ other_choices: z.array(z.string()),
93
+ });
94
+ export type ChoiceResultCommand = z.input<typeof ChoiceResultCommand>;
95
+
96
+ export const AnyCommand = z.union([
97
+ StartOutboundCallCommand,
98
+ ListenInboundCommand,
99
+ RejectInboundCallCommand,
100
+ AcceptInboundCallCommand,
101
+ SetTaskCommand,
102
+ ReadScriptCommand,
103
+ AnswerQuestionCommand,
104
+ SetPersona,
105
+ SendInstructionCommand,
106
+ TransferCommand,
107
+ RegisteredHooksCommand,
108
+ ChoiceResultCommand,
93
109
  ]);
94
- export type Command = z.input<typeof anyCommand>;
110
+ export type Command = z.input<typeof AnyCommand>;
95
111
 
96
- export const inboundTunnelCommand = z.strictObject({
112
+ export const InboundTunnelCommand = z.strictObject({
97
113
  call_id: z.string(),
98
- command: anyCommand,
114
+ command: AnyCommand,
99
115
  });
100
- export type InboundTunnelCommand = z.input<typeof inboundTunnelCommand>;
116
+ export type InboundTunnelCommand = z.input<typeof InboundTunnelCommand>;
package/src/events.ts CHANGED
@@ -1,123 +1,136 @@
1
1
  import * as z from "zod";
2
2
 
3
- export const sessionStartedEvent = z.object({
3
+ export const SessionStartedEvent = z.object({
4
4
  event_type: z.literal("session-started"),
5
5
  session_id: z.string(),
6
6
  });
7
- export type SessionStartedEvent = z.input<typeof sessionStartedEvent>;
7
+ export type SessionStartedEvent = z.infer<typeof SessionStartedEvent>;
8
8
 
9
- export const inboundCallEvent = z.object({
9
+ export const InboundCallEvent = z.object({
10
10
  event_type: z.literal("inbound-call"),
11
11
  caller_number: z.e164().optional(),
12
12
  agent_number: z.e164().optional(),
13
13
  });
14
- export type InboundCallEvent = z.input<typeof inboundCallEvent>;
14
+ export type InboundCallEvent = z.infer<typeof InboundCallEvent>;
15
15
 
16
16
  /**
17
17
  * @description The caller has said something.
18
18
  */
19
- export const callerSpeechEvent = z.object({
19
+ export const CallerSpeechEvent = z.object({
20
20
  event_type: z.literal("caller-speech"),
21
21
 
22
22
  utterance: z.string(),
23
23
  });
24
- export type CallerSpeechEvent = z.input<typeof callerSpeechEvent>;
24
+ export type CallerSpeechEvent = z.infer<typeof CallerSpeechEvent>;
25
25
 
26
26
  /**
27
27
  * @description The agent has said something.
28
28
  */
29
- export const agentSpeechEvent = z.object({
29
+ export const AgentSpeechEvent = z.object({
30
30
  event_type: z.literal("agent-speech"),
31
31
 
32
32
  utterance: z.string(),
33
33
  interrupted: z.boolean().default(false),
34
34
  });
35
- export type AgentSpeechEvent = z.input<typeof agentSpeechEvent>;
35
+ export type AgentSpeechEvent = z.infer<typeof AgentSpeechEvent>;
36
36
 
37
- export const errorEvent = z.object({
37
+ export const ErrorEvent = z.object({
38
38
  event_type: z.literal("error"),
39
39
  content: z.string(),
40
40
  });
41
- export type ErrorEvent = z.input<typeof errorEvent>;
41
+ export type ErrorEvent = z.infer<typeof ErrorEvent>;
42
42
 
43
- export const warningEvent = z.object({
43
+ export const WarningEvent = z.object({
44
44
  event_type: z.literal("warning"),
45
45
  content: z.string(),
46
46
  });
47
- export type WarningEvent = z.input<typeof warningEvent>;
47
+ export type WarningEvent = z.infer<typeof WarningEvent>;
48
48
 
49
- export const agentQuestionEvent = z.object({
49
+ export const AgentQuestionEvent = z.object({
50
50
  event_type: z.literal("agent-question"),
51
51
  question_id: z.string(),
52
52
  question: z.string(),
53
53
  });
54
- export type AgentQuestionEvent = z.input<typeof agentQuestionEvent>;
54
+ export type AgentQuestionEvent = z.infer<typeof AgentQuestionEvent>;
55
55
 
56
- export const intentEvent = z.object({
56
+ export const IntentEvent = z.object({
57
57
  event_type: z.literal("intent"),
58
58
  intent_id: z.string(),
59
59
  intent_summary: z.string(),
60
60
  });
61
- export type IntentEvent = z.input<typeof intentEvent>;
61
+ export type IntentEvent = z.infer<typeof IntentEvent>;
62
62
 
63
- export const actionItemCompletedEvent = z.object({
63
+ export const ActionItemCompletedEvent = z.object({
64
64
  event_type: z.literal("action-item-done"),
65
65
  key: z.string(),
66
66
  payload: z.unknown(),
67
67
  });
68
- export type ActionItemCompletedEvent = z.input<typeof actionItemCompletedEvent>;
68
+ export type ActionItemCompletedEvent = z.infer<typeof ActionItemCompletedEvent>;
69
69
 
70
- export const taskCompletedEvent = z.object({
70
+ export const TaskCompletedEvent = z.object({
71
71
  event_type: z.literal("task-done"),
72
72
 
73
73
  task_id: z.string(),
74
74
  });
75
- export type TaskCompletedEvent = z.input<typeof taskCompletedEvent>;
75
+ export type TaskCompletedEvent = z.infer<typeof TaskCompletedEvent>;
76
76
 
77
- export const outboundCallConnected = z.object({
77
+ export const OutboundCallConnected = z.object({
78
78
  event_type: z.literal("outbound-call-connected"),
79
79
  });
80
- export type OutboundCallConnected = z.input<typeof outboundCallConnected>;
80
+ export type OutboundCallConnected = z.infer<typeof OutboundCallConnected>;
81
81
 
82
- export const outboundCallFailed = z.object({
82
+ export const OutboundCallFailed = z.object({
83
83
  event_type: z.literal("outbound-call-failed"),
84
84
 
85
85
  error_code: z.int(),
86
86
  error_reason: z.string(),
87
87
  });
88
- export type OutboundCallFailed = z.input<typeof outboundCallFailed>;
88
+ export type OutboundCallFailed = z.infer<typeof OutboundCallFailed>;
89
89
 
90
- export const botSessionEnded = z.object({
90
+ export const BotSessionEnded = z.object({
91
91
  event_type: z.literal("bot-session-ended"),
92
92
  });
93
- export type BotSessionEnded = z.input<typeof botSessionEnded>;
94
-
95
- export const anyEvent = z.union([
96
- sessionStartedEvent,
97
- inboundCallEvent,
98
- callerSpeechEvent,
99
- agentSpeechEvent,
100
- errorEvent,
101
- warningEvent,
102
- agentQuestionEvent,
103
- intentEvent,
104
- actionItemCompletedEvent,
105
- taskCompletedEvent,
106
- outboundCallConnected,
107
- outboundCallFailed,
108
- botSessionEnded,
93
+ export type BotSessionEnded = z.infer<typeof BotSessionEnded>;
94
+
95
+ export const ChoiceQueryEvent = z.object({
96
+ event_type: z.literal("choice-query"),
97
+ field_key: z.string(),
98
+ query: z.string(),
99
+ query_id: z.string(),
100
+ });
101
+ export type ChoiceQueryEvent = z.infer<typeof ChoiceQueryEvent>;
102
+
103
+ export const GuavaEvent = z.union([
104
+ SessionStartedEvent,
105
+ InboundCallEvent,
106
+ CallerSpeechEvent,
107
+ AgentSpeechEvent,
108
+ ErrorEvent,
109
+ WarningEvent,
110
+ AgentQuestionEvent,
111
+ IntentEvent,
112
+ ActionItemCompletedEvent,
113
+ TaskCompletedEvent,
114
+ OutboundCallConnected,
115
+ OutboundCallFailed,
116
+ BotSessionEnded,
117
+ ChoiceQueryEvent,
109
118
  ]);
110
- export type GuavaEvent = z.input<typeof anyEvent>;
119
+ export type GuavaEvent = z.infer<typeof GuavaEvent>;
120
+
121
+ const _KNOWN_EVENT_TYPES = new Set(
122
+ GuavaEvent.options.map((schema) => schema.shape.event_type.value),
123
+ );
111
124
 
112
125
  export function decodeEvent(
113
126
  serialized_event: string | ArrayBuffer | Buffer | Buffer[],
114
127
  ): GuavaEvent | null {
115
128
  let data: Record<string, any>;
116
- if (typeof serialized_event == "string") {
129
+ if (typeof serialized_event === "string") {
117
130
  data = JSON.parse(serialized_event);
118
131
  } else if (serialized_event instanceof ArrayBuffer) {
119
132
  data = JSON.parse(new TextDecoder().decode(serialized_event));
120
- } else if (serialized_event instanceof Array) {
133
+ } else if (Array.isArray(serialized_event)) {
121
134
  let decoded = "";
122
135
  for (const buf of serialized_event) {
123
136
  decoded += buf.toString("utf8");
@@ -127,16 +140,18 @@ export function decodeEvent(
127
140
  data = JSON.parse(serialized_event.toString("utf8"));
128
141
  }
129
142
 
130
- try {
131
- return anyEvent.parse(data);
132
- } catch (e) {
133
- // TODO logging global use signleton to warn
143
+ if (!_KNOWN_EVENT_TYPES.has(data.event_type)) {
144
+ process.emitWarning(
145
+ `Received an unknown event type ${data.event_type}. Update to a newer version of this SDK.`,
146
+ );
134
147
  return null;
135
148
  }
149
+
150
+ return GuavaEvent.parse(data);
136
151
  }
137
152
 
138
- export const inboundTunnelEvent = z.object({
153
+ export const InboundTunnelEvent = z.object({
139
154
  call_id: z.string(),
140
- event: anyEvent,
155
+ event: GuavaEvent,
141
156
  });
142
- export type inboundTunnelEvent = z.input<typeof inboundTunnelEvent>;
157
+ export type InboundTunnelEvent = z.infer<typeof InboundTunnelEvent>;
@@ -1,3 +1,45 @@
1
+ export function mockAppointmentsForFuture(nextNDays: number = 25): string[] {
2
+ const timeZone = "America/Los_Angeles";
3
+ const mockAppointments: string[] = [];
4
+
5
+ const now = new Date();
6
+
7
+ // Get today's date parts in America/Los_Angeles
8
+ const laDateParts = new Intl.DateTimeFormat("en-CA", {
9
+ timeZone,
10
+ year: "numeric",
11
+ month: "2-digit",
12
+ day: "2-digit",
13
+ }).formatToParts(now);
14
+
15
+ const year = Number(laDateParts.find((p) => p.type === "year")?.value);
16
+ const month = Number(laDateParts.find((p) => p.type === "month")?.value);
17
+ const day = Number(laDateParts.find((p) => p.type === "day")?.value);
18
+
19
+ const times = [
20
+ [9, 0],
21
+ [10, 30],
22
+ [13, 0],
23
+ [15, 30],
24
+ ] as const;
25
+
26
+ for (let offset = 1; offset <= nextNDays; offset++) {
27
+ const baseDate = new Date(Date.UTC(year, month - 1, day + offset));
28
+
29
+ const y = baseDate.getUTCFullYear();
30
+ const m = String(baseDate.getUTCMonth() + 1).padStart(2, "0");
31
+ const d = String(baseDate.getUTCDate()).padStart(2, "0");
32
+
33
+ for (const [hour, minute] of times) {
34
+ const hh = String(hour).padStart(2, "0");
35
+ const mm = String(minute).padStart(2, "0");
36
+ mockAppointments.push(`${y}-${m}-${d}T${hh}:${mm}:00`);
37
+ }
38
+ }
39
+
40
+ return mockAppointments;
41
+ }
42
+
1
43
  export const PROPERTY_INSURANCE_POLICY = `HARPER VALLEY PROPERTY INSURANCE
2
44
  COMPREHENSIVE RESIDENTIAL PROGRAM MANUAL
3
45
 
@@ -1,12 +1,13 @@
1
1
  import OpenAI, { toFile } from "openai";
2
- import { type Logger } from "../logging.ts";
2
+ import { zodTextFormat } from "openai/helpers/zod";
3
+ import { type Logger, getDefaultLogger } from "../logging.ts";
3
4
  import * as z from "zod";
5
+ import { telemetryClient } from "../telemetry.ts";
4
6
 
5
7
  // from beta.py
6
8
  // TODO: Remove after beta
7
9
  function beta_create_openai_client(logger: Logger) {
8
- const baseUrl =
9
- process.env.GUAVA_BASE_URL ?? "https://guava-dev.gridspace.com/";
10
+ const baseUrl = process.env.GUAVA_BASE_URL ?? "https://guava-dev.gridspace.com/";
10
11
  // to get it working with OpenAI TS/JS client
11
12
  const basedUrl = new URL("openai/v1/", baseUrl);
12
13
  logger.info(`Creating beta OpenAI client`);
@@ -16,6 +17,7 @@ function beta_create_openai_client(logger: Logger) {
16
17
  });
17
18
  }
18
19
 
20
+ @telemetryClient.trackClass()
19
21
  export class IntentRecognizer<Choices extends readonly string[]> {
20
22
  private client: OpenAI;
21
23
  private intentChoices: Choices;
@@ -43,40 +45,36 @@ Possible Choices: ${this.intentChoices}.
43
45
  }
44
46
  }
45
47
 
48
+ @telemetryClient.trackClass()
46
49
  export class DocumentQA {
47
50
  private client: OpenAI;
48
51
  private vector_store: Promise<OpenAI.VectorStore>;
49
52
  private logger: Logger;
53
+
50
54
  constructor(
51
55
  vector_store_name: string,
52
56
  document: string,
53
- logger: Logger,
57
+ logger: Logger = getDefaultLogger(),
54
58
  client?: OpenAI,
55
59
  ) {
56
60
  this.client = client ?? beta_create_openai_client(logger);
57
- this.vector_store = this.getOrCreateVectorStore(
58
- vector_store_name,
59
- document,
60
- );
61
+ this.vector_store = this.getOrCreateVectorStore(vector_store_name, document);
61
62
  this.logger = logger;
62
63
  }
63
64
 
64
65
  async getOrCreateVectorStore(vector_store_name: string, document: string) {
65
66
  const encoder = new TextEncoder();
66
67
  const document_buffer = encoder.encode(document);
67
- const document_hash_buffer = await crypto.subtle.digest(
68
- "SHA-256",
69
- document_buffer,
70
- );
68
+ const document_hash_buffer = await crypto.subtle.digest("SHA-256", document_buffer);
71
69
  const u8view = new Uint8Array(document_hash_buffer);
72
70
  const document_hash: string = Array.from(u8view)
73
71
  .map((b) => b.toString(16).padStart(2, "0"))
74
72
  .join("");
75
73
  for await (const vs of this.client.vectorStores.list()) {
76
74
  if (
77
- vs.name == vector_store_name &&
75
+ vs.name === vector_store_name &&
78
76
  vs.metadata &&
79
- vs.metadata["document_hash"] == document_hash
77
+ vs.metadata.document_hash === document_hash
80
78
  ) {
81
79
  this.logger.info("Re-using existing vector store...");
82
80
  return vs;
@@ -96,10 +94,7 @@ export class DocumentQA {
96
94
  this.logger.info("Uploading file...");
97
95
  await this.client.vectorStores.files.uploadAndPoll(
98
96
  vector_store.id,
99
- await toFile(
100
- new Blob([document], { type: "text/plain" }),
101
- "document.txt",
102
- ),
97
+ await toFile(new Blob([document], { type: "text/plain" }), "document.txt"),
103
98
  );
104
99
 
105
100
  this.logger.info("Updating vector store metadata...");
@@ -131,3 +126,63 @@ export class DocumentQA {
131
126
  return response.output_text;
132
127
  }
133
128
  }
129
+
130
+ const filterSchema = z.object({
131
+ matchingAppointments: z.array(z.string()).describe("List of datetimes matching the query."),
132
+ otherAppointments: z
133
+ .array(z.string())
134
+ .describe("If no datetimes match the query, list of datetimes to suggest."),
135
+ });
136
+
137
+ @telemetryClient.trackClass()
138
+ export class DatetimeFilter {
139
+ private client: OpenAI;
140
+ private sourceList: string[];
141
+
142
+ constructor(
143
+ { sourceList, client }: { sourceList: string[]; client?: OpenAI },
144
+ logger: Logger = getDefaultLogger(),
145
+ ) {
146
+ this.sourceList = sourceList;
147
+ this.client = client ?? beta_create_openai_client(logger);
148
+ }
149
+
150
+ async filter(
151
+ query: string,
152
+ { maxResults = 5 }: { maxResults?: number } = {},
153
+ ): Promise<[string[], string[]]> {
154
+ const appointmentTimesStr = this.sourceList.join("\n");
155
+ const today = new Date().toLocaleDateString("en-US", {
156
+ month: "long",
157
+ day: "numeric",
158
+ year: "numeric",
159
+ });
160
+ const prompt = `\
161
+ Return a few datetimes in the list matching the query. If no datetimes match the query, return a few other datetimes from the list that can be used as close suggestions.
162
+ NEVER HALLUCINATE DATETIMES THAT ARE NOT IN THE LIST.
163
+ Query: ${query}
164
+ Today's Date: ${today}
165
+ Appointment Times:
166
+ ${appointmentTimesStr}
167
+ ==================
168
+ You must return at most ${maxResults} options per list.`;
169
+
170
+ const response = await this.client.responses.parse({
171
+ model: "gpt-5-mini",
172
+ input: [{ role: "system", content: prompt }],
173
+ text: { format: zodTextFormat(filterSchema, "filter") },
174
+ reasoning: { effort: "medium" },
175
+ });
176
+
177
+ const output = response.output_parsed;
178
+
179
+ if (!output) {
180
+ throw new Error("Failed to produce parseable output.");
181
+ }
182
+
183
+ return [
184
+ output.matchingAppointments.slice(0, maxResults),
185
+ output.otherAppointments.slice(0, maxResults),
186
+ ];
187
+ }
188
+ }