@justram/pie 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.
Files changed (83) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +236 -0
  4. package/bin/pie +14 -0
  5. package/dist/cache/index.d.ts +4 -0
  6. package/dist/cache/index.js +3 -0
  7. package/dist/cache/warm.d.ts +3 -0
  8. package/dist/cache/warm.js +23 -0
  9. package/dist/cli/args.d.ts +30 -0
  10. package/dist/cli/args.js +185 -0
  11. package/dist/cli/attachments.d.ts +7 -0
  12. package/dist/cli/attachments.js +29 -0
  13. package/dist/cli/config.d.ts +22 -0
  14. package/dist/cli/config.js +20 -0
  15. package/dist/cli/image.d.ts +17 -0
  16. package/dist/cli/image.js +73 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1 -0
  19. package/dist/cli/oauth.d.ts +14 -0
  20. package/dist/cli/oauth.js +178 -0
  21. package/dist/cli/stream.d.ts +7 -0
  22. package/dist/cli/stream.js +73 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +15 -0
  25. package/dist/core/cache/file.d.ts +4 -0
  26. package/dist/core/cache/file.js +44 -0
  27. package/dist/core/cache/key.d.ts +2 -0
  28. package/dist/core/cache/key.js +12 -0
  29. package/dist/core/cache/memory.d.ts +4 -0
  30. package/dist/core/cache/memory.js +33 -0
  31. package/dist/core/cache/types.d.ts +19 -0
  32. package/dist/core/cache/types.js +1 -0
  33. package/dist/core/errors.d.ts +39 -0
  34. package/dist/core/errors.js +50 -0
  35. package/dist/core/events.d.ts +87 -0
  36. package/dist/core/events.js +1 -0
  37. package/dist/core/extract.d.ts +4 -0
  38. package/dist/core/extract.js +384 -0
  39. package/dist/core/frontmatter.d.ts +5 -0
  40. package/dist/core/frontmatter.js +58 -0
  41. package/dist/core/helpers.d.ts +5 -0
  42. package/dist/core/helpers.js +80 -0
  43. package/dist/core/schema/normalize.d.ts +7 -0
  44. package/dist/core/schema/normalize.js +187 -0
  45. package/dist/core/setup.d.ts +13 -0
  46. package/dist/core/setup.js +174 -0
  47. package/dist/core/types.d.ts +143 -0
  48. package/dist/core/types.js +1 -0
  49. package/dist/core/validators/assert.d.ts +1 -0
  50. package/dist/core/validators/assert.js +18 -0
  51. package/dist/core/validators/command.d.ts +1 -0
  52. package/dist/core/validators/command.js +10 -0
  53. package/dist/core/validators/http.d.ts +1 -0
  54. package/dist/core/validators/http.js +28 -0
  55. package/dist/core/validators/index.d.ts +22 -0
  56. package/dist/core/validators/index.js +55 -0
  57. package/dist/core/validators/shell.d.ts +9 -0
  58. package/dist/core/validators/shell.js +24 -0
  59. package/dist/errors.d.ts +1 -0
  60. package/dist/errors.js +1 -0
  61. package/dist/events.d.ts +1 -0
  62. package/dist/events.js +1 -0
  63. package/dist/extract.d.ts +4 -0
  64. package/dist/extract.js +18 -0
  65. package/dist/index.d.ts +13 -0
  66. package/dist/index.js +8 -0
  67. package/dist/main.d.ts +9 -0
  68. package/dist/main.js +571 -0
  69. package/dist/models.d.ts +21 -0
  70. package/dist/models.js +21 -0
  71. package/dist/recipes/index.d.ts +34 -0
  72. package/dist/recipes/index.js +185 -0
  73. package/dist/runtime/node.d.ts +2 -0
  74. package/dist/runtime/node.js +71 -0
  75. package/dist/runtime/types.d.ts +32 -0
  76. package/dist/runtime/types.js +1 -0
  77. package/dist/setup.d.ts +2 -0
  78. package/dist/setup.js +1 -0
  79. package/dist/types.d.ts +1 -0
  80. package/dist/types.js +1 -0
  81. package/dist/utils/helpers.d.ts +5 -0
  82. package/dist/utils/helpers.js +80 -0
  83. package/package.json +71 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { Message } from "@mariozechner/pi-ai";
2
+ import type { ExtractOptions, ExtractStream } from "./types.js";
3
+ export declare function extract<T>(input: string | Message[], options: ExtractOptions<T>): ExtractStream<T>;
4
+ export declare function extractSync<T>(input: string | Message[], options: ExtractOptions<T>): Promise<T>;
@@ -0,0 +1,384 @@
1
+ import { EventStream, isContextOverflow, validateToolArguments } from "@mariozechner/pi-ai";
2
+ import { buildMessages, findToolCall, getTextContent, parseJsonFromText } from "../utils/helpers.js";
3
+ import { computeCacheKey } from "./cache/key.js";
4
+ import { createMemoryCache } from "./cache/memory.js";
5
+ import { AbortError, ExtractError, MaxTurnsError } from "./errors.js";
6
+ import { normalizeToolSchema } from "./schema/normalize.js";
7
+ import { createValidationEmitter, runValidators } from "./validators/index.js";
8
+ const RESPOND_TOOL_NAME = "respond";
9
+ const RESPOND_TOOL_DESCRIPTION = "Return structured data that matches the schema.";
10
+ const DEFAULT_MAX_TOKENS = 32000;
11
+ const DEFAULT_CACHE = createMemoryCache();
12
+ export function extract(input, options) {
13
+ const maxTurns = options.maxTurns ?? 3;
14
+ const stream = new EventStream((event) => event.type === "complete" || event.type === "error", (event) => event.type === "complete" ? { data: event.result, turns: event.turns, usage: event.usage } : undefined);
15
+ const abortController = createAbortController(options.signal);
16
+ void runExtraction(input, options, maxTurns, stream, abortController.signal).catch(() => {
17
+ // Error event already emitted in runExtraction.
18
+ });
19
+ return Object.assign(stream, {
20
+ abort: () => abortController.abort(new AbortError()),
21
+ });
22
+ }
23
+ export async function extractSync(input, options) {
24
+ const maxTurns = options.maxTurns ?? 3;
25
+ const result = await runExtraction(input, options, maxTurns, null, options.signal);
26
+ return result.data;
27
+ }
28
+ async function runExtraction(input, options, maxTurns, eventStream, signal) {
29
+ eventStream?.push({ type: "start", maxTurns });
30
+ const model = options.model;
31
+ if (!model) {
32
+ const error = new ExtractError('Missing required option "model".');
33
+ eventStream?.push({ type: "error", error, turns: 0 });
34
+ throw error;
35
+ }
36
+ if (signal?.aborted) {
37
+ const error = toAbortError(signal.reason);
38
+ eventStream?.push({ type: "error", error, turns: 0 });
39
+ throw error;
40
+ }
41
+ // Warn if thinking is requested but model doesn't support it
42
+ const thinkingRequested = options.thinking && options.thinking !== "off";
43
+ if (thinkingRequested && !model.reasoning) {
44
+ eventStream?.push({
45
+ type: "warning",
46
+ code: "thinking_unsupported",
47
+ message: `Model "${model.provider}:${model.id}" does not support extended thinking. The 'thinking' option will be ignored.`,
48
+ });
49
+ }
50
+ const { cacheKey, cacheStore, cacheTtl, cacheRevalidate } = resolveCache(input, options);
51
+ if (cacheKey && cacheStore) {
52
+ const entry = await cacheStore.get(cacheKey);
53
+ if (entry) {
54
+ const age = Date.now() - entry.timestamp;
55
+ const expired = cacheTtl !== undefined && age > cacheTtl;
56
+ if (!expired) {
57
+ if (cacheRevalidate) {
58
+ const emitter = createValidationEmitter((e) => eventStream?.push(e));
59
+ try {
60
+ await runValidators(entry.data, options, emitter, signal);
61
+ eventStream?.push({ type: "cache_hit", key: cacheKey, age });
62
+ eventStream?.push({ type: "complete", result: entry.data, turns: entry.turns, usage: entry.usage });
63
+ return { data: entry.data, turns: entry.turns, usage: entry.usage };
64
+ }
65
+ catch {
66
+ await cacheStore.delete(cacheKey);
67
+ }
68
+ }
69
+ else {
70
+ eventStream?.push({ type: "cache_hit", key: cacheKey, age });
71
+ eventStream?.push({ type: "complete", result: entry.data, turns: entry.turns, usage: entry.usage });
72
+ return { data: entry.data, turns: entry.turns, usage: entry.usage };
73
+ }
74
+ }
75
+ }
76
+ eventStream?.push({ type: "cache_miss", key: cacheKey });
77
+ }
78
+ const messages = buildMessages(input, options.attachments);
79
+ const { schema: toolSchema, unwrapKey } = normalizeToolSchema(model, options.schema);
80
+ const tool = {
81
+ name: RESPOND_TOOL_NAME,
82
+ description: RESPOND_TOOL_DESCRIPTION,
83
+ parameters: toolSchema,
84
+ };
85
+ const validationTool = {
86
+ name: RESPOND_TOOL_NAME,
87
+ description: RESPOND_TOOL_DESCRIPTION,
88
+ parameters: toolSchema,
89
+ };
90
+ const streamFn = options.streamFn;
91
+ if (!streamFn) {
92
+ const error = new ExtractError('Missing required option "streamFn".');
93
+ eventStream?.push({ type: "error", error, turns: 0 });
94
+ throw error;
95
+ }
96
+ const maxTokens = Math.min(model.maxTokens, DEFAULT_MAX_TOKENS);
97
+ const totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0 };
98
+ let lastError;
99
+ for (let turn = 1; turn <= maxTurns; turn++) {
100
+ if (signal?.aborted) {
101
+ const error = toAbortError(signal.reason);
102
+ eventStream?.push({ type: "error", error, turns: turn - 1 });
103
+ throw error;
104
+ }
105
+ eventStream?.push({ type: "turn_start", turn });
106
+ const context = {
107
+ systemPrompt: [
108
+ options.prompt,
109
+ "When you are ready, call the 'respond' tool with your structured answer.",
110
+ "Do not include extra commentary in the tool call arguments.",
111
+ ].join("\n\n"),
112
+ messages,
113
+ tools: [tool],
114
+ };
115
+ eventStream?.push({ type: "llm_start" });
116
+ let assistant;
117
+ try {
118
+ // Convert "off" or undefined to undefined (disabled), otherwise pass through
119
+ const reasoning = options.thinking && options.thinking !== "off" ? options.thinking : undefined;
120
+ assistant = await callLlm(streamFn, model, context, {
121
+ apiKey: options.apiKey,
122
+ signal,
123
+ maxTokens,
124
+ reasoning,
125
+ thinkingBudgets: options.thinkingBudgets,
126
+ onModelSelected: (selected) => eventStream?.push({ type: "llm_selected", model: selected }),
127
+ }, eventStream);
128
+ }
129
+ catch (error) {
130
+ const err = toError(error);
131
+ eventStream?.push({ type: "error", error: err, turns: turn });
132
+ throw err;
133
+ }
134
+ const turnUsage = mapUsage(assistant.usage);
135
+ accumulateUsage(totalUsage, turnUsage);
136
+ eventStream?.push({ type: "llm_end", message: assistant, usage: turnUsage });
137
+ const toolCall = findToolCall(assistant, RESPOND_TOOL_NAME);
138
+ if (toolCall) {
139
+ eventStream?.push({ type: "tool_call", toolCall });
140
+ eventStream?.push({ type: "json_extracted", source: "tool_call" });
141
+ }
142
+ let args = toolCall?.arguments;
143
+ let jsonSource = toolCall ? "tool_call" : null;
144
+ if (!toolCall) {
145
+ args = parseJsonFromText(assistant);
146
+ if (args !== null) {
147
+ jsonSource = "text";
148
+ eventStream?.push({ type: "json_extracted", source: "text" });
149
+ }
150
+ }
151
+ if (args === null) {
152
+ eventStream?.push({ type: "thinking", text: getTextContent(assistant) });
153
+ messages.push(assistant);
154
+ messages.push(createContinueMessage());
155
+ eventStream?.push({ type: "turn_end", turn, hasResult: false });
156
+ continue;
157
+ }
158
+ const emitter = createValidationEmitter((e) => eventStream?.push(e));
159
+ let data;
160
+ try {
161
+ emitter.start("schema");
162
+ const normalizedArgs = normalizeToolArguments(args, unwrapKey);
163
+ const validationToolCall = {
164
+ type: "toolCall",
165
+ id: toolCall?.id ?? "respond_fallback",
166
+ name: RESPOND_TOOL_NAME,
167
+ arguments: normalizedArgs,
168
+ };
169
+ const validated = validateToolArguments(validationTool, validationToolCall);
170
+ data = unwrapToolArguments(validated, unwrapKey);
171
+ emitter.pass("schema");
172
+ }
173
+ catch (error) {
174
+ const err = toError(error);
175
+ lastError = err;
176
+ emitter.fail("schema", err.message);
177
+ messages.push(assistant);
178
+ if (jsonSource === "tool_call") {
179
+ messages.push(createSchemaToolResult(toolCall?.id ?? "respond_fallback", err));
180
+ }
181
+ else {
182
+ messages.push(createSchemaFeedbackMessage(err));
183
+ }
184
+ eventStream?.push({ type: "turn_end", turn, hasResult: false });
185
+ continue;
186
+ }
187
+ try {
188
+ await runValidators(data, options, emitter, signal);
189
+ eventStream?.push({ type: "turn_end", turn, hasResult: true });
190
+ const result = {
191
+ data,
192
+ turns: turn,
193
+ usage: { ...totalUsage },
194
+ };
195
+ if (cacheKey && cacheStore) {
196
+ try {
197
+ await cacheStore.set(cacheKey, { data, timestamp: Date.now(), turns: turn, usage: { ...totalUsage } });
198
+ eventStream?.push({ type: "cache_set", key: cacheKey });
199
+ }
200
+ catch {
201
+ // Cache write failure shouldn't fail extraction.
202
+ }
203
+ }
204
+ eventStream?.push({ type: "complete", result: data, turns: turn, usage: { ...totalUsage } });
205
+ return result;
206
+ }
207
+ catch (error) {
208
+ const err = toError(error);
209
+ lastError = err;
210
+ messages.push(assistant);
211
+ if (jsonSource === "tool_call") {
212
+ messages.push(createSchemaToolResult(toolCall?.id ?? "respond_fallback", err));
213
+ }
214
+ else {
215
+ messages.push(createSchemaFeedbackMessage(err));
216
+ }
217
+ eventStream?.push({ type: "turn_end", turn, hasResult: false });
218
+ }
219
+ }
220
+ const err = new MaxTurnsError(`Extraction failed after ${maxTurns} turns`, maxTurns, lastError);
221
+ eventStream?.push({ type: "error", error: err, turns: maxTurns });
222
+ throw err;
223
+ }
224
+ function resolveCache(input, options) {
225
+ const raw = options.cache;
226
+ if (!raw) {
227
+ return { cacheKey: null, cacheStore: null, cacheRevalidate: false };
228
+ }
229
+ const cacheOptions = typeof raw === "boolean" ? {} : raw;
230
+ const store = cacheOptions.store ?? DEFAULT_CACHE;
231
+ const revalidate = cacheOptions.revalidate ?? false;
232
+ // If caller provides function validators, we only allow caching when revalidate is enabled.
233
+ if ((options.validate || options.validateAsync) && !revalidate) {
234
+ return { cacheKey: null, cacheStore: null, cacheRevalidate: false };
235
+ }
236
+ const inputText = typeof input === "string" ? input : JSON.stringify(input);
237
+ const key = cacheOptions.key
238
+ ? cacheOptions.key(inputText, options)
239
+ : computeCacheKey(inputText, options);
240
+ return { cacheKey: key, cacheStore: store, cacheTtl: cacheOptions.ttl, cacheRevalidate: revalidate };
241
+ }
242
+ async function callLlm(streamFn, model, context, options, eventStream) {
243
+ if (!streamFn) {
244
+ throw new Error("streamFn is required");
245
+ }
246
+ const s = streamFn(model, context, options);
247
+ let message = null;
248
+ for await (const event of s) {
249
+ if (event.type === "text_delta") {
250
+ eventStream?.push({ type: "llm_delta", delta: event.delta });
251
+ }
252
+ if (event.type === "thinking_delta") {
253
+ eventStream?.push({ type: "thinking", text: event.delta });
254
+ }
255
+ if (event.type === "done") {
256
+ message = event.message;
257
+ }
258
+ if (event.type === "error") {
259
+ throw createLlmError(event.error, model);
260
+ }
261
+ }
262
+ return message ?? (await s.result());
263
+ }
264
+ function mapUsage(usage) {
265
+ return {
266
+ inputTokens: usage.input,
267
+ outputTokens: usage.output,
268
+ totalTokens: usage.totalTokens,
269
+ cost: usage.cost.total,
270
+ };
271
+ }
272
+ function createLlmError(error, model) {
273
+ const provider = error.provider ?? model.provider;
274
+ const modelId = error.model ?? model.id;
275
+ const api = error.api ?? model.api;
276
+ const stopReason = error.stopReason ?? "error";
277
+ const details = error.errorMessage?.trim();
278
+ const contentDetails = getTextContent(error);
279
+ const genericErrorPatterns = [/unknown error occurred/i, /unkown error ocurred/i, /^llm error$/i];
280
+ const isGeneric = details ? genericErrorPatterns.some((pattern) => pattern.test(details)) : false;
281
+ const hint = isGeneric
282
+ ? " Provider returned a generic error; this often means the model is not enabled for your account, the API key lacks access, or quota was exceeded. If this model works in another client, verify you are using the same provider and credentials."
283
+ : "";
284
+ const isOverflow = isContextOverflow(error, model.contextWindow);
285
+ const overflowHint = isOverflow
286
+ ? ` Input exceeds the model context window (${model.contextWindow} tokens). Reduce input length or attachments.`
287
+ : "";
288
+ const metadata = [
289
+ `provider=${provider}`,
290
+ `model=${modelId}`,
291
+ `api=${api}`,
292
+ `stopReason=${stopReason}`,
293
+ model.baseUrl ? `baseUrl=${model.baseUrl}` : null,
294
+ ]
295
+ .filter((value) => Boolean(value))
296
+ .join(", ");
297
+ const message = details
298
+ ? `LLM request failed (${metadata}). Provider message: ${details}.${hint}${overflowHint}`
299
+ : `LLM request failed (${metadata}).${hint}${overflowHint}`;
300
+ const contentSuffix = contentDetails && contentDetails !== details ? ` Provider content: ${contentDetails}` : "";
301
+ return new ExtractError(`${message}${contentSuffix}`);
302
+ }
303
+ function accumulateUsage(target, extra) {
304
+ target.inputTokens += extra.inputTokens;
305
+ target.outputTokens += extra.outputTokens;
306
+ target.totalTokens += extra.totalTokens;
307
+ target.cost += extra.cost;
308
+ }
309
+ function normalizeToolArguments(args, unwrapKey) {
310
+ if (!unwrapKey) {
311
+ return args;
312
+ }
313
+ if (args && typeof args === "object" && !Array.isArray(args) && unwrapKey in args) {
314
+ return args;
315
+ }
316
+ return { [unwrapKey]: args };
317
+ }
318
+ function unwrapToolArguments(data, unwrapKey) {
319
+ if (!unwrapKey) {
320
+ return data;
321
+ }
322
+ if (!data || typeof data !== "object" || !(unwrapKey in data)) {
323
+ throw new Error(`Validation failed for tool "${RESPOND_TOOL_NAME}": missing "${unwrapKey}" field.`);
324
+ }
325
+ return data[unwrapKey];
326
+ }
327
+ function toError(error) {
328
+ if (error instanceof Error) {
329
+ return error;
330
+ }
331
+ return new Error(typeof error === "string" ? error : "Unknown error");
332
+ }
333
+ function toAbortError(reason) {
334
+ if (reason instanceof AbortError) {
335
+ return reason;
336
+ }
337
+ if (reason instanceof Error) {
338
+ return new AbortError(reason.message);
339
+ }
340
+ return new AbortError(typeof reason === "string" ? reason : "Extraction aborted");
341
+ }
342
+ function createContinueMessage() {
343
+ return {
344
+ role: "user",
345
+ content: "Continue. Call the 'respond' tool when ready with your structured response.",
346
+ timestamp: Date.now(),
347
+ };
348
+ }
349
+ function createSchemaToolResult(toolCallId, error) {
350
+ return {
351
+ role: "toolResult",
352
+ toolCallId,
353
+ toolName: RESPOND_TOOL_NAME,
354
+ content: [{ type: "text", text: `Validation error: ${error.message}` }],
355
+ isError: true,
356
+ timestamp: Date.now(),
357
+ };
358
+ }
359
+ function createSchemaFeedbackMessage(error) {
360
+ return {
361
+ role: "user",
362
+ content: [
363
+ "Validation error in your previous JSON output:",
364
+ error.message,
365
+ "",
366
+ "Continue. Call the 'respond' tool when ready with your corrected structured response.",
367
+ ].join("\n"),
368
+ timestamp: Date.now(),
369
+ };
370
+ }
371
+ function createAbortController(signal) {
372
+ const controller = new AbortController();
373
+ if (!signal) {
374
+ return controller;
375
+ }
376
+ if (signal.aborted) {
377
+ controller.abort(signal.reason);
378
+ return controller;
379
+ }
380
+ signal.addEventListener("abort", () => {
381
+ controller.abort(signal.reason);
382
+ }, { once: true });
383
+ return controller;
384
+ }
@@ -0,0 +1,5 @@
1
+ export interface FrontmatterParseResult {
2
+ frontmatter: Record<string, unknown>;
3
+ body: string;
4
+ }
5
+ export declare function parseFrontmatter(content: string): FrontmatterParseResult;
@@ -0,0 +1,58 @@
1
+ export function parseFrontmatter(content) {
2
+ const frontmatter = {};
3
+ const normalized = content.replace(/\r\n/g, "\n");
4
+ if (!normalized.startsWith("---")) {
5
+ return { frontmatter, body: normalized };
6
+ }
7
+ const endIndex = normalized.indexOf("\n---", 3);
8
+ if (endIndex === -1) {
9
+ return { frontmatter, body: normalized };
10
+ }
11
+ const frontmatterBlock = normalized.slice(4, endIndex);
12
+ const body = normalized.slice(endIndex + 4).trim();
13
+ const lines = frontmatterBlock.split("\n");
14
+ let currentKey = null;
15
+ for (const line of lines) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith("#")) {
18
+ continue;
19
+ }
20
+ const listMatch = trimmed.match(/^-\s+(.*)$/);
21
+ if (listMatch && currentKey) {
22
+ const existing = frontmatter[currentKey];
23
+ if (!Array.isArray(existing)) {
24
+ frontmatter[currentKey] = [];
25
+ }
26
+ frontmatter[currentKey].push(parseScalar(listMatch[1]));
27
+ continue;
28
+ }
29
+ const keyMatch = trimmed.match(/^([\w-]+):\s*(.*)$/);
30
+ if (keyMatch) {
31
+ currentKey = keyMatch[1];
32
+ const rawValue = keyMatch[2];
33
+ if (rawValue.length === 0) {
34
+ frontmatter[currentKey] = null;
35
+ }
36
+ else {
37
+ frontmatter[currentKey] = parseScalar(rawValue);
38
+ }
39
+ continue;
40
+ }
41
+ currentKey = null;
42
+ }
43
+ return { frontmatter, body };
44
+ }
45
+ function parseScalar(value) {
46
+ let parsed = value.trim();
47
+ if ((parsed.startsWith('"') && parsed.endsWith('"')) || (parsed.startsWith("'") && parsed.endsWith("'"))) {
48
+ parsed = parsed.slice(1, -1);
49
+ }
50
+ const lower = parsed.toLowerCase();
51
+ if (lower === "true")
52
+ return true;
53
+ if (lower === "false")
54
+ return false;
55
+ if (/^-?\d+(\.\d+)?$/.test(parsed))
56
+ return Number(parsed);
57
+ return parsed;
58
+ }
@@ -0,0 +1,5 @@
1
+ import type { AssistantMessage, ImageContent, Message, ToolCall } from "@mariozechner/pi-ai";
2
+ export declare function buildMessages(input: string | Message[], attachments?: ImageContent[]): Message[];
3
+ export declare function findToolCall(message: AssistantMessage, toolName: string): ToolCall | undefined;
4
+ export declare function getTextContent(message: AssistantMessage): string;
5
+ export declare function parseJsonFromText(message: AssistantMessage): unknown | null;
@@ -0,0 +1,80 @@
1
+ export function buildMessages(input, attachments) {
2
+ if (typeof input !== "string") {
3
+ return input;
4
+ }
5
+ const content = attachments?.length
6
+ ? [{ type: "text", text: input }, ...attachments]
7
+ : input;
8
+ const message = {
9
+ role: "user",
10
+ content,
11
+ timestamp: Date.now(),
12
+ };
13
+ return [message];
14
+ }
15
+ export function findToolCall(message, toolName) {
16
+ for (const content of message.content) {
17
+ if (content.type === "toolCall" && content.name === toolName) {
18
+ return content;
19
+ }
20
+ }
21
+ return undefined;
22
+ }
23
+ export function getTextContent(message) {
24
+ return message.content
25
+ .filter((content) => content.type === "text")
26
+ .map((content) => content.text)
27
+ .join("\n")
28
+ .trim();
29
+ }
30
+ export function parseJsonFromText(message) {
31
+ const text = getTextContent(message);
32
+ if (!text) {
33
+ return null;
34
+ }
35
+ const candidates = collectJsonCandidates(text);
36
+ for (const candidate of candidates) {
37
+ const parsed = tryParseJson(candidate);
38
+ if (parsed !== null) {
39
+ return parsed;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ function collectJsonCandidates(text) {
45
+ const candidates = [];
46
+ const fenced = extractFencedJson(text);
47
+ if (fenced) {
48
+ candidates.push(fenced);
49
+ }
50
+ candidates.push(text);
51
+ const objectCandidate = sliceBetween(text, "{", "}");
52
+ if (objectCandidate) {
53
+ candidates.push(objectCandidate);
54
+ }
55
+ const arrayCandidate = sliceBetween(text, "[", "]");
56
+ if (arrayCandidate) {
57
+ candidates.push(arrayCandidate);
58
+ }
59
+ return candidates;
60
+ }
61
+ function extractFencedJson(text) {
62
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
63
+ return match ? match[1].trim() : null;
64
+ }
65
+ function sliceBetween(text, open, close) {
66
+ const start = text.indexOf(open);
67
+ const end = text.lastIndexOf(close);
68
+ if (start === -1 || end === -1 || end <= start) {
69
+ return null;
70
+ }
71
+ return text.slice(start, end + 1).trim();
72
+ }
73
+ function tryParseJson(text) {
74
+ try {
75
+ return JSON.parse(text);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
@@ -0,0 +1,7 @@
1
+ import type { Api, Model } from "@mariozechner/pi-ai";
2
+ import type { TSchema } from "@sinclair/typebox";
3
+ export type NormalizedToolSchema = {
4
+ schema: TSchema;
5
+ unwrapKey: string | null;
6
+ };
7
+ export declare function normalizeToolSchema(model: Model<Api>, schema: TSchema): NormalizedToolSchema;