@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,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forgetAiMemory,
|
|
3
|
+
listAiMemories,
|
|
4
|
+
rememberAiMemory,
|
|
5
|
+
resolveAiMemoryScope,
|
|
6
|
+
searchAiMemories,
|
|
7
|
+
} from '../../memory';
|
|
8
|
+
import {
|
|
9
|
+
MIRA_MEMORY_CATEGORIES,
|
|
10
|
+
type MiraMemoryCategory,
|
|
11
|
+
} from '../definitions/memory';
|
|
12
|
+
import type { MiraToolContext } from '../mira-tools';
|
|
13
|
+
|
|
14
|
+
function toMemoryCategory(value: unknown): MiraMemoryCategory | undefined {
|
|
15
|
+
if (typeof value !== 'string') return undefined;
|
|
16
|
+
return MIRA_MEMORY_CATEGORIES.includes(value as MiraMemoryCategory)
|
|
17
|
+
? (value as MiraMemoryCategory)
|
|
18
|
+
: undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseMemoryCategory(
|
|
22
|
+
categoryInput: unknown,
|
|
23
|
+
options: { fieldName?: string; required?: boolean } = {}
|
|
24
|
+
): { category: MiraMemoryCategory | null; error: string | null } {
|
|
25
|
+
const { fieldName = 'category', required = false } = options;
|
|
26
|
+
const allowed = MIRA_MEMORY_CATEGORIES.join(', ');
|
|
27
|
+
|
|
28
|
+
if (categoryInput === undefined || categoryInput === null) {
|
|
29
|
+
if (!required) return { category: null, error: null };
|
|
30
|
+
return {
|
|
31
|
+
category: null,
|
|
32
|
+
error: `${fieldName} is required. Allowed: ${allowed}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parsedCategory = toMemoryCategory(categoryInput);
|
|
37
|
+
if (!parsedCategory) {
|
|
38
|
+
return {
|
|
39
|
+
category: null,
|
|
40
|
+
error: `Invalid ${fieldName}. Allowed: ${allowed}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { category: parsedCategory, error: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getMemoryWorkspaceId(ctx: MiraToolContext) {
|
|
48
|
+
return ctx.workspaceContext?.wsId ?? ctx.wsId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveMiraMemoryScope(ctx: MiraToolContext) {
|
|
52
|
+
return resolveAiMemoryScope({
|
|
53
|
+
customId: ctx.chatId ?? 'mira-chat',
|
|
54
|
+
product: 'mira',
|
|
55
|
+
source: 'mira_chat',
|
|
56
|
+
surface: 'mira_chat',
|
|
57
|
+
userId: ctx.userId,
|
|
58
|
+
wsId: getMemoryWorkspaceId(ctx),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function keyValueMemory(memory: {
|
|
63
|
+
category?: string | null;
|
|
64
|
+
key?: string | null;
|
|
65
|
+
updatedAt?: string | null;
|
|
66
|
+
value: string;
|
|
67
|
+
}) {
|
|
68
|
+
return {
|
|
69
|
+
category: memory.category ?? 'fact',
|
|
70
|
+
key: memory.key ?? memory.value.slice(0, 80),
|
|
71
|
+
updatedAt: memory.updatedAt ?? new Date().toISOString(),
|
|
72
|
+
value: memory.value,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function executeRemember(
|
|
77
|
+
args: Record<string, unknown>,
|
|
78
|
+
ctx: MiraToolContext
|
|
79
|
+
) {
|
|
80
|
+
const key = args.key as string;
|
|
81
|
+
const value = args.value as string;
|
|
82
|
+
const { category, error: categoryError } = parseMemoryCategory(
|
|
83
|
+
args.category,
|
|
84
|
+
{
|
|
85
|
+
required: true,
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (categoryError || !category) {
|
|
90
|
+
return {
|
|
91
|
+
error:
|
|
92
|
+
categoryError ??
|
|
93
|
+
'Invalid category. Allowed: preference, fact, conversation_topic, event, person',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = await rememberAiMemory({
|
|
98
|
+
category,
|
|
99
|
+
key,
|
|
100
|
+
scope: resolveMiraMemoryScope(ctx),
|
|
101
|
+
value,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!result.ok) return { error: result.error };
|
|
105
|
+
if (result.skipped) {
|
|
106
|
+
return {
|
|
107
|
+
action: 'skipped',
|
|
108
|
+
message: `Memory "${key}" was not saved: ${result.reason}`,
|
|
109
|
+
success: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
action: 'created',
|
|
115
|
+
message: `Remembered: "${key}"`,
|
|
116
|
+
success: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function executeRecall(
|
|
121
|
+
args: Record<string, unknown>,
|
|
122
|
+
ctx: MiraToolContext
|
|
123
|
+
) {
|
|
124
|
+
const query = (args.query as string | null | undefined) ?? null;
|
|
125
|
+
const { category, error: categoryError } = parseMemoryCategory(args.category);
|
|
126
|
+
if (categoryError) return { error: categoryError };
|
|
127
|
+
|
|
128
|
+
const maxResults = (args.maxResults as number) || 10;
|
|
129
|
+
const scope = resolveMiraMemoryScope(ctx);
|
|
130
|
+
|
|
131
|
+
const result = query?.trim()
|
|
132
|
+
? await searchAiMemories({
|
|
133
|
+
category,
|
|
134
|
+
limit: maxResults,
|
|
135
|
+
query,
|
|
136
|
+
scope,
|
|
137
|
+
})
|
|
138
|
+
: await listAiMemories({
|
|
139
|
+
category,
|
|
140
|
+
limit: maxResults,
|
|
141
|
+
scope,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!result.ok) return { error: result.error };
|
|
145
|
+
|
|
146
|
+
const memories = (result.value ?? []).map((memory) =>
|
|
147
|
+
keyValueMemory({
|
|
148
|
+
category:
|
|
149
|
+
'category' in memory
|
|
150
|
+
? memory.category
|
|
151
|
+
: typeof memory.metadata?.memoryCategory === 'string'
|
|
152
|
+
? memory.metadata.memoryCategory
|
|
153
|
+
: category,
|
|
154
|
+
key:
|
|
155
|
+
'key' in memory
|
|
156
|
+
? memory.key
|
|
157
|
+
: typeof memory.metadata?.memoryKey === 'string'
|
|
158
|
+
? memory.metadata.memoryKey
|
|
159
|
+
: null,
|
|
160
|
+
updatedAt: memory.updatedAt,
|
|
161
|
+
value:
|
|
162
|
+
'value' in memory
|
|
163
|
+
? memory.value
|
|
164
|
+
: memory.summary || memory.content || memory.title || '',
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
count: memories.length,
|
|
170
|
+
memories,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function executeDeleteMemory(
|
|
175
|
+
args: Record<string, unknown>,
|
|
176
|
+
ctx: MiraToolContext
|
|
177
|
+
) {
|
|
178
|
+
const key = args.key as string;
|
|
179
|
+
const result = await forgetAiMemory({
|
|
180
|
+
key,
|
|
181
|
+
scope: resolveMiraMemoryScope(ctx),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (!result.ok) return { error: result.error };
|
|
185
|
+
if (result.skipped) {
|
|
186
|
+
return {
|
|
187
|
+
message: `Memory "${key}" was not deleted: ${result.reason}`,
|
|
188
|
+
success: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { message: `Memory "${key}" deleted`, success: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function executeListMemories(
|
|
196
|
+
args: Record<string, unknown>,
|
|
197
|
+
ctx: MiraToolContext
|
|
198
|
+
) {
|
|
199
|
+
const { category, error: categoryError } = parseMemoryCategory(args.category);
|
|
200
|
+
if (categoryError) return { error: categoryError };
|
|
201
|
+
|
|
202
|
+
const result = await listAiMemories({
|
|
203
|
+
category,
|
|
204
|
+
scope: resolveMiraMemoryScope(ctx),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!result.ok) return { error: result.error };
|
|
208
|
+
|
|
209
|
+
const memories = (result.value ?? []).map((memory) =>
|
|
210
|
+
keyValueMemory({
|
|
211
|
+
category: memory.category,
|
|
212
|
+
key: memory.key,
|
|
213
|
+
updatedAt: memory.updatedAt,
|
|
214
|
+
value: memory.summary || memory.content || memory.title || '',
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
count: memories.length,
|
|
220
|
+
memories,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function executeMergeMemories(
|
|
225
|
+
args: Record<string, unknown>,
|
|
226
|
+
ctx: MiraToolContext
|
|
227
|
+
) {
|
|
228
|
+
const keysToDelete = args.keysToDelete as string[];
|
|
229
|
+
const newKey = args.newKey as string;
|
|
230
|
+
const newValue = args.newValue as string;
|
|
231
|
+
const { category: newCategory, error: categoryError } = parseMemoryCategory(
|
|
232
|
+
args.newCategory,
|
|
233
|
+
{
|
|
234
|
+
fieldName: 'newCategory',
|
|
235
|
+
required: true,
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (categoryError || !newCategory) {
|
|
240
|
+
return {
|
|
241
|
+
error:
|
|
242
|
+
categoryError ??
|
|
243
|
+
'Invalid newCategory. Allowed: preference, fact, conversation_topic, event, person',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!keysToDelete || keysToDelete.length === 0) {
|
|
248
|
+
return { error: 'No keys provided to delete' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const scope = resolveMiraMemoryScope(ctx);
|
|
252
|
+
const rememberResult = await rememberAiMemory({
|
|
253
|
+
category: newCategory,
|
|
254
|
+
key: newKey,
|
|
255
|
+
scope,
|
|
256
|
+
value: newValue,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!rememberResult.ok) return { error: rememberResult.error };
|
|
260
|
+
|
|
261
|
+
await Promise.all(
|
|
262
|
+
keysToDelete
|
|
263
|
+
.filter((key) => key !== newKey)
|
|
264
|
+
.map((key) =>
|
|
265
|
+
forgetAiMemory({
|
|
266
|
+
key,
|
|
267
|
+
reason: `Merged into ${newKey}`,
|
|
268
|
+
scope,
|
|
269
|
+
})
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
message: `Merged ${keysToDelete.length} memories into "${newKey}"`,
|
|
275
|
+
success: !rememberResult.skipped,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { google } from '@ai-sdk/google';
|
|
2
|
+
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { withAiMemory } from '../../memory';
|
|
5
|
+
import type { MiraToolContext } from '../mira-tools';
|
|
6
|
+
|
|
7
|
+
const PARALLEL_CHECKS_MODEL = 'gemini-3.1-flash-lite';
|
|
8
|
+
|
|
9
|
+
const ParallelChecksArgsSchema = z.object({
|
|
10
|
+
question: z
|
|
11
|
+
.string()
|
|
12
|
+
.transform((value) => value.trim())
|
|
13
|
+
.refine((value) => value.length > 0, {
|
|
14
|
+
message: 'Missing required `question`.',
|
|
15
|
+
})
|
|
16
|
+
.transform((value) => value.slice(0, 2000)),
|
|
17
|
+
context: z
|
|
18
|
+
.string()
|
|
19
|
+
.transform((value) => value.trim())
|
|
20
|
+
.optional()
|
|
21
|
+
.transform((value) => (value ? value.slice(0, 8000) : undefined)),
|
|
22
|
+
checks: z
|
|
23
|
+
.array(z.enum(['assumptions', 'factuality', 'risk', 'implementation']))
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(4)
|
|
26
|
+
.optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
type CheckKind = z.infer<typeof ParallelChecksArgsSchema>['checks'] extends
|
|
30
|
+
| Array<infer T>
|
|
31
|
+
| undefined
|
|
32
|
+
? T
|
|
33
|
+
: never;
|
|
34
|
+
|
|
35
|
+
type ParallelCheckResult = {
|
|
36
|
+
label: string;
|
|
37
|
+
finding: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const CHECK_INSTRUCTIONS: Record<CheckKind, string> = {
|
|
41
|
+
assumptions:
|
|
42
|
+
'You identify hidden assumptions, missing premises, and unclear decision points. Return concise findings only.',
|
|
43
|
+
factuality:
|
|
44
|
+
'You check whether claims need verification, external evidence, or clearer source attribution. Return concise findings only.',
|
|
45
|
+
implementation:
|
|
46
|
+
'You review implementation feasibility, sequencing, interfaces, and likely integration risks. Return concise findings only.',
|
|
47
|
+
risk: 'You review failure modes, user-impact risks, regressions, and test gaps. Return concise findings only.',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const DEFAULT_CHECKS: CheckKind[] = ['assumptions', 'factuality', 'risk'];
|
|
51
|
+
|
|
52
|
+
function buildPrompt({
|
|
53
|
+
check,
|
|
54
|
+
context,
|
|
55
|
+
question,
|
|
56
|
+
}: {
|
|
57
|
+
check: CheckKind;
|
|
58
|
+
context?: string;
|
|
59
|
+
question: string;
|
|
60
|
+
}) {
|
|
61
|
+
return `Review this request from the "${check}" perspective.
|
|
62
|
+
|
|
63
|
+
Question or scenario:
|
|
64
|
+
${question}
|
|
65
|
+
|
|
66
|
+
${context ? `Relevant context:\n${context}\n\n` : ''}Return:
|
|
67
|
+
- 1 to 3 concise findings
|
|
68
|
+
- Any blocker, if present
|
|
69
|
+
- "No material issues" if this perspective has nothing important`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runCheck({
|
|
73
|
+
abortSignal,
|
|
74
|
+
check,
|
|
75
|
+
context,
|
|
76
|
+
question,
|
|
77
|
+
toolContext,
|
|
78
|
+
}: {
|
|
79
|
+
abortSignal?: AbortSignal;
|
|
80
|
+
check: CheckKind;
|
|
81
|
+
context?: string;
|
|
82
|
+
question: string;
|
|
83
|
+
toolContext: MiraToolContext;
|
|
84
|
+
}): Promise<ParallelCheckResult> {
|
|
85
|
+
const agent = new ToolLoopAgent({
|
|
86
|
+
model: await withAiMemory({
|
|
87
|
+
addMemory: 'never',
|
|
88
|
+
customId: toolContext.chatId
|
|
89
|
+
? `${toolContext.chatId}-parallel-checks-${check}`
|
|
90
|
+
: `parallel-checks-${check}`,
|
|
91
|
+
model: google(PARALLEL_CHECKS_MODEL),
|
|
92
|
+
product: 'mira',
|
|
93
|
+
source: 'mira_parallel_checks_tool',
|
|
94
|
+
surface: 'mira_parallel_checks_tool',
|
|
95
|
+
userId: toolContext.userId,
|
|
96
|
+
wsId: toolContext.workspaceContext?.wsId ?? toolContext.wsId,
|
|
97
|
+
}),
|
|
98
|
+
instructions: CHECK_INSTRUCTIONS[check],
|
|
99
|
+
stopWhen: stepCountIs(2),
|
|
100
|
+
providerOptions: {
|
|
101
|
+
google: {
|
|
102
|
+
thinkingConfig: {
|
|
103
|
+
thinkingBudget: 0,
|
|
104
|
+
includeThoughts: false,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await agent.generate({
|
|
111
|
+
prompt: buildPrompt({ check, context, question }),
|
|
112
|
+
abortSignal,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
label: check,
|
|
117
|
+
finding: result.text.trim() || 'No material issues.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function executeParallelChecks(
|
|
122
|
+
args: Record<string, unknown>,
|
|
123
|
+
ctx: MiraToolContext,
|
|
124
|
+
options?: { abortSignal?: AbortSignal }
|
|
125
|
+
) {
|
|
126
|
+
const parsed = ParallelChecksArgsSchema.safeParse(args);
|
|
127
|
+
if (!parsed.success) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: parsed.error.issues[0]?.message ?? 'Invalid parallel check input.',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { context, question } = parsed.data;
|
|
135
|
+
const checks = parsed.data.checks ?? DEFAULT_CHECKS;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const results = await Promise.all(
|
|
139
|
+
checks.map((check) =>
|
|
140
|
+
runCheck({
|
|
141
|
+
abortSignal: options?.abortSignal,
|
|
142
|
+
check,
|
|
143
|
+
context,
|
|
144
|
+
question,
|
|
145
|
+
toolContext: ctx,
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const issueCount = results.filter(
|
|
151
|
+
(result) => !/no material issues/i.test(result.finding)
|
|
152
|
+
).length;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
summary:
|
|
157
|
+
issueCount > 0
|
|
158
|
+
? `Parallel checks found ${issueCount} perspective(s) with material notes.`
|
|
159
|
+
: 'Parallel checks found no material issues.',
|
|
160
|
+
checks: results,
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: 'Parallel checks were cancelled.',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.error('executeParallelChecks provider error:', error);
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: 'Parallel checks failed. Please try again.',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createAdminClient } from '@tuturuuu/supabase/next/server';
|
|
2
|
+
import QRCode from 'qrcode';
|
|
3
|
+
import type { MiraToolContext } from '../mira-tools';
|
|
4
|
+
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SIZE = 512;
|
|
7
|
+
const DEFAULT_FOREGROUND = '#000000';
|
|
8
|
+
const DEFAULT_BACKGROUND = '#FFFFFF';
|
|
9
|
+
const DRIVE_QR_PREFIX = 'drive/mira/qr';
|
|
10
|
+
|
|
11
|
+
function normalizeHexColor(value: unknown, fallback: string): string {
|
|
12
|
+
if (typeof value !== 'string') return fallback;
|
|
13
|
+
const normalized = value.trim();
|
|
14
|
+
if (!/^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(normalized)) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
return normalized.toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeSize(value: unknown): number {
|
|
21
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
22
|
+
return DEFAULT_SIZE;
|
|
23
|
+
}
|
|
24
|
+
const rounded = Math.round(value);
|
|
25
|
+
return Math.min(2048, Math.max(128, rounded));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeFileName(value: unknown): string {
|
|
29
|
+
if (typeof value !== 'string') {
|
|
30
|
+
return `qr-${Date.now()}.png`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (!trimmed) {
|
|
35
|
+
return `qr-${Date.now()}.png`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const withoutExt = trimmed.replace(/\.[A-Za-z0-9]+$/, '');
|
|
39
|
+
const safeBase = withoutExt
|
|
40
|
+
.replace(/[^A-Za-z0-9-_ ]/g, '')
|
|
41
|
+
.replace(/\s+/g, '-')
|
|
42
|
+
.replace(/-+/g, '-')
|
|
43
|
+
.replace(/^-|-$/g, '')
|
|
44
|
+
.slice(0, 64);
|
|
45
|
+
|
|
46
|
+
const finalBase = safeBase || `qr-${Date.now()}`;
|
|
47
|
+
return `${finalBase}.png`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toPngBytes(dataUrl: string): Uint8Array {
|
|
51
|
+
if (!dataUrl.startsWith('data:')) {
|
|
52
|
+
throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const commaIndex = dataUrl.indexOf(',');
|
|
56
|
+
if (commaIndex < 0) {
|
|
57
|
+
throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const metadata = dataUrl.slice(5, commaIndex);
|
|
61
|
+
if (!/^[^,]*;base64$/i.test(metadata)) {
|
|
62
|
+
throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mime = metadata.slice(0, -';base64'.length).toLowerCase();
|
|
66
|
+
if (mime && mime !== 'image/png') {
|
|
67
|
+
throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const base64 = dataUrl.slice(commaIndex + 1).trim();
|
|
71
|
+
if (!base64) {
|
|
72
|
+
throw new Error("Malformed data URL: expected 'data:[mime];base64,<data>'");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Uint8Array.from(Buffer.from(base64, 'base64'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readQrValue(args: Record<string, unknown>): string {
|
|
79
|
+
for (const key of ['value', 'text', 'url', 'content', 'payload']) {
|
|
80
|
+
const value = args[key];
|
|
81
|
+
if (typeof value === 'string' && value.trim()) {
|
|
82
|
+
return value.trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function executeCreateQrCode(
|
|
90
|
+
args: Record<string, unknown>,
|
|
91
|
+
ctx: MiraToolContext
|
|
92
|
+
): Promise<unknown> {
|
|
93
|
+
const rawValue = readQrValue(args);
|
|
94
|
+
if (!rawValue) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: 'QR value is required.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const size = normalizeSize(args.size);
|
|
102
|
+
const foregroundColor = normalizeHexColor(
|
|
103
|
+
args.foregroundColor,
|
|
104
|
+
DEFAULT_FOREGROUND
|
|
105
|
+
);
|
|
106
|
+
const backgroundColor = normalizeHexColor(
|
|
107
|
+
args.backgroundColor,
|
|
108
|
+
DEFAULT_BACKGROUND
|
|
109
|
+
);
|
|
110
|
+
const fileName = sanitizeFileName(args.fileName);
|
|
111
|
+
|
|
112
|
+
const fileId = crypto.randomUUID();
|
|
113
|
+
const wsId = getWorkspaceContextWorkspaceId(ctx);
|
|
114
|
+
const storagePath = `${wsId}/${DRIVE_QR_PREFIX}/${fileId}-${fileName}`;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const qrDataUrl = await QRCode.toDataURL(rawValue, {
|
|
118
|
+
type: 'image/png',
|
|
119
|
+
width: size,
|
|
120
|
+
margin: 2,
|
|
121
|
+
color: {
|
|
122
|
+
dark: foregroundColor,
|
|
123
|
+
light: backgroundColor,
|
|
124
|
+
},
|
|
125
|
+
errorCorrectionLevel: 'M',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const sbAdmin = await createAdminClient();
|
|
129
|
+
const pngBytes = toPngBytes(qrDataUrl);
|
|
130
|
+
|
|
131
|
+
const { error: uploadError } = await sbAdmin.storage
|
|
132
|
+
.from('workspaces')
|
|
133
|
+
.upload(storagePath, pngBytes, {
|
|
134
|
+
contentType: 'image/png',
|
|
135
|
+
upsert: false,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (uploadError) {
|
|
139
|
+
throw new Error(`Upload failed: ${uploadError.message}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { data: urlData, error: signedUrlError } = await sbAdmin.storage
|
|
143
|
+
.from('workspaces')
|
|
144
|
+
.createSignedUrl(storagePath, 60 * 60 * 24 * 30);
|
|
145
|
+
|
|
146
|
+
if (signedUrlError || !urlData?.signedUrl) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Signed URL failed: ${signedUrlError?.message ?? 'No signed URL returned'}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
qrCodeUrl: urlData.signedUrl,
|
|
155
|
+
downloadUrl: urlData.signedUrl,
|
|
156
|
+
storagePath,
|
|
157
|
+
drivePath: storagePath.replace(`${wsId}/`, ''),
|
|
158
|
+
fileName,
|
|
159
|
+
size,
|
|
160
|
+
foregroundColor,
|
|
161
|
+
backgroundColor,
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error:
|
|
167
|
+
error instanceof Error ? error.message : 'Failed to generate QR code.',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|