@tanstack-ai-browser/afm 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Package.swift +33 -0
- package/README.md +62 -2
- package/dist/adapter.d.ts +96 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +490 -0
- package/dist/electron.d.ts +22 -0
- package/dist/electron.d.ts.map +1 -0
- package/dist/electron.js +131 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/messages.d.ts +12 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +29 -0
- package/dist/model-meta.d.ts +23 -0
- package/dist/model-meta.d.ts.map +1 -0
- package/dist/model-meta.js +20 -0
- package/dist/native.d.ts +163 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +21 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +133 -0
- package/electron/AfmNode.swift +81 -0
- package/electron/afm.c +94 -0
- package/electron/afm.node +0 -0
- package/electron/afm.node.sha256 +1 -0
- package/electron/build.mjs +54 -0
- package/electron/preload.cjs +35 -0
- package/electron/sources-hash.mjs +40 -0
- package/ios/Sources/AfmCore/Bridge.swift +315 -0
- package/ios/Sources/AfmCore/Conversation.swift +49 -0
- package/ios/Sources/AfmCore/Schema.swift +90 -0
- package/ios/Sources/AfmPlugin/AfmPlugin.swift +89 -0
- package/package.json +98 -4
- package/src/adapter.test.ts +380 -0
- package/src/adapter.ts +574 -0
- package/src/electron.ts +180 -0
- package/src/index.ts +27 -0
- package/src/messages.ts +37 -0
- package/src/model-meta.ts +37 -0
- package/src/native-build.test.ts +13 -0
- package/src/native.ts +119 -0
- package/src/schema.test.ts +146 -0
- package/src/schema.ts +153 -0
package/src/schema.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema (what `chat()` gives adapters for tools and structured output)
|
|
3
|
+
* read into the framework's own schema (`NativeSchema`), which the model's
|
|
4
|
+
* output is held to: a tool's arguments always fit its parameters.
|
|
5
|
+
*
|
|
6
|
+
* Properties keep their order, which the model writes them in. Each object is
|
|
7
|
+
* named by its path, so two objects never share a name. A property that may be
|
|
8
|
+
* null is optional. `$ref`s to `$defs` (or `definitions`) become references to
|
|
9
|
+
* named definitions; one to a plain value is written in place.
|
|
10
|
+
*/
|
|
11
|
+
import type { NativeGenerationSchema, NativeSchema } from "./native.js"
|
|
12
|
+
|
|
13
|
+
type Json = Record<string, unknown>
|
|
14
|
+
|
|
15
|
+
const isObject = (value: unknown): value is Json => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
16
|
+
|
|
17
|
+
/** Drops undefined fields: the native bridge takes JSON. */
|
|
18
|
+
function defined<T extends object>(value: T): T {
|
|
19
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isNull = (schema: unknown) =>
|
|
23
|
+
isObject(schema) && (schema.type === "null" || (Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null"))
|
|
24
|
+
|
|
25
|
+
/** Whether a schema allows null: `type: [..., "null"]`, or a null choice in `anyOf`. */
|
|
26
|
+
function nullable(schema: unknown): boolean {
|
|
27
|
+
if (!isObject(schema)) return false
|
|
28
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true
|
|
29
|
+
const variants = schema.anyOf ?? schema.oneOf
|
|
30
|
+
return Array.isArray(variants) && variants.some(isNull)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const number = (value: unknown) => (typeof value === "number" && Number.isFinite(value) ? value : undefined)
|
|
34
|
+
|
|
35
|
+
/** A schema's name for the framework: letters, digits and underscores. */
|
|
36
|
+
const typeName = (name: string) => name.replace(/[^A-Za-z0-9_]/g, "_") || "Value"
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The framework's schema for a JSON Schema, named `name` (a tool's name, or
|
|
40
|
+
* the structured answer's).
|
|
41
|
+
*/
|
|
42
|
+
export function toNativeSchema(schema: unknown, name: string): NativeGenerationSchema {
|
|
43
|
+
const json = isObject(schema) ? schema : {}
|
|
44
|
+
const defs: Json = { ...(isObject(json.definitions) ? json.definitions : {}), ...(isObject(json.$defs) ? json.$defs : {}) }
|
|
45
|
+
const definitions = new Map<string, NativeSchema>()
|
|
46
|
+
|
|
47
|
+
const convert = (value: unknown, path: string): NativeSchema => {
|
|
48
|
+
if (!isObject(value)) return { kind: "string" }
|
|
49
|
+
if (typeof value.$ref === "string") {
|
|
50
|
+
const target = value.$ref.split("/").at(-1) ?? ""
|
|
51
|
+
const def = defs[target]
|
|
52
|
+
if (!isObject(def)) return { kind: "string" }
|
|
53
|
+
const converted = definitions.get(target) ?? convertDefinition(def, target)
|
|
54
|
+
// Only named schemas (objects, choices) can be referred to.
|
|
55
|
+
return converted.kind === "object" || converted.kind === "anyOf" ? { kind: "reference", name: typeName(target) } : converted
|
|
56
|
+
}
|
|
57
|
+
if ("const" in value && typeof value.const === "string") return { kind: "string", constant: value.const }
|
|
58
|
+
if (Array.isArray(value.enum)) {
|
|
59
|
+
const members: Array<unknown> = value.enum.filter((v) => v !== null)
|
|
60
|
+
if (members.every((v) => typeof v === "string")) {
|
|
61
|
+
return members.length > 0 ? { kind: "string", choices: members } : { kind: "string" }
|
|
62
|
+
}
|
|
63
|
+
// true and false both: any boolean. One alone, the framework can't hold it to.
|
|
64
|
+
if (members.every((v) => typeof v === "boolean")) {
|
|
65
|
+
if (new Set(members).size === 2) return { kind: "boolean" }
|
|
66
|
+
throw new Error(`${path}: an enum of only ${String(members[0])} isn't supported (Apple's framework can't hold a boolean to one value)`)
|
|
67
|
+
}
|
|
68
|
+
// Integers (and strings among them): a choice of exact values, each integer
|
|
69
|
+
// a range of itself, which the framework holds the model to. A decimal
|
|
70
|
+
// can't be: a range of one decimal fails in Apple's generator.
|
|
71
|
+
if (members.some((v) => typeof v === "number" && !Number.isInteger(v))) {
|
|
72
|
+
throw new Error(`${path}: an enum of decimal numbers isn't supported (Apple's framework can't hold a decimal to exact values)`)
|
|
73
|
+
}
|
|
74
|
+
if (members.every((v) => typeof v === "string" || Number.isInteger(v))) {
|
|
75
|
+
const choices: Array<NativeSchema> = members.map((v) =>
|
|
76
|
+
typeof v === "string" ? { kind: "string", constant: v } : { kind: "integer", minimum: v as number, maximum: v as number }
|
|
77
|
+
)
|
|
78
|
+
if (choices.length === 1) return choices[0]
|
|
79
|
+
return defined({ kind: "anyOf" as const, name: typeName(path), description: typeof value.description === "string" ? value.description : undefined, choices })
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`${path}: an enum mixing ${[...new Set(members.map((v) => typeof v))].join(" and ")} values isn't supported by Apple's framework`)
|
|
82
|
+
}
|
|
83
|
+
const variants = value.anyOf ?? value.oneOf
|
|
84
|
+
if (Array.isArray(variants)) {
|
|
85
|
+
const choices = variants.filter((v) => !isNull(v))
|
|
86
|
+
if (choices.length === 1) return convert({ ...choices[0], description: value.description ?? choices[0].description }, path)
|
|
87
|
+
if (choices.length > 1) {
|
|
88
|
+
return defined({
|
|
89
|
+
kind: "anyOf" as const,
|
|
90
|
+
name: typeName(path),
|
|
91
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
92
|
+
choices: choices.map((choice, i) => convert(choice, `${path}_${i + 1}`))
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const types = Array.isArray(value.type) ? value.type.filter((t) => t !== "null") : [value.type]
|
|
97
|
+
const type = typeof types[0] === "string" ? types[0] : isObject(value.properties) ? "object" : value.items ? "array" : "string"
|
|
98
|
+
switch (type) {
|
|
99
|
+
case "object": {
|
|
100
|
+
const required = Array.isArray(value.required) ? value.required : []
|
|
101
|
+
const properties = Object.entries(isObject(value.properties) ? value.properties : {}).map(([key, property]) =>
|
|
102
|
+
defined({
|
|
103
|
+
name: key,
|
|
104
|
+
description: isObject(property) && typeof property.description === "string" ? property.description : undefined,
|
|
105
|
+
optional: !required.includes(key) || nullable(property),
|
|
106
|
+
schema: convert(property, `${path}_${key}`)
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
return defined({
|
|
110
|
+
kind: "object" as const,
|
|
111
|
+
name: typeName(path),
|
|
112
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
113
|
+
properties
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
case "array":
|
|
117
|
+
return defined({
|
|
118
|
+
kind: "array" as const,
|
|
119
|
+
items: convert(Array.isArray(value.items) ? value.items[0] : value.items, `${path}_item`),
|
|
120
|
+
minimumElements: number(value.minItems),
|
|
121
|
+
maximumElements: number(value.maxItems)
|
|
122
|
+
})
|
|
123
|
+
case "integer":
|
|
124
|
+
case "number":
|
|
125
|
+
return defined({
|
|
126
|
+
kind: type,
|
|
127
|
+
minimum: number(value.minimum) ?? number(value.exclusiveMinimum),
|
|
128
|
+
maximum: number(value.maximum) ?? number(value.exclusiveMaximum)
|
|
129
|
+
})
|
|
130
|
+
case "boolean":
|
|
131
|
+
return { kind: "boolean" }
|
|
132
|
+
case "null":
|
|
133
|
+
return { kind: "null" }
|
|
134
|
+
default:
|
|
135
|
+
return { kind: "string" }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const convertDefinition = (def: Json, target: string): NativeSchema => {
|
|
140
|
+
// Set first, so a definition that refers to itself finds its name.
|
|
141
|
+
definitions.set(target, { kind: "object", name: typeName(target), properties: [] })
|
|
142
|
+
const converted = convert(def, target)
|
|
143
|
+
const named = converted.kind === "object" || converted.kind === "anyOf" ? { ...converted, name: typeName(target) } : converted
|
|
144
|
+
definitions.set(target, named)
|
|
145
|
+
return named
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const root = convert(json, name)
|
|
149
|
+
return {
|
|
150
|
+
root,
|
|
151
|
+
definitions: [...definitions.values()].filter((d) => d.kind === "object" || d.kind === "anyOf")
|
|
152
|
+
}
|
|
153
|
+
}
|