summary.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { Provider } from "@/provider/provider"
  2. import { fn } from "@/util/fn"
  3. import z from "zod"
  4. import { Session } from "."
  5. import { generateText } from "ai"
  6. import { MessageV2 } from "./message-v2"
  7. import { Flag } from "@/flag/flag"
  8. import { Identifier } from "@/id/id"
  9. import { Snapshot } from "@/snapshot"
  10. export namespace SessionSummary {
  11. export const summarize = fn(
  12. z.object({
  13. sessionID: z.string(),
  14. messageID: z.string(),
  15. providerID: z.string(),
  16. }),
  17. async (input) => {
  18. const all = await Session.messages(input.sessionID)
  19. await Promise.all([
  20. summarizeSession({ sessionID: input.sessionID, messages: all }),
  21. summarizeMessage({ messageID: input.messageID, messages: all }),
  22. ])
  23. },
  24. )
  25. async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
  26. const diffs = await computeDiff({ messages: input.messages })
  27. await Session.update(input.sessionID, (draft) => {
  28. draft.summary = {
  29. diffs,
  30. }
  31. })
  32. }
  33. async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
  34. const messages = input.messages.filter(
  35. (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
  36. )
  37. const userMsg = messages.find((m) => m.info.id === input.messageID)!
  38. const diffs = await computeDiff({ messages })
  39. userMsg.info.summary = {
  40. diffs,
  41. text: "",
  42. }
  43. if (
  44. Flag.OPENCODE_EXPERIMENTAL_TURN_SUMMARY &&
  45. messages.every((m) => m.info.role !== "assistant" || m.info.time.completed)
  46. ) {
  47. const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
  48. const small = await Provider.getSmallModel(assistantMsg.providerID)
  49. if (!small) return
  50. const result = await generateText({
  51. model: small.language,
  52. maxOutputTokens: 100,
  53. messages: [
  54. {
  55. role: "user",
  56. content: `
  57. Summarize the following conversation into 2 sentences MAX explaining what the assistant did and why. Do not explain the user's input.
  58. <conversation>
  59. ${JSON.stringify(MessageV2.toModelMessage(messages))}
  60. </conversation>
  61. `,
  62. },
  63. ],
  64. })
  65. userMsg.info.summary = {
  66. text: result.text,
  67. diffs: [],
  68. }
  69. }
  70. await Session.updateMessage(userMsg.info)
  71. }
  72. export const diff = fn(
  73. z.object({
  74. sessionID: Identifier.schema("session"),
  75. messageID: Identifier.schema("message").optional(),
  76. }),
  77. async (input) => {
  78. let all = await Session.messages(input.sessionID)
  79. if (input.messageID)
  80. all = all.filter(
  81. (x) => x.info.id === input.messageID || (x.info.role === "assistant" && x.info.parentID === input.messageID),
  82. )
  83. return computeDiff({
  84. messages: all,
  85. })
  86. },
  87. )
  88. async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
  89. let from: string | undefined
  90. let to: string | undefined
  91. // scan assistant messages to find earliest from and latest to
  92. // snapshot
  93. for (const item of input.messages) {
  94. if (!from) {
  95. for (const part of item.parts) {
  96. if (part.type === "step-start" && part.snapshot) {
  97. from = part.snapshot
  98. break
  99. }
  100. }
  101. }
  102. for (const part of item.parts) {
  103. if (part.type === "step-finish" && part.snapshot) {
  104. to = part.snapshot
  105. break
  106. }
  107. }
  108. }
  109. if (from && to) return Snapshot.diffFull(from, to)
  110. return []
  111. }
  112. }