@tuturuuu/ai 0.0.10
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.
- package/README.md +76 -0
- package/package.json +106 -0
- package/src/api-key-hash.ts +28 -0
- package/src/calendar/events.ts +34 -0
- package/src/calendar/route.ts +114 -0
- package/src/chat/credit-source.ts +1 -0
- package/src/chat/google/chat-request-schema.ts +150 -0
- package/src/chat/google/default-system-instruction.ts +198 -0
- package/src/chat/google/message-file-processing.ts +212 -0
- package/src/chat/google/mira-step-preparation.ts +221 -0
- package/src/chat/google/new/route.ts +368 -0
- package/src/chat/google/route-auth.ts +81 -0
- package/src/chat/google/route-chat-resolution.ts +98 -0
- package/src/chat/google/route-credits.ts +61 -0
- package/src/chat/google/route-message-preparation.ts +331 -0
- package/src/chat/google/route-mira-runtime.ts +206 -0
- package/src/chat/google/route.ts +632 -0
- package/src/chat/google/stream-finish-persistence.ts +722 -0
- package/src/chat/google/summary/route.ts +153 -0
- package/src/chat/mira-render-ui-policy.ts +540 -0
- package/src/chat/mira-system-instruction.ts +484 -0
- package/src/chat-sdk/adapters.ts +389 -0
- package/src/chat-sdk/registry.ts +197 -0
- package/src/chat-sdk.ts +33 -0
- package/src/core.ts +3 -0
- package/src/credits/cap-output-tokens.ts +90 -0
- package/src/credits/check-credits.ts +232 -0
- package/src/credits/constants.ts +30 -0
- package/src/credits/index.ts +46 -0
- package/src/credits/model-mapping.ts +92 -0
- package/src/credits/reservations.ts +514 -0
- package/src/credits/resolve-plan-model.ts +219 -0
- package/src/credits/sync-gateway-models.ts +351 -0
- package/src/credits/types.ts +109 -0
- package/src/credits/use-ai-credits.ts +3 -0
- package/src/embeddings/metered.ts +283 -0
- package/src/executions/route.ts +137 -0
- package/src/generate/route.ts +411 -0
- package/src/hooks.ts +7 -0
- package/src/meetings/summary/route.ts +7 -0
- package/src/meetings/transcription/route.ts +134 -0
- package/src/memory/client.ts +158 -0
- package/src/memory/config.ts +38 -0
- package/src/memory/index.ts +32 -0
- package/src/memory/ingest.ts +51 -0
- package/src/memory/middleware.ts +35 -0
- package/src/memory/operations.ts +480 -0
- package/src/memory/scope.ts +102 -0
- package/src/memory/settings.ts +121 -0
- package/src/memory/types.ts +101 -0
- package/src/memory/workspace.ts +36 -0
- package/src/memory.ts +1 -0
- package/src/mind/patch.ts +146 -0
- package/src/mind/route.ts +687 -0
- package/src/mind/tools.ts +1500 -0
- package/src/mind/types.ts +20 -0
- package/src/object/core.ts +3 -0
- package/src/object/flashcards/route.ts +140 -0
- package/src/object/quizzes/explanation/route.ts +145 -0
- package/src/object/quizzes/route.ts +142 -0
- package/src/object/types.ts +187 -0
- package/src/object/year-plan/route.ts +196 -0
- package/src/react.ts +1 -0
- package/src/scheduling/algorithm.ts +791 -0
- package/src/scheduling/default.ts +36 -0
- package/src/scheduling/duration-optimizer.ts +689 -0
- package/src/scheduling/index.ts +79 -0
- package/src/scheduling/priority-calculator.ts +187 -0
- package/src/scheduling/recurrence-calculator.ts +621 -0
- package/src/scheduling/templates.ts +892 -0
- package/src/scheduling/types.ts +136 -0
- package/src/scheduling/web-adapter.ts +308 -0
- package/src/scheduling.ts +6 -0
- package/src/supported-actions.ts +1 -0
- package/src/supported-providers.ts +6 -0
- package/src/tools/context-builder.ts +372 -0
- package/src/tools/core.ts +1 -0
- package/src/tools/definitions/calendar.ts +106 -0
- package/src/tools/definitions/finance.ts +197 -0
- package/src/tools/definitions/image.ts +74 -0
- package/src/tools/definitions/memory.ts +83 -0
- package/src/tools/definitions/meta.ts +154 -0
- package/src/tools/definitions/render-ui.ts +81 -0
- package/src/tools/definitions/tasks.ts +343 -0
- package/src/tools/definitions/time-tracking.ts +381 -0
- package/src/tools/definitions/workspace-context.ts +45 -0
- package/src/tools/definitions/workspace-user-chat.ts +111 -0
- package/src/tools/executors/calendar.ts +371 -0
- package/src/tools/executors/chat.ts +15 -0
- package/src/tools/executors/finance.ts +638 -0
- package/src/tools/executors/helpers/encryption.ts +107 -0
- package/src/tools/executors/image.ts +247 -0
- package/src/tools/executors/markitdown.ts +684 -0
- package/src/tools/executors/memory.ts +277 -0
- package/src/tools/executors/parallel-checks.ts +176 -0
- package/src/tools/executors/qr.ts +170 -0
- package/src/tools/executors/scope-helpers.ts +192 -0
- package/src/tools/executors/search.ts +149 -0
- package/src/tools/executors/settings.ts +40 -0
- package/src/tools/executors/tasks.ts +1087 -0
- package/src/tools/executors/theme.ts +23 -0
- package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
- package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
- package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
- package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
- package/src/tools/executors/timer/timer-helpers.ts +372 -0
- package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
- package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
- package/src/tools/executors/timer/timer-mutations.ts +19 -0
- package/src/tools/executors/timer/timer-queries.ts +18 -0
- package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
- package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
- package/src/tools/executors/timer/timer-session-queries.ts +153 -0
- package/src/tools/executors/timer/timer-session-updates.ts +200 -0
- package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
- package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
- package/src/tools/executors/timer.ts +22 -0
- package/src/tools/executors/user.ts +60 -0
- package/src/tools/executors/workspace.ts +135 -0
- package/src/tools/json-render-catalog.ts +875 -0
- package/src/tools/mira-tool-definitions.ts +55 -0
- package/src/tools/mira-tool-dispatcher.ts +265 -0
- package/src/tools/mira-tool-metadata.ts +164 -0
- package/src/tools/mira-tool-names.ts +95 -0
- package/src/tools/mira-tool-render-ui.ts +54 -0
- package/src/tools/mira-tool-types.ts +17 -0
- package/src/tools/mira-tools.ts +167 -0
- package/src/tools/normalize-render-ui-input.ts +321 -0
- package/src/tools/workspace-context.ts +233 -0
- package/src/types.ts +38 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { MAX_CHAT_MESSAGE_LENGTH } from '@tuturuuu/utils/constants';
|
|
2
|
+
import type {
|
|
3
|
+
FilePart,
|
|
4
|
+
ImagePart,
|
|
5
|
+
ModelMessage,
|
|
6
|
+
TextPart,
|
|
7
|
+
UIMessage,
|
|
8
|
+
} from 'ai';
|
|
9
|
+
import { convertToModelMessages } from 'ai';
|
|
10
|
+
import { processMessagesWithFiles } from './message-file-processing';
|
|
11
|
+
|
|
12
|
+
export const MAX_CONTEXT_MESSAGES = 10;
|
|
13
|
+
|
|
14
|
+
const YOUTUBE_URL_REGEX =
|
|
15
|
+
/https?:\/\/(?:www\.|m\.|music\.)?(?:youtube\.com|youtube-nocookie\.com)\/\S+|https?:\/\/(?:www\.)?youtu\.be\/\S+/gi;
|
|
16
|
+
|
|
17
|
+
type SupabaseRpcClientLike = {
|
|
18
|
+
message: string;
|
|
19
|
+
chat_id: string;
|
|
20
|
+
source: 'Mira' | 'Rewise';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function validateModelMessages(modelMessages: ModelMessage[]): Response | null {
|
|
24
|
+
for (const message of modelMessages) {
|
|
25
|
+
if (
|
|
26
|
+
typeof message.content === 'string' &&
|
|
27
|
+
message.content.length > MAX_CHAT_MESSAGE_LENGTH
|
|
28
|
+
) {
|
|
29
|
+
return new Response(
|
|
30
|
+
`Message too long (max ${MAX_CHAT_MESSAGE_LENGTH} characters)`,
|
|
31
|
+
{ status: 400 }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(message.content)) {
|
|
36
|
+
for (const part of message.content) {
|
|
37
|
+
if (
|
|
38
|
+
part.type === 'text' &&
|
|
39
|
+
part.text.length > MAX_CHAT_MESSAGE_LENGTH
|
|
40
|
+
) {
|
|
41
|
+
return new Response(
|
|
42
|
+
`Message too long (max ${MAX_CHAT_MESSAGE_LENGTH} characters)`,
|
|
43
|
+
{ status: 400 }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function truncateProcessedMessages(
|
|
54
|
+
processedMessages: ModelMessage[]
|
|
55
|
+
): ModelMessage[] {
|
|
56
|
+
if (processedMessages.length <= MAX_CONTEXT_MESSAGES) {
|
|
57
|
+
return processedMessages;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const systemMessages = processedMessages.filter(
|
|
61
|
+
(message) => message.role === 'system'
|
|
62
|
+
);
|
|
63
|
+
const nonSystemMessages = processedMessages.filter(
|
|
64
|
+
(message) => message.role !== 'system'
|
|
65
|
+
);
|
|
66
|
+
if (systemMessages.length >= MAX_CONTEXT_MESSAGES) {
|
|
67
|
+
return systemMessages.slice(-MAX_CONTEXT_MESSAGES);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const allowedNonSystemCount = Math.max(
|
|
71
|
+
MAX_CONTEXT_MESSAGES - systemMessages.length,
|
|
72
|
+
0
|
|
73
|
+
);
|
|
74
|
+
const truncatedMessages = [
|
|
75
|
+
...systemMessages,
|
|
76
|
+
...nonSystemMessages.slice(-allowedNonSystemCount),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
console.info('Truncated processed chat context', {
|
|
80
|
+
originalLength: systemMessages.length + nonSystemMessages.length,
|
|
81
|
+
resultingLength: truncatedMessages.length,
|
|
82
|
+
preservedSystemMessages: systemMessages.length,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return truncatedMessages;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getTextFromContent(content: ModelMessage['content']): string {
|
|
89
|
+
if (typeof content === 'string') {
|
|
90
|
+
return content;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Array.isArray(content)) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return content
|
|
98
|
+
.map((part) => (part.type === 'text' ? part.text : ''))
|
|
99
|
+
.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeYoutubeVideoUrlForGemini(
|
|
103
|
+
value: string
|
|
104
|
+
): string | null {
|
|
105
|
+
try {
|
|
106
|
+
const parsed = new URL(value);
|
|
107
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
108
|
+
if (parsed.protocol !== 'https:') return null;
|
|
109
|
+
|
|
110
|
+
if (hostname === 'youtu.be' || hostname === 'www.youtu.be') {
|
|
111
|
+
const videoId = parsed.pathname.split('/').filter(Boolean)[0];
|
|
112
|
+
return videoId ? `https://youtu.be/${videoId}` : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isYoutubeHost =
|
|
116
|
+
hostname === 'youtube.com' ||
|
|
117
|
+
hostname === 'www.youtube.com' ||
|
|
118
|
+
hostname === 'm.youtube.com' ||
|
|
119
|
+
hostname === 'music.youtube.com' ||
|
|
120
|
+
hostname === 'youtube-nocookie.com' ||
|
|
121
|
+
hostname === 'www.youtube-nocookie.com';
|
|
122
|
+
if (!isYoutubeHost) return null;
|
|
123
|
+
|
|
124
|
+
if (parsed.pathname === '/watch') {
|
|
125
|
+
const videoId = parsed.searchParams.get('v');
|
|
126
|
+
return videoId ? `https://www.youtube.com/watch?v=${videoId}` : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const prefix of ['/shorts/', '/embed/', '/live/']) {
|
|
130
|
+
if (parsed.pathname.startsWith(prefix)) {
|
|
131
|
+
const videoId = parsed.pathname.slice(prefix.length).split('/')[0];
|
|
132
|
+
return videoId ? `https://www.youtube.com${prefix}${videoId}` : null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getFirstYoutubeVideoUrl(
|
|
143
|
+
content: ModelMessage['content']
|
|
144
|
+
): string | null {
|
|
145
|
+
const text = getTextFromContent(content);
|
|
146
|
+
for (const match of text.matchAll(YOUTUBE_URL_REGEX)) {
|
|
147
|
+
const normalized = normalizeYoutubeVideoUrlForGemini(
|
|
148
|
+
match[0].replace(/[),.;!?]+$/g, '')
|
|
149
|
+
);
|
|
150
|
+
if (normalized) {
|
|
151
|
+
return normalized;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasYoutubeVideoFilePart(content: ModelMessage['content']): boolean {
|
|
159
|
+
if (!Array.isArray(content)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return content.some(
|
|
164
|
+
(part) =>
|
|
165
|
+
part.type === 'file' &&
|
|
166
|
+
part.mediaType === 'video/mp4' &&
|
|
167
|
+
typeof part.data === 'string' &&
|
|
168
|
+
normalizeYoutubeVideoUrlForGemini(part.data) !== null
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function attachYoutubeVideoInputToLatestUserMessage(
|
|
173
|
+
messages: ModelMessage[],
|
|
174
|
+
enabled: boolean
|
|
175
|
+
): ModelMessage[] {
|
|
176
|
+
if (!enabled) {
|
|
177
|
+
return messages;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const latestUserIndex = messages.findLastIndex(
|
|
181
|
+
(message) => message.role === 'user'
|
|
182
|
+
);
|
|
183
|
+
if (latestUserIndex === -1) {
|
|
184
|
+
return messages;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const latestUserMessage = messages[latestUserIndex]!;
|
|
188
|
+
if (hasYoutubeVideoFilePart(latestUserMessage.content)) {
|
|
189
|
+
return messages;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const youtubeUrl = getFirstYoutubeVideoUrl(latestUserMessage.content);
|
|
193
|
+
if (!youtubeUrl) {
|
|
194
|
+
return messages;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const contentParts: Array<TextPart | ImagePart | FilePart> = [];
|
|
198
|
+
if (typeof latestUserMessage.content === 'string') {
|
|
199
|
+
contentParts.push({ type: 'text', text: latestUserMessage.content });
|
|
200
|
+
} else if (Array.isArray(latestUserMessage.content)) {
|
|
201
|
+
for (const part of latestUserMessage.content) {
|
|
202
|
+
if (
|
|
203
|
+
part.type === 'text' ||
|
|
204
|
+
part.type === 'image' ||
|
|
205
|
+
part.type === 'file'
|
|
206
|
+
) {
|
|
207
|
+
contentParts.push(part);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
contentParts.push({
|
|
213
|
+
type: 'file',
|
|
214
|
+
data: youtubeUrl,
|
|
215
|
+
mediaType: 'video/mp4',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const processedMessages = [...messages];
|
|
219
|
+
processedMessages[latestUserIndex] = {
|
|
220
|
+
role: 'user',
|
|
221
|
+
content: contentParts,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return processedMessages;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function prepareProcessedMessages(
|
|
228
|
+
normalizedMessages: UIMessage[],
|
|
229
|
+
wsId: string | undefined,
|
|
230
|
+
chatId: string,
|
|
231
|
+
request?: Pick<Request, 'headers'>,
|
|
232
|
+
options?: { attachYoutubeVideoInput?: boolean }
|
|
233
|
+
): Promise<{ processedMessages: ModelMessage[] } | { error: Response }> {
|
|
234
|
+
const modelMessages = await convertToModelMessages(normalizedMessages);
|
|
235
|
+
const validationError = validateModelMessages(modelMessages);
|
|
236
|
+
if (validationError) {
|
|
237
|
+
return { error: validationError };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const processedMessages =
|
|
241
|
+
wsId && chatId
|
|
242
|
+
? await processMessagesWithFiles(modelMessages, wsId, chatId, request)
|
|
243
|
+
: modelMessages;
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
processedMessages: truncateProcessedMessages(
|
|
247
|
+
attachYoutubeVideoInputToLatestUserMessage(
|
|
248
|
+
processedMessages,
|
|
249
|
+
options?.attachYoutubeVideoInput ?? false
|
|
250
|
+
)
|
|
251
|
+
),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function extractLatestUserMessageContent(
|
|
256
|
+
processedMessages: ModelMessage[]
|
|
257
|
+
): string {
|
|
258
|
+
const userMessages = processedMessages.filter(
|
|
259
|
+
(msg: ModelMessage) => msg.role === 'user'
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const lastMessage = userMessages[userMessages.length - 1];
|
|
263
|
+
if (typeof lastMessage?.content === 'string') {
|
|
264
|
+
return lastMessage.content;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (Array.isArray(lastMessage?.content)) {
|
|
268
|
+
return lastMessage.content
|
|
269
|
+
.map((part) => {
|
|
270
|
+
if (part.type === 'text') return part.text;
|
|
271
|
+
if (part.type === 'image') return '[Image attached]';
|
|
272
|
+
if (part.type === 'file') {
|
|
273
|
+
if (
|
|
274
|
+
part.mediaType === 'video/mp4' &&
|
|
275
|
+
typeof part.data === 'string' &&
|
|
276
|
+
normalizeYoutubeVideoUrlForGemini(part.data)
|
|
277
|
+
) {
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return `[File: ${(part as { name?: string }).name || 'attached'}]`;
|
|
282
|
+
}
|
|
283
|
+
return '';
|
|
284
|
+
})
|
|
285
|
+
.filter(Boolean)
|
|
286
|
+
.join('\n');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return 'Message with attachments';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
type PersistLatestUserMessageParams = {
|
|
293
|
+
processedMessages: ModelMessage[];
|
|
294
|
+
chatId: string;
|
|
295
|
+
insertChatMessage: (
|
|
296
|
+
args: SupabaseRpcClientLike
|
|
297
|
+
) => PromiseLike<{ error: { message: string } | null }>;
|
|
298
|
+
source: 'Mira' | 'Rewise';
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export async function persistLatestUserMessage({
|
|
302
|
+
processedMessages,
|
|
303
|
+
chatId,
|
|
304
|
+
insertChatMessage,
|
|
305
|
+
source,
|
|
306
|
+
}: PersistLatestUserMessageParams): Promise<Response | null> {
|
|
307
|
+
if (processedMessages.length === 1) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const messageContent = extractLatestUserMessageContent(processedMessages);
|
|
312
|
+
if (!messageContent) {
|
|
313
|
+
console.log('No message found');
|
|
314
|
+
throw new Error('No message found');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const { error: insertMsgError } = await insertChatMessage({
|
|
318
|
+
message: messageContent,
|
|
319
|
+
chat_id: chatId,
|
|
320
|
+
source,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (insertMsgError) {
|
|
324
|
+
console.log('ERROR ORIGIN: ROOT START');
|
|
325
|
+
console.log(insertMsgError);
|
|
326
|
+
throw new Error(insertMsgError.message);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log('User message saved to database');
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { TypedSupabaseClient } from '@tuturuuu/supabase/next/client';
|
|
2
|
+
import { normalizeWorkspaceContextId } from '@tuturuuu/utils/constants';
|
|
3
|
+
import { getPermissions } from '@tuturuuu/utils/workspace-helper';
|
|
4
|
+
import type { NextRequest } from 'next/server';
|
|
5
|
+
import { buildMiraContext } from '../../tools/context-builder';
|
|
6
|
+
import {
|
|
7
|
+
createMiraStreamTools,
|
|
8
|
+
type MiraToolContext,
|
|
9
|
+
} from '../../tools/mira-tools';
|
|
10
|
+
import type { MiraWorkspaceContextState } from '../../tools/workspace-context';
|
|
11
|
+
import { resolveWorkspaceContextState } from '../../tools/workspace-context';
|
|
12
|
+
import { buildMiraSystemInstruction } from '../mira-system-instruction';
|
|
13
|
+
import type { ChatRequestTaskBoardContext } from './chat-request-schema';
|
|
14
|
+
|
|
15
|
+
type PermissionResultLike = {
|
|
16
|
+
withoutPermission?: (permission: unknown) => boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SupabaseClientLike = TypedSupabaseClient;
|
|
20
|
+
|
|
21
|
+
type PrepareMiraRuntimeParams = {
|
|
22
|
+
isMiraMode?: boolean;
|
|
23
|
+
wsId?: string;
|
|
24
|
+
workspaceContextId?: string;
|
|
25
|
+
creditWsId?: string;
|
|
26
|
+
request: NextRequest;
|
|
27
|
+
user?: {
|
|
28
|
+
email?: string | null;
|
|
29
|
+
id: string;
|
|
30
|
+
};
|
|
31
|
+
userId: string;
|
|
32
|
+
chatId: string;
|
|
33
|
+
supabase: SupabaseClientLike;
|
|
34
|
+
toolSupabase?: SupabaseClientLike;
|
|
35
|
+
timezone?: string;
|
|
36
|
+
taskBoardContext?: ChatRequestTaskBoardContext;
|
|
37
|
+
/** Optional callback for render_ui context-aware fallback. */
|
|
38
|
+
getSteps?: () => unknown[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function formatTaskBoardReference({
|
|
42
|
+
boardId,
|
|
43
|
+
boardName,
|
|
44
|
+
}: ChatRequestTaskBoardContext) {
|
|
45
|
+
return boardName ? `${boardName} (${boardId})` : boardId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildTaskBoardContextInstruction({
|
|
49
|
+
resolvedWorkspaceContext,
|
|
50
|
+
taskBoardContext,
|
|
51
|
+
}: {
|
|
52
|
+
resolvedWorkspaceContext: MiraWorkspaceContextState;
|
|
53
|
+
taskBoardContext?: ChatRequestTaskBoardContext;
|
|
54
|
+
}) {
|
|
55
|
+
if (!taskBoardContext) return null;
|
|
56
|
+
|
|
57
|
+
const workspaceName =
|
|
58
|
+
taskBoardContext.workspaceName?.trim() || resolvedWorkspaceContext.name;
|
|
59
|
+
const workspaceId = normalizeWorkspaceContextId(
|
|
60
|
+
taskBoardContext.workspaceId || resolvedWorkspaceContext.wsId
|
|
61
|
+
);
|
|
62
|
+
const workspaceKind = resolvedWorkspaceContext.personal
|
|
63
|
+
? 'personal workspace'
|
|
64
|
+
: 'shared workspace';
|
|
65
|
+
const selectedList = taskBoardContext.selectedList ?? null;
|
|
66
|
+
const selectedListName = selectedList?.name?.trim() || 'Untitled list';
|
|
67
|
+
const selectedListStatus = selectedList?.status?.trim() || 'unknown';
|
|
68
|
+
const selectedListLine = selectedList
|
|
69
|
+
? `Selected/default task list: ${selectedListName} [${selectedListStatus}] (list id: ${selectedList.id}).`
|
|
70
|
+
: 'Selected/default task list: none selected in the client yet.';
|
|
71
|
+
const listLines =
|
|
72
|
+
taskBoardContext.lists.length > 0
|
|
73
|
+
? taskBoardContext.lists
|
|
74
|
+
.map((list) => {
|
|
75
|
+
const listName = list.name?.trim() || 'Untitled list';
|
|
76
|
+
const status = list.status?.trim() || 'unknown';
|
|
77
|
+
return `- ${listName} [${status}] (list id: ${list.id})`;
|
|
78
|
+
})
|
|
79
|
+
.join('\n')
|
|
80
|
+
: '- No task lists are currently loaded in the client.';
|
|
81
|
+
|
|
82
|
+
return `## Current Task Board
|
|
83
|
+
|
|
84
|
+
The user is currently viewing workspace ${workspaceName} (${workspaceKind}).
|
|
85
|
+
- Current workspace id: ${workspaceId}
|
|
86
|
+
- Current task board: ${formatTaskBoardReference(taskBoardContext)}
|
|
87
|
+
- Current board id: ${taskBoardContext.boardId}
|
|
88
|
+
- ${selectedListLine}
|
|
89
|
+
|
|
90
|
+
Visible task lists on this board:
|
|
91
|
+
${listLines}
|
|
92
|
+
|
|
93
|
+
Use these list names and statuses when the user refers to "this board", "this task board", or asks to create or move board tasks. The current workspace id and current board id above are authoritative, including ids that look like all-zero UUIDs or ids that map from the "internal" slug. Prefer these known workspace/board/list ids over rediscovering the same context. Do not reject the current workspace id based on its shape or display name, and do not call workspace context tools just to rediscover this board context.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function prepareMiraRuntime({
|
|
97
|
+
isMiraMode,
|
|
98
|
+
wsId,
|
|
99
|
+
workspaceContextId,
|
|
100
|
+
creditWsId,
|
|
101
|
+
request,
|
|
102
|
+
user,
|
|
103
|
+
userId,
|
|
104
|
+
chatId,
|
|
105
|
+
supabase,
|
|
106
|
+
toolSupabase,
|
|
107
|
+
timezone,
|
|
108
|
+
taskBoardContext,
|
|
109
|
+
getSteps,
|
|
110
|
+
}: PrepareMiraRuntimeParams): Promise<{
|
|
111
|
+
miraSystemPrompt?: string;
|
|
112
|
+
miraTools?: ReturnType<typeof createMiraStreamTools>;
|
|
113
|
+
}> {
|
|
114
|
+
if (!isMiraMode || !wsId) {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const miraSupabase = toolSupabase ?? supabase;
|
|
119
|
+
|
|
120
|
+
let resolvedWorkspaceContext: MiraWorkspaceContextState;
|
|
121
|
+
try {
|
|
122
|
+
resolvedWorkspaceContext = await resolveWorkspaceContextState({
|
|
123
|
+
supabase: miraSupabase,
|
|
124
|
+
userId,
|
|
125
|
+
requestedWorkspaceContextId: workspaceContextId,
|
|
126
|
+
fallbackWorkspaceId: wsId,
|
|
127
|
+
});
|
|
128
|
+
} catch (workspaceContextErr) {
|
|
129
|
+
console.error(
|
|
130
|
+
'Failed to resolve Mira workspace context, falling back to current workspace:',
|
|
131
|
+
workspaceContextErr
|
|
132
|
+
);
|
|
133
|
+
resolvedWorkspaceContext = {
|
|
134
|
+
workspaceContextId: wsId,
|
|
135
|
+
wsId,
|
|
136
|
+
name: 'Current workspace',
|
|
137
|
+
personal: false,
|
|
138
|
+
memberCount: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let withoutPermission: PermissionResultLike['withoutPermission'];
|
|
143
|
+
const denyPermissionByDefault = () => true;
|
|
144
|
+
withoutPermission = denyPermissionByDefault;
|
|
145
|
+
try {
|
|
146
|
+
const permissionsResult = (await getPermissions({
|
|
147
|
+
wsId: resolvedWorkspaceContext.wsId,
|
|
148
|
+
...(user ? { user } : { request }),
|
|
149
|
+
})) as PermissionResultLike | null;
|
|
150
|
+
if (permissionsResult?.withoutPermission) {
|
|
151
|
+
withoutPermission = permissionsResult.withoutPermission;
|
|
152
|
+
}
|
|
153
|
+
} catch (permErr) {
|
|
154
|
+
console.error('Failed to get permissions for Mira tools:', permErr);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ctx: MiraToolContext = {
|
|
158
|
+
userId,
|
|
159
|
+
wsId,
|
|
160
|
+
creditWsId,
|
|
161
|
+
workspaceContext: resolvedWorkspaceContext,
|
|
162
|
+
chatId,
|
|
163
|
+
supabase: miraSupabase,
|
|
164
|
+
timezone,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
let miraSystemPrompt: string;
|
|
168
|
+
try {
|
|
169
|
+
const { contextString, soul, isFirstInteraction } = await buildMiraContext({
|
|
170
|
+
userId,
|
|
171
|
+
wsId: resolvedWorkspaceContext.wsId,
|
|
172
|
+
supabase: miraSupabase,
|
|
173
|
+
timezone,
|
|
174
|
+
withoutPermission,
|
|
175
|
+
});
|
|
176
|
+
const dynamicInstruction = buildMiraSystemInstruction({
|
|
177
|
+
soul,
|
|
178
|
+
isFirstInteraction,
|
|
179
|
+
withoutPermission,
|
|
180
|
+
});
|
|
181
|
+
const workspaceContextInstruction = `## Workspace Context\n\nCurrent task/calendar/finance workspace context: ${resolvedWorkspaceContext.name} (${resolvedWorkspaceContext.personal ? 'personal' : 'shared'} workspace).\nUse this workspace for "my tasks", "my calendar", and "my finance" requests. Only switch to another workspace when the user explicitly names a different workspace.`;
|
|
182
|
+
const taskBoardContextInstruction = buildTaskBoardContextInstruction({
|
|
183
|
+
resolvedWorkspaceContext,
|
|
184
|
+
taskBoardContext,
|
|
185
|
+
});
|
|
186
|
+
miraSystemPrompt = [
|
|
187
|
+
contextString,
|
|
188
|
+
workspaceContextInstruction,
|
|
189
|
+
taskBoardContextInstruction,
|
|
190
|
+
dynamicInstruction,
|
|
191
|
+
]
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join('\n\n');
|
|
194
|
+
} catch (ctxErr) {
|
|
195
|
+
console.error(
|
|
196
|
+
'Failed to build Mira context (continuing with default instruction):',
|
|
197
|
+
ctxErr
|
|
198
|
+
);
|
|
199
|
+
miraSystemPrompt = buildMiraSystemInstruction({ withoutPermission });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
miraSystemPrompt,
|
|
204
|
+
miraTools: createMiraStreamTools(ctx, withoutPermission, getSteps),
|
|
205
|
+
};
|
|
206
|
+
}
|