index.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import { Bus } from "@/bus"
  2. import { BusEvent } from "@/bus/bus-event"
  3. import { Identifier } from "@/id/id"
  4. import { Instance } from "@/project/instance"
  5. import { Log } from "@/util/log"
  6. import z from "zod"
  7. export namespace Question {
  8. const log = Log.create({ service: "question" })
  9. export const Option = z
  10. .object({
  11. label: z.string().describe("Display text (1-5 words, concise)"),
  12. description: z.string().describe("Explanation of choice"),
  13. })
  14. .meta({
  15. ref: "QuestionOption",
  16. })
  17. export type Option = z.infer<typeof Option>
  18. export const Info = z
  19. .object({
  20. question: z.string().describe("Complete question"),
  21. header: z.string().max(12).describe("Very short label (max 12 chars)"),
  22. options: z.array(Option).describe("Available choices"),
  23. multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
  24. custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),
  25. })
  26. .meta({
  27. ref: "QuestionInfo",
  28. })
  29. export type Info = z.infer<typeof Info>
  30. export const Request = z
  31. .object({
  32. id: Identifier.schema("question"),
  33. sessionID: Identifier.schema("session"),
  34. questions: z.array(Info).describe("Questions to ask"),
  35. tool: z
  36. .object({
  37. messageID: z.string(),
  38. callID: z.string(),
  39. })
  40. .optional(),
  41. })
  42. .meta({
  43. ref: "QuestionRequest",
  44. })
  45. export type Request = z.infer<typeof Request>
  46. export const Answer = z.array(z.string()).meta({
  47. ref: "QuestionAnswer",
  48. })
  49. export type Answer = z.infer<typeof Answer>
  50. export const Reply = z.object({
  51. answers: z
  52. .array(Answer)
  53. .describe("User answers in order of questions (each answer is an array of selected labels)"),
  54. })
  55. export type Reply = z.infer<typeof Reply>
  56. export const Event = {
  57. Asked: BusEvent.define("question.asked", Request),
  58. Replied: BusEvent.define(
  59. "question.replied",
  60. z.object({
  61. sessionID: z.string(),
  62. requestID: z.string(),
  63. answers: z.array(Answer),
  64. }),
  65. ),
  66. Rejected: BusEvent.define(
  67. "question.rejected",
  68. z.object({
  69. sessionID: z.string(),
  70. requestID: z.string(),
  71. }),
  72. ),
  73. }
  74. const state = Instance.state(async () => {
  75. const pending: Record<
  76. string,
  77. {
  78. info: Request
  79. resolve: (answers: Answer[]) => void
  80. reject: (e: any) => void
  81. }
  82. > = {}
  83. return {
  84. pending,
  85. }
  86. })
  87. export async function ask(input: {
  88. sessionID: string
  89. questions: Info[]
  90. tool?: { messageID: string; callID: string }
  91. }): Promise<Answer[]> {
  92. const s = await state()
  93. const id = Identifier.ascending("question")
  94. log.info("asking", { id, questions: input.questions.length })
  95. return new Promise<Answer[]>((resolve, reject) => {
  96. const info: Request = {
  97. id,
  98. sessionID: input.sessionID,
  99. questions: input.questions,
  100. tool: input.tool,
  101. }
  102. s.pending[id] = {
  103. info,
  104. resolve,
  105. reject,
  106. }
  107. Bus.publish(Event.Asked, info)
  108. })
  109. }
  110. export async function reply(input: { requestID: string; answers: Answer[] }): Promise<void> {
  111. const s = await state()
  112. const existing = s.pending[input.requestID]
  113. if (!existing) {
  114. log.warn("reply for unknown request", { requestID: input.requestID })
  115. return
  116. }
  117. delete s.pending[input.requestID]
  118. log.info("replied", { requestID: input.requestID, answers: input.answers })
  119. Bus.publish(Event.Replied, {
  120. sessionID: existing.info.sessionID,
  121. requestID: existing.info.id,
  122. answers: input.answers,
  123. })
  124. existing.resolve(input.answers)
  125. }
  126. export async function reject(requestID: string): Promise<void> {
  127. const s = await state()
  128. const existing = s.pending[requestID]
  129. if (!existing) {
  130. log.warn("reject for unknown request", { requestID })
  131. return
  132. }
  133. delete s.pending[requestID]
  134. log.info("rejected", { requestID })
  135. Bus.publish(Event.Rejected, {
  136. sessionID: existing.info.sessionID,
  137. requestID: existing.info.id,
  138. })
  139. existing.reject(new RejectedError())
  140. }
  141. export class RejectedError extends Error {
  142. constructor() {
  143. super("The user dismissed this question")
  144. }
  145. }
  146. export async function list() {
  147. return state().then((x) => Object.values(x.pending).map((x) => x.info))
  148. }
  149. }