summary.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { Provider } from "@/provider/provider"
  2. import { fn } from "@/util/fn"
  3. import z from "zod"
  4. import { Session } from "."
  5. import { generateText, type ModelMessage } from "ai"
  6. import { MessageV2 } from "./message-v2"
  7. import { Identifier } from "@/id/id"
  8. import { Snapshot } from "@/snapshot"
  9. import { ProviderTransform } from "@/provider/transform"
  10. import { SystemPrompt } from "./system"
  11. import { Log } from "@/util/log"
  12. export namespace SessionSummary {
  13. const log = Log.create({ service: "session.summary" })
  14. export const summarize = fn(
  15. z.object({
  16. sessionID: z.string(),
  17. messageID: z.string(),
  18. }),
  19. async (input) => {
  20. const all = await Session.messages(input.sessionID)
  21. await Promise.all([
  22. summarizeSession({ sessionID: input.sessionID, messages: all }),
  23. summarizeMessage({ messageID: input.messageID, messages: all }),
  24. ])
  25. },
  26. )
  27. async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
  28. const files = new Set(
  29. input.messages
  30. .flatMap((x) => x.parts)
  31. .filter((x) => x.type === "patch")
  32. .flatMap((x) => x.files),
  33. )
  34. console.log(files)
  35. const diffs = await computeDiff({ messages: input.messages }).then((x) =>
  36. x.filter((x) => files.has(x.file)),
  37. )
  38. await Session.update(input.sessionID, (draft) => {
  39. draft.summary = {
  40. diffs,
  41. }
  42. })
  43. }
  44. async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
  45. const messages = input.messages.filter(
  46. (m) =>
  47. m.info.id === input.messageID ||
  48. (m.info.role === "assistant" && m.info.parentID === input.messageID),
  49. )
  50. const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
  51. const userMsg = msgWithParts.info as MessageV2.User
  52. const diffs = await computeDiff({ messages })
  53. userMsg.summary = {
  54. ...userMsg.summary,
  55. diffs,
  56. }
  57. await Session.updateMessage(userMsg)
  58. const assistantMsg = messages.find((m) => m.info.role === "assistant")!
  59. .info as MessageV2.Assistant
  60. const small = await Provider.getSmallModel(assistantMsg.providerID)
  61. if (!small) return
  62. const textPart = msgWithParts.parts.find(
  63. (p) => p.type === "text" && !p.synthetic,
  64. ) as MessageV2.TextPart
  65. if (textPart && !userMsg.summary?.title) {
  66. const result = await generateText({
  67. maxOutputTokens: small.info.reasoning ? 1500 : 20,
  68. providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, {}),
  69. messages: [
  70. ...SystemPrompt.title(small.providerID).map(
  71. (x): ModelMessage => ({
  72. role: "system",
  73. content: x,
  74. }),
  75. ),
  76. {
  77. role: "user" as const,
  78. content: textPart?.text ?? "",
  79. },
  80. ],
  81. model: small.language,
  82. })
  83. log.info("title", { title: result.text })
  84. userMsg.summary.title = result.text
  85. await Session.updateMessage(userMsg)
  86. }
  87. if (
  88. messages.some(
  89. (m) =>
  90. m.info.role === "assistant" &&
  91. m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
  92. )
  93. ) {
  94. const result = await generateText({
  95. model: small.language,
  96. maxOutputTokens: 100,
  97. messages: [
  98. {
  99. role: "user",
  100. content: `
  101. Summarize the following conversation into 2 sentences MAX explaining what the assistant did and why. Do not explain the user's input. Do not speak in the third person about the assistant.
  102. <conversation>
  103. ${JSON.stringify(MessageV2.toModelMessage(messages))}
  104. </conversation>
  105. `,
  106. },
  107. ],
  108. })
  109. userMsg.summary.body = result.text
  110. log.info("body", { body: result.text })
  111. await Session.updateMessage(userMsg)
  112. }
  113. }
  114. export const diff = fn(
  115. z.object({
  116. sessionID: Identifier.schema("session"),
  117. messageID: Identifier.schema("message").optional(),
  118. }),
  119. async (input) => {
  120. let all = await Session.messages(input.sessionID)
  121. if (input.messageID)
  122. all = all.filter(
  123. (x) =>
  124. x.info.id === input.messageID ||
  125. (x.info.role === "assistant" && x.info.parentID === input.messageID),
  126. )
  127. return computeDiff({
  128. messages: all,
  129. })
  130. },
  131. )
  132. async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
  133. let from: string | undefined
  134. let to: string | undefined
  135. // scan assistant messages to find earliest from and latest to
  136. // snapshot
  137. for (const item of input.messages) {
  138. if (!from) {
  139. for (const part of item.parts) {
  140. if (part.type === "step-start" && part.snapshot) {
  141. from = part.snapshot
  142. break
  143. }
  144. }
  145. }
  146. for (const part of item.parts) {
  147. if (part.type === "step-finish" && part.snapshot) {
  148. to = part.snapshot
  149. break
  150. }
  151. }
  152. }
  153. if (from && to) return Snapshot.diffFull(from, to)
  154. return []
  155. }
  156. }