index.ts 4.4 KB

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