freesail 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -5
- package/docs/A2UX_Protocol.md +183 -0
- package/docs/Agents.md +218 -0
- package/docs/Architecture.md +285 -0
- package/docs/CatalogReference.md +377 -0
- package/docs/GettingStarted.md +230 -0
- package/examples/demo/package.json +21 -0
- package/examples/demo/public/index.html +381 -0
- package/examples/demo/server.js +253 -0
- package/package.json +38 -5
- package/packages/core/package.json +48 -0
- package/packages/core/src/functions.ts +403 -0
- package/packages/core/src/index.ts +214 -0
- package/packages/core/src/parser.ts +270 -0
- package/packages/core/src/protocol.ts +254 -0
- package/packages/core/src/store.ts +452 -0
- package/packages/core/src/transport.ts +439 -0
- package/packages/core/src/types.ts +209 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/lit-ui/package.json +44 -0
- package/packages/lit-ui/src/catalogs/standard/catalog.json +405 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Badge.ts +96 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Button.ts +147 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Card.ts +78 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Checkbox.ts +94 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Column.ts +66 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Divider.ts +59 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Image.ts +54 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Input.ts +125 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Progress.ts +79 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Row.ts +68 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Select.ts +110 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Spacer.ts +37 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Spinner.ts +76 -0
- package/packages/lit-ui/src/catalogs/standard/elements/Text.ts +86 -0
- package/packages/lit-ui/src/catalogs/standard/elements/index.ts +18 -0
- package/packages/lit-ui/src/catalogs/standard/index.ts +17 -0
- package/packages/lit-ui/src/index.ts +84 -0
- package/packages/lit-ui/src/renderer.ts +211 -0
- package/packages/lit-ui/src/types.ts +49 -0
- package/packages/lit-ui/src/utils/define-props.ts +157 -0
- package/packages/lit-ui/src/utils/index.ts +2 -0
- package/packages/lit-ui/src/utils/registry.ts +139 -0
- package/packages/lit-ui/tsconfig.json +11 -0
- package/packages/server/package.json +61 -0
- package/packages/server/src/adapters/index.ts +5 -0
- package/packages/server/src/adapters/langchain.ts +175 -0
- package/packages/server/src/adapters/openai.ts +209 -0
- package/packages/server/src/catalog-loader.ts +311 -0
- package/packages/server/src/index.ts +142 -0
- package/packages/server/src/stream.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/tsconfig.base.json +23 -0
- package/index.js +0 -3
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Converts Freesail tools to OpenAI function calling format.
|
|
5
|
+
* This adapter allows using Freesail with the OpenAI API directly.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This adapter requires 'openai' as a peer dependency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FreesailStream } from '../stream.js';
|
|
11
|
+
import type { CatalogToToolConverter, OpenAITool } from '../catalog-loader.js';
|
|
12
|
+
import { toolCallToMessages, updateDataToMessage } from '../catalog-loader.js';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export interface OpenAIAdapterOptions {
|
|
19
|
+
/** The SSE stream to send messages to */
|
|
20
|
+
stream: FreesailStream;
|
|
21
|
+
/** Catalog ID being used */
|
|
22
|
+
catalogId: string;
|
|
23
|
+
/** Tool converter instance */
|
|
24
|
+
converter: CatalogToToolConverter;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ToolCallResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
message: string;
|
|
30
|
+
surfaceId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// OpenAI Adapter
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* OpenAIAdapter handles tool calls from OpenAI and converts them
|
|
39
|
+
* to A2UX messages sent via the stream.
|
|
40
|
+
*/
|
|
41
|
+
export class OpenAIAdapter {
|
|
42
|
+
private stream: FreesailStream;
|
|
43
|
+
private catalogId: string;
|
|
44
|
+
private converter: CatalogToToolConverter;
|
|
45
|
+
|
|
46
|
+
constructor(options: OpenAIAdapterOptions) {
|
|
47
|
+
this.stream = options.stream;
|
|
48
|
+
this.catalogId = options.catalogId;
|
|
49
|
+
this.converter = options.converter;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get tools in OpenAI format
|
|
54
|
+
*/
|
|
55
|
+
getTools(): OpenAITool[] {
|
|
56
|
+
return this.converter.generateOpenAITools(this.catalogId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Handle a tool call from OpenAI
|
|
61
|
+
*/
|
|
62
|
+
handleToolCall(
|
|
63
|
+
toolName: string,
|
|
64
|
+
args: Record<string, unknown>
|
|
65
|
+
): ToolCallResult {
|
|
66
|
+
switch (toolName) {
|
|
67
|
+
case 'render_ui':
|
|
68
|
+
return this.handleRenderUI(args);
|
|
69
|
+
case 'update_data':
|
|
70
|
+
return this.handleUpdateData(args);
|
|
71
|
+
default:
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
message: `Unknown tool: ${toolName}`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle render_ui tool call
|
|
81
|
+
*/
|
|
82
|
+
private handleRenderUI(args: Record<string, unknown>): ToolCallResult {
|
|
83
|
+
const { surfaceId, components } = args as {
|
|
84
|
+
surfaceId: string;
|
|
85
|
+
components: Array<{ id: string; component: string; [key: string]: unknown }>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (!surfaceId || !components) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
message: 'Missing required parameters: surfaceId, components'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const messages = toolCallToMessages({ surfaceId, components }, this.catalogId);
|
|
97
|
+
|
|
98
|
+
for (const message of messages) {
|
|
99
|
+
this.stream.send(message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
message: `UI surface "${surfaceId}" created with ${components.length} components.`,
|
|
105
|
+
surfaceId,
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
message: `Failed to render UI: ${error}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Handle update_data tool call
|
|
117
|
+
*/
|
|
118
|
+
private handleUpdateData(args: Record<string, unknown>): ToolCallResult {
|
|
119
|
+
const { surfaceId, path, value, operation } = args as {
|
|
120
|
+
surfaceId: string;
|
|
121
|
+
path?: string;
|
|
122
|
+
value: unknown;
|
|
123
|
+
operation?: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (!surfaceId || value === undefined) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
message: 'Missing required parameters: surfaceId, value',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const message = updateDataToMessage({ surfaceId, path, value, operation });
|
|
135
|
+
this.stream.send(message);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
message: `Data updated at path "${path || '/'}" in surface "${surfaceId}".`,
|
|
140
|
+
surfaceId,
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
message: `Failed to update data: ${error}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create an OpenAI adapter instance
|
|
153
|
+
*/
|
|
154
|
+
export function createOpenAIAdapter(options: OpenAIAdapterOptions): OpenAIAdapter {
|
|
155
|
+
return new OpenAIAdapter(options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// =============================================================================
|
|
159
|
+
// OpenAI Chat Completions Helper
|
|
160
|
+
// =============================================================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Process OpenAI chat completion response and handle tool calls
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* const response = await openai.chat.completions.create({
|
|
168
|
+
* model: 'gpt-4',
|
|
169
|
+
* messages,
|
|
170
|
+
* tools: adapter.getTools(),
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* const results = processOpenAIResponse(response, adapter);
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export function processOpenAIResponse(
|
|
177
|
+
response: {
|
|
178
|
+
choices: Array<{
|
|
179
|
+
message: {
|
|
180
|
+
tool_calls?: Array<{
|
|
181
|
+
function: { name: string; arguments: string }
|
|
182
|
+
}>
|
|
183
|
+
}
|
|
184
|
+
}>
|
|
185
|
+
},
|
|
186
|
+
adapter: OpenAIAdapter
|
|
187
|
+
): ToolCallResult[] {
|
|
188
|
+
const results: ToolCallResult[] = [];
|
|
189
|
+
const toolCalls = response.choices[0]?.message?.tool_calls;
|
|
190
|
+
|
|
191
|
+
if (!toolCalls) {
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const toolCall of toolCalls) {
|
|
196
|
+
try {
|
|
197
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
198
|
+
const result = adapter.handleToolCall(toolCall.function.name, args);
|
|
199
|
+
results.push(result);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
results.push({
|
|
202
|
+
success: false,
|
|
203
|
+
message: `Failed to parse tool call arguments: ${error}`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog Loader & Tool Converter
|
|
3
|
+
*
|
|
4
|
+
* Converts catalog definitions to LLM-compatible tool schemas.
|
|
5
|
+
* This is the core of the "Context Injector" - generating
|
|
6
|
+
* JSON Schema tools from component catalogs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ServerToClientMessage, A2UXComponent } from '@freesail/core';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface CatalogComponent {
|
|
16
|
+
description?: string;
|
|
17
|
+
properties?: Record<string, PropertySchema>;
|
|
18
|
+
required?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PropertySchema {
|
|
22
|
+
type: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
enum?: string[];
|
|
25
|
+
default?: unknown;
|
|
26
|
+
items?: PropertySchema;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CatalogDefinition {
|
|
30
|
+
id: string;
|
|
31
|
+
version: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
components: Record<string, CatalogComponent>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ToolSchema {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
parameters: {
|
|
41
|
+
type: 'object';
|
|
42
|
+
properties: Record<string, unknown>;
|
|
43
|
+
required: string[];
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OpenAITool {
|
|
48
|
+
type: 'function';
|
|
49
|
+
function: ToolSchema;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Catalog Loader
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load a catalog from a URL or file path
|
|
58
|
+
*/
|
|
59
|
+
export async function loadCatalog(source: string): Promise<CatalogDefinition> {
|
|
60
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
61
|
+
const response = await fetch(source);
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to load catalog: ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
return response.json() as Promise<CatalogDefinition>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// For file paths, use dynamic import (works in ESM)
|
|
69
|
+
const fs = await import('fs/promises');
|
|
70
|
+
const content = await fs.readFile(source, 'utf-8');
|
|
71
|
+
return JSON.parse(content) as CatalogDefinition;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Tool Converter
|
|
76
|
+
// =============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* CatalogToToolConverter generates LLM tool definitions from catalog schemas.
|
|
80
|
+
* It's the bridge between UI catalogs and AI function calling.
|
|
81
|
+
*/
|
|
82
|
+
export class CatalogToToolConverter {
|
|
83
|
+
private catalogs: Map<string, CatalogDefinition> = new Map();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Register a catalog
|
|
87
|
+
*/
|
|
88
|
+
addCatalog(catalog: CatalogDefinition): void {
|
|
89
|
+
this.catalogs.set(catalog.id, catalog);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get registered catalog
|
|
94
|
+
*/
|
|
95
|
+
getCatalog(catalogId: string): CatalogDefinition | undefined {
|
|
96
|
+
return this.catalogs.get(catalogId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate the render_ui tool schema for a catalog
|
|
101
|
+
*/
|
|
102
|
+
generateRenderUITool(catalogId: string): ToolSchema {
|
|
103
|
+
const catalog = this.catalogs.get(catalogId);
|
|
104
|
+
if (!catalog) {
|
|
105
|
+
throw new Error(`Catalog not found: ${catalogId}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Build component union schema
|
|
109
|
+
const componentSchemas = this.buildComponentSchemas(catalog);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
name: 'render_ui',
|
|
113
|
+
description: `Render a user interface using the ${catalog.name || catalogId} component catalog. ` +
|
|
114
|
+
`Use this tool to create visual UI elements. Components are specified as a flat list with ` +
|
|
115
|
+
`parent-child relationships defined by ID references in the 'children' array.`,
|
|
116
|
+
parameters: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
surfaceId: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'Unique identifier for this UI surface',
|
|
122
|
+
},
|
|
123
|
+
components: {
|
|
124
|
+
type: 'array',
|
|
125
|
+
description: 'Array of component definitions. First component should have id "root".',
|
|
126
|
+
items: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
id: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Unique identifier for this component',
|
|
132
|
+
},
|
|
133
|
+
component: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: 'Component type from catalog',
|
|
136
|
+
enum: Object.keys(catalog.components),
|
|
137
|
+
},
|
|
138
|
+
children: {
|
|
139
|
+
type: 'array',
|
|
140
|
+
description: 'Array of child component IDs (for container components)',
|
|
141
|
+
items: { type: 'string' },
|
|
142
|
+
},
|
|
143
|
+
...componentSchemas,
|
|
144
|
+
},
|
|
145
|
+
required: ['id', 'component'],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
required: ['surfaceId', 'components'],
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate update_data tool schema
|
|
156
|
+
*/
|
|
157
|
+
generateUpdateDataTool(): ToolSchema {
|
|
158
|
+
return {
|
|
159
|
+
name: 'update_data',
|
|
160
|
+
description: 'Update data in a UI surface without recreating components. ' +
|
|
161
|
+
'Use this to change text, values, or state in an existing UI.',
|
|
162
|
+
parameters: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
surfaceId: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The surface ID to update',
|
|
168
|
+
},
|
|
169
|
+
path: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'JSON Pointer path to the data to update (e.g., "/user/name")',
|
|
172
|
+
},
|
|
173
|
+
value: {
|
|
174
|
+
description: 'The new value to set',
|
|
175
|
+
},
|
|
176
|
+
operation: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
enum: ['add', 'replace', 'remove'],
|
|
179
|
+
description: 'The operation to perform (default: replace)',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
required: ['surfaceId', 'value'],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate all tools for a catalog as OpenAI format
|
|
189
|
+
*/
|
|
190
|
+
generateOpenAITools(catalogId: string): OpenAITool[] {
|
|
191
|
+
return [
|
|
192
|
+
{ type: 'function', function: this.generateRenderUITool(catalogId) },
|
|
193
|
+
{ type: 'function', function: this.generateUpdateDataTool() },
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generate system prompt section describing available components
|
|
199
|
+
*/
|
|
200
|
+
generateSystemPromptSection(catalogId: string): string {
|
|
201
|
+
const catalog = this.catalogs.get(catalogId);
|
|
202
|
+
if (!catalog) {
|
|
203
|
+
throw new Error(`Catalog not found: ${catalogId}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let prompt = `## Available UI Components (${catalog.name || catalogId})\n\n`;
|
|
207
|
+
prompt += `You can create UIs using the following components:\n\n`;
|
|
208
|
+
|
|
209
|
+
for (const [name, component] of Object.entries(catalog.components)) {
|
|
210
|
+
prompt += `### ${name}\n`;
|
|
211
|
+
prompt += `${component.description || 'No description'}\n\n`;
|
|
212
|
+
|
|
213
|
+
if (component.properties) {
|
|
214
|
+
prompt += `Properties:\n`;
|
|
215
|
+
for (const [propName, prop] of Object.entries(component.properties)) {
|
|
216
|
+
const required = component.required?.includes(propName) ? ' (required)' : '';
|
|
217
|
+
const enumValues = prop.enum ? ` [${prop.enum.join(', ')}]` : '';
|
|
218
|
+
prompt += `- \`${propName}\` (${prop.type}${required}): ${prop.description || ''}${enumValues}\n`;
|
|
219
|
+
}
|
|
220
|
+
prompt += '\n';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
prompt += `\n## UI Structure\n`;
|
|
225
|
+
prompt += `Components are defined as a flat array with relationships via 'children' IDs:\n`;
|
|
226
|
+
prompt += `\`\`\`json
|
|
227
|
+
{
|
|
228
|
+
"surfaceId": "my_surface",
|
|
229
|
+
"components": [
|
|
230
|
+
{ "id": "root", "component": "Column", "children": ["title", "content"] },
|
|
231
|
+
{ "id": "title", "component": "Text", "text": "Hello", "variant": "h1" },
|
|
232
|
+
{ "id": "content", "component": "Text", "text": "World" }
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
\`\`\`\n`;
|
|
236
|
+
|
|
237
|
+
return prompt;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ===========================================================================
|
|
241
|
+
// Private Methods
|
|
242
|
+
// ===========================================================================
|
|
243
|
+
|
|
244
|
+
private buildComponentSchemas(catalog: CatalogDefinition): Record<string, unknown> {
|
|
245
|
+
const allProperties: Record<string, unknown> = {};
|
|
246
|
+
|
|
247
|
+
for (const component of Object.values(catalog.components)) {
|
|
248
|
+
if (component.properties) {
|
|
249
|
+
for (const [propName, prop] of Object.entries(component.properties)) {
|
|
250
|
+
// Merge property schemas (last wins for duplicates)
|
|
251
|
+
allProperties[propName] = this.convertPropertySchema(prop);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return allProperties;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private convertPropertySchema(prop: PropertySchema): Record<string, unknown> {
|
|
260
|
+
const schema: Record<string, unknown> = {
|
|
261
|
+
type: prop.type === 'integer' ? 'number' : prop.type,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (prop.description) {
|
|
265
|
+
schema.description = prop.description;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (prop.enum) {
|
|
269
|
+
schema.enum = prop.enum;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (prop.items) {
|
|
273
|
+
schema.items = this.convertPropertySchema(prop.items);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return schema;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// =============================================================================
|
|
281
|
+
// Tool Execution Helpers
|
|
282
|
+
// =============================================================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Convert render_ui tool call to A2UX messages
|
|
286
|
+
*/
|
|
287
|
+
export function toolCallToMessages(
|
|
288
|
+
toolCall: { surfaceId: string; components: A2UXComponent[] },
|
|
289
|
+
catalogId: string
|
|
290
|
+
): ServerToClientMessage[] {
|
|
291
|
+
return [
|
|
292
|
+
{ createSurface: { surfaceId: toolCall.surfaceId, catalogId } },
|
|
293
|
+
{ updateComponents: { surfaceId: toolCall.surfaceId, components: toolCall.components } },
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Convert update_data tool call to A2UX message
|
|
299
|
+
*/
|
|
300
|
+
export function updateDataToMessage(
|
|
301
|
+
toolCall: { surfaceId: string; path?: string; value: unknown; operation?: string }
|
|
302
|
+
): ServerToClientMessage {
|
|
303
|
+
return {
|
|
304
|
+
updateDataModel: {
|
|
305
|
+
surfaceId: toolCall.surfaceId,
|
|
306
|
+
path: toolCall.path ?? '/',
|
|
307
|
+
op: (toolCall.operation as 'add' | 'replace' | 'remove') ?? 'replace',
|
|
308
|
+
value: toolCall.value,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freesail Server
|
|
3
|
+
*
|
|
4
|
+
* SSE Stream Engine & Framework Adapters for A2UX Protocol.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Stream
|
|
8
|
+
export { FreesailStream, StreamStore } from './stream.js';
|
|
9
|
+
export type { StreamOptions, StreamStats } from './stream.js';
|
|
10
|
+
|
|
11
|
+
// Catalog Loader
|
|
12
|
+
export {
|
|
13
|
+
CatalogToToolConverter,
|
|
14
|
+
loadCatalog,
|
|
15
|
+
toolCallToMessages,
|
|
16
|
+
updateDataToMessage,
|
|
17
|
+
} from './catalog-loader.js';
|
|
18
|
+
export type {
|
|
19
|
+
CatalogDefinition,
|
|
20
|
+
CatalogComponent,
|
|
21
|
+
PropertySchema,
|
|
22
|
+
ToolSchema,
|
|
23
|
+
OpenAITool,
|
|
24
|
+
} from './catalog-loader.js';
|
|
25
|
+
|
|
26
|
+
// Adapters
|
|
27
|
+
export {
|
|
28
|
+
// LangChain
|
|
29
|
+
createLangChainTools,
|
|
30
|
+
createLangChainRenderUITool,
|
|
31
|
+
createLangChainUpdateDataTool,
|
|
32
|
+
// OpenAI
|
|
33
|
+
OpenAIAdapter,
|
|
34
|
+
createOpenAIAdapter,
|
|
35
|
+
processOpenAIResponse,
|
|
36
|
+
} from './adapters/index.js';
|
|
37
|
+
|
|
38
|
+
export type {
|
|
39
|
+
LangChainAdapterOptions,
|
|
40
|
+
OpenAIAdapterOptions,
|
|
41
|
+
ToolCallResult,
|
|
42
|
+
} from './adapters/index.js';
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Express Middleware Helper
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
49
|
+
import { FreesailStream, StreamStore } from './stream.js';
|
|
50
|
+
|
|
51
|
+
export interface ExpressRequest extends IncomingMessage {
|
|
52
|
+
body?: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ExpressResponse extends ServerResponse {
|
|
56
|
+
// Express adds these
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const globalStreamStore = new StreamStore();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create SSE endpoint middleware for Express
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* import express from 'express';
|
|
67
|
+
* import { createSSEHandler } from '@freesail/server';
|
|
68
|
+
*
|
|
69
|
+
* const app = express();
|
|
70
|
+
*
|
|
71
|
+
* app.get('/api/stream', createSSEHandler(async (stream, req) => {
|
|
72
|
+
* // Your agent logic here
|
|
73
|
+
* stream.createSurface('main', 'standard_catalog_v1');
|
|
74
|
+
* stream.updateComponents('main', [...]);
|
|
75
|
+
* }));
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function createSSEHandler(
|
|
79
|
+
handler: (stream: FreesailStream, req: ExpressRequest) => Promise<void>,
|
|
80
|
+
options?: { debug?: boolean }
|
|
81
|
+
): (req: ExpressRequest, res: ExpressResponse) => void {
|
|
82
|
+
return async (req: ExpressRequest, res: ExpressResponse) => {
|
|
83
|
+
const stream = globalStreamStore.create({
|
|
84
|
+
response: res,
|
|
85
|
+
debug: options?.debug,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await handler(stream, req);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('[Freesail SSE Handler Error]', error);
|
|
92
|
+
stream.sendRaw('error', { message: String(error) });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create action endpoint middleware for Express
|
|
99
|
+
* Handles userAction messages from clients.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* app.post('/api/action', express.json(), createActionHandler(async (action, req) => {
|
|
104
|
+
* console.log('User action:', action);
|
|
105
|
+
* // Handle action and potentially trigger new UI updates
|
|
106
|
+
* }));
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function createActionHandler(
|
|
110
|
+
handler: (
|
|
111
|
+
action: { surfaceId: string; action: string; context: Record<string, unknown> },
|
|
112
|
+
req: ExpressRequest
|
|
113
|
+
) => Promise<void>
|
|
114
|
+
): (req: ExpressRequest, res: ExpressResponse) => void {
|
|
115
|
+
return async (req: ExpressRequest, res: ExpressResponse) => {
|
|
116
|
+
try {
|
|
117
|
+
const body = req.body as { userAction?: { surfaceId: string; action: string; context: Record<string, unknown> } };
|
|
118
|
+
|
|
119
|
+
if (!body?.userAction) {
|
|
120
|
+
res.statusCode = 400;
|
|
121
|
+
res.end(JSON.stringify({ error: 'Invalid request: missing userAction' }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await handler(body.userAction, req);
|
|
126
|
+
|
|
127
|
+
res.statusCode = 200;
|
|
128
|
+
res.end(JSON.stringify({ success: true }));
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('[Freesail Action Handler Error]', error);
|
|
131
|
+
res.statusCode = 500;
|
|
132
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the global stream store
|
|
139
|
+
*/
|
|
140
|
+
export function getStreamStore(): StreamStore {
|
|
141
|
+
return globalStreamStore;
|
|
142
|
+
}
|