expo-ai-kit 0.6.0 → 0.7.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/README.md +35 -3
- package/build/index.d.ts +57 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +180 -0
- package/build/index.js.map +1 -1
- package/build/tools.d.ts +41 -0
- package/build/tools.d.ts.map +1 -0
- package/build/tools.js +86 -0
- package/build/tools.js.map +1 -0
- package/build/types.d.ts +111 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/package.json +4 -2
- package/src/index.ts +219 -0
- package/src/tools.ts +126 -0
- package/src/types.ts +124 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { JSONSchema, ToolSet } from './types';
|
|
2
|
+
import { extractJson } from './structured';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Pure helpers for generateText() tool calling.
|
|
6
|
+
//
|
|
7
|
+
// Like structured.ts, this module imports no native module so its logic can be
|
|
8
|
+
// unit-tested in plain Node. generateText() (in index.ts) drives the inference
|
|
9
|
+
// + tool-execution loop on top of these.
|
|
10
|
+
//
|
|
11
|
+
// On-device backends have no native tool-call channel, so we define a tiny text
|
|
12
|
+
// protocol: the model emits a JSON object `{"tool": "<name>", "arguments": {…}}`
|
|
13
|
+
// to request a call, or plain text to answer. We parse that back out tolerantly
|
|
14
|
+
// (reusing extractJson), validate the name + args, and run the tool in JS.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Build the instruction appended to the system prompt to enable tool calling. */
|
|
18
|
+
export function buildToolInstruction(tools: ToolSet): string {
|
|
19
|
+
const names = Object.keys(tools);
|
|
20
|
+
const lines: string[] = [
|
|
21
|
+
'You have access to the following tools. Use one only when it helps answer the request:',
|
|
22
|
+
'',
|
|
23
|
+
];
|
|
24
|
+
for (const name of names) {
|
|
25
|
+
const tool = tools[name];
|
|
26
|
+
lines.push(`- ${name}: ${tool.description}`);
|
|
27
|
+
lines.push(` arguments JSON Schema: ${JSON.stringify(tool.parameters)}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push(
|
|
30
|
+
'',
|
|
31
|
+
'To call a tool, respond with ONLY a JSON object of this exact form and nothing else:',
|
|
32
|
+
'{"tool": "<tool name>", "arguments": { ... }}',
|
|
33
|
+
'',
|
|
34
|
+
'Rules:',
|
|
35
|
+
'- Call at most one tool per response.',
|
|
36
|
+
`- "tool" must be exactly one of: ${names.join(', ')}.`,
|
|
37
|
+
'- "arguments" must conform to that tool\'s arguments JSON Schema.',
|
|
38
|
+
'- If you do not need a tool, answer the user directly in plain text with no JSON.',
|
|
39
|
+
'- After you receive a tool result, use it to answer; do not repeat the same call.'
|
|
40
|
+
);
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What {@link parseToolCall} found in a model response.
|
|
46
|
+
*
|
|
47
|
+
* - `tool`: a well-formed call to a known tool (args still need schema validation).
|
|
48
|
+
* - `unknown-tool`: looked like a tool call but the name isn't in the tool set.
|
|
49
|
+
* - `text`: no tool call — treat the response as the final answer.
|
|
50
|
+
*/
|
|
51
|
+
export type ParsedToolCall =
|
|
52
|
+
| { kind: 'tool'; toolName: string; args: unknown }
|
|
53
|
+
| { kind: 'unknown-tool'; toolName: string }
|
|
54
|
+
| { kind: 'text' };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect a tool call in model output.
|
|
58
|
+
*
|
|
59
|
+
* A response is a tool call when it contains a JSON object (possibly wrapped in
|
|
60
|
+
* prose or a ```json fence) with a string `tool` field. We tolerate `arguments`
|
|
61
|
+
* or `args` for the payload. If `tool` names something not in `toolNames` it's
|
|
62
|
+
* reported as `unknown-tool` so the loop can re-prompt instead of leaking the
|
|
63
|
+
* raw JSON as an answer. Anything else is plain `text`.
|
|
64
|
+
*/
|
|
65
|
+
export function parseToolCall(text: string, toolNames: string[]): ParsedToolCall {
|
|
66
|
+
const parsed = extractJson(text);
|
|
67
|
+
if (!parsed.ok) return { kind: 'text' };
|
|
68
|
+
|
|
69
|
+
const value = parsed.value;
|
|
70
|
+
if (!isPlainObject(value) || typeof value.tool !== 'string') {
|
|
71
|
+
return { kind: 'text' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const toolName = value.tool;
|
|
75
|
+
if (!toolNames.includes(toolName)) {
|
|
76
|
+
return { kind: 'unknown-tool', toolName };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rawArgs = 'arguments' in value ? value.arguments : value.args;
|
|
80
|
+
return { kind: 'tool', toolName, args: rawArgs ?? {} };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Follow-up prompt when the model named a tool that doesn't exist. */
|
|
84
|
+
export function buildUnknownToolRepair(toolName: string, toolNames: string[]): string {
|
|
85
|
+
return (
|
|
86
|
+
`The tool "${toolName}" does not exist. ` +
|
|
87
|
+
`Available tools are: ${toolNames.join(', ')}. ` +
|
|
88
|
+
'Either call one of these tools using the required JSON form, or answer in plain text.'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Follow-up prompt when a tool's proposed arguments failed schema validation. */
|
|
93
|
+
export function buildToolArgsRepair(toolName: string, errors: string[]): string {
|
|
94
|
+
const detail = errors.slice(0, 8).join('; ');
|
|
95
|
+
return (
|
|
96
|
+
`The arguments for "${toolName}" did not match its schema: ${detail}. ` +
|
|
97
|
+
'Respond again with ONLY the corrected {"tool": "' +
|
|
98
|
+
toolName +
|
|
99
|
+
'", "arguments": { ... }} JSON — no prose, no markdown code fences.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render a tool result as the user-turn text fed back to the model.
|
|
105
|
+
* Non-string results are JSON-encoded; strings pass through as-is.
|
|
106
|
+
*/
|
|
107
|
+
export function formatToolResult(toolName: string, result: unknown): string {
|
|
108
|
+
const body =
|
|
109
|
+
typeof result === 'string' ? result : safeStringify(result);
|
|
110
|
+
return `Result of calling the tool "${toolName}":\n${body}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function safeStringify(value: unknown): string {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.stringify(value) ?? String(value);
|
|
116
|
+
} catch {
|
|
117
|
+
return String(value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
122
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Re-export for callers that want the schema type alongside tool helpers.
|
|
126
|
+
export type { JSONSchema };
|
package/src/types.ts
CHANGED
|
@@ -219,6 +219,130 @@ export type GenerateObjectResult<T = unknown> = {
|
|
|
219
219
|
};
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Tool / Function Calling (generateText)
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A tool (function) the model may call to fetch data or take an action.
|
|
228
|
+
*
|
|
229
|
+
* The model never runs anything itself — it *proposes* a call (a name + JSON
|
|
230
|
+
* arguments), {@link generateText} validates the arguments against `parameters`,
|
|
231
|
+
* and only then invokes your `execute`. The result is fed back into the
|
|
232
|
+
* conversation so the model can use it to produce its final answer.
|
|
233
|
+
*
|
|
234
|
+
* @typeParam TArgs - Shape of the validated arguments passed to `execute`.
|
|
235
|
+
* @typeParam TResult - What `execute` returns (serialized back to the model).
|
|
236
|
+
*/
|
|
237
|
+
export type Tool<TArgs = any, TResult = any> = {
|
|
238
|
+
/**
|
|
239
|
+
* What the tool does and when to use it. This is how the model decides
|
|
240
|
+
* whether a request matches this tool — make it specific and action-oriented.
|
|
241
|
+
*/
|
|
242
|
+
description: string;
|
|
243
|
+
/**
|
|
244
|
+
* JSON Schema for the tool's arguments. The model is told to conform to it,
|
|
245
|
+
* and the args it proposes are validated against it (same pragmatic subset as
|
|
246
|
+
* {@link generateObject}) before `execute` runs. Keep it small and shallow.
|
|
247
|
+
*/
|
|
248
|
+
parameters: JSONSchema;
|
|
249
|
+
/**
|
|
250
|
+
* Runs the tool with the validated arguments and returns a result.
|
|
251
|
+
*
|
|
252
|
+
* **Optional on purpose.** If you omit it, {@link generateText} does not run
|
|
253
|
+
* anything — it stops with `finishReason: 'tool-calls'` and hands you the
|
|
254
|
+
* proposed call so you can confirm, gate, or execute it yourself.
|
|
255
|
+
*/
|
|
256
|
+
execute?: (args: TArgs) => TResult | Promise<TResult>;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** A map of tool name → {@link Tool}, passed to {@link generateText}. */
|
|
260
|
+
export type ToolSet = Record<string, Tool>;
|
|
261
|
+
|
|
262
|
+
/** A tool invocation the model proposed (name + validated arguments). */
|
|
263
|
+
export type ToolCall = {
|
|
264
|
+
/** The tool's key in the {@link ToolSet}. */
|
|
265
|
+
toolName: string;
|
|
266
|
+
/** Arguments the model supplied, validated against the tool's `parameters`. */
|
|
267
|
+
args: unknown;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** The outcome of running a {@link ToolCall} via its `execute`. */
|
|
271
|
+
export type ToolResult = {
|
|
272
|
+
/** The tool's key in the {@link ToolSet}. */
|
|
273
|
+
toolName: string;
|
|
274
|
+
/** The arguments that were passed to `execute`. */
|
|
275
|
+
args: unknown;
|
|
276
|
+
/** Whatever `execute` returned (or `{ error }` if it threw). */
|
|
277
|
+
result: unknown;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** One model round-trip in the {@link generateText} loop. */
|
|
281
|
+
export type StepResult = {
|
|
282
|
+
/** Assistant text produced this step (empty when the step only called a tool). */
|
|
283
|
+
text: string;
|
|
284
|
+
/** Tool calls proposed this step (at most one in the current protocol). */
|
|
285
|
+
toolCalls: ToolCall[];
|
|
286
|
+
/** Results of the tool calls executed this step. */
|
|
287
|
+
toolResults: ToolResult[];
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Why {@link generateText} stopped.
|
|
292
|
+
*
|
|
293
|
+
* - `'stop'`: the model produced a final text answer.
|
|
294
|
+
* - `'tool-calls'`: stopped because a proposed tool has no `execute` — the call
|
|
295
|
+
* is returned for you to handle (human-in-the-loop).
|
|
296
|
+
* - `'max-steps'`: hit the `maxSteps` cap while still calling tools. Raise the cap.
|
|
297
|
+
*/
|
|
298
|
+
export type GenerateTextFinishReason = 'stop' | 'tool-calls' | 'max-steps';
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Options for {@link generateText}.
|
|
302
|
+
*/
|
|
303
|
+
export type GenerateTextOptions = {
|
|
304
|
+
/** Tools the model may call. Omit (or pass `{}`) for a plain text generation. */
|
|
305
|
+
tools?: ToolSet;
|
|
306
|
+
/**
|
|
307
|
+
* Maximum number of model round-trips (each call + tool execution is one step).
|
|
308
|
+
* Bounds the tool-calling chain so it can't run away. Defaults to 5.
|
|
309
|
+
*/
|
|
310
|
+
maxSteps?: number;
|
|
311
|
+
/**
|
|
312
|
+
* System prompt used when the messages array has no system message. The tool
|
|
313
|
+
* instructions are appended to it (or to the array's system message if present).
|
|
314
|
+
*/
|
|
315
|
+
systemPrompt?: string;
|
|
316
|
+
/**
|
|
317
|
+
* Abort the request. Behaves like {@link LLMSendOptions.signal} — the returned
|
|
318
|
+
* promise rejects with an INFERENCE_CANCELLED {@link ModelError}.
|
|
319
|
+
*/
|
|
320
|
+
signal?: AbortSignal;
|
|
321
|
+
/**
|
|
322
|
+
* How many times to re-prompt within a step when the model emits a malformed
|
|
323
|
+
* tool call, an unknown tool name, or arguments that fail schema validation.
|
|
324
|
+
* Defaults to 2. If it still can't comply, `generateText` throws INFERENCE_FAILED.
|
|
325
|
+
*/
|
|
326
|
+
maxRepairAttempts?: number;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Result of {@link generateText}.
|
|
331
|
+
*/
|
|
332
|
+
export type GenerateTextResult = {
|
|
333
|
+
/** The final assistant text (empty if it stopped on a tool call without `execute`). */
|
|
334
|
+
text: string;
|
|
335
|
+
/** Every step taken, in order — useful for tracing or debugging. */
|
|
336
|
+
steps: StepResult[];
|
|
337
|
+
/** All tool calls across every step, flattened. */
|
|
338
|
+
toolCalls: ToolCall[];
|
|
339
|
+
/** All tool results across every step, flattened. */
|
|
340
|
+
toolResults: ToolResult[];
|
|
341
|
+
/** Why generation stopped. See {@link GenerateTextFinishReason}. */
|
|
342
|
+
finishReason: GenerateTextFinishReason;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
|
|
222
346
|
// ============================================================================
|
|
223
347
|
// Model Types
|
|
224
348
|
// ============================================================================
|