@tanstack-ai-browser/apfel 0.2.0 → 0.2.2
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/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +9 -0
- package/package.json +1 -1
- package/src/adapter.ts +14 -0
- package/dist/guided-schema.d.ts +0 -20
- package/dist/guided-schema.d.ts.map +0 -1
- package/dist/guided-schema.js +0 -136
- package/dist/tool-calls.d.ts +0 -32
- package/dist/tool-calls.d.ts.map +0 -1
- package/dist/tool-calls.js +0 -123
- package/dist/tool-repair.d.ts +0 -56
- package/dist/tool-repair.d.ts.map +0 -1
- package/dist/tool-repair.js +0 -252
- package/dist/tools.d.ts +0 -68
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -469
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAA;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAA;AAIjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC,CAAA;AAED,mGAAmG;AACnG,MAAM,MAAM,qBAAqB,GAAG;IAClC,+BAA+B;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,2FAA2F;AAC3F,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,aAAa,GAAG,WAAW,CAAA;AAEvF,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GAAG,2BAA2B,CAAC,UAAU,EAAE,qBAAqB,CAAC,GAAG;IAC9F,sEAAsE;IACtE,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,CAAA;IACrE,6EAA6E;IAC7E,YAAY,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IACzD,uFAAuF;IACvF,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;CACtE,CAAA;AAID,+FAA+F;AAC/F,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,gBAAgB,CAAA;AACjE,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,gBAAgB,CAAA"}
|
package/dist/adapter.js
CHANGED
|
@@ -24,6 +24,15 @@ export function apfelText(modelOrConfig, maybeConfig = {}) {
|
|
|
24
24
|
// apfel runs on the user's own machine: there's no secret key to expose.
|
|
25
25
|
dangerouslyAllowBrowser: true
|
|
26
26
|
});
|
|
27
|
+
const hooks = adapter;
|
|
28
|
+
const convert = hooks.chatCompletionsTokenUsage.bind(adapter);
|
|
29
|
+
hooks.chatCompletionsTokenUsage = (usage, source) => {
|
|
30
|
+
const result = convert(usage, source);
|
|
31
|
+
if (!result)
|
|
32
|
+
return result;
|
|
33
|
+
const cachedTokens = usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
34
|
+
return { ...result, promptTokensDetails: { ...result.promptTokensDetails, cachedTokens } };
|
|
35
|
+
};
|
|
27
36
|
const availability = async (signal) => {
|
|
28
37
|
try {
|
|
29
38
|
const timeout = AbortSignal.timeout(10_000);
|
package/package.json
CHANGED
package/src/adapter.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { openaiCompatibleText } from "@tanstack/ai-openai/compatible"
|
|
10
10
|
import type { OpenAICompatibleChatAdapter } from "@tanstack/ai-openai/compatible"
|
|
11
|
+
import type { TokenUsage } from "@tanstack/ai"
|
|
11
12
|
|
|
12
13
|
import { APFEL_MODEL_META } from "./model-meta.js"
|
|
13
14
|
import type { ApfelChatModel } from "./model-meta.js"
|
|
@@ -75,6 +76,19 @@ export function apfelText(modelOrConfig?: ApfelModel | ApfelConfig, maybeConfig:
|
|
|
75
76
|
dangerouslyAllowBrowser: true
|
|
76
77
|
}) as OpenAICompatibleChatAdapter<ApfelModel, ApfelTextModelOptions>
|
|
77
78
|
|
|
79
|
+
// apfel answers each request from a new session holding its messages, so
|
|
80
|
+
// the whole prompt is read every time, and nothing is cached (apfel reports
|
|
81
|
+
// no count): cachedTokens 0, unless apfel counts some itself.
|
|
82
|
+
type Usage = (usage: { prompt_tokens_details?: { cached_tokens?: number } | null } | null | undefined, source?: unknown) => TokenUsage | undefined
|
|
83
|
+
const hooks = adapter as unknown as { chatCompletionsTokenUsage: Usage }
|
|
84
|
+
const convert = hooks.chatCompletionsTokenUsage.bind(adapter)
|
|
85
|
+
hooks.chatCompletionsTokenUsage = (usage, source) => {
|
|
86
|
+
const result = convert(usage, source)
|
|
87
|
+
if (!result) return result
|
|
88
|
+
const cachedTokens = usage?.prompt_tokens_details?.cached_tokens ?? 0
|
|
89
|
+
return { ...result, promptTokensDetails: { ...result.promptTokensDetails, cachedTokens } }
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
const availability = async (signal?: AbortSignal): Promise<Availability> => {
|
|
79
93
|
try {
|
|
80
94
|
const timeout = AbortSignal.timeout(10_000)
|
package/dist/guided-schema.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool schemas in the subset of JSON Schema that apfel's guided generation
|
|
3
|
-
* enforces, for the tool step's arguments (see adapter.ts, `stepSchema`).
|
|
4
|
-
* apfel rejects the keywords it can't enforce with HTTP 400.
|
|
5
|
-
*/
|
|
6
|
-
type JsonSchema = Record<string, unknown>;
|
|
7
|
-
/**
|
|
8
|
-
* The schema in the subset guided generation enforces: types, properties,
|
|
9
|
-
* required, items, enum, const and descriptions. Local `$ref`s are inlined,
|
|
10
|
-
* `anyOf`/`oneOf`/type unions become their first non-null choice, and
|
|
11
|
-
* constraints it can't enforce (lengths, patterns, formats…) are dropped.
|
|
12
|
-
*/
|
|
13
|
-
export declare function toGuidedSchema(schema: unknown, root?: JsonSchema, depth?: number): JsonSchema;
|
|
14
|
-
/**
|
|
15
|
-
* Arguments written under `toGuidedSchema`, without the nulls it allows in
|
|
16
|
-
* optional fields: a null is a field the model left out.
|
|
17
|
-
*/
|
|
18
|
-
export declare function withoutNulls(value: unknown, schema: JsonSchema): unknown;
|
|
19
|
-
export {};
|
|
20
|
-
//# sourceMappingURL=guided-schema.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guided-schema.d.ts","sourceRoot":"","sources":["../src/guided-schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAoBzC;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,IAAI,GAAE,UAA2C,EACjD,KAAK,SAAI,GACR,UAAU,CA4EZ;AA2BD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAuBxE"}
|
package/dist/guided-schema.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool schemas in the subset of JSON Schema that apfel's guided generation
|
|
3
|
-
* enforces, for the tool step's arguments (see adapter.ts, `stepSchema`).
|
|
4
|
-
* apfel rejects the keywords it can't enforce with HTTP 400.
|
|
5
|
-
*/
|
|
6
|
-
/** Keywords guided generation can enforce; apfel rejects others with 400. */
|
|
7
|
-
const KEPT = ["type", "description", "enum", "const"];
|
|
8
|
-
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
9
|
-
/** The schema a local `$ref` (`#/$defs/X`, `#/definitions/X`) points to. */
|
|
10
|
-
function resolveRef(ref, root) {
|
|
11
|
-
if (!ref.startsWith("#/"))
|
|
12
|
-
return undefined;
|
|
13
|
-
let node = root;
|
|
14
|
-
for (const key of ref.slice(2).split("/")) {
|
|
15
|
-
node = isObject(node)
|
|
16
|
-
? node[key.replace(/~1/g, "/").replace(/~0/g, "~")]
|
|
17
|
-
: undefined;
|
|
18
|
-
}
|
|
19
|
-
return isObject(node) ? node : undefined;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* The schema in the subset guided generation enforces: types, properties,
|
|
23
|
-
* required, items, enum, const and descriptions. Local `$ref`s are inlined,
|
|
24
|
-
* `anyOf`/`oneOf`/type unions become their first non-null choice, and
|
|
25
|
-
* constraints it can't enforce (lengths, patterns, formats…) are dropped.
|
|
26
|
-
*/
|
|
27
|
-
export function toGuidedSchema(schema, root = isObject(schema) ? schema : {}, depth = 0) {
|
|
28
|
-
if (!isObject(schema) || depth > 12)
|
|
29
|
-
return { type: "string" };
|
|
30
|
-
if (typeof schema.$ref === "string") {
|
|
31
|
-
const target = resolveRef(schema.$ref, root);
|
|
32
|
-
return target
|
|
33
|
-
? toGuidedSchema({ ...target, ...withoutKey(schema, "$ref") }, root, depth + 1)
|
|
34
|
-
: { type: "string" };
|
|
35
|
-
}
|
|
36
|
-
const choices = (schema.anyOf ?? schema.oneOf);
|
|
37
|
-
if (Array.isArray(choices)) {
|
|
38
|
-
const choice = choices.find((c) => isObject(c) && c.type !== "null") ?? choices[0];
|
|
39
|
-
return toGuidedSchema({
|
|
40
|
-
...(isObject(choice) ? choice : {}),
|
|
41
|
-
...(schema.description !== undefined && {
|
|
42
|
-
description: schema.description
|
|
43
|
-
})
|
|
44
|
-
}, root, depth + 1);
|
|
45
|
-
}
|
|
46
|
-
if (Array.isArray(schema.allOf)) {
|
|
47
|
-
const merged = schema.allOf.reduce((all, part) => (isObject(part) ? mergeSchemas(all, part) : all), withoutKey(schema, "allOf"));
|
|
48
|
-
return toGuidedSchema(merged, root, depth + 1);
|
|
49
|
-
}
|
|
50
|
-
const out = {};
|
|
51
|
-
for (const key of KEPT) {
|
|
52
|
-
if (schema[key] !== undefined)
|
|
53
|
-
out[key] = schema[key];
|
|
54
|
-
}
|
|
55
|
-
if (Array.isArray(out.type)) {
|
|
56
|
-
out.type = out.type.find((t) => t !== "null") ?? "string";
|
|
57
|
-
}
|
|
58
|
-
if (out.type === undefined) {
|
|
59
|
-
if (isObject(schema.properties))
|
|
60
|
-
out.type = "object";
|
|
61
|
-
else if (schema.items !== undefined)
|
|
62
|
-
out.type = "array";
|
|
63
|
-
else if (Array.isArray(out.enum))
|
|
64
|
-
out.type = typeof out.enum[0];
|
|
65
|
-
else if (out.const === undefined)
|
|
66
|
-
out.type = "string";
|
|
67
|
-
}
|
|
68
|
-
if (out.type === "object") {
|
|
69
|
-
const properties = isObject(schema.properties) ? schema.properties : {};
|
|
70
|
-
out.properties = Object.fromEntries(Object.entries(properties).map(([key, value]) => [
|
|
71
|
-
key,
|
|
72
|
-
toGuidedSchema(value, root, depth + 1)
|
|
73
|
-
]));
|
|
74
|
-
out.required = Array.isArray(schema.required)
|
|
75
|
-
? schema.required.filter((key) => typeof key === "string" && key in properties)
|
|
76
|
-
: [];
|
|
77
|
-
// An optional field can also be null, as in the Prompt API adapter
|
|
78
|
-
// (Gemini Nano wrote 0 for coordinates it didn't have). apfel reads a
|
|
79
|
-
// nullable field as optional, and takes only strings in an enum, so an
|
|
80
|
-
// enum gets no null of its own. `withoutNulls` drops any null written.
|
|
81
|
-
const required = new Set(out.required);
|
|
82
|
-
for (const [key, value] of Object.entries(out.properties)) {
|
|
83
|
-
if (!required.has(key))
|
|
84
|
-
value.type = [value.type, "null"];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (out.type === "array") {
|
|
88
|
-
out.items = toGuidedSchema(schema.items ?? {}, root, depth + 1);
|
|
89
|
-
}
|
|
90
|
-
return out;
|
|
91
|
-
}
|
|
92
|
-
function withoutKey(schema, key) {
|
|
93
|
-
const rest = { ...schema };
|
|
94
|
-
delete rest[key];
|
|
95
|
-
return rest;
|
|
96
|
-
}
|
|
97
|
-
function mergeSchemas(a, b) {
|
|
98
|
-
const merged = { ...a, ...b };
|
|
99
|
-
if (isObject(a.properties) || isObject(b.properties)) {
|
|
100
|
-
merged.properties = {
|
|
101
|
-
...(isObject(a.properties) ? a.properties : {}),
|
|
102
|
-
...(isObject(b.properties) ? b.properties : {})
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
if (Array.isArray(a.required) || Array.isArray(b.required)) {
|
|
106
|
-
merged.required = [
|
|
107
|
-
...new Set([
|
|
108
|
-
...(Array.isArray(a.required) ? a.required : []),
|
|
109
|
-
...(Array.isArray(b.required) ? b.required : [])
|
|
110
|
-
])
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
return merged;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Arguments written under `toGuidedSchema`, without the nulls it allows in
|
|
117
|
-
* optional fields: a null is a field the model left out.
|
|
118
|
-
*/
|
|
119
|
-
export function withoutNulls(value, schema) {
|
|
120
|
-
if (Array.isArray(value)) {
|
|
121
|
-
const items = isObject(schema.items) ? schema.items : {};
|
|
122
|
-
return value.map((item) => withoutNulls(item, items));
|
|
123
|
-
}
|
|
124
|
-
if (!isObject(value))
|
|
125
|
-
return value;
|
|
126
|
-
const properties = isObject(schema.properties) ? schema.properties : {};
|
|
127
|
-
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
128
|
-
return Object.fromEntries(Object.entries(value).flatMap(([key, field]) => field === null && !required.includes(key)
|
|
129
|
-
? []
|
|
130
|
-
: [
|
|
131
|
-
[
|
|
132
|
-
key,
|
|
133
|
-
withoutNulls(field, isObject(properties[key]) ? properties[key] : {})
|
|
134
|
-
]
|
|
135
|
-
]));
|
|
136
|
-
}
|
package/dist/tool-calls.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool calls Apple's model writes as text. apfel turns the model's tool
|
|
3
|
-
* calls into OpenAI `tool_calls`, but when it doesn't recognize one it
|
|
4
|
-
* passes it on as text instead, like
|
|
5
|
-
* `{"tool_calls": [{"function": "get_weather", "arguments": {…}}]}`.
|
|
6
|
-
*/
|
|
7
|
-
export type ParsedToolCall = {
|
|
8
|
-
name: string;
|
|
9
|
-
args: Record<string, unknown>;
|
|
10
|
-
};
|
|
11
|
-
/** Where Apple's model starts a tool call it writes as text. */
|
|
12
|
-
export declare const JSON_TOOL_CALLS_MARKER = "{\"tool_calls\"";
|
|
13
|
-
/**
|
|
14
|
-
* Tool calls Apple's model wrote as JSON text. Its JSON varies and is often
|
|
15
|
-
* broken (`"function": "name"`, `"name: "name"`, stray strings in the
|
|
16
|
-
* arguments), so instead of parsing the whole thing, this finds each quoted
|
|
17
|
-
* name of a tool that exists and reads the `"arguments"` object after it.
|
|
18
|
-
*/
|
|
19
|
-
export declare function parseJsonToolCalls(text: string, toolNames: Iterable<string>): Array<ParsedToolCall>;
|
|
20
|
-
/**
|
|
21
|
-
* Splits a streaming answer into the text before the first tool call written
|
|
22
|
-
* as text (passed through as it arrives) and the tool-call JSON after it
|
|
23
|
-
* (held back and parsed once the reply ends).
|
|
24
|
-
*/
|
|
25
|
-
export declare function createToolCallSplitter(marker?: string): {
|
|
26
|
-
push: (text: string) => string;
|
|
27
|
-
flush: () => {
|
|
28
|
-
text: string;
|
|
29
|
-
markup: string | null;
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
//# sourceMappingURL=tool-calls.d.ts.map
|
package/dist/tool-calls.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tool-calls.d.ts","sourceRoot":"","sources":["../src/tool-calls.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAE5E,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,oBAAkB,CAAA;AAkCrD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC1B,KAAK,CAAC,cAAc,CAAC,CAiCvB;AAUD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,SAAyB;iBAKhD,MAAM;;;;;EA2B3B"}
|
package/dist/tool-calls.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool calls Apple's model writes as text. apfel turns the model's tool
|
|
3
|
-
* calls into OpenAI `tool_calls`, but when it doesn't recognize one it
|
|
4
|
-
* passes it on as text instead, like
|
|
5
|
-
* `{"tool_calls": [{"function": "get_weather", "arguments": {…}}]}`.
|
|
6
|
-
*/
|
|
7
|
-
/** Where Apple's model starts a tool call it writes as text. */
|
|
8
|
-
export const JSON_TOOL_CALLS_MARKER = '{"tool_calls"';
|
|
9
|
-
/** The `{…}` that starts at `text[start]`, braces in strings ignored. */
|
|
10
|
-
function balancedObject(text, start) {
|
|
11
|
-
let depth = 0;
|
|
12
|
-
let inString = false;
|
|
13
|
-
for (let i = start; i < text.length; i++) {
|
|
14
|
-
const c = text[i];
|
|
15
|
-
if (inString) {
|
|
16
|
-
if (c === "\\")
|
|
17
|
-
i++;
|
|
18
|
-
else if (c === '"')
|
|
19
|
-
inString = false;
|
|
20
|
-
}
|
|
21
|
-
else if (c === '"')
|
|
22
|
-
inString = true;
|
|
23
|
-
else if (c === "{")
|
|
24
|
-
depth++;
|
|
25
|
-
else if (c === "}" && --depth === 0)
|
|
26
|
-
return text.slice(start, i + 1);
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
/** JSON arguments, read leniently: stray strings without a key are dropped. */
|
|
31
|
-
function parseJsonArgs(raw) {
|
|
32
|
-
for (const candidate of [raw, raw.replace(/,\s*"[^"]*"\s*(?=[,}])/g, "")]) {
|
|
33
|
-
try {
|
|
34
|
-
const value = JSON.parse(candidate);
|
|
35
|
-
const args = typeof value === "string" ? JSON.parse(value) : value;
|
|
36
|
-
if (args && typeof args === "object") {
|
|
37
|
-
return args;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// Try the next reading.
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Tool calls Apple's model wrote as JSON text. Its JSON varies and is often
|
|
48
|
-
* broken (`"function": "name"`, `"name: "name"`, stray strings in the
|
|
49
|
-
* arguments), so instead of parsing the whole thing, this finds each quoted
|
|
50
|
-
* name of a tool that exists and reads the `"arguments"` object after it.
|
|
51
|
-
*/
|
|
52
|
-
export function parseJsonToolCalls(text, toolNames) {
|
|
53
|
-
const mentions = [];
|
|
54
|
-
for (const name of toolNames) {
|
|
55
|
-
const quoted = `"${name}"`;
|
|
56
|
-
for (let at = text.indexOf(quoted); at >= 0; at = text.indexOf(quoted, at + 1)) {
|
|
57
|
-
mentions.push({ name, at });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
mentions.sort((a, b) => a.at - b.at);
|
|
61
|
-
const calls = [];
|
|
62
|
-
mentions.forEach(({ name, at }, i) => {
|
|
63
|
-
// The arguments belong to the last mention of a name before them. The
|
|
64
|
-
// model sometimes writes a broken "arguments" and then a good one: the
|
|
65
|
-
// first that reads is used.
|
|
66
|
-
const part = text.slice(at, mentions[i + 1]?.at ?? text.length);
|
|
67
|
-
for (const key of part.matchAll(/"arguments"?\s*:\s*/g)) {
|
|
68
|
-
const from = key.index + key[0].length;
|
|
69
|
-
const raw = part[from] === "{"
|
|
70
|
-
? balancedObject(part, from)
|
|
71
|
-
: /^"(?:[^"\\]|\\.)*"/.exec(part.slice(from))?.[0];
|
|
72
|
-
const args = raw ? parseJsonArgs(raw) : null;
|
|
73
|
-
if (args) {
|
|
74
|
-
calls.push({ name, args });
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
return calls;
|
|
80
|
-
}
|
|
81
|
-
/** Length of the longest end of `text` that could start `marker`. */
|
|
82
|
-
function partialMarker(text, marker) {
|
|
83
|
-
for (let n = Math.min(text.length, marker.length); n > 0; n--) {
|
|
84
|
-
if (marker.startsWith(text.slice(-n)))
|
|
85
|
-
return n;
|
|
86
|
-
}
|
|
87
|
-
return 0;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Splits a streaming answer into the text before the first tool call written
|
|
91
|
-
* as text (passed through as it arrives) and the tool-call JSON after it
|
|
92
|
-
* (held back and parsed once the reply ends).
|
|
93
|
-
*/
|
|
94
|
-
export function createToolCallSplitter(marker = JSON_TOOL_CALLS_MARKER) {
|
|
95
|
-
let held = "";
|
|
96
|
-
let markup = null;
|
|
97
|
-
/** Text that can be shown now. */
|
|
98
|
-
function push(text) {
|
|
99
|
-
if (markup !== null) {
|
|
100
|
-
markup += text;
|
|
101
|
-
return "";
|
|
102
|
-
}
|
|
103
|
-
held += text;
|
|
104
|
-
const at = held.indexOf(marker);
|
|
105
|
-
if (at >= 0) {
|
|
106
|
-
const before = held.slice(0, at);
|
|
107
|
-
markup = held.slice(at);
|
|
108
|
-
held = "";
|
|
109
|
-
return before;
|
|
110
|
-
}
|
|
111
|
-
const keep = partialMarker(held, marker);
|
|
112
|
-
const out = held.slice(0, held.length - keep);
|
|
113
|
-
held = held.slice(out.length);
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
/** The rest of the text, and the tool-call JSON if any. */
|
|
117
|
-
function flush() {
|
|
118
|
-
const rest = held;
|
|
119
|
-
held = "";
|
|
120
|
-
return { text: rest, markup };
|
|
121
|
-
}
|
|
122
|
-
return { push, flush };
|
|
123
|
-
}
|
package/dist/tool-repair.d.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool-call repair. Apple's model picks the right tool but often gets its
|
|
3
|
-
* arguments wrong: it renames fields (`limit` for `numResults`, `site` for
|
|
4
|
-
* `includeDomains`) and drops optional ones it was asked to set. So after the
|
|
5
|
-
* model calls a tool, its arguments are written again with apfel's guided
|
|
6
|
-
* generation, which holds the model to the tool's JSON Schema:
|
|
7
|
-
*
|
|
8
|
-
* 1. For each optional field, a yes/no answer: does the request give it a
|
|
9
|
-
* value? (Asked for the arguments directly, the model leaves out optional
|
|
10
|
-
* numbers and arrays even when the request states them.)
|
|
11
|
-
* 2. The arguments, with the fields from step 1 required and the rest left
|
|
12
|
-
* out. Fields the model already got right (schema name, right type) are
|
|
13
|
-
* kept as it wrote them.
|
|
14
|
-
*
|
|
15
|
-
* Works from the schema alone, for any tool, MCP tools included. Through
|
|
16
|
-
* `chat()` with search, calendar, file and weather tools, the calls came out
|
|
17
|
-
* right 27 times in 32 with this, against 18 without; the misses left are
|
|
18
|
-
* the model not calling a tool at all, which this can't fix.
|
|
19
|
-
*/
|
|
20
|
-
type JsonSchema = Record<string, unknown>;
|
|
21
|
-
/**
|
|
22
|
-
* The schema in the subset guided generation enforces: types, properties,
|
|
23
|
-
* required, items, enum, const and descriptions. Local `$ref`s are inlined,
|
|
24
|
-
* `anyOf`/`oneOf`/type unions become their first non-null choice, and
|
|
25
|
-
* constraints it can't enforce (lengths, patterns, formats…) are dropped.
|
|
26
|
-
*/
|
|
27
|
-
export declare function toGuidedSchema(schema: unknown, root?: JsonSchema, depth?: number): JsonSchema;
|
|
28
|
-
/**
|
|
29
|
-
* The name of the tool the model meant, when it got the name slightly wrong
|
|
30
|
-
* (case, `-` for `_`…). Undefined if no tool matches.
|
|
31
|
-
*/
|
|
32
|
-
export declare function matchToolName(name: string, toolNames: Iterable<string>): string | undefined;
|
|
33
|
-
/** A chat completion request apfel answers with JSON matching `schema`. */
|
|
34
|
-
export type GuidedRequest = {
|
|
35
|
-
/** Instructions, sent before the conversation. */
|
|
36
|
-
instructions: string;
|
|
37
|
-
schema: JsonSchema;
|
|
38
|
-
name: string;
|
|
39
|
-
};
|
|
40
|
-
/** Sends a guided request; resolves to the model's JSON, parsed. */
|
|
41
|
-
export type GuidedComplete = (request: GuidedRequest) => Promise<unknown>;
|
|
42
|
-
export type RepairableTool = {
|
|
43
|
-
name: string;
|
|
44
|
-
description: string;
|
|
45
|
-
inputSchema?: unknown;
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* The tool call's arguments, written again under the tool's schema.
|
|
49
|
-
* Undefined if the tool takes no arguments or its schema isn't an object
|
|
50
|
-
* (the model's own arguments are used then).
|
|
51
|
-
*/
|
|
52
|
-
export declare function repairToolArguments(tool: RepairableTool, complete: GuidedComplete,
|
|
53
|
-
/** The model's own arguments, which may use the wrong field names. */
|
|
54
|
-
draft?: string): Promise<Record<string, unknown> | undefined>;
|
|
55
|
-
export {};
|
|
56
|
-
//# sourceMappingURL=tool-repair.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tool-repair.d.ts","sourceRoot":"","sources":["../src/tool-repair.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAoBzC;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,IAAI,GAAE,UAA2C,EACjD,KAAK,SAAI,GACR,UAAU,CAkEZ;AA4FD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,sBAMtE;AAED,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GAAG;IAC1B,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,oEAAoE;AACpE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEzE,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,cAAc;AACxB,sEAAsE;AACtE,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAyE9C"}
|
package/dist/tool-repair.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool-call repair. Apple's model picks the right tool but often gets its
|
|
3
|
-
* arguments wrong: it renames fields (`limit` for `numResults`, `site` for
|
|
4
|
-
* `includeDomains`) and drops optional ones it was asked to set. So after the
|
|
5
|
-
* model calls a tool, its arguments are written again with apfel's guided
|
|
6
|
-
* generation, which holds the model to the tool's JSON Schema:
|
|
7
|
-
*
|
|
8
|
-
* 1. For each optional field, a yes/no answer: does the request give it a
|
|
9
|
-
* value? (Asked for the arguments directly, the model leaves out optional
|
|
10
|
-
* numbers and arrays even when the request states them.)
|
|
11
|
-
* 2. The arguments, with the fields from step 1 required and the rest left
|
|
12
|
-
* out. Fields the model already got right (schema name, right type) are
|
|
13
|
-
* kept as it wrote them.
|
|
14
|
-
*
|
|
15
|
-
* Works from the schema alone, for any tool, MCP tools included. Through
|
|
16
|
-
* `chat()` with search, calendar, file and weather tools, the calls came out
|
|
17
|
-
* right 27 times in 32 with this, against 18 without; the misses left are
|
|
18
|
-
* the model not calling a tool at all, which this can't fix.
|
|
19
|
-
*/
|
|
20
|
-
/** Keywords guided generation can enforce; apfel rejects others with 400. */
|
|
21
|
-
const KEPT = ["type", "description", "enum", "const"];
|
|
22
|
-
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
23
|
-
/** The schema a local `$ref` (`#/$defs/X`, `#/definitions/X`) points to. */
|
|
24
|
-
function resolveRef(ref, root) {
|
|
25
|
-
if (!ref.startsWith("#/"))
|
|
26
|
-
return undefined;
|
|
27
|
-
let node = root;
|
|
28
|
-
for (const key of ref.slice(2).split("/")) {
|
|
29
|
-
node = isObject(node)
|
|
30
|
-
? node[key.replace(/~1/g, "/").replace(/~0/g, "~")]
|
|
31
|
-
: undefined;
|
|
32
|
-
}
|
|
33
|
-
return isObject(node) ? node : undefined;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* The schema in the subset guided generation enforces: types, properties,
|
|
37
|
-
* required, items, enum, const and descriptions. Local `$ref`s are inlined,
|
|
38
|
-
* `anyOf`/`oneOf`/type unions become their first non-null choice, and
|
|
39
|
-
* constraints it can't enforce (lengths, patterns, formats…) are dropped.
|
|
40
|
-
*/
|
|
41
|
-
export function toGuidedSchema(schema, root = isObject(schema) ? schema : {}, depth = 0) {
|
|
42
|
-
if (!isObject(schema) || depth > 12)
|
|
43
|
-
return { type: "string" };
|
|
44
|
-
if (typeof schema.$ref === "string") {
|
|
45
|
-
const target = resolveRef(schema.$ref, root);
|
|
46
|
-
return target
|
|
47
|
-
? toGuidedSchema({ ...target, ...withoutKey(schema, "$ref") }, root, depth + 1)
|
|
48
|
-
: { type: "string" };
|
|
49
|
-
}
|
|
50
|
-
const choices = (schema.anyOf ?? schema.oneOf);
|
|
51
|
-
if (Array.isArray(choices)) {
|
|
52
|
-
const choice = choices.find((c) => isObject(c) && c.type !== "null") ?? choices[0];
|
|
53
|
-
return toGuidedSchema({
|
|
54
|
-
...(isObject(choice) ? choice : {}),
|
|
55
|
-
...(schema.description !== undefined && {
|
|
56
|
-
description: schema.description
|
|
57
|
-
})
|
|
58
|
-
}, root, depth + 1);
|
|
59
|
-
}
|
|
60
|
-
if (Array.isArray(schema.allOf)) {
|
|
61
|
-
const merged = schema.allOf.reduce((all, part) => (isObject(part) ? mergeSchemas(all, part) : all), withoutKey(schema, "allOf"));
|
|
62
|
-
return toGuidedSchema(merged, root, depth + 1);
|
|
63
|
-
}
|
|
64
|
-
const out = {};
|
|
65
|
-
for (const key of KEPT) {
|
|
66
|
-
if (schema[key] !== undefined)
|
|
67
|
-
out[key] = schema[key];
|
|
68
|
-
}
|
|
69
|
-
if (Array.isArray(out.type)) {
|
|
70
|
-
out.type = out.type.find((t) => t !== "null") ?? "string";
|
|
71
|
-
}
|
|
72
|
-
if (out.type === undefined) {
|
|
73
|
-
if (isObject(schema.properties))
|
|
74
|
-
out.type = "object";
|
|
75
|
-
else if (schema.items !== undefined)
|
|
76
|
-
out.type = "array";
|
|
77
|
-
else if (Array.isArray(out.enum))
|
|
78
|
-
out.type = typeof out.enum[0];
|
|
79
|
-
else if (out.const === undefined)
|
|
80
|
-
out.type = "string";
|
|
81
|
-
}
|
|
82
|
-
if (out.type === "object") {
|
|
83
|
-
const properties = isObject(schema.properties) ? schema.properties : {};
|
|
84
|
-
out.properties = Object.fromEntries(Object.entries(properties).map(([key, value]) => [
|
|
85
|
-
key,
|
|
86
|
-
toGuidedSchema(value, root, depth + 1)
|
|
87
|
-
]));
|
|
88
|
-
out.required = Array.isArray(schema.required)
|
|
89
|
-
? schema.required.filter((key) => typeof key === "string" && key in properties)
|
|
90
|
-
: [];
|
|
91
|
-
}
|
|
92
|
-
if (out.type === "array") {
|
|
93
|
-
out.items = toGuidedSchema(schema.items ?? {}, root, depth + 1);
|
|
94
|
-
}
|
|
95
|
-
return out;
|
|
96
|
-
}
|
|
97
|
-
function withoutKey(schema, key) {
|
|
98
|
-
const rest = { ...schema };
|
|
99
|
-
delete rest[key];
|
|
100
|
-
return rest;
|
|
101
|
-
}
|
|
102
|
-
function mergeSchemas(a, b) {
|
|
103
|
-
const merged = { ...a, ...b };
|
|
104
|
-
if (isObject(a.properties) || isObject(b.properties)) {
|
|
105
|
-
merged.properties = {
|
|
106
|
-
...(isObject(a.properties) ? a.properties : {}),
|
|
107
|
-
...(isObject(b.properties) ? b.properties : {})
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
if (Array.isArray(a.required) || Array.isArray(b.required)) {
|
|
111
|
-
merged.required = [
|
|
112
|
-
...new Set([
|
|
113
|
-
...(Array.isArray(a.required) ? a.required : []),
|
|
114
|
-
...(Array.isArray(b.required) ? b.required : [])
|
|
115
|
-
])
|
|
116
|
-
];
|
|
117
|
-
}
|
|
118
|
-
return merged;
|
|
119
|
-
}
|
|
120
|
-
/** Whether `value` fits a guided schema's type and choices. */
|
|
121
|
-
function fits(value, schema) {
|
|
122
|
-
if (Array.isArray(schema.enum) && !schema.enum.includes(value))
|
|
123
|
-
return false;
|
|
124
|
-
if (schema.const !== undefined)
|
|
125
|
-
return value === schema.const;
|
|
126
|
-
switch (schema.type) {
|
|
127
|
-
case "string":
|
|
128
|
-
return typeof value === "string";
|
|
129
|
-
case "integer":
|
|
130
|
-
return Number.isInteger(value);
|
|
131
|
-
case "number":
|
|
132
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
133
|
-
case "boolean":
|
|
134
|
-
return typeof value === "boolean";
|
|
135
|
-
case "array":
|
|
136
|
-
return (Array.isArray(value) &&
|
|
137
|
-
value.every((item) => fits(item, (schema.items ?? {}))));
|
|
138
|
-
case "object":
|
|
139
|
-
return isObject(value);
|
|
140
|
-
default:
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* The fields of the model's own arguments that already have a schema's
|
|
146
|
-
* name and a value of the right type. They're kept as they are: with several
|
|
147
|
-
* calls to one tool in a reply, they're what tells the calls apart.
|
|
148
|
-
*/
|
|
149
|
-
function validDraftFields(draft, properties) {
|
|
150
|
-
let parsed;
|
|
151
|
-
try {
|
|
152
|
-
parsed = draft ? JSON.parse(draft) : undefined;
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return {};
|
|
156
|
-
}
|
|
157
|
-
if (!isObject(parsed))
|
|
158
|
-
return {};
|
|
159
|
-
return Object.fromEntries(Object.entries(parsed).filter(([key, value]) => Object.hasOwn(properties, key) &&
|
|
160
|
-
value !== null &&
|
|
161
|
-
fits(value, properties[key])));
|
|
162
|
-
}
|
|
163
|
-
/** One line per field: name, type or choices, and description. */
|
|
164
|
-
function describeFields(properties) {
|
|
165
|
-
return Object.entries(properties)
|
|
166
|
-
.map(([key, field]) => {
|
|
167
|
-
const kind = Array.isArray(field.enum)
|
|
168
|
-
? field.enum.map((v) => JSON.stringify(v)).join(" | ")
|
|
169
|
-
: String(field.type ?? "any");
|
|
170
|
-
const about = typeof field.description === "string" ? `: ${field.description}` : "";
|
|
171
|
-
return `- ${key} (${kind})${about}`;
|
|
172
|
-
})
|
|
173
|
-
.join("\n");
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* The name of the tool the model meant, when it got the name slightly wrong
|
|
177
|
-
* (case, `-` for `_`…). Undefined if no tool matches.
|
|
178
|
-
*/
|
|
179
|
-
export function matchToolName(name, toolNames) {
|
|
180
|
-
const names = [...toolNames];
|
|
181
|
-
if (names.includes(name))
|
|
182
|
-
return name;
|
|
183
|
-
const plain = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
184
|
-
const matches = names.filter((n) => plain(n) === plain(name));
|
|
185
|
-
return matches.length === 1 ? matches[0] : undefined;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* The tool call's arguments, written again under the tool's schema.
|
|
189
|
-
* Undefined if the tool takes no arguments or its schema isn't an object
|
|
190
|
-
* (the model's own arguments are used then).
|
|
191
|
-
*/
|
|
192
|
-
export async function repairToolArguments(tool, complete,
|
|
193
|
-
/** The model's own arguments, which may use the wrong field names. */
|
|
194
|
-
draft) {
|
|
195
|
-
const schema = toGuidedSchema(tool.inputSchema);
|
|
196
|
-
const properties = (schema.properties ?? {});
|
|
197
|
-
if (schema.type !== "object" || Object.keys(properties).length === 0) {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
const required = schema.required;
|
|
201
|
-
const optional = Object.keys(properties).filter((key) => !required.includes(key));
|
|
202
|
-
const about = `the tool ${tool.name}${tool.description ? ` (${tool.description})` : ""}`;
|
|
203
|
-
// With several calls in one reply, the draft says which one this is.
|
|
204
|
-
const attempt = draft && draft !== "{}"
|
|
205
|
-
? `\nThe assistant's first attempt at this call, which may use wrong field names: ${draft.slice(0, 1000)}`
|
|
206
|
-
: "";
|
|
207
|
-
let chosen = [];
|
|
208
|
-
if (optional.length > 0) {
|
|
209
|
-
const answer = await complete({
|
|
210
|
-
name: "fields",
|
|
211
|
-
instructions: `You are deciding the arguments of a call to ${about}, for the user's latest request. Its fields:\n${describeFields(properties)}\nFor each optional field, answer true only if the conversation states a value for it, otherwise false.`,
|
|
212
|
-
schema: {
|
|
213
|
-
type: "object",
|
|
214
|
-
properties: Object.fromEntries(optional.map((key) => [
|
|
215
|
-
key,
|
|
216
|
-
{
|
|
217
|
-
type: "boolean",
|
|
218
|
-
description: `Does the request state a value for ${key}?`
|
|
219
|
-
}
|
|
220
|
-
])),
|
|
221
|
-
required: optional
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
if (isObject(answer))
|
|
225
|
-
chosen = optional.filter((key) => answer[key] === true);
|
|
226
|
-
}
|
|
227
|
-
const kept = validDraftFields(draft, properties);
|
|
228
|
-
const fields = [
|
|
229
|
-
...new Set([
|
|
230
|
-
...required,
|
|
231
|
-
...chosen,
|
|
232
|
-
...optional.filter((key) => key in kept)
|
|
233
|
-
])
|
|
234
|
-
];
|
|
235
|
-
const args = await complete({
|
|
236
|
-
name: tool.name,
|
|
237
|
-
instructions: `Write the arguments for a call to ${about} that does what the user asks in their latest request.${attempt}`,
|
|
238
|
-
schema: {
|
|
239
|
-
type: "object",
|
|
240
|
-
properties: Object.fromEntries(fields.map((key) => [key, properties[key]])),
|
|
241
|
-
required: fields
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
if (!isObject(args))
|
|
245
|
-
return undefined;
|
|
246
|
-
// Optional fields left empty weren't really set.
|
|
247
|
-
const written = Object.fromEntries(Object.entries(args).filter(([key, value]) => required.includes(key) ||
|
|
248
|
-
!(value === "" ||
|
|
249
|
-
value === null ||
|
|
250
|
-
(Array.isArray(value) && value.length === 0))));
|
|
251
|
-
return { ...written, ...kept };
|
|
252
|
-
}
|
package/dist/tools.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool calls by prompt, a copy of the Prompt API adapter's
|
|
3
|
-
* (`@tanstack-ai-browser/prompt-api`'s `tools.ts`): keep the two
|
|
4
|
-
* identical. Apple's model writes malformed
|
|
5
|
-
* native tool calls that apfel can't parse, so the system prompt lists the
|
|
6
|
-
* tools as JSON and asks for a call as JSON in a ```tool_call fence; results
|
|
7
|
-
* go back in a ```tool_result fence. The adapter reads calls out of the
|
|
8
|
-
* streamed text.
|
|
9
|
-
*/
|
|
10
|
-
import type { ModelMessage, Tool } from "@tanstack/ai";
|
|
11
|
-
/** A tool call read from the model's text. */
|
|
12
|
-
export type ParsedToolCall = {
|
|
13
|
-
name: string;
|
|
14
|
-
arguments: Record<string, unknown>;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* The system prompt with the tools and how to call them; unchanged without
|
|
18
|
-
* tools.
|
|
19
|
-
*/
|
|
20
|
-
export declare function toolSystemPrompt(system: string, tools: ReadonlyArray<Tool>): string;
|
|
21
|
-
/** Earlier tool calls in the conversation, as the model would have written them. */
|
|
22
|
-
export declare function toolCallFence(calls: NonNullable<ModelMessage["toolCalls"]>): string;
|
|
23
|
-
/** A tool's result line for a ```tool_result fence. */
|
|
24
|
-
export declare function toolResultLine(id: string, name: string, content: string, error?: string): string;
|
|
25
|
-
/** Tool results as the model reads them. */
|
|
26
|
-
export declare const toolResultFence: (lines: ReadonlyArray<string>) => string;
|
|
27
|
-
/**
|
|
28
|
-
* The first tool call in a fence's body: a JSON object (or an array of them,
|
|
29
|
-
* or one per line) with `name` (or `tool`, `tool_name`) and `arguments` (or
|
|
30
|
-
* `parameters`), the arguments possibly a JSON string, maybe wrapped in a
|
|
31
|
-
* `tool_call` or `function` key; or a Python-style call, `[name(key="v")]`;
|
|
32
|
-
* or `call:name{key:value}`.
|
|
33
|
-
*/
|
|
34
|
-
export declare function parseToolCall(body: string): ParsedToolCall | undefined;
|
|
35
|
-
/**
|
|
36
|
-
* Reads a streamed reply: `push` each piece and get back the text that can
|
|
37
|
-
* be shown now; text inside a ```tool_call fence (or another call's
|
|
38
|
-
* delimiters: `<tool_call>`, `<|tool_call>`, `<|tool_call_start|>`, or a
|
|
39
|
-
* Python-style `[name(…)]`) is held back. The first
|
|
40
|
-
* fence that closes ends the reply (stop generating then): `fence` is its
|
|
41
|
-
* text, `call` the call in it, if it holds one (else the fence is a failed
|
|
42
|
-
* call, for `repairToolCall`), and `trailing` the text after it. Other code
|
|
43
|
-
* fences are text. `end` gives the rest, reading an unclosed fence as the
|
|
44
|
-
* reply's last.
|
|
45
|
-
*/
|
|
46
|
-
export declare function toolCallReader(): {
|
|
47
|
-
readonly call: ParsedToolCall | undefined;
|
|
48
|
-
readonly fence: string | undefined;
|
|
49
|
-
/** Text that came after the fence: shown after the call. */
|
|
50
|
-
readonly trailing: string;
|
|
51
|
-
push(piece: string): string;
|
|
52
|
-
end(): string;
|
|
53
|
-
};
|
|
54
|
-
/** Why a call doesn't fit the tools (an unknown tool, or arguments that fail its schema), if it doesn't. */
|
|
55
|
-
export declare function invalidToolCall(call: ParsedToolCall, tools: ReadonlyArray<Tool>): string | undefined;
|
|
56
|
-
/** Why a reply's ```tool_call fence isn't a valid call, if it isn't. */
|
|
57
|
-
export declare const toolCallError: (call: ParsedToolCall | undefined, tools: ReadonlyArray<Tool>) => string | undefined;
|
|
58
|
-
/** The message after a failed call, asking for it again. */
|
|
59
|
-
export declare const repairPrompt: (reason: string) => string;
|
|
60
|
-
/**
|
|
61
|
-
* One repair of a failed tool call, with output held to a schema (as webgllm
|
|
62
|
-
* repairs): the tool's arguments if `name` is a tool, else first the tool's
|
|
63
|
-
* name, then its arguments. `generate(schema)` asks the model, with the
|
|
64
|
-
* conversation, its failed attempt and `repairPrompt`, for JSON that fits
|
|
65
|
-
* `schema`, and returns it parsed. Undefined if the repair fails too.
|
|
66
|
-
*/
|
|
67
|
-
export declare function repairToolCall(tools: ReadonlyArray<Tool>, name: string | undefined, generate: (schema: Record<string, unknown>) => Promise<unknown>): Promise<ParsedToolCall | undefined>;
|
|
68
|
-
//# sourceMappingURL=tools.d.ts.map
|
package/dist/tools.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAEtD,8CAA8C;AAC9C,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAiBjF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,MAAM,CAiCnF;AAED,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,GAAG,MAAM,CAWnF;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAUhG;AAED,4CAA4C;AAC5C,eAAO,MAAM,eAAe,UAAW,aAAa,CAAC,MAAM,CAAC,WAAqD,CAAA;AAEjH;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CA0CtE;AAyHD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc;aA4CtB,IAAI;aAGJ,KAAK;IAGT,4DAA4D;aACxD,QAAQ;IAGZ,IAAI,QAAQ,MAAM,GAAG,MAAM;IAI3B,GAAG,IAAI,MAAM;EAkBhB;AA2DD,4GAA4G;AAC5G,wBAAgB,eAAe,CAC7B,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GACzB,MAAM,GAAG,SAAS,CAKpB;AAED,wEAAwE;AACxE,eAAO,MAAM,aAAa,SAClB,cAAc,GAAG,SAAS,SACzB,aAAa,CAAC,IAAI,CAAC,uBAIsB,CAAA;AAElD,4DAA4D;AAC5D,eAAO,MAAM,YAAY,WAAY,MAAM,WACgB,CAAA;AAE3D;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,EAC1B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAC9D,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CA2BrC"}
|
package/dist/tools.js
DELETED
|
@@ -1,469 +0,0 @@
|
|
|
1
|
-
const DELIMITERS = [
|
|
2
|
-
{ start: "```tool_call", end: "```" },
|
|
3
|
-
{ start: "```tool-call", end: "```" },
|
|
4
|
-
{ start: "<tool_call>", end: "</tool_call>" },
|
|
5
|
-
{ start: "<|tool_call>", end: "<tool_call|>" },
|
|
6
|
-
{ start: "<|tool_call_start|>", end: "<|tool_call_end|>" }
|
|
7
|
-
];
|
|
8
|
-
/** A Python-style call, `[name(...)]`. */
|
|
9
|
-
const PYTHON_CALL = /\[(\w+)\(/;
|
|
10
|
-
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
11
|
-
/**
|
|
12
|
-
* The system prompt with the tools and how to call them; unchanged without
|
|
13
|
-
* tools.
|
|
14
|
-
*/
|
|
15
|
-
export function toolSystemPrompt(system, tools) {
|
|
16
|
-
if (tools.length === 0)
|
|
17
|
-
return system;
|
|
18
|
-
const definitions = tools.map((tool) => ({
|
|
19
|
-
name: tool.name,
|
|
20
|
-
description: tool.description ?? "No description provided.",
|
|
21
|
-
parameters: tool.inputSchema ?? { type: "object", properties: {} }
|
|
22
|
-
}));
|
|
23
|
-
const instructions = `You are a helpful AI assistant with access to tools.
|
|
24
|
-
|
|
25
|
-
# Available Tools
|
|
26
|
-
${JSON.stringify(definitions, null, 2)}
|
|
27
|
-
|
|
28
|
-
# Tool Calling Instructions
|
|
29
|
-
Only request one tool call at a time. Wait for tool results before asking for another tool.
|
|
30
|
-
|
|
31
|
-
To call a tool, output JSON in this exact format inside a \`\`\`tool_call code fence:
|
|
32
|
-
|
|
33
|
-
\`\`\`tool_call
|
|
34
|
-
{"name": "tool_name", "arguments": {"param1": "value1", "param2": "value2"}}
|
|
35
|
-
\`\`\`
|
|
36
|
-
|
|
37
|
-
Tool responses will be provided in \`\`\`tool_result fences. Each line contains JSON like:
|
|
38
|
-
\`\`\`tool_result
|
|
39
|
-
{"id": "call_123", "name": "tool_name", "result": {...}, "error": false}
|
|
40
|
-
\`\`\`
|
|
41
|
-
Use the \`result\` payload (and treat \`error\` as a boolean flag) when continuing the conversation.
|
|
42
|
-
|
|
43
|
-
Important:
|
|
44
|
-
- Use exact tool and parameter names from the schema above
|
|
45
|
-
- Arguments must be a valid JSON object matching the tool's parameters
|
|
46
|
-
- You can include brief reasoning before or after the tool call
|
|
47
|
-
- If no tool is needed, respond directly without tool_call fences`;
|
|
48
|
-
return system.trim() ? `${system.trim()}\n\n${instructions}` : instructions;
|
|
49
|
-
}
|
|
50
|
-
/** Earlier tool calls in the conversation, as the model would have written them. */
|
|
51
|
-
export function toolCallFence(calls) {
|
|
52
|
-
const lines = calls.map((call) => {
|
|
53
|
-
let args;
|
|
54
|
-
try {
|
|
55
|
-
args = JSON.parse(call.function.arguments || "{}");
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
args = call.function.arguments;
|
|
59
|
-
}
|
|
60
|
-
return JSON.stringify({ name: call.function.name, arguments: args, ...(call.id && { id: call.id }) });
|
|
61
|
-
});
|
|
62
|
-
return `\`\`\`tool_call\n${lines.join("\n")}\n\`\`\``;
|
|
63
|
-
}
|
|
64
|
-
/** A tool's result line for a ```tool_result fence. */
|
|
65
|
-
export function toolResultLine(id, name, content, error) {
|
|
66
|
-
let result = error ?? content;
|
|
67
|
-
if (error === undefined) {
|
|
68
|
-
try {
|
|
69
|
-
result = JSON.parse(content);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// Plain text.
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return JSON.stringify({ name, result: result ?? null, error: error !== undefined, ...(id && { id }) });
|
|
76
|
-
}
|
|
77
|
-
/** Tool results as the model reads them. */
|
|
78
|
-
export const toolResultFence = (lines) => `\`\`\`tool_result\n${lines.join("\n")}\n\`\`\``;
|
|
79
|
-
/**
|
|
80
|
-
* The first tool call in a fence's body: a JSON object (or an array of them,
|
|
81
|
-
* or one per line) with `name` (or `tool`, `tool_name`) and `arguments` (or
|
|
82
|
-
* `parameters`), the arguments possibly a JSON string, maybe wrapped in a
|
|
83
|
-
* `tool_call` or `function` key; or a Python-style call, `[name(key="v")]`;
|
|
84
|
-
* or `call:name{key:value}`.
|
|
85
|
-
*/
|
|
86
|
-
export function parseToolCall(body) {
|
|
87
|
-
const text = body.trim();
|
|
88
|
-
// `call:name{key:value,…}`
|
|
89
|
-
const colon = /call:(\w+)\{([^}]*)\}/.exec(text);
|
|
90
|
-
if (colon)
|
|
91
|
-
return { name: colon[1], arguments: parseColonArguments(colon[2]) };
|
|
92
|
-
// Python style, `[name(key="value")]` or `name(key="value")`
|
|
93
|
-
if (/^\[?\s*\w+\(/.test(text)) {
|
|
94
|
-
const call = parsePythonCall(text);
|
|
95
|
-
if (call)
|
|
96
|
-
return call;
|
|
97
|
-
}
|
|
98
|
-
const candidates = [];
|
|
99
|
-
try {
|
|
100
|
-
const value = JSON.parse(text);
|
|
101
|
-
candidates.push(...(Array.isArray(value) ? value : [value]));
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
for (const line of text.split("\n")) {
|
|
105
|
-
try {
|
|
106
|
-
candidates.push(JSON.parse(line));
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// Not JSON.
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
for (const candidate of candidates) {
|
|
114
|
-
// A call wrapped in a "tool_call" or "function" key is read too.
|
|
115
|
-
const call = isObject(candidate) && isObject(candidate.tool_call ?? candidate.function)
|
|
116
|
-
? (candidate.tool_call ?? candidate.function)
|
|
117
|
-
: candidate;
|
|
118
|
-
if (!isObject(call))
|
|
119
|
-
continue;
|
|
120
|
-
const name = call.name ?? call.tool ?? call.tool_name;
|
|
121
|
-
if (typeof name !== "string" || !name)
|
|
122
|
-
continue;
|
|
123
|
-
let args = call.arguments ?? call.parameters ?? {};
|
|
124
|
-
if (typeof args === "string") {
|
|
125
|
-
try {
|
|
126
|
-
args = JSON.parse(args);
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Left as is, and rejected below.
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (isObject(args))
|
|
133
|
-
return { name, arguments: args };
|
|
134
|
-
}
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
/** `key:value,…` pairs, values read as numbers, booleans or null where they are. */
|
|
138
|
-
function parseColonArguments(text) {
|
|
139
|
-
const args = {};
|
|
140
|
-
for (const pair of text.split(",")) {
|
|
141
|
-
const at = pair.indexOf(":");
|
|
142
|
-
if (at <= 0)
|
|
143
|
-
continue;
|
|
144
|
-
const key = pair.slice(0, at).trim();
|
|
145
|
-
const raw = pair.slice(at + 1).trim();
|
|
146
|
-
const number = Number(raw);
|
|
147
|
-
args[key] =
|
|
148
|
-
raw === "true" ? true : raw === "false" ? false : raw === "null" ? null : raw !== "" && !Number.isNaN(number) ? number : raw;
|
|
149
|
-
}
|
|
150
|
-
return args;
|
|
151
|
-
}
|
|
152
|
-
/** The index of the bracket that closes the one at `open`, past quoted text, or -1. */
|
|
153
|
-
function closing(text, open) {
|
|
154
|
-
const pairs = { "(": ")", "[": "]", "{": "}" };
|
|
155
|
-
const stack = [];
|
|
156
|
-
let quote = "";
|
|
157
|
-
for (let i = open; i < text.length; i++) {
|
|
158
|
-
const c = text[i];
|
|
159
|
-
if (quote) {
|
|
160
|
-
if (c === "\\")
|
|
161
|
-
i++;
|
|
162
|
-
else if (c === quote)
|
|
163
|
-
quote = "";
|
|
164
|
-
}
|
|
165
|
-
else if (c === '"' || c === "'")
|
|
166
|
-
quote = c;
|
|
167
|
-
else if (pairs[c])
|
|
168
|
-
stack.push(pairs[c]);
|
|
169
|
-
else if (c === stack.at(-1)) {
|
|
170
|
-
stack.pop();
|
|
171
|
-
if (stack.length === 0)
|
|
172
|
-
return i;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return -1;
|
|
176
|
-
}
|
|
177
|
-
/** `text` split at its top-level commas (not inside quotes or brackets). */
|
|
178
|
-
function splitTopLevel(text) {
|
|
179
|
-
const parts = [];
|
|
180
|
-
let depth = 0;
|
|
181
|
-
let quote = "";
|
|
182
|
-
let from = 0;
|
|
183
|
-
for (let i = 0; i < text.length; i++) {
|
|
184
|
-
const c = text[i];
|
|
185
|
-
if (quote) {
|
|
186
|
-
if (c === "\\")
|
|
187
|
-
i++;
|
|
188
|
-
else if (c === quote)
|
|
189
|
-
quote = "";
|
|
190
|
-
}
|
|
191
|
-
else if (c === '"' || c === "'")
|
|
192
|
-
quote = c;
|
|
193
|
-
else if ("([{".includes(c))
|
|
194
|
-
depth++;
|
|
195
|
-
else if (")]}".includes(c))
|
|
196
|
-
depth--;
|
|
197
|
-
else if (c === "," && depth === 0) {
|
|
198
|
-
parts.push(text.slice(from, i));
|
|
199
|
-
from = i + 1;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
parts.push(text.slice(from));
|
|
203
|
-
return parts.map((part) => part.trim()).filter(Boolean);
|
|
204
|
-
}
|
|
205
|
-
/** A Python literal (string, number, True/False/None, list, dict) as a value. */
|
|
206
|
-
function pythonValue(raw) {
|
|
207
|
-
const text = raw.trim();
|
|
208
|
-
if (text === "True")
|
|
209
|
-
return true;
|
|
210
|
-
if (text === "False")
|
|
211
|
-
return false;
|
|
212
|
-
if (text === "None")
|
|
213
|
-
return null;
|
|
214
|
-
if (/^(["']).*\1$/s.test(text))
|
|
215
|
-
return text.slice(1, -1).replace(/\\(["'\\])/g, "$1");
|
|
216
|
-
const json = text
|
|
217
|
-
.replace(/'((?:[^'\\]|\\.)*)'/g, (_, inner) => JSON.stringify(inner.replace(/\\'/g, "'")))
|
|
218
|
-
.replace(/\bTrue\b/g, "true")
|
|
219
|
-
.replace(/\bFalse\b/g, "false")
|
|
220
|
-
.replace(/\bNone\b/g, "null");
|
|
221
|
-
try {
|
|
222
|
-
return JSON.parse(json);
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return text;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
/** The first Python-style call in `text`: `name(key=value, …)`, maybe in brackets. */
|
|
229
|
-
function parsePythonCall(text) {
|
|
230
|
-
const match = /(\w+)\(/.exec(text);
|
|
231
|
-
if (!match)
|
|
232
|
-
return undefined;
|
|
233
|
-
const open = match.index + match[1].length;
|
|
234
|
-
const close = closing(text, open);
|
|
235
|
-
if (close === -1)
|
|
236
|
-
return undefined;
|
|
237
|
-
const args = {};
|
|
238
|
-
for (const part of splitTopLevel(text.slice(open + 1, close))) {
|
|
239
|
-
const at = part.indexOf("=");
|
|
240
|
-
if (at <= 0)
|
|
241
|
-
continue;
|
|
242
|
-
args[part.slice(0, at).trim()] = pythonValue(part.slice(at + 1));
|
|
243
|
-
}
|
|
244
|
-
return { name: match[1], arguments: args };
|
|
245
|
-
}
|
|
246
|
-
/** The JSON object `text` starts with, up to its closing brace (or all of `text`). */
|
|
247
|
-
function leadingObject(text) {
|
|
248
|
-
const start = text.indexOf("{");
|
|
249
|
-
let depth = 0;
|
|
250
|
-
let quoted = false;
|
|
251
|
-
for (let i = start; i >= 0 && i < text.length; i++) {
|
|
252
|
-
const c = text[i];
|
|
253
|
-
if (quoted) {
|
|
254
|
-
if (c === "\\")
|
|
255
|
-
i++;
|
|
256
|
-
else if (c === '"')
|
|
257
|
-
quoted = false;
|
|
258
|
-
}
|
|
259
|
-
else if (c === '"')
|
|
260
|
-
quoted = true;
|
|
261
|
-
else if (c === "{")
|
|
262
|
-
depth++;
|
|
263
|
-
else if (c === "}" && --depth === 0)
|
|
264
|
-
return text.slice(start, i + 1);
|
|
265
|
-
}
|
|
266
|
-
return text;
|
|
267
|
-
}
|
|
268
|
-
/** Characters at the end of `text` that could start a fence. */
|
|
269
|
-
function partialFenceStart(text) {
|
|
270
|
-
for (let size = Math.min(text.length, Math.max(...DELIMITERS.map((d) => d.start.length)) - 1); size > 0; size--) {
|
|
271
|
-
const end = text.slice(-size);
|
|
272
|
-
if (DELIMITERS.some((d) => d.start.startsWith(end)))
|
|
273
|
-
return size;
|
|
274
|
-
}
|
|
275
|
-
return 0;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Reads a streamed reply: `push` each piece and get back the text that can
|
|
279
|
-
* be shown now; text inside a ```tool_call fence (or another call's
|
|
280
|
-
* delimiters: `<tool_call>`, `<|tool_call>`, `<|tool_call_start|>`, or a
|
|
281
|
-
* Python-style `[name(…)]`) is held back. The first
|
|
282
|
-
* fence that closes ends the reply (stop generating then): `fence` is its
|
|
283
|
-
* text, `call` the call in it, if it holds one (else the fence is a failed
|
|
284
|
-
* call, for `repairToolCall`), and `trailing` the text after it. Other code
|
|
285
|
-
* fences are text. `end` gives the rest, reading an unclosed fence as the
|
|
286
|
-
* reply's last.
|
|
287
|
-
*/
|
|
288
|
-
export function toolCallReader() {
|
|
289
|
-
let buffer = "";
|
|
290
|
-
// The delimiter of the call being read; a Python-style call is read whole.
|
|
291
|
-
let start;
|
|
292
|
-
let fence;
|
|
293
|
-
let call;
|
|
294
|
-
// Text after the fence, in the piece that closed it.
|
|
295
|
-
let trailing = "";
|
|
296
|
-
// A reply that starts with "{" may be a call without its fence (Apple's
|
|
297
|
-
// model writes one sometimes): held back until the reply ends.
|
|
298
|
-
let bare;
|
|
299
|
-
const read = () => {
|
|
300
|
-
let shown = "";
|
|
301
|
-
if (fence !== undefined)
|
|
302
|
-
return shown;
|
|
303
|
-
if (bare === undefined && buffer.trim())
|
|
304
|
-
bare = buffer.trimStart().startsWith("{");
|
|
305
|
-
if (bare)
|
|
306
|
-
return shown;
|
|
307
|
-
if (!start) {
|
|
308
|
-
const python = PYTHON_CALL.exec(buffer);
|
|
309
|
-
const found = [
|
|
310
|
-
...DELIMITERS.map((d) => ({ d, at: buffer.indexOf(d.start) })),
|
|
311
|
-
...(python ? [{ d: { start: python[0], end: ")]", python: true }, at: python.index }] : [])
|
|
312
|
-
]
|
|
313
|
-
.filter((f) => f.at !== -1)
|
|
314
|
-
.sort((a, b) => a.at - b.at)[0];
|
|
315
|
-
if (!found) {
|
|
316
|
-
const keep = partialFenceStart(buffer);
|
|
317
|
-
shown += buffer.slice(0, buffer.length - keep);
|
|
318
|
-
buffer = buffer.slice(buffer.length - keep);
|
|
319
|
-
return shown;
|
|
320
|
-
}
|
|
321
|
-
shown += buffer.slice(0, found.at);
|
|
322
|
-
buffer = buffer.slice(found.at);
|
|
323
|
-
start = found.d;
|
|
324
|
-
}
|
|
325
|
-
const close = buffer.indexOf(start.end, start.start.length);
|
|
326
|
-
if (close === -1)
|
|
327
|
-
return shown;
|
|
328
|
-
fence = buffer.slice(0, close + start.end.length);
|
|
329
|
-
trailing = buffer.slice(fence.length);
|
|
330
|
-
call = parseToolCall(start.python ? fence : fence.slice(start.start.length, close));
|
|
331
|
-
return shown;
|
|
332
|
-
};
|
|
333
|
-
return {
|
|
334
|
-
get call() {
|
|
335
|
-
return call;
|
|
336
|
-
},
|
|
337
|
-
get fence() {
|
|
338
|
-
return fence;
|
|
339
|
-
},
|
|
340
|
-
/** Text that came after the fence: shown after the call. */
|
|
341
|
-
get trailing() {
|
|
342
|
-
return trailing;
|
|
343
|
-
},
|
|
344
|
-
push(piece) {
|
|
345
|
-
buffer += piece;
|
|
346
|
-
return read();
|
|
347
|
-
},
|
|
348
|
-
end() {
|
|
349
|
-
if (fence !== undefined)
|
|
350
|
-
return "";
|
|
351
|
-
if (start) {
|
|
352
|
-
fence = buffer;
|
|
353
|
-
call = parseToolCall(start.python ? buffer : buffer.slice(start.start.length));
|
|
354
|
-
return "";
|
|
355
|
-
}
|
|
356
|
-
const rest = buffer;
|
|
357
|
-
buffer = "";
|
|
358
|
-
const bareCall = bare ? parseToolCall(leadingObject(rest)) : undefined;
|
|
359
|
-
if (bareCall) {
|
|
360
|
-
fence = rest;
|
|
361
|
-
call = bareCall;
|
|
362
|
-
return "";
|
|
363
|
-
}
|
|
364
|
-
return rest;
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
const matches = (type, value) => type === "string"
|
|
369
|
-
? typeof value === "string"
|
|
370
|
-
: type === "number"
|
|
371
|
-
? typeof value === "number"
|
|
372
|
-
: type === "integer"
|
|
373
|
-
? Number.isInteger(value)
|
|
374
|
-
: type === "boolean"
|
|
375
|
-
? typeof value === "boolean"
|
|
376
|
-
: type === "array"
|
|
377
|
-
? Array.isArray(value)
|
|
378
|
-
: type === "object"
|
|
379
|
-
? isObject(value)
|
|
380
|
-
: type === "null"
|
|
381
|
-
? value === null
|
|
382
|
-
: true;
|
|
383
|
-
/** Why `value` doesn't fit `schema` (types, enums, required and unknown fields), if it doesn't. */
|
|
384
|
-
function schemaError(schema, value, path) {
|
|
385
|
-
if (!isObject(schema))
|
|
386
|
-
return undefined;
|
|
387
|
-
if (Array.isArray(schema.enum) &&
|
|
388
|
-
!schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
|
|
389
|
-
return `${path} must be one of ${JSON.stringify(schema.enum)}`;
|
|
390
|
-
}
|
|
391
|
-
const types = [schema.type].flat().filter((t) => t !== undefined);
|
|
392
|
-
if (types.length > 0 && !types.some((t) => matches(t, value)))
|
|
393
|
-
return `${path} must be ${types.join(" or ")}`;
|
|
394
|
-
if (isObject(value)) {
|
|
395
|
-
const properties = isObject(schema.properties) ? schema.properties : {};
|
|
396
|
-
for (const key of Array.isArray(schema.required) ? schema.required : []) {
|
|
397
|
-
if (typeof key === "string" && !(key in value))
|
|
398
|
-
return `${path}.${key} is missing`;
|
|
399
|
-
}
|
|
400
|
-
for (const [key, field] of Object.entries(value)) {
|
|
401
|
-
if (key in properties) {
|
|
402
|
-
const error = schemaError(properties[key], field, `${path}.${key}`);
|
|
403
|
-
if (error)
|
|
404
|
-
return error;
|
|
405
|
-
}
|
|
406
|
-
else if (schema.additionalProperties === false) {
|
|
407
|
-
return `${path}.${key} isn't a parameter`;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (Array.isArray(value)) {
|
|
412
|
-
for (const [i, item] of value.entries()) {
|
|
413
|
-
const error = schemaError(schema.items, item, `${path}[${i}]`);
|
|
414
|
-
if (error)
|
|
415
|
-
return error;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
|
-
/** Why a call doesn't fit the tools (an unknown tool, or arguments that fail its schema), if it doesn't. */
|
|
421
|
-
export function invalidToolCall(call, tools) {
|
|
422
|
-
const tool = tools.find((t) => t.name === call.name);
|
|
423
|
-
if (!tool)
|
|
424
|
-
return `there's no tool named "${call.name}" (the tools are ${tools.map((t) => t.name).join(", ")})`;
|
|
425
|
-
return schemaError(tool.inputSchema, call.arguments, "arguments");
|
|
426
|
-
}
|
|
427
|
-
/** Why a reply's ```tool_call fence isn't a valid call, if it isn't. */
|
|
428
|
-
export const toolCallError = (call, tools) => call
|
|
429
|
-
? invalidToolCall(call, tools)
|
|
430
|
-
: 'it isn\'t JSON with "name" and "arguments"';
|
|
431
|
-
/** The message after a failed call, asking for it again. */
|
|
432
|
-
export const repairPrompt = (reason) => `That tool call wasn't valid: ${reason}. Write it again.`;
|
|
433
|
-
/**
|
|
434
|
-
* One repair of a failed tool call, with output held to a schema (as webgllm
|
|
435
|
-
* repairs): the tool's arguments if `name` is a tool, else first the tool's
|
|
436
|
-
* name, then its arguments. `generate(schema)` asks the model, with the
|
|
437
|
-
* conversation, its failed attempt and `repairPrompt`, for JSON that fits
|
|
438
|
-
* `schema`, and returns it parsed. Undefined if the repair fails too.
|
|
439
|
-
*/
|
|
440
|
-
export async function repairToolCall(tools, name, generate) {
|
|
441
|
-
if (tools.length === 0)
|
|
442
|
-
return undefined;
|
|
443
|
-
try {
|
|
444
|
-
let tool = tools.find((t) => t.name === name);
|
|
445
|
-
if (!tool) {
|
|
446
|
-
const picked = await generate({
|
|
447
|
-
type: "object",
|
|
448
|
-
properties: {
|
|
449
|
-
name: { type: "string", enum: tools.map((t) => t.name) }
|
|
450
|
-
},
|
|
451
|
-
required: ["name"]
|
|
452
|
-
});
|
|
453
|
-
tool = tools.find((t) => isObject(picked) && t.name === picked.name);
|
|
454
|
-
if (!tool)
|
|
455
|
-
return undefined;
|
|
456
|
-
}
|
|
457
|
-
const args = await generate(tool.inputSchema ?? {
|
|
458
|
-
type: "object",
|
|
459
|
-
properties: {}
|
|
460
|
-
});
|
|
461
|
-
if (!isObject(args))
|
|
462
|
-
return undefined;
|
|
463
|
-
const call = { name: tool.name, arguments: args };
|
|
464
|
-
return invalidToolCall(call, tools) ? undefined : call;
|
|
465
|
-
}
|
|
466
|
-
catch {
|
|
467
|
-
return undefined;
|
|
468
|
-
}
|
|
469
|
-
}
|