@miraiclip/assistant 0.1.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.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/index.cjs +784 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +358 -0
- package/dist/index.d.ts +358 -0
- package/dist/index.js +774 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var ChatModelError = class extends Error {
|
|
3
|
+
constructor(provider, message, status) {
|
|
4
|
+
super(`${provider}: ${message}`);
|
|
5
|
+
this.provider = provider;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "ChatModelError";
|
|
8
|
+
}
|
|
9
|
+
provider;
|
|
10
|
+
status;
|
|
11
|
+
};
|
|
12
|
+
function defaultFetch() {
|
|
13
|
+
if (typeof fetch !== "function") throw new Error("No global fetch: pass `fetch` explicitly");
|
|
14
|
+
return (url, init) => fetch(url, init);
|
|
15
|
+
}
|
|
16
|
+
function defineChatModel(model) {
|
|
17
|
+
if (!model.id || !/^[a-z0-9][a-z0-9_-]*$/i.test(model.id)) {
|
|
18
|
+
throw new Error(`Chat model id must be alphanumeric with - or _ (got "${model.id}")`);
|
|
19
|
+
}
|
|
20
|
+
if (typeof model.complete !== "function") throw new Error(`Chat model "${model.id}" has no complete()`);
|
|
21
|
+
return model;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/openai.ts
|
|
25
|
+
function toOpenAIMessages(messages) {
|
|
26
|
+
return messages.map((m) => {
|
|
27
|
+
if (m.role === "assistant") {
|
|
28
|
+
const out = { role: "assistant", content: m.content || null };
|
|
29
|
+
if (m.toolCalls?.length) {
|
|
30
|
+
out.tool_calls = m.toolCalls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: JSON.stringify(c.arguments) } }));
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
if (m.role === "tool") return { role: "tool", tool_call_id: m.toolCallId, content: m.content };
|
|
35
|
+
return { role: m.role, content: m.content };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function parseToolArguments(raw) {
|
|
39
|
+
if (!raw.trim()) return {};
|
|
40
|
+
try {
|
|
41
|
+
const value = JSON.parse(raw);
|
|
42
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : { __invalidArguments: raw };
|
|
43
|
+
} catch {
|
|
44
|
+
return { __invalidArguments: raw };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var FINISH = {
|
|
48
|
+
stop: "stop",
|
|
49
|
+
tool_calls: "tool_calls",
|
|
50
|
+
function_call: "tool_calls",
|
|
51
|
+
length: "length",
|
|
52
|
+
content_filter: "content_filter"
|
|
53
|
+
};
|
|
54
|
+
function openAIChatModel(options) {
|
|
55
|
+
const base = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
56
|
+
const id = options.id ?? "openai";
|
|
57
|
+
const doFetch = options.fetch ?? defaultFetch();
|
|
58
|
+
const stream = options.stream ?? true;
|
|
59
|
+
const models = [{ id: options.model }, ...(options.models ?? []).filter((m) => m.id !== options.model)];
|
|
60
|
+
return defineChatModel({
|
|
61
|
+
id,
|
|
62
|
+
label: options.label ?? "OpenAI",
|
|
63
|
+
models,
|
|
64
|
+
async complete(request, opts = {}) {
|
|
65
|
+
const body = {
|
|
66
|
+
model: request.model ?? options.model,
|
|
67
|
+
messages: toOpenAIMessages(request.messages),
|
|
68
|
+
...request.tools?.length ? { tools: request.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.inputSchema } })) } : {},
|
|
69
|
+
...request.maxOutputTokens ? { max_completion_tokens: request.maxOutputTokens } : {},
|
|
70
|
+
...stream ? { stream: true, stream_options: { include_usage: true } } : {},
|
|
71
|
+
...options.params,
|
|
72
|
+
...request.params
|
|
73
|
+
};
|
|
74
|
+
const headers = { "Content-Type": "application/json", ...options.headers };
|
|
75
|
+
if (options.apiKey) headers["Authorization"] = `Bearer ${options.apiKey}`;
|
|
76
|
+
if (options.organization) headers["OpenAI-Organization"] = options.organization;
|
|
77
|
+
if (options.project) headers["OpenAI-Project"] = options.project;
|
|
78
|
+
const res = await doFetch(`${base}/chat/completions`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers,
|
|
81
|
+
body: JSON.stringify(body),
|
|
82
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const err = await res.json().catch(() => null);
|
|
86
|
+
throw new ChatModelError(id, err?.error?.message ?? `HTTP ${res.status}`, res.status);
|
|
87
|
+
}
|
|
88
|
+
if (!stream || !res.body || !(res.headers.get("content-type") ?? "").includes("event-stream")) {
|
|
89
|
+
return fromCompletion(await res.json(), opts.onText);
|
|
90
|
+
}
|
|
91
|
+
return readStream(res.body, opts.onText);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function fromCompletion(json2, onText) {
|
|
96
|
+
const choice = json2.choices?.[0];
|
|
97
|
+
const content = choice?.message?.content ?? "";
|
|
98
|
+
if (content) onText?.(content);
|
|
99
|
+
const out = {
|
|
100
|
+
content,
|
|
101
|
+
toolCalls: (choice?.message?.tool_calls ?? []).map((c) => ({ id: c.id, name: c.function.name, arguments: parseToolArguments(c.function.arguments) })),
|
|
102
|
+
finish: FINISH[choice?.finish_reason ?? ""] ?? "other"
|
|
103
|
+
};
|
|
104
|
+
if (json2.model) out.model = json2.model;
|
|
105
|
+
if (json2.usage) out.usage = usage(json2.usage);
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
var usage = (u) => ({
|
|
109
|
+
...u.prompt_tokens !== void 0 ? { inputTokens: u.prompt_tokens } : {},
|
|
110
|
+
...u.completion_tokens !== void 0 ? { outputTokens: u.completion_tokens } : {}
|
|
111
|
+
});
|
|
112
|
+
async function readStream(body, onText) {
|
|
113
|
+
const reader = body.getReader();
|
|
114
|
+
const decoder = new TextDecoder();
|
|
115
|
+
let buffer = "";
|
|
116
|
+
let content = "";
|
|
117
|
+
let finish = "other";
|
|
118
|
+
let model;
|
|
119
|
+
let use;
|
|
120
|
+
const calls = /* @__PURE__ */ new Map();
|
|
121
|
+
const handle = (data) => {
|
|
122
|
+
if (data === "[DONE]") return;
|
|
123
|
+
let chunk;
|
|
124
|
+
try {
|
|
125
|
+
chunk = JSON.parse(data);
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (chunk.model) model = chunk.model;
|
|
130
|
+
if (chunk.usage) use = usage(chunk.usage);
|
|
131
|
+
for (const choice of chunk.choices ?? []) {
|
|
132
|
+
const delta = choice.delta;
|
|
133
|
+
if (delta?.content) {
|
|
134
|
+
content += delta.content;
|
|
135
|
+
onText?.(delta.content);
|
|
136
|
+
}
|
|
137
|
+
for (const tc of delta?.tool_calls ?? []) {
|
|
138
|
+
const call = calls.get(tc.index) ?? { id: "", name: "", args: "" };
|
|
139
|
+
if (tc.id) call.id = tc.id;
|
|
140
|
+
if (tc.function?.name) call.name += tc.function.name;
|
|
141
|
+
if (tc.function?.arguments) call.args += tc.function.arguments;
|
|
142
|
+
calls.set(tc.index, call);
|
|
143
|
+
}
|
|
144
|
+
if (choice.finish_reason) finish = FINISH[choice.finish_reason] ?? "other";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
for (; ; ) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
150
|
+
let nl;
|
|
151
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
152
|
+
const line = buffer.slice(0, nl).replace(/\r$/, "");
|
|
153
|
+
buffer = buffer.slice(nl + 1);
|
|
154
|
+
if (line.startsWith("data:")) handle(line.slice(5).trim());
|
|
155
|
+
}
|
|
156
|
+
if (done) break;
|
|
157
|
+
}
|
|
158
|
+
if (buffer.startsWith("data:")) handle(buffer.slice(5).trim());
|
|
159
|
+
const toolCalls = [...calls.entries()].sort(([a], [b]) => a - b).map(([index, c]) => ({ id: c.id || `call_${index}`, name: c.name, arguments: parseToolArguments(c.args) }));
|
|
160
|
+
const out = { content, toolCalls, finish };
|
|
161
|
+
if (model) out.model = model;
|
|
162
|
+
if (use) out.usage = use;
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/http.ts
|
|
167
|
+
function createChatHandler(model, options = {}) {
|
|
168
|
+
const base = (options.basePath ?? "").replace(/\/+$/, "");
|
|
169
|
+
const maxBody = options.maxBodyBytes ?? 2e6;
|
|
170
|
+
return async function handle(request) {
|
|
171
|
+
const path = new URL(request.url).pathname;
|
|
172
|
+
if (base && path !== base && !path.startsWith(`${base}/`)) return null;
|
|
173
|
+
const rest = path.slice(base.length).replace(/\/+$/, "");
|
|
174
|
+
const refused = await options.authorize?.(request);
|
|
175
|
+
if (refused) return refused;
|
|
176
|
+
if (rest === "" && request.method === "GET") {
|
|
177
|
+
return json(200, { id: model.id, label: model.label, models: model.models ?? [] });
|
|
178
|
+
}
|
|
179
|
+
if (rest !== "/chat" || request.method !== "POST") return json(404, { error: "not found" });
|
|
180
|
+
const text = await request.text();
|
|
181
|
+
if (text.length > maxBody) return json(413, { error: "request too large" });
|
|
182
|
+
let chat;
|
|
183
|
+
try {
|
|
184
|
+
chat = JSON.parse(text).request;
|
|
185
|
+
} catch {
|
|
186
|
+
return json(400, { error: "body must be JSON { request }" });
|
|
187
|
+
}
|
|
188
|
+
if (!chat || !Array.isArray(chat.messages)) return json(400, { error: "request.messages is required" });
|
|
189
|
+
if (options.prepare) chat = options.prepare(chat);
|
|
190
|
+
const encoder = new TextEncoder();
|
|
191
|
+
const body = new ReadableStream({
|
|
192
|
+
async start(controller) {
|
|
193
|
+
const send = (value) => controller.enqueue(encoder.encode(`${JSON.stringify(value)}
|
|
194
|
+
`));
|
|
195
|
+
try {
|
|
196
|
+
const response = await model.complete(chat, { signal: request.signal, onText: (delta) => send({ type: "text", delta }) });
|
|
197
|
+
send({ type: "done", response });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const status = err?.status;
|
|
200
|
+
send({ type: "error", error: err?.message ?? String(err), ...status ? { status } : {} });
|
|
201
|
+
} finally {
|
|
202
|
+
controller.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
return new Response(body, { status: 200, headers: { "content-type": "application/x-ndjson", "cache-control": "no-store" } });
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
var json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
210
|
+
async function remoteChatModel(endpoint, options = {}) {
|
|
211
|
+
const doFetch = options.fetch ?? defaultFetch();
|
|
212
|
+
const root = endpoint.replace(/\/+$/, "");
|
|
213
|
+
let info = options.info;
|
|
214
|
+
if (!info) {
|
|
215
|
+
const res = await doFetch(`${root}/`, { headers: { Accept: "application/json", ...options.headers } });
|
|
216
|
+
if (!res.ok) throw new ChatModelError("remote", `HTTP ${res.status}`, res.status);
|
|
217
|
+
info = await res.json();
|
|
218
|
+
}
|
|
219
|
+
const { id, label, models } = info;
|
|
220
|
+
return {
|
|
221
|
+
id,
|
|
222
|
+
label,
|
|
223
|
+
...models ? { models } : {},
|
|
224
|
+
async complete(request, opts = {}) {
|
|
225
|
+
const res = await doFetch(`${root}/chat`, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/json", Accept: "application/x-ndjson", ...options.headers },
|
|
228
|
+
body: JSON.stringify({ request }),
|
|
229
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
230
|
+
});
|
|
231
|
+
if (!res.ok || !res.body) {
|
|
232
|
+
const err = await res.json().catch(() => null);
|
|
233
|
+
throw new ChatModelError(id, err?.error ?? `HTTP ${res.status}`, res.status);
|
|
234
|
+
}
|
|
235
|
+
const reader = res.body.getReader();
|
|
236
|
+
const decoder = new TextDecoder();
|
|
237
|
+
let buffer = "";
|
|
238
|
+
let final;
|
|
239
|
+
const handle = (line) => {
|
|
240
|
+
if (!line.trim()) return;
|
|
241
|
+
const event = JSON.parse(line);
|
|
242
|
+
if (event.type === "text" && event.delta) opts.onText?.(event.delta);
|
|
243
|
+
else if (event.type === "done") final = event.response;
|
|
244
|
+
else if (event.type === "error") throw new ChatModelError(id, event.error ?? "failed", event.status);
|
|
245
|
+
};
|
|
246
|
+
for (; ; ) {
|
|
247
|
+
const { done, value } = await reader.read();
|
|
248
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
249
|
+
let nl;
|
|
250
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
251
|
+
handle(buffer.slice(0, nl));
|
|
252
|
+
buffer = buffer.slice(nl + 1);
|
|
253
|
+
}
|
|
254
|
+
if (done) break;
|
|
255
|
+
}
|
|
256
|
+
handle(buffer);
|
|
257
|
+
if (!final) throw new ChatModelError(id, "the server closed the stream without a response");
|
|
258
|
+
return final;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/tools.ts
|
|
264
|
+
import {
|
|
265
|
+
ANIMATION_EASINGS,
|
|
266
|
+
ANIMATION_PRESETS,
|
|
267
|
+
animationCommands,
|
|
268
|
+
applyCommands,
|
|
269
|
+
commandCatalog,
|
|
270
|
+
builtinTransitionParamSchemas,
|
|
271
|
+
describeAnimation,
|
|
272
|
+
describeProject,
|
|
273
|
+
findCuts,
|
|
274
|
+
fitAnimation,
|
|
275
|
+
readAnimation,
|
|
276
|
+
toToolDefinitions
|
|
277
|
+
} from "@miraiclip/core";
|
|
278
|
+
function defineTool(tool) {
|
|
279
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(tool.name)) throw new Error(`Tool name must match ^[a-zA-Z0-9_-]{1,64}$ (got "${tool.name}")`);
|
|
280
|
+
return tool;
|
|
281
|
+
}
|
|
282
|
+
function toolsFromDefinitions(definitions, run, options = {}) {
|
|
283
|
+
return definitions.map((d) => {
|
|
284
|
+
const def = d;
|
|
285
|
+
const name = def.function?.name ?? def.name;
|
|
286
|
+
return defineTool({
|
|
287
|
+
name,
|
|
288
|
+
description: def.function?.description ?? def.description ?? "",
|
|
289
|
+
inputSchema: def.function?.parameters ?? def.input_schema ?? { type: "object", properties: {} },
|
|
290
|
+
async run(input, ctx) {
|
|
291
|
+
const out = await run(name, input, ctx);
|
|
292
|
+
if (!out.ok) return { ok: false, error: out.error ?? "failed" };
|
|
293
|
+
const changes = options.changes?.(name, input, out.result);
|
|
294
|
+
return { ok: true, result: out.result, ...changes?.length ? { changes } : {} };
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
var S = 1e6;
|
|
300
|
+
var seconds = (us) => `${Math.round(us / 1e5) / 10}s`;
|
|
301
|
+
var toUs = (s) => typeof s === "number" && Number.isFinite(s) ? Math.round(s * S) : void 0;
|
|
302
|
+
var PHRASES = {
|
|
303
|
+
"clip/add": (p) => p.kind === "text" ? `Added text "${String(p.text ?? "").slice(0, 40)}" at ${seconds(Number(p.startUs))}` : `Added a ${p.kind} clip at ${seconds(Number(p.startUs))}`,
|
|
304
|
+
"clip/remove": (p) => `Removed clip ${p.clipId}`,
|
|
305
|
+
"clip/move": (p) => `Moved clip ${p.clipId}${p.startUs !== void 0 ? ` to ${seconds(Number(p.startUs))}` : ""}`,
|
|
306
|
+
"clip/trim": (p) => `Trimmed clip ${p.clipId}`,
|
|
307
|
+
"clip/split": (p) => `Split clip ${p.clipId} at ${seconds(Number(p.atUs))}`,
|
|
308
|
+
"clip/duplicate": (p) => `Duplicated clip ${p.clipId}`,
|
|
309
|
+
"clip/set-property": (p) => `Changed ${Object.keys(p).filter((k) => k !== "clipId").join(", ")} of clip ${p.clipId}`,
|
|
310
|
+
"track/add": (p) => `Added a ${p.kind} track`,
|
|
311
|
+
"track/remove": (p) => `Removed track ${p.id}`,
|
|
312
|
+
"track/set-property": (p) => `Changed track ${p.trackId}: ${Object.keys(p).filter((k) => k !== "trackId").join(", ")}`,
|
|
313
|
+
"effect/add": (p) => `Added ${p.kind} to clip ${p.clipId}`,
|
|
314
|
+
"effect/update": (p) => `Adjusted an effect on clip ${p.clipId}`,
|
|
315
|
+
"effect/remove": (p) => `Removed an effect from clip ${p.clipId}`,
|
|
316
|
+
"transition/add": (p) => `Added a ${p.kind} transition`,
|
|
317
|
+
"transition/remove": () => "Removed a transition",
|
|
318
|
+
"project/set-settings": () => "Changed the project settings"
|
|
319
|
+
};
|
|
320
|
+
function describeCommands(commands) {
|
|
321
|
+
const lines = [];
|
|
322
|
+
const keyframed = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const c of commands) {
|
|
324
|
+
const p = c.payload ?? {};
|
|
325
|
+
if (c.type.startsWith("keyframe/")) {
|
|
326
|
+
const id = String(p.clipId);
|
|
327
|
+
if (!keyframed.has(id)) lines.push(`Changed the animation of clip ${id}`);
|
|
328
|
+
keyframed.add(id);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const phrase = PHRASES[c.type];
|
|
332
|
+
lines.push(phrase ? phrase(p) : `${c.type}`);
|
|
333
|
+
}
|
|
334
|
+
return lines;
|
|
335
|
+
}
|
|
336
|
+
var US_NOTE = "Times in commands are integer MICROSECONDS (1 s = 1000000).";
|
|
337
|
+
function editorTools(options = {}) {
|
|
338
|
+
const transitionKinds = options.transitionKinds ?? Object.keys(builtinTransitionParamSchemas);
|
|
339
|
+
const presetIds = (slot) => ANIMATION_PRESETS.filter((p) => p.slot === slot).map((p) => p.id.split(":")[1]);
|
|
340
|
+
return [
|
|
341
|
+
defineTool({
|
|
342
|
+
name: "get_state",
|
|
343
|
+
description: `The project as it is now (after your edits so far): settings, assets, tracks with clips (ids, kinds, time ranges), transitions. ${US_NOTE}`,
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: { includeJson: { type: "boolean", description: "Also return the full document JSON (verbose; only for exact field values)." } },
|
|
347
|
+
additionalProperties: false
|
|
348
|
+
},
|
|
349
|
+
run(input, { project }) {
|
|
350
|
+
const summary = describeProject(project.toJSON());
|
|
351
|
+
return { ok: true, result: input.includeJson ? { summary, document: project.toJSON() } : summary };
|
|
352
|
+
}
|
|
353
|
+
}),
|
|
354
|
+
defineTool({
|
|
355
|
+
name: "get_command_schema",
|
|
356
|
+
description: "The JSON Schema of one editing command's payload. Look it up before using a command in apply_commands for the first time.",
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: { type: { type: "string", description: 'Command type, e.g. "clip/add".' } },
|
|
360
|
+
required: ["type"],
|
|
361
|
+
additionalProperties: false
|
|
362
|
+
},
|
|
363
|
+
run(input, { project }) {
|
|
364
|
+
const catalog = project.commandCatalog();
|
|
365
|
+
const schema = catalog[String(input.type)];
|
|
366
|
+
return schema ? { ok: true, result: schema } : { ok: false, error: { error: `unknown command "${input.type}"`, validTypes: Object.keys(catalog).sort() } };
|
|
367
|
+
}
|
|
368
|
+
}),
|
|
369
|
+
defineTool({
|
|
370
|
+
name: "apply_commands",
|
|
371
|
+
description: `Apply editing commands as one all-or-nothing batch. On failure nothing changes and the error names the failing index and why (invalid payload issues, or the engine's rejection reason) \u2014 fix it and retry. Give new clips, tracks and assets your own readable ids. ${US_NOTE}`,
|
|
372
|
+
inputSchema: applyCommandsSchema(options.commandCatalog ?? commandCatalog()),
|
|
373
|
+
run(input, { project }) {
|
|
374
|
+
const commands = input.commands;
|
|
375
|
+
if (!Array.isArray(commands) || commands.length === 0) return { ok: false, error: "commands must be a non-empty array of { type, payload }" };
|
|
376
|
+
const result = applyCommands(project, commands);
|
|
377
|
+
if (!result.ok) return { ok: false, error: result };
|
|
378
|
+
return { ok: true, result: { applied: result.applied }, changes: describeCommands(commands) };
|
|
379
|
+
}
|
|
380
|
+
}),
|
|
381
|
+
defineTool({
|
|
382
|
+
name: "add_transition",
|
|
383
|
+
description: "Add (or replace, or remove) a transition on cuts \u2014 where one clip ends exactly where the next starts on the same video track. Pick the cut by its two clip ids, by a time near it, or all cuts. A transition overlaps both clips around the cut, so its length is capped by the spare footage there; cuts with none are skipped and reported.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
kind: { type: "string", enum: [...transitionKinds, "none"], description: '"none" removes the transition.' },
|
|
388
|
+
fromClipId: { type: "string", description: "The clip ending at the cut." },
|
|
389
|
+
toClipId: { type: "string", description: "The clip starting at the cut." },
|
|
390
|
+
nearSeconds: { type: "number", description: "Pick the cut closest to this time instead." },
|
|
391
|
+
allCuts: { type: "boolean", description: "Apply to every cut." },
|
|
392
|
+
durationSeconds: { type: "number", minimum: 0.1, maximum: 3, description: "Default 0.6." },
|
|
393
|
+
direction: { type: "string", enum: ["left", "right", "up", "down"], description: "wipe / slide only." }
|
|
394
|
+
},
|
|
395
|
+
required: ["kind"],
|
|
396
|
+
additionalProperties: false
|
|
397
|
+
},
|
|
398
|
+
run(input, { project }) {
|
|
399
|
+
const cuts = findCuts(project.getState().doc);
|
|
400
|
+
if (cuts.length === 0) return { ok: false, error: "There are no cuts: transitions need two clips touching on the same video track." };
|
|
401
|
+
let targets;
|
|
402
|
+
if (input.allCuts === true) targets = cuts;
|
|
403
|
+
else if (typeof input.fromClipId === "string" || typeof input.toClipId === "string") {
|
|
404
|
+
targets = cuts.filter((c) => (!input.fromClipId || c.fromClipId === input.fromClipId) && (!input.toClipId || c.toClipId === input.toClipId));
|
|
405
|
+
} else {
|
|
406
|
+
const at = toUs(input.nearSeconds) ?? project.getState().playheadUs;
|
|
407
|
+
targets = [cuts.reduce((a, b) => Math.abs(b.atUs - at) < Math.abs(a.atUs - at) ? b : a)];
|
|
408
|
+
}
|
|
409
|
+
if (targets.length === 0) return { ok: false, error: { error: "no cut matches", cuts: cuts.map(cutInfo) } };
|
|
410
|
+
const kind = String(input.kind);
|
|
411
|
+
const wanted = toUs(input.durationSeconds) ?? 6e5;
|
|
412
|
+
const commands = [];
|
|
413
|
+
const changes = [];
|
|
414
|
+
const skipped = [];
|
|
415
|
+
for (const cut of targets) {
|
|
416
|
+
if (cut.transition) commands.push({ type: "transition/remove", payload: { transitionId: cut.transition.id } });
|
|
417
|
+
if (kind === "none") {
|
|
418
|
+
if (cut.transition) changes.push(`Removed the transition at ${seconds(cut.atUs)}`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const durationUs = Math.min(wanted, cut.maxTransitionUs);
|
|
422
|
+
if (durationUs < 1e5) {
|
|
423
|
+
skipped.push({ ...cutInfo(cut), reason: `no spare footage (${cut.short === "from" ? "the first clip ends where its media ends" : cut.short === "to" ? "the second clip starts at its media's start" : "both clips use all their media"})` });
|
|
424
|
+
if (cut.transition) commands.pop();
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const params = input.direction && (kind === "wipe" || kind === "slide") ? { direction: input.direction } : void 0;
|
|
428
|
+
commands.push({ type: "transition/add", payload: { kind, fromClipId: cut.fromClipId, toClipId: cut.toClipId, durationUs, ...params ? { params } : {} } });
|
|
429
|
+
changes.push(`${cut.transition ? "Replaced the transition with" : "Added"} ${kind} at ${seconds(cut.atUs)} (${seconds(durationUs)})`);
|
|
430
|
+
}
|
|
431
|
+
if (commands.length) {
|
|
432
|
+
const result = applyCommands(project, commands);
|
|
433
|
+
if (!result.ok) return { ok: false, error: result };
|
|
434
|
+
}
|
|
435
|
+
return { ok: true, result: { changed: changes.length, skipped }, changes };
|
|
436
|
+
}
|
|
437
|
+
}),
|
|
438
|
+
defineTool({
|
|
439
|
+
name: "animate_clip",
|
|
440
|
+
description: `Give a visual clip (video, image, text) an entrance (in), a loop while on screen, and/or an exit (out), as keyframes. Slots you leave out keep their current animation; "none" removes one; reset:true removes all. In/out presets: ${presetIds("in").join(", ")}. Loops: ${presetIds("loop").join(", ")}. Exits stay on the clip's end through trims.`,
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
clipId: { type: "string" },
|
|
445
|
+
in: { type: "string", enum: [...presetIds("in"), "none"] },
|
|
446
|
+
loop: { type: "string", enum: [...presetIds("loop"), "none"] },
|
|
447
|
+
out: { type: "string", enum: [...presetIds("out"), "none"] },
|
|
448
|
+
inSeconds: { type: "number", minimum: 0.2, maximum: 3, description: "Default 0.6." },
|
|
449
|
+
outSeconds: { type: "number", minimum: 0.2, maximum: 3, description: "Default 0.6." },
|
|
450
|
+
easing: { type: "string", enum: ANIMATION_EASINGS.map((e) => e.id), description: "Default smooth." },
|
|
451
|
+
reset: { type: "boolean" }
|
|
452
|
+
},
|
|
453
|
+
required: ["clipId"],
|
|
454
|
+
additionalProperties: false
|
|
455
|
+
},
|
|
456
|
+
run(input, { project }) {
|
|
457
|
+
const clip = project.getState().doc.clips[String(input.clipId)];
|
|
458
|
+
if (!clip) return { ok: false, error: `no clip "${input.clipId}"` };
|
|
459
|
+
if (clip.kind === "audio") return { ok: false, error: "audio clips have no picture to animate (use volume or fades)" };
|
|
460
|
+
const current = input.reset === true ? {} : readAnimation(clip).recipe;
|
|
461
|
+
const next = { ...current };
|
|
462
|
+
const easing = typeof input.easing === "string" ? input.easing : "smooth";
|
|
463
|
+
for (const slot of ["in", "loop", "out"]) {
|
|
464
|
+
const name = input[slot];
|
|
465
|
+
if (name === void 0) continue;
|
|
466
|
+
if (name === "none") {
|
|
467
|
+
delete next[slot];
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const durationUs = slot === "loop" ? 0 : toUs(input[slot === "in" ? "inSeconds" : "outSeconds"]) ?? current[slot]?.durationUs ?? 6e5;
|
|
471
|
+
next[slot] = { preset: `${slot}:${name}`, durationUs, easing: slot === "loop" ? "linear" : easing };
|
|
472
|
+
}
|
|
473
|
+
const recipe = fitAnimation(next, clip.durationUs);
|
|
474
|
+
const result = applyCommands(project, animationCommands(clip, recipe));
|
|
475
|
+
if (!result.ok) return { ok: false, error: result };
|
|
476
|
+
const summary = describeAnimation(recipe) || "no animation";
|
|
477
|
+
return { ok: true, result: { clipId: clip.id, animation: summary }, changes: [`${clip.id}: ${summary}`] };
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
];
|
|
481
|
+
}
|
|
482
|
+
function cutInfo(c) {
|
|
483
|
+
return { fromClipId: c.fromClipId, toClipId: c.toClipId, atSeconds: c.atUs / S, maxSeconds: c.maxTransitionUs / S, ...c.transition ? { transition: c.transition.kind } : {} };
|
|
484
|
+
}
|
|
485
|
+
function applyCommandsSchema(catalog) {
|
|
486
|
+
const [dispatch] = toToolDefinitions(catalog, { mode: "dispatch" });
|
|
487
|
+
return {
|
|
488
|
+
type: "object",
|
|
489
|
+
properties: {
|
|
490
|
+
commands: { type: "array", minItems: 1, items: dispatch.input_schema, description: "Commands applied in order, as one batch." }
|
|
491
|
+
},
|
|
492
|
+
required: ["commands"],
|
|
493
|
+
additionalProperties: false
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/fork.ts
|
|
498
|
+
import { createProject } from "@miraiclip/core";
|
|
499
|
+
var seq = 0;
|
|
500
|
+
var genId = (prefix) => `${prefix}-${Date.now().toString(36)}${(++seq).toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
501
|
+
function withExplicitIds(command) {
|
|
502
|
+
const p = command.payload ?? {};
|
|
503
|
+
const set = (field, prefix) => p[field] === void 0 ? { ...command, payload: { ...p, [field]: genId(prefix) } } : command;
|
|
504
|
+
switch (command.type) {
|
|
505
|
+
case "clip/split":
|
|
506
|
+
case "clip/duplicate":
|
|
507
|
+
return set("newClipId", "clip");
|
|
508
|
+
case "effect/add":
|
|
509
|
+
return set("effectId", "fx");
|
|
510
|
+
case "transition/add":
|
|
511
|
+
return set("id", "tr");
|
|
512
|
+
default:
|
|
513
|
+
return command;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function forkProject(project, options = {}) {
|
|
517
|
+
const fork = (options.createFork ?? ((doc) => createProject(doc)))(project.toJSON());
|
|
518
|
+
const state = project.getState();
|
|
519
|
+
fork.setPlayhead(state.playheadUs);
|
|
520
|
+
fork.setSelection([...state.selection]);
|
|
521
|
+
const ensure = options.ensureIds ?? withExplicitIds;
|
|
522
|
+
const recorded = [];
|
|
523
|
+
let depth = 0;
|
|
524
|
+
let buffer = [];
|
|
525
|
+
return {
|
|
526
|
+
...fork,
|
|
527
|
+
get recorded() {
|
|
528
|
+
return recorded;
|
|
529
|
+
},
|
|
530
|
+
dispatch(command) {
|
|
531
|
+
const explicit = ensure(command);
|
|
532
|
+
const before = fork.getState().doc;
|
|
533
|
+
fork.dispatch(explicit);
|
|
534
|
+
if (fork.getState().doc === before) return;
|
|
535
|
+
if (depth > 0) buffer.push(explicit);
|
|
536
|
+
else recorded.push(explicit);
|
|
537
|
+
},
|
|
538
|
+
transaction(fn, label) {
|
|
539
|
+
if (depth > 0) {
|
|
540
|
+
fn();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
depth = 1;
|
|
544
|
+
buffer = [];
|
|
545
|
+
try {
|
|
546
|
+
fork.transaction(fn, label);
|
|
547
|
+
recorded.push(...buffer);
|
|
548
|
+
} finally {
|
|
549
|
+
depth = 0;
|
|
550
|
+
buffer = [];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function commitFork(project, fork, label) {
|
|
556
|
+
if (fork.recorded.length === 0) return { ok: true, applied: 0 };
|
|
557
|
+
try {
|
|
558
|
+
project.transaction(() => {
|
|
559
|
+
for (const command of fork.recorded) project.dispatch(command);
|
|
560
|
+
}, label);
|
|
561
|
+
return { ok: true, applied: fork.recorded.length };
|
|
562
|
+
} catch (err) {
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
error: `The project changed while the assistant was working, so its edit no longer applies (${err?.message ?? err}). Ask again.`
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/run.ts
|
|
571
|
+
import { describeProject as describeProject2 } from "@miraiclip/core";
|
|
572
|
+
var MAX_TOOL_RESULT = 2e4;
|
|
573
|
+
var MAX_OLD_TOOL_RESULT = 2e3;
|
|
574
|
+
var DEFAULT_INSTRUCTIONS = `You are the editing assistant inside a video editor. You change the user's project by calling tools; every change you make lands as ONE undo step when you finish, so act confidently.
|
|
575
|
+
|
|
576
|
+
How to work:
|
|
577
|
+
- The current project is summarized below. Clip, track and asset ids there are what tools expect. Call get_state to re-read it after big changes.
|
|
578
|
+
- Prefer the high-level tools when they fit (add_transition, animate_clip, and any audio or media tools). Use apply_commands for everything else, batching related commands; look up a command's payload with get_command_schema the first time you use it.
|
|
579
|
+
- Commands use integer microseconds (1 second = 1000000). High-level tools take seconds.
|
|
580
|
+
- Positions (transform x, y) are fractions of the frame: 0.5, 0.5 is the center.
|
|
581
|
+
- Only reference media that exists in the project or that a tool returned. Never invent file URLs.
|
|
582
|
+
- If a tool fails, read the error, fix the input and retry. If something can't be done, say why.
|
|
583
|
+
- If the request is ambiguous in a way that matters, ask one short question instead of guessing.
|
|
584
|
+
|
|
585
|
+
When you're done, reply in one to three short sentences: what you changed, in plain words (times in seconds, names over ids). No markdown headings.`;
|
|
586
|
+
function createAssistant(options) {
|
|
587
|
+
const tools = options.tools ?? editorTools();
|
|
588
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
589
|
+
const specs = tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
|
|
590
|
+
const maxSteps = options.maxSteps ?? 12;
|
|
591
|
+
return {
|
|
592
|
+
tools,
|
|
593
|
+
async run(project, request, turn = {}) {
|
|
594
|
+
const fork = forkProject(project, options.fork);
|
|
595
|
+
const emit = turn.onEvent ?? (() => {
|
|
596
|
+
});
|
|
597
|
+
const usage2 = { inputTokens: 0, outputTokens: 0 };
|
|
598
|
+
const changes = [];
|
|
599
|
+
const history = trimHistory(turn.history ?? []);
|
|
600
|
+
const added = [{ role: "user", content: request }];
|
|
601
|
+
const system = [
|
|
602
|
+
DEFAULT_INSTRUCTIONS,
|
|
603
|
+
options.instructions?.trim(),
|
|
604
|
+
`Current project:
|
|
605
|
+
${describeProject2(fork.toJSON())}`,
|
|
606
|
+
turn.context?.trim() ? `What the user is looking at: ${turn.context.trim()}` : void 0
|
|
607
|
+
].filter(Boolean).join("\n\n");
|
|
608
|
+
let reply = "";
|
|
609
|
+
let status = "no-changes";
|
|
610
|
+
let error;
|
|
611
|
+
try {
|
|
612
|
+
for (let step = 0; ; step++) {
|
|
613
|
+
if (step >= maxSteps) {
|
|
614
|
+
reply = (reply ? `${reply}
|
|
615
|
+
|
|
616
|
+
` : "") + `I stopped after ${maxSteps} steps; what I finished is applied. Ask me to continue if there's more.`;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
emit({ type: "step", index: step });
|
|
620
|
+
const response = await options.model.complete(
|
|
621
|
+
{
|
|
622
|
+
messages: [{ role: "system", content: system }, ...history, ...added],
|
|
623
|
+
tools: specs,
|
|
624
|
+
...options.modelId ? { model: options.modelId } : {},
|
|
625
|
+
...options.maxOutputTokens ? { maxOutputTokens: options.maxOutputTokens } : {},
|
|
626
|
+
...options.params ? { params: options.params } : {}
|
|
627
|
+
},
|
|
628
|
+
{ ...turn.signal ? { signal: turn.signal } : {}, onText: (delta) => emit({ type: "text", delta }) }
|
|
629
|
+
);
|
|
630
|
+
usage2.inputTokens += response.usage?.inputTokens ?? 0;
|
|
631
|
+
usage2.outputTokens += response.usage?.outputTokens ?? 0;
|
|
632
|
+
added.push({ role: "assistant", content: response.content, ...response.toolCalls.length ? { toolCalls: response.toolCalls } : {} });
|
|
633
|
+
if (response.toolCalls.length === 0) {
|
|
634
|
+
reply = response.content.trim();
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
for (const call of response.toolCalls) {
|
|
638
|
+
if (turn.signal?.aborted) throw abortError();
|
|
639
|
+
emit({ type: "tool-start", call });
|
|
640
|
+
const out = await runTool(byName.get(call.name), call, fork, turn.signal);
|
|
641
|
+
if (out.ok && out.changes?.length) changes.push(...out.changes);
|
|
642
|
+
emit({
|
|
643
|
+
type: "tool-end",
|
|
644
|
+
call,
|
|
645
|
+
ok: out.ok,
|
|
646
|
+
...out.ok && out.changes?.length ? { changes: out.changes } : {},
|
|
647
|
+
...!out.ok ? { error: short(out.error) } : {}
|
|
648
|
+
});
|
|
649
|
+
added.push({ role: "tool", toolCallId: call.id, name: call.name, content: cap(out.ok ? out.result ?? { ok: true } : { ok: false, error: out.error }, MAX_TOOL_RESULT) });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
status = fork.recorded.length === 0 ? "no-changes" : turn.apply === "review" ? "review" : "applied";
|
|
653
|
+
} catch (err) {
|
|
654
|
+
status = turn.signal?.aborted || err?.name === "AbortError" ? "canceled" : "failed";
|
|
655
|
+
error = err?.message ?? String(err);
|
|
656
|
+
}
|
|
657
|
+
let committed = false;
|
|
658
|
+
let discarded = false;
|
|
659
|
+
const label = turn.label ?? `Assistant: ${request.length > 40 ? `${request.slice(0, 39)}\u2026` : request}`;
|
|
660
|
+
const apply = () => {
|
|
661
|
+
if (committed) return { ok: true, applied: 0 };
|
|
662
|
+
if (discarded) return { ok: false, error: "This edit was discarded." };
|
|
663
|
+
if (status === "failed" || status === "canceled") return { ok: false, error: error ?? status };
|
|
664
|
+
const result = commitFork(project, fork, label);
|
|
665
|
+
if (result.ok) committed = true;
|
|
666
|
+
return result;
|
|
667
|
+
};
|
|
668
|
+
if (status === "applied") {
|
|
669
|
+
const result = apply();
|
|
670
|
+
if (!result.ok) {
|
|
671
|
+
status = "failed";
|
|
672
|
+
error = result.error;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
reply,
|
|
677
|
+
changes,
|
|
678
|
+
commands: fork.recorded,
|
|
679
|
+
status,
|
|
680
|
+
...error !== void 0 ? { error } : {},
|
|
681
|
+
preview: fork.toJSON(),
|
|
682
|
+
messages: [...history, ...added],
|
|
683
|
+
usage: usage2,
|
|
684
|
+
apply,
|
|
685
|
+
discard() {
|
|
686
|
+
if (!committed) discarded = true;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async function runTool(tool, call, project, signal) {
|
|
693
|
+
if (!tool) return { ok: false, error: `unknown tool "${call.name}"` };
|
|
694
|
+
if ("__invalidArguments" in call.arguments) return { ok: false, error: "the tool arguments weren't valid JSON; send a JSON object" };
|
|
695
|
+
try {
|
|
696
|
+
return await tool.run(call.arguments, { project, ...signal ? { signal } : {} });
|
|
697
|
+
} catch (err) {
|
|
698
|
+
if (err?.name === "AbortError") throw err;
|
|
699
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function cap(value, max) {
|
|
703
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
704
|
+
return text.length > max ? `${text.slice(0, max)}\u2026 [truncated ${text.length - max} chars]` : text;
|
|
705
|
+
}
|
|
706
|
+
var short = (e) => {
|
|
707
|
+
const text = typeof e === "string" ? e : JSON.stringify(e);
|
|
708
|
+
return text.length > 300 ? `${text.slice(0, 299)}\u2026` : text;
|
|
709
|
+
};
|
|
710
|
+
function trimHistory(history) {
|
|
711
|
+
const answered = new Set(history.flatMap((m) => m.role === "tool" ? [m.toolCallId] : []));
|
|
712
|
+
const out = [];
|
|
713
|
+
for (const m of history) {
|
|
714
|
+
if (m.role === "system") continue;
|
|
715
|
+
if (m.role === "tool") {
|
|
716
|
+
out.push(m.content.length > MAX_OLD_TOOL_RESULT ? { ...m, content: `${m.content.slice(0, MAX_OLD_TOOL_RESULT)}\u2026 [truncated]` } : m);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
out.push(m);
|
|
720
|
+
if (m.role === "assistant") {
|
|
721
|
+
for (const call of m.toolCalls ?? []) {
|
|
722
|
+
if (!answered.has(call.id)) out.push({ role: "tool", toolCallId: call.id, name: call.name, content: '{"ok":false,"error":"not run: the request was interrupted"}' });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return out;
|
|
727
|
+
}
|
|
728
|
+
function abortError() {
|
|
729
|
+
const err = new Error("canceled");
|
|
730
|
+
err.name = "AbortError";
|
|
731
|
+
return err;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/testing.ts
|
|
735
|
+
function scriptedChatModel(steps, options = {}) {
|
|
736
|
+
const requests = [];
|
|
737
|
+
let i = 0;
|
|
738
|
+
const model = defineChatModel({
|
|
739
|
+
id: options.id ?? "scripted",
|
|
740
|
+
label: "Scripted model",
|
|
741
|
+
async complete(request, opts = {}) {
|
|
742
|
+
requests.push(structuredClone(request));
|
|
743
|
+
let step = steps[Math.min(i, steps.length - 1)] ?? "Done.";
|
|
744
|
+
while (typeof step === "function") step = step(request, i);
|
|
745
|
+
i++;
|
|
746
|
+
const s = typeof step === "string" ? { text: step } : step;
|
|
747
|
+
if (s.text) opts.onText?.(s.text);
|
|
748
|
+
const toolCalls = (s.toolCalls ?? []).map((c, n) => ({ id: c.id ?? `call_${i}_${n}`, name: c.name, arguments: c.arguments }));
|
|
749
|
+
return { content: s.text ?? "", toolCalls, finish: toolCalls.length ? "tool_calls" : "stop", usage: { inputTokens: 10, outputTokens: 5 } };
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
return Object.assign(model, { requests });
|
|
753
|
+
}
|
|
754
|
+
export {
|
|
755
|
+
ChatModelError,
|
|
756
|
+
DEFAULT_INSTRUCTIONS,
|
|
757
|
+
applyCommandsSchema,
|
|
758
|
+
commitFork,
|
|
759
|
+
createAssistant,
|
|
760
|
+
createChatHandler,
|
|
761
|
+
defineChatModel,
|
|
762
|
+
defineTool,
|
|
763
|
+
describeCommands,
|
|
764
|
+
editorTools,
|
|
765
|
+
forkProject,
|
|
766
|
+
openAIChatModel,
|
|
767
|
+
parseToolArguments,
|
|
768
|
+
remoteChatModel,
|
|
769
|
+
scriptedChatModel,
|
|
770
|
+
toOpenAIMessages,
|
|
771
|
+
toolsFromDefinitions,
|
|
772
|
+
withExplicitIds
|
|
773
|
+
};
|
|
774
|
+
//# sourceMappingURL=index.js.map
|