@studious-lms/server 1.26.0 → 1.27.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 (129) hide show
  1. package/.env.example +3 -1
  2. package/.github/workflows/npm-publish.yml +1 -0
  3. package/AGENTS.md +84 -0
  4. package/dist/models/cloudStorage.d.ts +11 -0
  5. package/dist/models/cloudStorage.d.ts.map +1 -1
  6. package/dist/models/cloudStorage.js +18 -2
  7. package/dist/models/cloudStorage.js.map +1 -1
  8. package/dist/models/conversation.d.ts +1 -0
  9. package/dist/models/conversation.d.ts.map +1 -1
  10. package/dist/models/labChat.d.ts +40 -0
  11. package/dist/models/labChat.d.ts.map +1 -1
  12. package/dist/models/labChat.js +32 -2
  13. package/dist/models/labChat.js.map +1 -1
  14. package/dist/models/message.d.ts +3 -0
  15. package/dist/models/message.d.ts.map +1 -1
  16. package/dist/pipelines/aiLabChat.d.ts +2 -0
  17. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  18. package/dist/pipelines/aiLabChat.js +60 -16
  19. package/dist/pipelines/aiLabChat.js.map +1 -1
  20. package/dist/pipelines/aiLabChatContract.d.ts +170 -2
  21. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  22. package/dist/pipelines/aiLabChatContract.js +30 -2
  23. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  24. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  25. package/dist/pipelines/aiNewtonChat.js +30 -5
  26. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  27. package/dist/pipelines/labChatPrompt.d.ts +1 -1
  28. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  29. package/dist/pipelines/labChatPrompt.js +13 -14
  30. package/dist/pipelines/labChatPrompt.js.map +1 -1
  31. package/dist/routers/_app.d.ts +220 -10
  32. package/dist/routers/_app.d.ts.map +1 -1
  33. package/dist/routers/admin.d.ts +0 -1
  34. package/dist/routers/admin.d.ts.map +1 -1
  35. package/dist/routers/conversation.d.ts +1 -0
  36. package/dist/routers/conversation.d.ts.map +1 -1
  37. package/dist/routers/labChat.d.ts +10 -0
  38. package/dist/routers/labChat.d.ts.map +1 -1
  39. package/dist/routers/labChat.js +6 -3
  40. package/dist/routers/labChat.js.map +1 -1
  41. package/dist/routers/message.d.ts +37 -4
  42. package/dist/routers/message.d.ts.map +1 -1
  43. package/dist/routers/message.js +24 -7
  44. package/dist/routers/message.js.map +1 -1
  45. package/dist/routers/parent.d.ts +18 -0
  46. package/dist/routers/parent.d.ts.map +1 -1
  47. package/dist/routers/parent.js +10 -3
  48. package/dist/routers/parent.js.map +1 -1
  49. package/dist/routers/studentProgress.d.ts +44 -0
  50. package/dist/routers/studentProgress.d.ts.map +1 -1
  51. package/dist/routers/studentProgress.js +12 -3
  52. package/dist/routers/studentProgress.js.map +1 -1
  53. package/dist/routes/aiStream.js +32 -2
  54. package/dist/routes/aiStream.js.map +1 -1
  55. package/dist/seedDatabase.d.ts.map +1 -1
  56. package/dist/seedDatabase.js +107 -3
  57. package/dist/seedDatabase.js.map +1 -1
  58. package/dist/services/adminUser.d.ts +0 -1
  59. package/dist/services/adminUser.d.ts.map +1 -1
  60. package/dist/services/adminUser.js +7 -4
  61. package/dist/services/adminUser.js.map +1 -1
  62. package/dist/services/aiChatStream.d.ts +15 -0
  63. package/dist/services/aiChatStream.d.ts.map +1 -1
  64. package/dist/services/aiChatStream.js +144 -74
  65. package/dist/services/aiChatStream.js.map +1 -1
  66. package/dist/services/aiGeneration.d.ts +4 -0
  67. package/dist/services/aiGeneration.d.ts.map +1 -0
  68. package/dist/services/aiGeneration.js +88 -0
  69. package/dist/services/aiGeneration.js.map +1 -0
  70. package/dist/services/aiProposalTools.d.ts +4 -1
  71. package/dist/services/aiProposalTools.d.ts.map +1 -1
  72. package/dist/services/aiProposalTools.js +128 -3
  73. package/dist/services/aiProposalTools.js.map +1 -1
  74. package/dist/services/aiProposals.d.ts +70 -0
  75. package/dist/services/aiProposals.d.ts.map +1 -0
  76. package/dist/services/aiProposals.js +363 -0
  77. package/dist/services/aiProposals.js.map +1 -0
  78. package/dist/services/conversation.d.ts +1 -0
  79. package/dist/services/conversation.d.ts.map +1 -1
  80. package/dist/services/labChat.d.ts +10 -0
  81. package/dist/services/labChat.d.ts.map +1 -1
  82. package/dist/services/labChat.js +52 -3
  83. package/dist/services/labChat.js.map +1 -1
  84. package/dist/services/message.d.ts +24 -8
  85. package/dist/services/message.d.ts.map +1 -1
  86. package/dist/services/message.js +170 -117
  87. package/dist/services/message.js.map +1 -1
  88. package/dist/services/parentPortal.d.ts +14 -0
  89. package/dist/services/parentPortal.d.ts.map +1 -1
  90. package/dist/services/parentPortal.js +72 -2
  91. package/dist/services/parentPortal.js.map +1 -1
  92. package/dist/services/studentProgress.d.ts +50 -0
  93. package/dist/services/studentProgress.d.ts.map +1 -1
  94. package/dist/services/studentProgress.js +107 -2
  95. package/dist/services/studentProgress.js.map +1 -1
  96. package/dist/utils/inference.d.ts +10 -0
  97. package/dist/utils/inference.d.ts.map +1 -1
  98. package/dist/utils/inference.js +77 -2
  99. package/dist/utils/inference.js.map +1 -1
  100. package/dist/workers/processors/createRunJobWorker.js +17 -16
  101. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  102. package/package.json +1 -1
  103. package/prisma/migrations/20260811130000_add_ai_message_feedback/migration.sql +7 -0
  104. package/prisma/schema.prisma +8 -0
  105. package/src/models/cloudStorage.ts +19 -0
  106. package/src/models/labChat.ts +34 -0
  107. package/src/pipelines/aiLabChat.ts +79 -18
  108. package/src/pipelines/aiLabChatContract.ts +35 -0
  109. package/src/pipelines/aiNewtonChat.ts +34 -2
  110. package/src/pipelines/labChatPrompt.ts +11 -12
  111. package/src/routers/labChat.ts +5 -0
  112. package/src/routers/message.ts +34 -5
  113. package/src/routers/parent.ts +11 -0
  114. package/src/routers/studentProgress.ts +15 -0
  115. package/src/routes/aiStream.ts +32 -0
  116. package/src/seedDatabase.ts +107 -2
  117. package/src/services/adminUser.ts +5 -2
  118. package/src/services/aiChatStream.ts +83 -7
  119. package/src/services/aiGeneration.ts +92 -0
  120. package/src/services/aiProposalTools.ts +166 -1
  121. package/src/services/aiProposals.ts +428 -0
  122. package/src/services/labChat.ts +65 -0
  123. package/src/services/message.ts +205 -142
  124. package/src/services/parentPortal.ts +85 -0
  125. package/src/services/studentProgress.ts +135 -0
  126. package/src/utils/inference.ts +85 -0
  127. package/src/workers/processors/createRunJobWorker.ts +17 -17
  128. package/tests/unit/services/adminUser.test.ts +75 -0
  129. package/tests/unit/utils/adminPanelAccess.test.ts +26 -0
@@ -42,7 +42,6 @@ export declare function getUserForAdmin(id: string): Promise<{
42
42
  } | null;
43
43
  sessions: {
44
44
  type: import(".prisma/client").$Enums.SessionType;
45
- id: string;
46
45
  createdAt: Date | null;
47
46
  expiresAt: Date | null;
48
47
  }[];
@@ -1 +1 @@
1
- {"version":3,"file":"adminUser.d.ts","sourceRoot":"/","sources":["services/adminUser.ts"],"names":[],"mappings":"AAIA,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;;;;;;;;;GAsCA;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4C/C"}
1
+ {"version":3,"file":"adminUser.d.ts","sourceRoot":"/","sources":["services/adminUser.ts"],"names":[],"mappings":"AAIA,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;;;;;;;;;GAsCA;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+C/C"}
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="822d0af5-d814-5fdc-ba6b-21fe1cafebb4")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e2d7385a-4270-5961-b70a-36d37ce736b1")}catch(e){}}();
3
3
  import { TRPCError } from '@trpc/server';
4
4
  import { prisma } from '../lib/prisma.js';
5
5
  export async function searchUsersForAdmin(input) {
@@ -63,15 +63,18 @@ export async function getUserForAdmin(id) {
63
63
  },
64
64
  },
65
65
  sessions: {
66
+ where: { type: 'AUTH' },
66
67
  orderBy: { createdAt: 'desc' },
67
68
  take: 10,
68
- select: { id: true, type: true, createdAt: true, expiresAt: true },
69
+ // Session.id is the bearer token — never return it from admin APIs.
70
+ select: { type: true, createdAt: true, expiresAt: true },
69
71
  },
70
72
  _count: {
71
73
  select: {
72
74
  teacherIn: true,
73
75
  studentIn: true,
74
- sessions: true,
76
+ // Keep the count consistent with the AUTH-only session list above.
77
+ sessions: { where: { type: 'AUTH' } },
75
78
  submissions: true,
76
79
  files: true,
77
80
  },
@@ -84,4 +87,4 @@ export async function getUserForAdmin(id) {
84
87
  return user;
85
88
  }
86
89
  //# sourceMappingURL=adminUser.js.map
87
- //# debugId=822d0af5-d814-5fdc-ba6b-21fe1cafebb4
90
+ //# debugId=e2d7385a-4270-5961-b70a-36d37ce736b1
@@ -1 +1 @@
1
- {"version":3,"file":"adminUser.js","sources":["services/adminUser.ts"],"sourceRoot":"/","sourcesContent":["import type { Prisma } from '@prisma/client';\nimport { TRPCError } from '@trpc/server';\nimport { prisma } from '../lib/prisma.js';\n\nexport async function searchUsersForAdmin(input: {\n page: number;\n pageSize: number;\n query?: string;\n}) {\n const skip = input.page * input.pageSize;\n const where: Prisma.UserWhereInput = {};\n const q = input.query?.trim();\n if (q) {\n where.OR = [\n { username: { contains: q, mode: 'insensitive' } },\n { email: { contains: q, mode: 'insensitive' } },\n ];\n }\n\n const [items, total] = await Promise.all([\n prisma.user.findMany({\n where,\n orderBy: { username: 'asc' },\n skip,\n take: input.pageSize,\n select: {\n id: true,\n username: true,\n email: true,\n verified: true,\n school: { select: { id: true, name: true } },\n _count: {\n select: { teacherIn: true, studentIn: true, sessions: true },\n },\n },\n }),\n prisma.user.count({ where }),\n ]);\n\n return {\n items,\n total,\n page: input.page,\n pageSize: input.pageSize,\n pageCount: Math.max(1, Math.ceil(total / input.pageSize)),\n };\n}\n\nexport async function getUserForAdmin(id: string) {\n const user = await prisma.user.findUnique({\n where: { id },\n select: {\n id: true,\n username: true,\n email: true,\n verified: true,\n termsAcceptedAt: true,\n termsVersion: true,\n profile: {\n select: { displayName: true, location: true, createdAt: true },\n },\n school: { select: { id: true, name: true, subdomain: true } },\n schoolMemberships: {\n select: {\n id: true,\n role: true,\n status: true,\n createdAt: true,\n school: { select: { id: true, name: true } },\n },\n },\n sessions: {\n orderBy: { createdAt: 'desc' },\n take: 10,\n select: { id: true, type: true, createdAt: true, expiresAt: true },\n },\n _count: {\n select: {\n teacherIn: true,\n studentIn: true,\n sessions: true,\n submissions: true,\n files: true,\n },\n },\n },\n });\n\n if (!user) {\n throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });\n }\n return user;\n}\n"],"names":[],"mappings":"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","debug_id":"822d0af5-d814-5fdc-ba6b-21fe1cafebb4"}
1
+ {"version":3,"file":"adminUser.js","sources":["services/adminUser.ts"],"sourceRoot":"/","sourcesContent":["import type { Prisma } from '@prisma/client';\nimport { TRPCError } from '@trpc/server';\nimport { prisma } from '../lib/prisma.js';\n\nexport async function searchUsersForAdmin(input: {\n page: number;\n pageSize: number;\n query?: string;\n}) {\n const skip = input.page * input.pageSize;\n const where: Prisma.UserWhereInput = {};\n const q = input.query?.trim();\n if (q) {\n where.OR = [\n { username: { contains: q, mode: 'insensitive' } },\n { email: { contains: q, mode: 'insensitive' } },\n ];\n }\n\n const [items, total] = await Promise.all([\n prisma.user.findMany({\n where,\n orderBy: { username: 'asc' },\n skip,\n take: input.pageSize,\n select: {\n id: true,\n username: true,\n email: true,\n verified: true,\n school: { select: { id: true, name: true } },\n _count: {\n select: { teacherIn: true, studentIn: true, sessions: true },\n },\n },\n }),\n prisma.user.count({ where }),\n ]);\n\n return {\n items,\n total,\n page: input.page,\n pageSize: input.pageSize,\n pageCount: Math.max(1, Math.ceil(total / input.pageSize)),\n };\n}\n\nexport async function getUserForAdmin(id: string) {\n const user = await prisma.user.findUnique({\n where: { id },\n select: {\n id: true,\n username: true,\n email: true,\n verified: true,\n termsAcceptedAt: true,\n termsVersion: true,\n profile: {\n select: { displayName: true, location: true, createdAt: true },\n },\n school: { select: { id: true, name: true, subdomain: true } },\n schoolMemberships: {\n select: {\n id: true,\n role: true,\n status: true,\n createdAt: true,\n school: { select: { id: true, name: true } },\n },\n },\n sessions: {\n where: { type: 'AUTH' },\n orderBy: { createdAt: 'desc' },\n take: 10,\n // Session.id is the bearer token — never return it from admin APIs.\n select: { type: true, createdAt: true, expiresAt: true },\n },\n _count: {\n select: {\n teacherIn: true,\n studentIn: true,\n // Keep the count consistent with the AUTH-only session list above.\n sessions: { where: { type: 'AUTH' } },\n submissions: true,\n files: true,\n },\n },\n },\n });\n\n if (!user) {\n throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });\n }\n return user;\n}\n"],"names":[],"mappings":"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","debug_id":"e2d7385a-4270-5961-b70a-36d37ce736b1"}
@@ -13,6 +13,13 @@
13
13
  */
14
14
  import { type ModelMessage, type ToolSet } from 'ai';
15
15
  import { type AiUsageAttribution } from '../utils/inference.js';
16
+ /**
17
+ * Flag an in-flight stream for cancellation. `streamChatResponse` polls this
18
+ * key between chunks and aborts the model stream when it appears. The worker
19
+ * may run in a different process from the API, so the signal goes through
20
+ * Redis rather than an in-memory AbortController registry.
21
+ */
22
+ export declare function requestStreamCancellation(aiMessageId: string): Promise<void>;
16
23
  /** Default streaming model — matches the existing streamAIResponse path. */
17
24
  export declare const DEFAULT_STREAM_MODEL = "gpt-4o-mini";
18
25
  export interface StreamedToolCall {
@@ -44,6 +51,10 @@ export type StreamPart = {
44
51
  /** Set after generated files are persisted on the message. */
45
52
  fileId?: string;
46
53
  fileName?: string;
54
+ /** Links a propose_* tool call to its pending creation in message meta. */
55
+ proposalId?: string;
56
+ /** Class-files folder the generated file was saved into, if any. */
57
+ folderId?: string;
47
58
  status: 'executing' | 'completed' | 'failed';
48
59
  };
49
60
  export interface GeneratedDocFileLink {
@@ -51,6 +62,7 @@ export interface GeneratedDocFileLink {
51
62
  fileId: string;
52
63
  fileName: string;
53
64
  format?: string;
65
+ folderId?: string;
54
66
  }
55
67
  /** Apply pipeline-resolved formats onto generate_document tool parts. */
56
68
  export declare function applyResolvedFormatsToStreamParts(parts: StreamPart[], docs: Array<{
@@ -82,6 +94,9 @@ export interface StreamChatResult {
82
94
  toolCalls: StreamedToolCall[];
83
95
  /** Ordered text/tool parts, in the exact order the model produced them. */
84
96
  parts: StreamPart[];
97
+ /** True when the stream was stopped by a cancellation request; `text` and
98
+ * `parts` hold whatever was produced before the abort. */
99
+ cancelled: boolean;
85
100
  }
86
101
  /**
87
102
  * Stream a chat completion, broadcasting real text chunks and tool-call
@@ -1 +1 @@
1
- {"version":3,"file":"aiChatStream.d.ts","sourceRoot":"/","sources":["services/aiChatStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAA2B,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAK9E,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAKnF,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,gBAAgB,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IACE,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC9C,CAAC;AAEN,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,UAAU,EAAE,CAmBd;AASD,mFAAmF;AACnF,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,UAAU,EAAE,EACnB,KAAK,EAAE,oBAAoB,EAAE,GAC5B,UAAU,EAAE,CAuCd;AAED,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAS3E;AAmBD,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,2EAA2E;IAC3E,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA4K3B"}
1
+ {"version":3,"file":"aiChatStream.d.ts","sourceRoot":"/","sources":["services/aiChatStream.ts"],"names":[],"mappings":"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"}
@@ -12,15 +12,33 @@
12
12
  * terminal `message-stream-complete` / `message-updated` events.
13
13
  */
14
14
 
15
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b26c8408-f190-53b7-8fce-428be7591574")}catch(e){}}();
15
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b934901d-426b-59b0-933b-6718f701849e")}catch(e){}}();
16
16
  import { streamText, stepCountIs } from 'ai';
17
17
  import { openai } from '@ai-sdk/openai';
18
18
  import { getRedis } from '../lib/redis.js';
19
+ import { prisma } from '../lib/prisma.js';
19
20
  import { pusher, chatChannel } from '../lib/pusher.js';
20
21
  import { logger } from '../utils/logger.js';
21
22
  import { logInferenceUsage } from '../utils/inference.js';
22
23
  const STREAM_REDIS_TTL = 600; // 10 minutes
23
24
  const STREAM_KEY_PREFIX = 'stream:';
25
+ const CANCEL_KEY_PREFIX = 'stream:cancel:';
26
+ const CANCEL_CHECK_INTERVAL_MS = 1000;
27
+ function streamCancelKey(aiMessageId) {
28
+ return `${CANCEL_KEY_PREFIX}${aiMessageId}`;
29
+ }
30
+ /**
31
+ * Flag an in-flight stream for cancellation. `streamChatResponse` polls this
32
+ * key between chunks and aborts the model stream when it appears. The worker
33
+ * may run in a different process from the API, so the signal goes through
34
+ * Redis rather than an in-memory AbortController registry.
35
+ */
36
+ export async function requestStreamCancellation(aiMessageId) {
37
+ const redis = getRedis();
38
+ if (redis) {
39
+ await redis.set(streamCancelKey(aiMessageId), '1', 'EX', STREAM_REDIS_TTL);
40
+ }
41
+ }
24
42
  /** Default streaming model — matches the existing streamAIResponse path. */
25
43
  export const DEFAULT_STREAM_MODEL = 'gpt-4o-mini';
26
44
  /** Apply pipeline-resolved formats onto generate_document tool parts. */
@@ -82,6 +100,7 @@ export function enrichStreamPartsWithGeneratedFiles(parts, files) {
82
100
  fileName: file.fileName,
83
101
  status: 'completed',
84
102
  ...(file.format ? { format: file.format } : {}),
103
+ ...(file.folderId ? { folderId: file.folderId } : {}),
85
104
  };
86
105
  });
87
106
  }
@@ -108,7 +127,7 @@ function toolFieldFromInput(input, key) {
108
127
  }
109
128
  /** Pull a human-meaningful label out of a tool's input args, if present. */
110
129
  function toolTitleFromInput(input) {
111
- for (const key of ['title', 'name', 'query']) {
130
+ for (const key of ['targetTitle', 'title', 'name', 'query']) {
112
131
  const val = toolFieldFromInput(input, key);
113
132
  if (val)
114
133
  return val;
@@ -125,10 +144,42 @@ export async function streamChatResponse(options) {
125
144
  const { conversationId, aiMessageId, system, messages, tools, model = DEFAULT_STREAM_MODEL, temperature = 0.7, maxOutputTokens = 4096, maxSteps = 6, usageReason = 'chat_stream', attribution, } = options;
126
145
  const redis = getRedis();
127
146
  const streamKey = `${STREAM_KEY_PREFIX}${aiMessageId}`;
147
+ const cancelKey = streamCancelKey(aiMessageId);
128
148
  if (redis) {
129
149
  await redis.set(streamKey, '', 'EX', STREAM_REDIS_TTL);
150
+ // A stale flag from a previously cancelled run on the same message id
151
+ // (e.g. a rerun) must not kill this stream instantly.
152
+ await redis.del(cancelKey).catch(() => { });
130
153
  }
131
154
  let fullText = '';
155
+ let cancelled = false;
156
+ let lastCancelCheck = 0;
157
+ const abortController = new AbortController();
158
+ // Poll the cancel flag at most once a second: Redis when available,
159
+ // otherwise the message row (the cancel mutation flips it to CANCELLED).
160
+ const checkCancelled = async () => {
161
+ if (cancelled)
162
+ return true;
163
+ const now = Date.now();
164
+ if (now - lastCancelCheck < CANCEL_CHECK_INTERVAL_MS)
165
+ return false;
166
+ lastCancelCheck = now;
167
+ if (redis) {
168
+ const flag = await redis.get(cancelKey).catch(() => null);
169
+ if (flag)
170
+ cancelled = true;
171
+ }
172
+ else {
173
+ const row = await prisma.message
174
+ .findUnique({ where: { id: aiMessageId }, select: { status: true } })
175
+ .catch(() => null);
176
+ if (row?.status === 'CANCELLED')
177
+ cancelled = true;
178
+ }
179
+ if (cancelled)
180
+ abortController.abort();
181
+ return cancelled;
182
+ };
132
183
  const parts = [];
133
184
  const toolCalls = [];
134
185
  // Map AI SDK toolCallId -> index in `parts` so the executing indicator can
@@ -161,88 +212,106 @@ export async function streamChatResponse(options) {
161
212
  temperature,
162
213
  maxOutputTokens,
163
214
  stopWhen: stepCountIs(maxSteps),
215
+ abortSignal: abortController.signal,
164
216
  });
165
217
  // `fullStream` yields text deltas AND tool events in a single, correctly
166
218
  // ordered sequence — this is what makes inline tool calls possible. We
167
219
  // build an ordered `parts` list and broadcast each event as it happens.
168
- for await (const part of result.fullStream) {
169
- switch (part.type) {
170
- case 'text-delta': {
171
- appendText(part.text);
172
- if (redis) {
173
- await redis.append(streamKey, part.text).catch(() => { });
174
- }
175
- await broadcast('message-stream-chunk', {
176
- messageId: aiMessageId,
177
- chunk: part.text,
178
- });
220
+ try {
221
+ for await (const part of result.fullStream) {
222
+ if (await checkCancelled())
179
223
  break;
180
- }
181
- case 'tool-call': {
182
- const title = toolTitleFromInput(part.input);
183
- const format = toolFieldFromInput(part.input, 'format');
184
- toolPartIndexById.set(part.toolCallId, parts.length);
185
- parts.push({
186
- type: 'tool',
187
- toolCallId: part.toolCallId,
188
- toolName: part.toolName,
189
- ...(title ? { title } : {}),
190
- ...(format ? { format } : {}),
191
- status: 'executing',
192
- });
193
- await broadcast('message-tool-call', {
194
- messageId: aiMessageId,
195
- toolCallId: part.toolCallId,
196
- toolName: part.toolName,
197
- ...(title ? { title } : {}),
198
- ...(format ? { format } : {}),
199
- status: 'executing',
200
- });
224
+ if (part.type === 'abort') {
225
+ cancelled = true;
201
226
  break;
202
227
  }
203
- case 'tool-result':
204
- case 'tool-error': {
205
- const failed = part.type === 'tool-error';
206
- const deferred = isDeferredCompletionTool(part.toolName);
207
- // Deferred tools (e.g. generate_document) only queue work during the
208
- // stream — keep the spinner until the pipeline attaches a file.
209
- const status = failed
210
- ? 'failed'
211
- : deferred
212
- ? 'executing'
213
- : 'completed';
214
- const title = toolTitleFromInput(part.input);
215
- const format = toolFieldFromInput(part.input, 'format');
216
- const idx = toolPartIndexById.get(part.toolCallId);
217
- if (idx !== undefined) {
218
- const existing = parts[idx];
219
- if (existing && existing.type === 'tool') {
220
- existing.status = status;
221
- if (format)
222
- existing.format = format;
228
+ switch (part.type) {
229
+ case 'text-delta': {
230
+ appendText(part.text);
231
+ if (redis) {
232
+ await redis.append(streamKey, part.text).catch(() => { });
223
233
  }
234
+ await broadcast('message-stream-chunk', {
235
+ messageId: aiMessageId,
236
+ chunk: part.text,
237
+ });
238
+ break;
224
239
  }
225
- toolCalls.push({
226
- toolName: part.toolName,
227
- input: part.input,
228
- output: part.type === 'tool-result' ? part.output : undefined,
229
- status: failed ? 'failed' : 'completed',
230
- });
231
- await broadcast('message-tool-call', {
232
- messageId: aiMessageId,
233
- toolCallId: part.toolCallId,
234
- toolName: part.toolName,
235
- ...(title ? { title } : {}),
236
- ...(format ? { format } : {}),
237
- status,
238
- });
239
- break;
240
+ case 'tool-call': {
241
+ const title = toolTitleFromInput(part.input);
242
+ const format = toolFieldFromInput(part.input, 'format');
243
+ toolPartIndexById.set(part.toolCallId, parts.length);
244
+ parts.push({
245
+ type: 'tool',
246
+ toolCallId: part.toolCallId,
247
+ toolName: part.toolName,
248
+ ...(title ? { title } : {}),
249
+ ...(format ? { format } : {}),
250
+ status: 'executing',
251
+ });
252
+ await broadcast('message-tool-call', {
253
+ messageId: aiMessageId,
254
+ toolCallId: part.toolCallId,
255
+ toolName: part.toolName,
256
+ ...(title ? { title } : {}),
257
+ ...(format ? { format } : {}),
258
+ status: 'executing',
259
+ });
260
+ break;
261
+ }
262
+ case 'tool-result':
263
+ case 'tool-error': {
264
+ const failed = part.type === 'tool-error';
265
+ const deferred = isDeferredCompletionTool(part.toolName);
266
+ // Deferred tools (e.g. generate_document) only queue work during the
267
+ // stream — keep the spinner until the pipeline attaches a file.
268
+ const status = failed
269
+ ? 'failed'
270
+ : deferred
271
+ ? 'executing'
272
+ : 'completed';
273
+ const title = toolTitleFromInput(part.input);
274
+ const format = toolFieldFromInput(part.input, 'format');
275
+ const idx = toolPartIndexById.get(part.toolCallId);
276
+ if (idx !== undefined) {
277
+ const existing = parts[idx];
278
+ if (existing && existing.type === 'tool') {
279
+ existing.status = status;
280
+ if (format)
281
+ existing.format = format;
282
+ }
283
+ }
284
+ toolCalls.push({
285
+ toolName: part.toolName,
286
+ input: part.input,
287
+ output: part.type === 'tool-result' ? part.output : undefined,
288
+ status: failed ? 'failed' : 'completed',
289
+ });
290
+ await broadcast('message-tool-call', {
291
+ messageId: aiMessageId,
292
+ toolCallId: part.toolCallId,
293
+ toolName: part.toolName,
294
+ ...(title ? { title } : {}),
295
+ ...(format ? { format } : {}),
296
+ status,
297
+ });
298
+ break;
299
+ }
300
+ default:
301
+ // ignore reasoning/source/step/finish/raw parts
302
+ break;
240
303
  }
241
- default:
242
- // ignore reasoning/source/step/finish/raw parts
243
- break;
244
304
  }
245
305
  }
306
+ catch (streamError) {
307
+ // Aborting the model stream mid-iteration surfaces as an error from
308
+ // `fullStream`; when it is our own cancellation, keep the partials.
309
+ if (!cancelled)
310
+ throw streamError;
311
+ }
312
+ if (cancelled) {
313
+ logger.info('AI chat stream cancelled', { aiMessageId, conversationId });
314
+ }
246
315
  const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);
247
316
  await logInferenceUsage({
248
317
  model,
@@ -259,7 +328,7 @@ export async function streamChatResponse(options) {
259
328
  : undefined,
260
329
  attribution,
261
330
  });
262
- return { text: fullText, toolCalls, parts };
331
+ return { text: fullText, toolCalls, parts, cancelled };
263
332
  }
264
333
  catch (error) {
265
334
  await logInferenceUsage({
@@ -276,8 +345,9 @@ export async function streamChatResponse(options) {
276
345
  finally {
277
346
  if (redis) {
278
347
  await redis.del(streamKey).catch(() => { });
348
+ await redis.del(cancelKey).catch(() => { });
279
349
  }
280
350
  }
281
351
  }
282
352
  //# sourceMappingURL=aiChatStream.js.map
283
- //# debugId=b26c8408-f190-53b7-8fce-428be7591574
353
+ //# debugId=b934901d-426b-59b0-933b-6718f701849e
@@ -1 +1 @@
1
- {"version":3,"file":"aiChatStream.js","sources":["services/aiChatStream.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Shared AI chat streaming helper.\n *\n * Runs a genuine token-by-token stream via the Vercel AI SDK (`streamText`)\n * against OpenAI — the same provider the rest of the app's inference uses\n * (`OPENAI_API_KEY`). Real text tokens are broadcast over Pusher as they are\n * produced, and real tool calls are broadcast inline (in the order the model\n * invokes them) instead of being derived after the fact from a JSON blob.\n *\n * Callers own persistence: this helper returns the accumulated text and the\n * ordered tool calls so the pipeline can write `content`/`meta` and emit the\n * terminal `message-stream-complete` / `message-updated` events.\n */\nimport { streamText, stepCountIs, type ModelMessage, type ToolSet } from 'ai';\nimport { openai } from '@ai-sdk/openai';\nimport { getRedis } from '../lib/redis.js';\nimport { pusher, chatChannel } from '../lib/pusher.js';\nimport { logger } from '../utils/logger.js';\nimport { logInferenceUsage, type AiUsageAttribution } from '../utils/inference.js';\n\nconst STREAM_REDIS_TTL = 600; // 10 minutes\nconst STREAM_KEY_PREFIX = 'stream:';\n\n/** Default streaming model — matches the existing streamAIResponse path. */\nexport const DEFAULT_STREAM_MODEL = 'gpt-4o-mini';\n\nexport interface StreamedToolCall {\n toolName: string;\n /** Args the model passed to the tool. */\n input: unknown;\n /** Value the tool returned (undefined for client-side / no-result tools). */\n output?: unknown;\n status: 'completed' | 'failed';\n}\n\n/**\n * An ordered fragment of an AI message. The message is a sequence of these\n * parts in the exact order the model produced them, so a tool call rendered\n * \"between paragraph 1 and 2\" is represented as\n * `[{text}, {tool}, {text}]` — enabling true inline rendering (and reload\n * persistence) instead of text and tool calls living in separate buckets.\n */\nexport type StreamPart =\n | { type: 'text'; text: string }\n | {\n type: 'tool';\n toolCallId: string;\n toolName: string;\n /** Concrete entity label (e.g. assignment title / search query), if any. */\n title?: string;\n /** Requested output type from the tool args (pdf, pptx, xlsx, …). */\n format?: string;\n /** Set after generated files are persisted on the message. */\n fileId?: string;\n fileName?: string;\n status: 'executing' | 'completed' | 'failed';\n };\n\nexport interface GeneratedDocFileLink {\n title: string;\n fileId: string;\n fileName: string;\n format?: string;\n}\n\n/** Apply pipeline-resolved formats onto generate_document tool parts. */\nexport function applyResolvedFormatsToStreamParts(\n parts: StreamPart[],\n docs: Array<{ title: string; format: string }>,\n): StreamPart[] {\n if (docs.length === 0) return parts;\n\n const pending = [...docs];\n const takeDoc = (title?: string) => {\n if (title) {\n const idx = pending.findIndex(\n (d) => d.title.trim().toLowerCase() === title.trim().toLowerCase(),\n );\n if (idx >= 0) return pending.splice(idx, 1)[0];\n }\n return pending.shift();\n };\n\n return parts.map((part) => {\n if (part.type !== 'tool' || part.toolName !== 'generate_document') return part;\n const doc = takeDoc(part.title);\n return doc ? { ...part, format: doc.format } : part;\n });\n}\n\n/** Tools that queue async work after the model stream; UI stays loading until the pipeline finishes. */\nconst DEFERRED_COMPLETION_TOOLS = new Set(['generate_document']);\n\nfunction isDeferredCompletionTool(toolName: string): boolean {\n return DEFERRED_COMPLETION_TOOLS.has(toolName);\n}\n\n/** Attach file IDs to `generate_document` tool parts for inline download chips. */\nexport function enrichStreamPartsWithGeneratedFiles(\n parts: StreamPart[],\n files: GeneratedDocFileLink[],\n): StreamPart[] {\n if (files.length === 0) return parts;\n\n const docToolIndices = parts\n .map((part, index) => ({ part, index }))\n .filter(\n ({ part }) =>\n part.type === 'tool' && part.toolName === 'generate_document',\n );\n\n const unmatched = [...files];\n const fileForTool = (toolTitle?: string): GeneratedDocFileLink | undefined => {\n if (toolTitle) {\n const idx = unmatched.findIndex(\n (f) => f.title.trim().toLowerCase() === toolTitle.trim().toLowerCase(),\n );\n if (idx >= 0) return unmatched.splice(idx, 1)[0];\n }\n return unmatched.shift();\n };\n\n const fileByIndex = new Map<number, GeneratedDocFileLink>();\n for (const { part, index } of docToolIndices) {\n if (part.type !== 'tool') continue;\n const file = fileForTool(part.title);\n if (file) fileByIndex.set(index, file);\n }\n\n return parts.map((part, index) => {\n const file = fileByIndex.get(index);\n if (!file || part.type !== 'tool') return part;\n return {\n ...part,\n fileId: file.fileId,\n fileName: file.fileName,\n status: 'completed',\n ...(file.format ? { format: file.format } : {}),\n };\n });\n}\n\n/** Mark deferred tools completed (file linked) or failed (generation did not produce a file). */\nexport function finalizeDeferredToolParts(parts: StreamPart[]): StreamPart[] {\n return parts.map((part) => {\n if (part.type !== 'tool' || !isDeferredCompletionTool(part.toolName)) {\n return part;\n }\n if (part.fileId) return { ...part, status: 'completed' };\n if (part.status === 'executing') return { ...part, status: 'failed' };\n return part;\n });\n}\n\nfunction toolFieldFromInput(input: unknown, key: string): string | undefined {\n if (input && typeof input === 'object') {\n const val = (input as Record<string, unknown>)[key];\n if (typeof val === 'string' && val.trim()) return val.trim();\n }\n return undefined;\n}\n\n/** Pull a human-meaningful label out of a tool's input args, if present. */\nfunction toolTitleFromInput(input: unknown): string | undefined {\n for (const key of ['title', 'name', 'query'] as const) {\n const val = toolFieldFromInput(input, key);\n if (val) return val;\n }\n return undefined;\n}\n\nexport interface StreamChatOptions {\n conversationId: string;\n aiMessageId: string;\n system: string;\n messages: ModelMessage[];\n tools?: ToolSet;\n model?: string;\n temperature?: number;\n maxOutputTokens?: number;\n /** Max agent steps (tool round-trips). Defaults to 6. */\n maxSteps?: number;\n /** Reason recorded on the AiUsageLog row. Defaults to 'chat_stream'. */\n usageReason?: string;\n /** Who/what this usage is attributed to (user / school / class). */\n attribution?: AiUsageAttribution;\n}\n\nexport interface StreamChatResult {\n text: string;\n toolCalls: StreamedToolCall[];\n /** Ordered text/tool parts, in the exact order the model produced them. */\n parts: StreamPart[];\n}\n\n/**\n * Stream a chat completion, broadcasting real text chunks and tool-call\n * events over Pusher. Does NOT persist the message or emit the terminal\n * stream-complete event — the caller does that once it has assembled the\n * final `content`/`meta` (e.g. PDF attachments, structured proposals).\n */\nexport async function streamChatResponse(\n options: StreamChatOptions,\n): Promise<StreamChatResult> {\n const {\n conversationId,\n aiMessageId,\n system,\n messages,\n tools,\n model = DEFAULT_STREAM_MODEL,\n temperature = 0.7,\n maxOutputTokens = 4096,\n maxSteps = 6,\n usageReason = 'chat_stream',\n attribution,\n } = options;\n\n const redis = getRedis();\n const streamKey = `${STREAM_KEY_PREFIX}${aiMessageId}`;\n if (redis) {\n await redis.set(streamKey, '', 'EX', STREAM_REDIS_TTL);\n }\n\n let fullText = '';\n const parts: StreamPart[] = [];\n const toolCalls: StreamedToolCall[] = [];\n // Map AI SDK toolCallId -> index in `parts` so the executing indicator can\n // be flipped to completed/failed in place (preserving its position).\n const toolPartIndexById = new Map<string, number>();\n\n const appendText = (text: string) => {\n fullText += text;\n const last = parts[parts.length - 1];\n if (last && last.type === 'text') {\n last.text += text;\n } else {\n parts.push({ type: 'text', text });\n }\n };\n\n const broadcast = async (event: string, payload: Record<string, unknown>) => {\n try {\n await pusher.trigger(chatChannel(conversationId), event, payload);\n } catch (err) {\n logger.warn(`Failed to broadcast ${event}`, { err, aiMessageId });\n }\n };\n\n try {\n const result = streamText({\n model: openai(model),\n system,\n messages,\n ...(tools ? { tools } : {}),\n temperature,\n maxOutputTokens,\n stopWhen: stepCountIs(maxSteps),\n });\n\n // `fullStream` yields text deltas AND tool events in a single, correctly\n // ordered sequence — this is what makes inline tool calls possible. We\n // build an ordered `parts` list and broadcast each event as it happens.\n for await (const part of result.fullStream) {\n switch (part.type) {\n case 'text-delta': {\n appendText(part.text);\n if (redis) {\n await redis.append(streamKey, part.text).catch(() => {});\n }\n await broadcast('message-stream-chunk', {\n messageId: aiMessageId,\n chunk: part.text,\n });\n break;\n }\n case 'tool-call': {\n const title = toolTitleFromInput(part.input);\n const format = toolFieldFromInput(part.input, 'format');\n toolPartIndexById.set(part.toolCallId, parts.length);\n parts.push({\n type: 'tool',\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n ...(title ? { title } : {}),\n ...(format ? { format } : {}),\n status: 'executing',\n });\n await broadcast('message-tool-call', {\n messageId: aiMessageId,\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n ...(title ? { title } : {}),\n ...(format ? { format } : {}),\n status: 'executing',\n });\n break;\n }\n case 'tool-result':\n case 'tool-error': {\n const failed = part.type === 'tool-error';\n const deferred = isDeferredCompletionTool(part.toolName);\n // Deferred tools (e.g. generate_document) only queue work during the\n // stream — keep the spinner until the pipeline attaches a file.\n const status: 'executing' | 'completed' | 'failed' = failed\n ? 'failed'\n : deferred\n ? 'executing'\n : 'completed';\n const title = toolTitleFromInput(part.input);\n const format = toolFieldFromInput(part.input, 'format');\n const idx = toolPartIndexById.get(part.toolCallId);\n if (idx !== undefined) {\n const existing = parts[idx];\n if (existing && existing.type === 'tool') {\n existing.status = status;\n if (format) existing.format = format;\n }\n }\n toolCalls.push({\n toolName: part.toolName,\n input: part.input,\n output: part.type === 'tool-result' ? part.output : undefined,\n status: failed ? 'failed' : 'completed',\n });\n await broadcast('message-tool-call', {\n messageId: aiMessageId,\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n ...(title ? { title } : {}),\n ...(format ? { format } : {}),\n status,\n });\n break;\n }\n default:\n // ignore reasoning/source/step/finish/raw parts\n break;\n }\n }\n\n const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);\n await logInferenceUsage({\n model,\n operation: 'stream.text',\n success: true,\n estimatedInputTokens: 0,\n reason: usageReason,\n usage: usage\n ? {\n promptTokens: usage.inputTokens,\n completionTokens: usage.outputTokens,\n totalTokens: usage.totalTokens,\n }\n : undefined,\n attribution,\n });\n\n return { text: fullText, toolCalls, parts };\n } catch (error) {\n await logInferenceUsage({\n model,\n operation: 'stream.text',\n success: false,\n estimatedInputTokens: 0,\n reason: usageReason,\n error,\n attribution,\n });\n throw error;\n } finally {\n if (redis) {\n await redis.del(streamKey).catch(() => {});\n }\n }\n}\n"],"names":[],"mappings":"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","debug_id":"b26c8408-f190-53b7-8fce-428be7591574"}
1
+ {"version":3,"file":"aiChatStream.js","sources":["services/aiChatStream.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Shared AI chat streaming helper.\n *\n * Runs a genuine token-by-token stream via the Vercel AI SDK (`streamText`)\n * against OpenAI — the same provider the rest of the app's inference uses\n * (`OPENAI_API_KEY`). Real text tokens are broadcast over Pusher as they are\n * produced, and real tool calls are broadcast inline (in the order the model\n * invokes them) instead of being derived after the fact from a JSON blob.\n *\n * Callers own persistence: this helper returns the accumulated text and the\n * ordered tool calls so the pipeline can write `content`/`meta` and emit the\n * terminal `message-stream-complete` / `message-updated` events.\n */\nimport { streamText, stepCountIs, type ModelMessage, type ToolSet } from 'ai';\nimport { openai } from '@ai-sdk/openai';\nimport { getRedis } from '../lib/redis.js';\nimport { prisma } from '../lib/prisma.js';\nimport { pusher, chatChannel } from '../lib/pusher.js';\nimport { logger } from '../utils/logger.js';\nimport { logInferenceUsage, type AiUsageAttribution } from '../utils/inference.js';\n\nconst STREAM_REDIS_TTL = 600; // 10 minutes\nconst STREAM_KEY_PREFIX = 'stream:';\nconst CANCEL_KEY_PREFIX = 'stream:cancel:';\nconst CANCEL_CHECK_INTERVAL_MS = 1_000;\n\nfunction streamCancelKey(aiMessageId: string): string {\n return `${CANCEL_KEY_PREFIX}${aiMessageId}`;\n}\n\n/**\n * Flag an in-flight stream for cancellation. `streamChatResponse` polls this\n * key between chunks and aborts the model stream when it appears. The worker\n * may run in a different process from the API, so the signal goes through\n * Redis rather than an in-memory AbortController registry.\n */\nexport async function requestStreamCancellation(\n aiMessageId: string,\n): Promise<void> {\n const redis = getRedis();\n if (redis) {\n await redis.set(streamCancelKey(aiMessageId), '1', 'EX', STREAM_REDIS_TTL);\n }\n}\n\n/** Default streaming model — matches the existing streamAIResponse path. */\nexport const DEFAULT_STREAM_MODEL = 'gpt-4o-mini';\n\nexport interface StreamedToolCall {\n toolName: string;\n /** Args the model passed to the tool. */\n input: unknown;\n /** Value the tool returned (undefined for client-side / no-result tools). */\n output?: unknown;\n status: 'completed' | 'failed';\n}\n\n/**\n * An ordered fragment of an AI message. The message is a sequence of these\n * parts in the exact order the model produced them, so a tool call rendered\n * \"between paragraph 1 and 2\" is represented as\n * `[{text}, {tool}, {text}]` — enabling true inline rendering (and reload\n * persistence) instead of text and tool calls living in separate buckets.\n */\nexport type StreamPart =\n | { type: 'text'; text: string }\n | {\n type: 'tool';\n toolCallId: string;\n toolName: string;\n /** Concrete entity label (e.g. assignment title / search query), if any. */\n title?: string;\n /** Requested output type from the tool args (pdf, pptx, xlsx, …). */\n format?: string;\n /** Set after generated files are persisted on the message. */\n fileId?: string;\n fileName?: string;\n /** Links a propose_* tool call to its pending creation in message meta. */\n proposalId?: string;\n /** Class-files folder the generated file was saved into, if any. */\n folderId?: string;\n status: 'executing' | 'completed' | 'failed';\n };\n\nexport interface GeneratedDocFileLink {\n title: string;\n fileId: string;\n fileName: string;\n format?: string;\n folderId?: string;\n}\n\n/** Apply pipeline-resolved formats onto generate_document tool parts. */\nexport function applyResolvedFormatsToStreamParts(\n parts: StreamPart[],\n docs: Array<{ title: string; format: string }>,\n): StreamPart[] {\n if (docs.length === 0) return parts;\n\n const pending = [...docs];\n const takeDoc = (title?: string) => {\n if (title) {\n const idx = pending.findIndex(\n (d) => d.title.trim().toLowerCase() === title.trim().toLowerCase(),\n );\n if (idx >= 0) return pending.splice(idx, 1)[0];\n }\n return pending.shift();\n };\n\n return parts.map((part) => {\n if (part.type !== 'tool' || part.toolName !== 'generate_document') return part;\n const doc = takeDoc(part.title);\n return doc ? { ...part, format: doc.format } : part;\n });\n}\n\n/** Tools that queue async work after the model stream; UI stays loading until the pipeline finishes. */\nconst DEFERRED_COMPLETION_TOOLS = new Set(['generate_document']);\n\nfunction isDeferredCompletionTool(toolName: string): boolean {\n return DEFERRED_COMPLETION_TOOLS.has(toolName);\n}\n\n/** Attach file IDs to `generate_document` tool parts for inline download chips. */\nexport function enrichStreamPartsWithGeneratedFiles(\n parts: StreamPart[],\n files: GeneratedDocFileLink[],\n): StreamPart[] {\n if (files.length === 0) return parts;\n\n const docToolIndices = parts\n .map((part, index) => ({ part, index }))\n .filter(\n ({ part }) =>\n part.type === 'tool' && part.toolName === 'generate_document',\n );\n\n const unmatched = [...files];\n const fileForTool = (toolTitle?: string): GeneratedDocFileLink | undefined => {\n if (toolTitle) {\n const idx = unmatched.findIndex(\n (f) => f.title.trim().toLowerCase() === toolTitle.trim().toLowerCase(),\n );\n if (idx >= 0) return unmatched.splice(idx, 1)[0];\n }\n return unmatched.shift();\n };\n\n const fileByIndex = new Map<number, GeneratedDocFileLink>();\n for (const { part, index } of docToolIndices) {\n if (part.type !== 'tool') continue;\n const file = fileForTool(part.title);\n if (file) fileByIndex.set(index, file);\n }\n\n return parts.map((part, index) => {\n const file = fileByIndex.get(index);\n if (!file || part.type !== 'tool') return part;\n return {\n ...part,\n fileId: file.fileId,\n fileName: file.fileName,\n status: 'completed',\n ...(file.format ? { format: file.format } : {}),\n ...(file.folderId ? { folderId: file.folderId } : {}),\n };\n });\n}\n\n/** Mark deferred tools completed (file linked) or failed (generation did not produce a file). */\nexport function finalizeDeferredToolParts(parts: StreamPart[]): StreamPart[] {\n return parts.map((part) => {\n if (part.type !== 'tool' || !isDeferredCompletionTool(part.toolName)) {\n return part;\n }\n if (part.fileId) return { ...part, status: 'completed' };\n if (part.status === 'executing') return { ...part, status: 'failed' };\n return part;\n });\n}\n\nfunction toolFieldFromInput(input: unknown, key: string): string | undefined {\n if (input && typeof input === 'object') {\n const val = (input as Record<string, unknown>)[key];\n if (typeof val === 'string' && val.trim()) return val.trim();\n }\n return undefined;\n}\n\n/** Pull a human-meaningful label out of a tool's input args, if present. */\nfunction toolTitleFromInput(input: unknown): string | undefined {\n for (const key of ['targetTitle', 'title', 'name', 'query'] as const) {\n const val = toolFieldFromInput(input, key);\n if (val) return val;\n }\n return undefined;\n}\n\nexport interface StreamChatOptions {\n conversationId: string;\n aiMessageId: string;\n system: string;\n messages: ModelMessage[];\n tools?: ToolSet;\n model?: string;\n temperature?: number;\n maxOutputTokens?: number;\n /** Max agent steps (tool round-trips). Defaults to 6. */\n maxSteps?: number;\n /** Reason recorded on the AiUsageLog row. Defaults to 'chat_stream'. */\n usageReason?: string;\n /** Who/what this usage is attributed to (user / school / class). */\n attribution?: AiUsageAttribution;\n}\n\nexport interface StreamChatResult {\n text: string;\n toolCalls: StreamedToolCall[];\n /** Ordered text/tool parts, in the exact order the model produced them. */\n parts: StreamPart[];\n /** True when the stream was stopped by a cancellation request; `text` and\n * `parts` hold whatever was produced before the abort. */\n cancelled: boolean;\n}\n\n/**\n * Stream a chat completion, broadcasting real text chunks and tool-call\n * events over Pusher. Does NOT persist the message or emit the terminal\n * stream-complete event — the caller does that once it has assembled the\n * final `content`/`meta` (e.g. PDF attachments, structured proposals).\n */\nexport async function streamChatResponse(\n options: StreamChatOptions,\n): Promise<StreamChatResult> {\n const {\n conversationId,\n aiMessageId,\n system,\n messages,\n tools,\n model = DEFAULT_STREAM_MODEL,\n temperature = 0.7,\n maxOutputTokens = 4096,\n maxSteps = 6,\n usageReason = 'chat_stream',\n attribution,\n } = options;\n\n const redis = getRedis();\n const streamKey = `${STREAM_KEY_PREFIX}${aiMessageId}`;\n const cancelKey = streamCancelKey(aiMessageId);\n if (redis) {\n await redis.set(streamKey, '', 'EX', STREAM_REDIS_TTL);\n // A stale flag from a previously cancelled run on the same message id\n // (e.g. a rerun) must not kill this stream instantly.\n await redis.del(cancelKey).catch(() => {});\n }\n\n let fullText = '';\n let cancelled = false;\n let lastCancelCheck = 0;\n const abortController = new AbortController();\n\n // Poll the cancel flag at most once a second: Redis when available,\n // otherwise the message row (the cancel mutation flips it to CANCELLED).\n const checkCancelled = async (): Promise<boolean> => {\n if (cancelled) return true;\n const now = Date.now();\n if (now - lastCancelCheck < CANCEL_CHECK_INTERVAL_MS) return false;\n lastCancelCheck = now;\n if (redis) {\n const flag = await redis.get(cancelKey).catch(() => null);\n if (flag) cancelled = true;\n } else {\n const row = await prisma.message\n .findUnique({ where: { id: aiMessageId }, select: { status: true } })\n .catch(() => null);\n if (row?.status === 'CANCELLED') cancelled = true;\n }\n if (cancelled) abortController.abort();\n return cancelled;\n };\n\n const parts: StreamPart[] = [];\n const toolCalls: StreamedToolCall[] = [];\n // Map AI SDK toolCallId -> index in `parts` so the executing indicator can\n // be flipped to completed/failed in place (preserving its position).\n const toolPartIndexById = new Map<string, number>();\n\n const appendText = (text: string) => {\n fullText += text;\n const last = parts[parts.length - 1];\n if (last && last.type === 'text') {\n last.text += text;\n } else {\n parts.push({ type: 'text', text });\n }\n };\n\n const broadcast = async (event: string, payload: Record<string, unknown>) => {\n try {\n await pusher.trigger(chatChannel(conversationId), event, payload);\n } catch (err) {\n logger.warn(`Failed to broadcast ${event}`, { err, aiMessageId });\n }\n };\n\n try {\n const result = streamText({\n model: openai(model),\n system,\n messages,\n ...(tools ? { tools } : {}),\n temperature,\n maxOutputTokens,\n stopWhen: stepCountIs(maxSteps),\n abortSignal: abortController.signal,\n });\n\n // `fullStream` yields text deltas AND tool events in a single, correctly\n // ordered sequence — this is what makes inline tool calls possible. We\n // build an ordered `parts` list and broadcast each event as it happens.\n try {\n for await (const part of result.fullStream) {\n if (await checkCancelled()) break;\n if (part.type === 'abort') {\n cancelled = true;\n break;\n }\n switch (part.type) {\n case 'text-delta': {\n appendText(part.text);\n if (redis) {\n await redis.append(streamKey, part.text).catch(() => {});\n }\n await broadcast('message-stream-chunk', {\n messageId: aiMessageId,\n chunk: part.text,\n });\n break;\n }\n case 'tool-call': {\n const title = toolTitleFromInput(part.input);\n const format = toolFieldFromInput(part.input, 'format');\n toolPartIndexById.set(part.toolCallId, parts.length);\n parts.push({\n type: 'tool',\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n ...(title ? { title } : {}),\n ...(format ? { format } : {}),\n status: 'executing',\n });\n await broadcast('message-tool-call', {\n messageId: aiMessageId,\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n ...(title ? { title } : {}),\n ...(format ? { format } : {}),\n status: 'executing',\n });\n break;\n }\n case 'tool-result':\n case 'tool-error': {\n const failed = part.type === 'tool-error';\n const deferred = isDeferredCompletionTool(part.toolName);\n // Deferred tools (e.g. generate_document) only queue work during the\n // stream — keep the spinner until the pipeline attaches a file.\n const status: 'executing' | 'completed' | 'failed' = failed\n ? 'failed'\n : deferred\n ? 'executing'\n : 'completed';\n const title = toolTitleFromInput(part.input);\n const format = toolFieldFromInput(part.input, 'format');\n const idx = toolPartIndexById.get(part.toolCallId);\n if (idx !== undefined) {\n const existing = parts[idx];\n if (existing && existing.type === 'tool') {\n existing.status = status;\n if (format) existing.format = format;\n }\n }\n toolCalls.push({\n toolName: part.toolName,\n input: part.input,\n output: part.type === 'tool-result' ? part.output : undefined,\n status: failed ? 'failed' : 'completed',\n });\n await broadcast('message-tool-call', {\n messageId: aiMessageId,\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n ...(title ? { title } : {}),\n ...(format ? { format } : {}),\n status,\n });\n break;\n }\n default:\n // ignore reasoning/source/step/finish/raw parts\n break;\n }\n }\n } catch (streamError) {\n // Aborting the model stream mid-iteration surfaces as an error from\n // `fullStream`; when it is our own cancellation, keep the partials.\n if (!cancelled) throw streamError;\n }\n\n if (cancelled) {\n logger.info('AI chat stream cancelled', { aiMessageId, conversationId });\n }\n\n const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);\n await logInferenceUsage({\n model,\n operation: 'stream.text',\n success: true,\n estimatedInputTokens: 0,\n reason: usageReason,\n usage: usage\n ? {\n promptTokens: usage.inputTokens,\n completionTokens: usage.outputTokens,\n totalTokens: usage.totalTokens,\n }\n : undefined,\n attribution,\n });\n\n return { text: fullText, toolCalls, parts, cancelled };\n } catch (error) {\n await logInferenceUsage({\n model,\n operation: 'stream.text',\n success: false,\n estimatedInputTokens: 0,\n reason: usageReason,\n error,\n attribution,\n });\n throw error;\n } finally {\n if (redis) {\n await redis.del(streamKey).catch(() => {});\n await redis.del(cancelKey).catch(() => {});\n }\n }\n}\n"],"names":[],"mappings":"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","debug_id":"b934901d-426b-59b0-933b-6718f701849e"}
@@ -0,0 +1,4 @@
1
+ export declare function cancelAiGeneration(userId: string, messageId: string): Promise<{
2
+ success: boolean;
3
+ }>;
4
+ //# sourceMappingURL=aiGeneration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiGeneration.d.ts","sourceRoot":"/","sources":["services/aiGeneration.ts"],"names":[],"mappings":"AAiBA,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;GA0EzE"}