@matthesketh/utopia-ai 0.0.5 → 0.2.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/dist/adapters/anthropic.cjs +20 -13
- package/dist/adapters/anthropic.js +20 -13
- package/dist/adapters/google.cjs +17 -11
- package/dist/adapters/google.js +17 -11
- package/dist/adapters/ollama.cjs +31 -20
- package/dist/adapters/ollama.js +31 -20
- package/dist/adapters/openai.cjs +18 -7
- package/dist/adapters/openai.js +18 -7
- package/dist/index.cjs +58 -38
- package/dist/index.js +58 -38
- package/dist/mcp/index.cjs +44 -35
- package/dist/mcp/index.d.cts +0 -21
- package/dist/mcp/index.d.ts +0 -21
- package/dist/mcp/index.js +44 -35
- package/package.json +4 -4
|
@@ -37,16 +37,16 @@ function anthropicAdapter(config) {
|
|
|
37
37
|
let client = null;
|
|
38
38
|
async function getClient() {
|
|
39
39
|
if (client) return client;
|
|
40
|
-
let
|
|
40
|
+
let AnthropicCtor;
|
|
41
41
|
try {
|
|
42
42
|
const mod = await import("@anthropic-ai/sdk");
|
|
43
|
-
|
|
43
|
+
AnthropicCtor = mod.Anthropic ?? mod.default;
|
|
44
44
|
} catch {
|
|
45
45
|
throw new Error(
|
|
46
46
|
'@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
-
client = new
|
|
49
|
+
client = new AnthropicCtor({
|
|
50
50
|
apiKey: config.apiKey,
|
|
51
51
|
...config.baseURL ? { baseURL: config.baseURL } : {}
|
|
52
52
|
});
|
|
@@ -73,17 +73,22 @@ function anthropicAdapter(config) {
|
|
|
73
73
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
const response = await anthropic.messages.create(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
const response = await anthropic.messages.create(
|
|
77
|
+
body
|
|
78
|
+
);
|
|
79
|
+
if (!response.content || !Array.isArray(response.content)) {
|
|
80
|
+
throw new Error("Anthropic returned invalid response: missing content array");
|
|
81
|
+
}
|
|
82
|
+
const contentBlocks = response.content;
|
|
83
|
+
const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
84
|
+
const toolCalls = contentBlocks.filter((b) => b.type === "tool_use").map((b) => {
|
|
85
|
+
const tu = b;
|
|
86
|
+
return { id: tu.id, name: tu.name, arguments: tu.input ?? {} };
|
|
87
|
+
});
|
|
83
88
|
return {
|
|
84
89
|
content: textContent,
|
|
85
90
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
86
|
-
finishReason: mapStopReason(response.stop_reason),
|
|
91
|
+
finishReason: mapStopReason(response.stop_reason ?? ""),
|
|
87
92
|
usage: response.usage ? {
|
|
88
93
|
promptTokens: response.usage.input_tokens,
|
|
89
94
|
completionTokens: response.usage.output_tokens,
|
|
@@ -113,7 +118,9 @@ function anthropicAdapter(config) {
|
|
|
113
118
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
|
-
const stream = anthropic.messages.stream(
|
|
121
|
+
const stream = anthropic.messages.stream(
|
|
122
|
+
body
|
|
123
|
+
);
|
|
117
124
|
let promptTokens = 0;
|
|
118
125
|
for await (const event of stream) {
|
|
119
126
|
if (event.type === "message_start" && event.message?.usage) {
|
|
@@ -144,7 +151,7 @@ function anthropicAdapter(config) {
|
|
|
144
151
|
const outputTokens = event.usage?.output_tokens ?? 0;
|
|
145
152
|
yield {
|
|
146
153
|
delta: "",
|
|
147
|
-
finishReason: mapStopReason(event.delta.stop_reason),
|
|
154
|
+
finishReason: mapStopReason(event.delta.stop_reason ?? ""),
|
|
148
155
|
usage: event.usage ? {
|
|
149
156
|
promptTokens,
|
|
150
157
|
completionTokens: outputTokens,
|
|
@@ -3,16 +3,16 @@ function anthropicAdapter(config) {
|
|
|
3
3
|
let client = null;
|
|
4
4
|
async function getClient() {
|
|
5
5
|
if (client) return client;
|
|
6
|
-
let
|
|
6
|
+
let AnthropicCtor;
|
|
7
7
|
try {
|
|
8
8
|
const mod = await import("@anthropic-ai/sdk");
|
|
9
|
-
|
|
9
|
+
AnthropicCtor = mod.Anthropic ?? mod.default;
|
|
10
10
|
} catch {
|
|
11
11
|
throw new Error(
|
|
12
12
|
'@matthesketh/utopia-ai: "@anthropic-ai/sdk" package is required for the Anthropic adapter. Install it with: npm install @anthropic-ai/sdk'
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
|
-
client = new
|
|
15
|
+
client = new AnthropicCtor({
|
|
16
16
|
apiKey: config.apiKey,
|
|
17
17
|
...config.baseURL ? { baseURL: config.baseURL } : {}
|
|
18
18
|
});
|
|
@@ -39,17 +39,22 @@ function anthropicAdapter(config) {
|
|
|
39
39
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
const response = await anthropic.messages.create(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
const response = await anthropic.messages.create(
|
|
43
|
+
body
|
|
44
|
+
);
|
|
45
|
+
if (!response.content || !Array.isArray(response.content)) {
|
|
46
|
+
throw new Error("Anthropic returned invalid response: missing content array");
|
|
47
|
+
}
|
|
48
|
+
const contentBlocks = response.content;
|
|
49
|
+
const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
50
|
+
const toolCalls = contentBlocks.filter((b) => b.type === "tool_use").map((b) => {
|
|
51
|
+
const tu = b;
|
|
52
|
+
return { id: tu.id, name: tu.name, arguments: tu.input ?? {} };
|
|
53
|
+
});
|
|
49
54
|
return {
|
|
50
55
|
content: textContent,
|
|
51
56
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
52
|
-
finishReason: mapStopReason(response.stop_reason),
|
|
57
|
+
finishReason: mapStopReason(response.stop_reason ?? ""),
|
|
53
58
|
usage: response.usage ? {
|
|
54
59
|
promptTokens: response.usage.input_tokens,
|
|
55
60
|
completionTokens: response.usage.output_tokens,
|
|
@@ -79,7 +84,9 @@ function anthropicAdapter(config) {
|
|
|
79
84
|
body.tool_choice = toAnthropicToolChoice(request.toolChoice);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
const stream = anthropic.messages.stream(
|
|
87
|
+
const stream = anthropic.messages.stream(
|
|
88
|
+
body
|
|
89
|
+
);
|
|
83
90
|
let promptTokens = 0;
|
|
84
91
|
for await (const event of stream) {
|
|
85
92
|
if (event.type === "message_start" && event.message?.usage) {
|
|
@@ -110,7 +117,7 @@ function anthropicAdapter(config) {
|
|
|
110
117
|
const outputTokens = event.usage?.output_tokens ?? 0;
|
|
111
118
|
yield {
|
|
112
119
|
delta: "",
|
|
113
|
-
finishReason: mapStopReason(event.delta.stop_reason),
|
|
120
|
+
finishReason: mapStopReason(event.delta.stop_reason ?? ""),
|
|
114
121
|
usage: event.usage ? {
|
|
115
122
|
promptTokens,
|
|
116
123
|
completionTokens: outputTokens,
|
package/dist/adapters/google.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(google_exports, {
|
|
|
33
33
|
googleAdapter: () => googleAdapter
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(google_exports);
|
|
36
|
+
var toolCallCounter = 0;
|
|
36
37
|
function googleAdapter(config) {
|
|
37
38
|
let genAI = null;
|
|
38
39
|
async function getGenAI() {
|
|
@@ -72,13 +73,16 @@ function googleAdapter(config) {
|
|
|
72
73
|
const response = result.response;
|
|
73
74
|
const candidate = response.candidates?.[0];
|
|
74
75
|
const parts = candidate?.content?.parts ?? [];
|
|
75
|
-
const textParts = parts.filter((p) => p.text).map((p) => p.text);
|
|
76
|
-
const fnCalls = parts.filter((p) => p.functionCall);
|
|
77
|
-
const toolCalls = fnCalls.map((p) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
const textParts = parts.filter((p) => "text" in p && p.text).map((p) => p.text);
|
|
77
|
+
const fnCalls = parts.filter((p) => "functionCall" in p && p.functionCall);
|
|
78
|
+
const toolCalls = fnCalls.map((p) => {
|
|
79
|
+
const fc = p.functionCall;
|
|
80
|
+
return {
|
|
81
|
+
id: `call_${++toolCallCounter}_${Date.now().toString(36)}`,
|
|
82
|
+
name: fc.name,
|
|
83
|
+
arguments: fc.args ?? {}
|
|
84
|
+
};
|
|
85
|
+
});
|
|
82
86
|
return {
|
|
83
87
|
content: textParts.join(""),
|
|
84
88
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -112,7 +116,7 @@ function googleAdapter(config) {
|
|
|
112
116
|
});
|
|
113
117
|
for await (const chunk of result.stream) {
|
|
114
118
|
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
|
|
115
|
-
const text = parts.filter((p) => p.text).map((p) => p.text).join("");
|
|
119
|
+
const text = parts.filter((p) => "text" in p && p.text).map((p) => p.text).join("");
|
|
116
120
|
const finishReason = chunk.candidates?.[0]?.finishReason;
|
|
117
121
|
yield {
|
|
118
122
|
delta: text,
|
|
@@ -132,9 +136,11 @@ function googleAdapter(config) {
|
|
|
132
136
|
});
|
|
133
137
|
const inputs = Array.isArray(request.input) ? request.input : [request.input];
|
|
134
138
|
const result = await model.batchEmbedContents({
|
|
135
|
-
requests: inputs.map(
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
requests: inputs.map(
|
|
140
|
+
(text) => ({
|
|
141
|
+
content: { parts: [{ text }], role: "user" }
|
|
142
|
+
})
|
|
143
|
+
)
|
|
138
144
|
});
|
|
139
145
|
return {
|
|
140
146
|
embeddings: result.embeddings.map((e) => e.values),
|
package/dist/adapters/google.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapters/google.ts
|
|
2
|
+
var toolCallCounter = 0;
|
|
2
3
|
function googleAdapter(config) {
|
|
3
4
|
let genAI = null;
|
|
4
5
|
async function getGenAI() {
|
|
@@ -38,13 +39,16 @@ function googleAdapter(config) {
|
|
|
38
39
|
const response = result.response;
|
|
39
40
|
const candidate = response.candidates?.[0];
|
|
40
41
|
const parts = candidate?.content?.parts ?? [];
|
|
41
|
-
const textParts = parts.filter((p) => p.text).map((p) => p.text);
|
|
42
|
-
const fnCalls = parts.filter((p) => p.functionCall);
|
|
43
|
-
const toolCalls = fnCalls.map((p) =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
const textParts = parts.filter((p) => "text" in p && p.text).map((p) => p.text);
|
|
43
|
+
const fnCalls = parts.filter((p) => "functionCall" in p && p.functionCall);
|
|
44
|
+
const toolCalls = fnCalls.map((p) => {
|
|
45
|
+
const fc = p.functionCall;
|
|
46
|
+
return {
|
|
47
|
+
id: `call_${++toolCallCounter}_${Date.now().toString(36)}`,
|
|
48
|
+
name: fc.name,
|
|
49
|
+
arguments: fc.args ?? {}
|
|
50
|
+
};
|
|
51
|
+
});
|
|
48
52
|
return {
|
|
49
53
|
content: textParts.join(""),
|
|
50
54
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -78,7 +82,7 @@ function googleAdapter(config) {
|
|
|
78
82
|
});
|
|
79
83
|
for await (const chunk of result.stream) {
|
|
80
84
|
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
|
|
81
|
-
const text = parts.filter((p) => p.text).map((p) => p.text).join("");
|
|
85
|
+
const text = parts.filter((p) => "text" in p && p.text).map((p) => p.text).join("");
|
|
82
86
|
const finishReason = chunk.candidates?.[0]?.finishReason;
|
|
83
87
|
yield {
|
|
84
88
|
delta: text,
|
|
@@ -98,9 +102,11 @@ function googleAdapter(config) {
|
|
|
98
102
|
});
|
|
99
103
|
const inputs = Array.isArray(request.input) ? request.input : [request.input];
|
|
100
104
|
const result = await model.batchEmbedContents({
|
|
101
|
-
requests: inputs.map(
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
requests: inputs.map(
|
|
106
|
+
(text) => ({
|
|
107
|
+
content: { parts: [{ text }], role: "user" }
|
|
108
|
+
})
|
|
109
|
+
)
|
|
104
110
|
});
|
|
105
111
|
return {
|
|
106
112
|
embeddings: result.embeddings.map((e) => e.values),
|
package/dist/adapters/ollama.cjs
CHANGED
|
@@ -23,41 +23,42 @@ __export(ollama_exports, {
|
|
|
23
23
|
ollamaAdapter: () => ollamaAdapter
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(ollama_exports);
|
|
26
|
+
var ollamaToolCallCounter = 0;
|
|
26
27
|
function ollamaAdapter(config = {}) {
|
|
27
28
|
const baseURL = (config.baseURL ?? "http://localhost:11434").replace(/\/$/, "");
|
|
28
29
|
return {
|
|
29
30
|
async chat(request) {
|
|
30
31
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
32
|
+
const options = {};
|
|
33
|
+
if (request.temperature !== void 0) options.temperature = request.temperature;
|
|
34
|
+
if (request.topP !== void 0) options.top_p = request.topP;
|
|
35
|
+
if (request.maxTokens !== void 0) options.num_predict = request.maxTokens;
|
|
36
|
+
if (request.stop) options.stop = request.stop;
|
|
31
37
|
const body = {
|
|
32
38
|
model,
|
|
33
39
|
messages: toOllamaMessages(request.messages),
|
|
34
40
|
stream: false,
|
|
35
|
-
options
|
|
41
|
+
options
|
|
36
42
|
};
|
|
37
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
38
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
39
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
40
|
-
if (request.stop) body.options.stop = request.stop;
|
|
41
43
|
if (request.tools?.length) {
|
|
42
44
|
body.tools = request.tools.map(toOllamaTool);
|
|
43
45
|
}
|
|
44
46
|
const response = await fetch(`${baseURL}/api/chat`, {
|
|
45
47
|
method: "POST",
|
|
46
48
|
headers: { "Content-Type": "application/json" },
|
|
47
|
-
body: JSON.stringify(body)
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
signal: AbortSignal.timeout(6e4)
|
|
48
51
|
});
|
|
49
52
|
if (!response.ok) {
|
|
50
53
|
const text = await response.text();
|
|
51
54
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
52
55
|
}
|
|
53
56
|
const data = await response.json();
|
|
54
|
-
const toolCalls = (data.message?.tool_calls ?? []).map(
|
|
55
|
-
(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})
|
|
60
|
-
);
|
|
57
|
+
const toolCalls = (data.message?.tool_calls ?? []).map((tc) => ({
|
|
58
|
+
id: `call_${++ollamaToolCallCounter}_${Date.now().toString(36)}`,
|
|
59
|
+
name: tc.function.name,
|
|
60
|
+
arguments: tc.function.arguments ?? {}
|
|
61
|
+
}));
|
|
61
62
|
return {
|
|
62
63
|
content: data.message?.content ?? "",
|
|
63
64
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -72,28 +73,33 @@ function ollamaAdapter(config = {}) {
|
|
|
72
73
|
},
|
|
73
74
|
async *stream(request) {
|
|
74
75
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
76
|
+
const streamOptions = {};
|
|
77
|
+
if (request.temperature !== void 0) streamOptions.temperature = request.temperature;
|
|
78
|
+
if (request.topP !== void 0) streamOptions.top_p = request.topP;
|
|
79
|
+
if (request.maxTokens !== void 0) streamOptions.num_predict = request.maxTokens;
|
|
80
|
+
if (request.stop) streamOptions.stop = request.stop;
|
|
75
81
|
const body = {
|
|
76
82
|
model,
|
|
77
83
|
messages: toOllamaMessages(request.messages),
|
|
78
84
|
stream: true,
|
|
79
|
-
options:
|
|
85
|
+
options: streamOptions
|
|
80
86
|
};
|
|
81
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
82
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
83
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
84
|
-
if (request.stop) body.options.stop = request.stop;
|
|
85
87
|
if (request.tools?.length) {
|
|
86
88
|
body.tools = request.tools.map(toOllamaTool);
|
|
87
89
|
}
|
|
88
90
|
const response = await fetch(`${baseURL}/api/chat`, {
|
|
89
91
|
method: "POST",
|
|
90
92
|
headers: { "Content-Type": "application/json" },
|
|
91
|
-
body: JSON.stringify(body)
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
signal: AbortSignal.timeout(6e4)
|
|
92
95
|
});
|
|
93
96
|
if (!response.ok) {
|
|
94
97
|
const text = await response.text();
|
|
95
98
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
96
99
|
}
|
|
100
|
+
if (!response.body) {
|
|
101
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
102
|
+
}
|
|
97
103
|
const reader = response.body.getReader();
|
|
98
104
|
const decoder = new TextDecoder();
|
|
99
105
|
let buffer = "";
|
|
@@ -105,7 +111,12 @@ function ollamaAdapter(config = {}) {
|
|
|
105
111
|
buffer = lines.pop() ?? "";
|
|
106
112
|
for (const line of lines) {
|
|
107
113
|
if (!line.trim()) continue;
|
|
108
|
-
|
|
114
|
+
let data;
|
|
115
|
+
try {
|
|
116
|
+
data = JSON.parse(line);
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
109
120
|
yield {
|
|
110
121
|
delta: data.message?.content ?? "",
|
|
111
122
|
finishReason: data.done ? "stop" : void 0,
|
package/dist/adapters/ollama.js
CHANGED
|
@@ -1,39 +1,40 @@
|
|
|
1
1
|
// src/adapters/ollama.ts
|
|
2
|
+
var ollamaToolCallCounter = 0;
|
|
2
3
|
function ollamaAdapter(config = {}) {
|
|
3
4
|
const baseURL = (config.baseURL ?? "http://localhost:11434").replace(/\/$/, "");
|
|
4
5
|
return {
|
|
5
6
|
async chat(request) {
|
|
6
7
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
8
|
+
const options = {};
|
|
9
|
+
if (request.temperature !== void 0) options.temperature = request.temperature;
|
|
10
|
+
if (request.topP !== void 0) options.top_p = request.topP;
|
|
11
|
+
if (request.maxTokens !== void 0) options.num_predict = request.maxTokens;
|
|
12
|
+
if (request.stop) options.stop = request.stop;
|
|
7
13
|
const body = {
|
|
8
14
|
model,
|
|
9
15
|
messages: toOllamaMessages(request.messages),
|
|
10
16
|
stream: false,
|
|
11
|
-
options
|
|
17
|
+
options
|
|
12
18
|
};
|
|
13
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
14
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
15
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
16
|
-
if (request.stop) body.options.stop = request.stop;
|
|
17
19
|
if (request.tools?.length) {
|
|
18
20
|
body.tools = request.tools.map(toOllamaTool);
|
|
19
21
|
}
|
|
20
22
|
const response = await fetch(`${baseURL}/api/chat`, {
|
|
21
23
|
method: "POST",
|
|
22
24
|
headers: { "Content-Type": "application/json" },
|
|
23
|
-
body: JSON.stringify(body)
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
signal: AbortSignal.timeout(6e4)
|
|
24
27
|
});
|
|
25
28
|
if (!response.ok) {
|
|
26
29
|
const text = await response.text();
|
|
27
30
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
28
31
|
}
|
|
29
32
|
const data = await response.json();
|
|
30
|
-
const toolCalls = (data.message?.tool_calls ?? []).map(
|
|
31
|
-
(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
})
|
|
36
|
-
);
|
|
33
|
+
const toolCalls = (data.message?.tool_calls ?? []).map((tc) => ({
|
|
34
|
+
id: `call_${++ollamaToolCallCounter}_${Date.now().toString(36)}`,
|
|
35
|
+
name: tc.function.name,
|
|
36
|
+
arguments: tc.function.arguments ?? {}
|
|
37
|
+
}));
|
|
37
38
|
return {
|
|
38
39
|
content: data.message?.content ?? "",
|
|
39
40
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
@@ -48,28 +49,33 @@ function ollamaAdapter(config = {}) {
|
|
|
48
49
|
},
|
|
49
50
|
async *stream(request) {
|
|
50
51
|
const model = request.model ?? config.defaultModel ?? "llama3.2";
|
|
52
|
+
const streamOptions = {};
|
|
53
|
+
if (request.temperature !== void 0) streamOptions.temperature = request.temperature;
|
|
54
|
+
if (request.topP !== void 0) streamOptions.top_p = request.topP;
|
|
55
|
+
if (request.maxTokens !== void 0) streamOptions.num_predict = request.maxTokens;
|
|
56
|
+
if (request.stop) streamOptions.stop = request.stop;
|
|
51
57
|
const body = {
|
|
52
58
|
model,
|
|
53
59
|
messages: toOllamaMessages(request.messages),
|
|
54
60
|
stream: true,
|
|
55
|
-
options:
|
|
61
|
+
options: streamOptions
|
|
56
62
|
};
|
|
57
|
-
if (request.temperature !== void 0) body.options.temperature = request.temperature;
|
|
58
|
-
if (request.topP !== void 0) body.options.top_p = request.topP;
|
|
59
|
-
if (request.maxTokens !== void 0) body.options.num_predict = request.maxTokens;
|
|
60
|
-
if (request.stop) body.options.stop = request.stop;
|
|
61
63
|
if (request.tools?.length) {
|
|
62
64
|
body.tools = request.tools.map(toOllamaTool);
|
|
63
65
|
}
|
|
64
66
|
const response = await fetch(`${baseURL}/api/chat`, {
|
|
65
67
|
method: "POST",
|
|
66
68
|
headers: { "Content-Type": "application/json" },
|
|
67
|
-
body: JSON.stringify(body)
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
signal: AbortSignal.timeout(6e4)
|
|
68
71
|
});
|
|
69
72
|
if (!response.ok) {
|
|
70
73
|
const text = await response.text();
|
|
71
74
|
throw new Error(`Ollama error ${response.status}: ${text}`);
|
|
72
75
|
}
|
|
76
|
+
if (!response.body) {
|
|
77
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
78
|
+
}
|
|
73
79
|
const reader = response.body.getReader();
|
|
74
80
|
const decoder = new TextDecoder();
|
|
75
81
|
let buffer = "";
|
|
@@ -81,7 +87,12 @@ function ollamaAdapter(config = {}) {
|
|
|
81
87
|
buffer = lines.pop() ?? "";
|
|
82
88
|
for (const line of lines) {
|
|
83
89
|
if (!line.trim()) continue;
|
|
84
|
-
|
|
90
|
+
let data;
|
|
91
|
+
try {
|
|
92
|
+
data = JSON.parse(line);
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
85
96
|
yield {
|
|
86
97
|
delta: data.message?.content ?? "",
|
|
87
98
|
finishReason: data.done ? "stop" : void 0,
|
package/dist/adapters/openai.cjs
CHANGED
|
@@ -37,16 +37,16 @@ function openaiAdapter(config) {
|
|
|
37
37
|
let client = null;
|
|
38
38
|
async function getClient() {
|
|
39
39
|
if (client) return client;
|
|
40
|
-
let
|
|
40
|
+
let OpenAICtor;
|
|
41
41
|
try {
|
|
42
42
|
const mod = await import("openai");
|
|
43
|
-
|
|
43
|
+
OpenAICtor = mod.OpenAI ?? mod.default;
|
|
44
44
|
} catch {
|
|
45
45
|
throw new Error(
|
|
46
46
|
'@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
-
client = new
|
|
49
|
+
client = new OpenAICtor({
|
|
50
50
|
apiKey: config.apiKey,
|
|
51
51
|
baseURL: config.baseURL,
|
|
52
52
|
organization: config.organization
|
|
@@ -72,11 +72,20 @@ function openaiAdapter(config) {
|
|
|
72
72
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
const response = await openai.chat.completions.create(
|
|
75
|
+
const response = await openai.chat.completions.create(
|
|
76
|
+
body
|
|
77
|
+
);
|
|
78
|
+
if (!response.choices?.length) {
|
|
79
|
+
throw new Error("OpenAI returned empty choices array");
|
|
80
|
+
}
|
|
76
81
|
const choice = response.choices[0];
|
|
77
82
|
return {
|
|
78
83
|
content: choice.message.content ?? "",
|
|
79
|
-
toolCalls: choice.message.tool_calls?.map(
|
|
84
|
+
toolCalls: choice.message.tool_calls?.map(
|
|
85
|
+
(tc) => fromOpenAIToolCall(
|
|
86
|
+
tc
|
|
87
|
+
)
|
|
88
|
+
),
|
|
80
89
|
finishReason: mapFinishReason(choice.finish_reason),
|
|
81
90
|
usage: response.usage ? {
|
|
82
91
|
promptTokens: response.usage.prompt_tokens,
|
|
@@ -106,7 +115,9 @@ function openaiAdapter(config) {
|
|
|
106
115
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
|
-
const stream = await openai.chat.completions.create(
|
|
118
|
+
const stream = await openai.chat.completions.create(
|
|
119
|
+
body
|
|
120
|
+
);
|
|
110
121
|
for await (const chunk of stream) {
|
|
111
122
|
const delta = chunk.choices?.[0]?.delta;
|
|
112
123
|
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
@@ -218,7 +229,7 @@ function toOpenAIToolChoice(choice) {
|
|
|
218
229
|
if (choice && typeof choice === "object" && "name" in choice) {
|
|
219
230
|
return { type: "function", function: { name: choice.name } };
|
|
220
231
|
}
|
|
221
|
-
return
|
|
232
|
+
return void 0;
|
|
222
233
|
}
|
|
223
234
|
function fromOpenAIToolCall(tc) {
|
|
224
235
|
return {
|
package/dist/adapters/openai.js
CHANGED
|
@@ -3,16 +3,16 @@ function openaiAdapter(config) {
|
|
|
3
3
|
let client = null;
|
|
4
4
|
async function getClient() {
|
|
5
5
|
if (client) return client;
|
|
6
|
-
let
|
|
6
|
+
let OpenAICtor;
|
|
7
7
|
try {
|
|
8
8
|
const mod = await import("openai");
|
|
9
|
-
|
|
9
|
+
OpenAICtor = mod.OpenAI ?? mod.default;
|
|
10
10
|
} catch {
|
|
11
11
|
throw new Error(
|
|
12
12
|
'@matthesketh/utopia-ai: "openai" package is required for the OpenAI adapter. Install it with: npm install openai'
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
|
-
client = new
|
|
15
|
+
client = new OpenAICtor({
|
|
16
16
|
apiKey: config.apiKey,
|
|
17
17
|
baseURL: config.baseURL,
|
|
18
18
|
organization: config.organization
|
|
@@ -38,11 +38,20 @@ function openaiAdapter(config) {
|
|
|
38
38
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
const response = await openai.chat.completions.create(
|
|
41
|
+
const response = await openai.chat.completions.create(
|
|
42
|
+
body
|
|
43
|
+
);
|
|
44
|
+
if (!response.choices?.length) {
|
|
45
|
+
throw new Error("OpenAI returned empty choices array");
|
|
46
|
+
}
|
|
42
47
|
const choice = response.choices[0];
|
|
43
48
|
return {
|
|
44
49
|
content: choice.message.content ?? "",
|
|
45
|
-
toolCalls: choice.message.tool_calls?.map(
|
|
50
|
+
toolCalls: choice.message.tool_calls?.map(
|
|
51
|
+
(tc) => fromOpenAIToolCall(
|
|
52
|
+
tc
|
|
53
|
+
)
|
|
54
|
+
),
|
|
46
55
|
finishReason: mapFinishReason(choice.finish_reason),
|
|
47
56
|
usage: response.usage ? {
|
|
48
57
|
promptTokens: response.usage.prompt_tokens,
|
|
@@ -72,7 +81,9 @@ function openaiAdapter(config) {
|
|
|
72
81
|
body.tool_choice = toOpenAIToolChoice(request.toolChoice);
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
|
-
const stream = await openai.chat.completions.create(
|
|
84
|
+
const stream = await openai.chat.completions.create(
|
|
85
|
+
body
|
|
86
|
+
);
|
|
76
87
|
for await (const chunk of stream) {
|
|
77
88
|
const delta = chunk.choices?.[0]?.delta;
|
|
78
89
|
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
@@ -184,7 +195,7 @@ function toOpenAIToolChoice(choice) {
|
|
|
184
195
|
if (choice && typeof choice === "object" && "name" in choice) {
|
|
185
196
|
return { type: "function", function: { name: choice.name } };
|
|
186
197
|
}
|
|
187
|
-
return
|
|
198
|
+
return void 0;
|
|
188
199
|
}
|
|
189
200
|
function fromOpenAIToolCall(tc) {
|
|
190
201
|
return {
|
package/dist/index.cjs
CHANGED
|
@@ -44,7 +44,10 @@ function createAI(adapter, options) {
|
|
|
44
44
|
}
|
|
45
45
|
return response;
|
|
46
46
|
} catch (err) {
|
|
47
|
-
hooks?.onError?.(err
|
|
47
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
48
|
+
method: "chat",
|
|
49
|
+
request: req
|
|
50
|
+
});
|
|
48
51
|
throw err;
|
|
49
52
|
}
|
|
50
53
|
},
|
|
@@ -58,7 +61,10 @@ function createAI(adapter, options) {
|
|
|
58
61
|
try {
|
|
59
62
|
yield* source.call(adapter, req);
|
|
60
63
|
} catch (err) {
|
|
61
|
-
hooks?.onError?.(err
|
|
64
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
65
|
+
method: "stream",
|
|
66
|
+
request: req
|
|
67
|
+
});
|
|
62
68
|
throw err;
|
|
63
69
|
}
|
|
64
70
|
};
|
|
@@ -71,12 +77,7 @@ function createAI(adapter, options) {
|
|
|
71
77
|
return adapter.embeddings(request);
|
|
72
78
|
},
|
|
73
79
|
async run(options2) {
|
|
74
|
-
const {
|
|
75
|
-
tools,
|
|
76
|
-
maxRounds = 10,
|
|
77
|
-
onToolCall,
|
|
78
|
-
...requestBase
|
|
79
|
-
} = options2;
|
|
80
|
+
const { tools, maxRounds = 10, onToolCall, ...requestBase } = options2;
|
|
80
81
|
const messages = [...options2.messages];
|
|
81
82
|
const toolDefs = tools.map((t) => t.definition);
|
|
82
83
|
const handlerMap = new Map(tools.map((t) => [t.definition.name, t.handler]));
|
|
@@ -95,7 +96,10 @@ function createAI(adapter, options) {
|
|
|
95
96
|
try {
|
|
96
97
|
response = await withRetry(() => adapter.chat(req2), retry);
|
|
97
98
|
} catch (err) {
|
|
98
|
-
hooks?.onError?.(err
|
|
99
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
100
|
+
method: "run",
|
|
101
|
+
request: req2
|
|
102
|
+
});
|
|
99
103
|
throw err;
|
|
100
104
|
}
|
|
101
105
|
if (hooks?.onAfterChat) {
|
|
@@ -124,19 +128,21 @@ function createAI(adapter, options) {
|
|
|
124
128
|
try {
|
|
125
129
|
result = await handler(call.arguments);
|
|
126
130
|
} catch (err) {
|
|
127
|
-
result = err.message
|
|
131
|
+
result = err instanceof Error ? err.message : String(err);
|
|
128
132
|
isError = true;
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
onToolCall?.(call, result);
|
|
132
136
|
messages.push({
|
|
133
137
|
role: "tool",
|
|
134
|
-
content: [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "tool_result",
|
|
141
|
+
id: call.id,
|
|
142
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
143
|
+
isError
|
|
144
|
+
}
|
|
145
|
+
]
|
|
140
146
|
});
|
|
141
147
|
}
|
|
142
148
|
}
|
|
@@ -154,7 +160,10 @@ function createAI(adapter, options) {
|
|
|
154
160
|
try {
|
|
155
161
|
finalResponse = await withRetry(() => adapter.chat(req), retry);
|
|
156
162
|
} catch (err) {
|
|
157
|
-
hooks?.onError?.(err
|
|
163
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
164
|
+
method: "run",
|
|
165
|
+
request: req
|
|
166
|
+
});
|
|
158
167
|
throw err;
|
|
159
168
|
}
|
|
160
169
|
if (hooks?.onAfterChat) {
|
|
@@ -173,8 +182,8 @@ async function withRetry(fn, config) {
|
|
|
173
182
|
try {
|
|
174
183
|
return await fn();
|
|
175
184
|
} catch (err) {
|
|
176
|
-
lastError = err;
|
|
177
|
-
if (attempt < maxRetries && shouldRetry(
|
|
185
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
186
|
+
if (attempt < maxRetries && shouldRetry(lastError)) {
|
|
178
187
|
await sleep(baseDelay * Math.pow(2, attempt));
|
|
179
188
|
continue;
|
|
180
189
|
}
|
|
@@ -204,17 +213,20 @@ async function streamSSE(res, stream, options) {
|
|
|
204
213
|
res.writeHead(200, {
|
|
205
214
|
"Content-Type": "text/event-stream",
|
|
206
215
|
"Cache-Control": "no-cache",
|
|
207
|
-
|
|
216
|
+
Connection: "keep-alive",
|
|
208
217
|
"X-Accel-Buffering": "no"
|
|
209
218
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
try {
|
|
220
|
+
for await (const chunk of stream) {
|
|
221
|
+
options?.onChunk?.(chunk);
|
|
222
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
213
223
|
|
|
214
224
|
`);
|
|
225
|
+
}
|
|
226
|
+
res.write("data: [DONE]\n\n");
|
|
227
|
+
} finally {
|
|
228
|
+
res.end();
|
|
215
229
|
}
|
|
216
|
-
res.write("data: [DONE]\n\n");
|
|
217
|
-
res.end();
|
|
218
230
|
}
|
|
219
231
|
async function collectStream(stream) {
|
|
220
232
|
let result = "";
|
|
@@ -224,25 +236,33 @@ async function collectStream(stream) {
|
|
|
224
236
|
return result;
|
|
225
237
|
}
|
|
226
238
|
async function* parseSSEStream(response) {
|
|
239
|
+
if (!response.body) {
|
|
240
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
241
|
+
}
|
|
227
242
|
const reader = response.body.getReader();
|
|
228
243
|
const decoder = new TextDecoder();
|
|
229
244
|
let buffer = "";
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
try {
|
|
246
|
+
while (true) {
|
|
247
|
+
const { done, value } = await reader.read();
|
|
248
|
+
if (done) break;
|
|
249
|
+
buffer += decoder.decode(value, { stream: true });
|
|
250
|
+
const lines = buffer.split("\n");
|
|
251
|
+
buffer = lines.pop() ?? "";
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
if (line.startsWith("data: ")) {
|
|
254
|
+
const data = line.slice(6).trim();
|
|
255
|
+
if (data === "[DONE]") return;
|
|
256
|
+
try {
|
|
257
|
+
yield JSON.parse(data);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
243
260
|
}
|
|
244
261
|
}
|
|
245
262
|
}
|
|
263
|
+
} finally {
|
|
264
|
+
reader.cancel().catch(() => {
|
|
265
|
+
});
|
|
246
266
|
}
|
|
247
267
|
}
|
|
248
268
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,10 @@ function createAI(adapter, options) {
|
|
|
15
15
|
}
|
|
16
16
|
return response;
|
|
17
17
|
} catch (err) {
|
|
18
|
-
hooks?.onError?.(err
|
|
18
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
19
|
+
method: "chat",
|
|
20
|
+
request: req
|
|
21
|
+
});
|
|
19
22
|
throw err;
|
|
20
23
|
}
|
|
21
24
|
},
|
|
@@ -29,7 +32,10 @@ function createAI(adapter, options) {
|
|
|
29
32
|
try {
|
|
30
33
|
yield* source.call(adapter, req);
|
|
31
34
|
} catch (err) {
|
|
32
|
-
hooks?.onError?.(err
|
|
35
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
36
|
+
method: "stream",
|
|
37
|
+
request: req
|
|
38
|
+
});
|
|
33
39
|
throw err;
|
|
34
40
|
}
|
|
35
41
|
};
|
|
@@ -42,12 +48,7 @@ function createAI(adapter, options) {
|
|
|
42
48
|
return adapter.embeddings(request);
|
|
43
49
|
},
|
|
44
50
|
async run(options2) {
|
|
45
|
-
const {
|
|
46
|
-
tools,
|
|
47
|
-
maxRounds = 10,
|
|
48
|
-
onToolCall,
|
|
49
|
-
...requestBase
|
|
50
|
-
} = options2;
|
|
51
|
+
const { tools, maxRounds = 10, onToolCall, ...requestBase } = options2;
|
|
51
52
|
const messages = [...options2.messages];
|
|
52
53
|
const toolDefs = tools.map((t) => t.definition);
|
|
53
54
|
const handlerMap = new Map(tools.map((t) => [t.definition.name, t.handler]));
|
|
@@ -66,7 +67,10 @@ function createAI(adapter, options) {
|
|
|
66
67
|
try {
|
|
67
68
|
response = await withRetry(() => adapter.chat(req2), retry);
|
|
68
69
|
} catch (err) {
|
|
69
|
-
hooks?.onError?.(err
|
|
70
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
71
|
+
method: "run",
|
|
72
|
+
request: req2
|
|
73
|
+
});
|
|
70
74
|
throw err;
|
|
71
75
|
}
|
|
72
76
|
if (hooks?.onAfterChat) {
|
|
@@ -95,19 +99,21 @@ function createAI(adapter, options) {
|
|
|
95
99
|
try {
|
|
96
100
|
result = await handler(call.arguments);
|
|
97
101
|
} catch (err) {
|
|
98
|
-
result = err.message
|
|
102
|
+
result = err instanceof Error ? err.message : String(err);
|
|
99
103
|
isError = true;
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
onToolCall?.(call, result);
|
|
103
107
|
messages.push({
|
|
104
108
|
role: "tool",
|
|
105
|
-
content: [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "tool_result",
|
|
112
|
+
id: call.id,
|
|
113
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
114
|
+
isError
|
|
115
|
+
}
|
|
116
|
+
]
|
|
111
117
|
});
|
|
112
118
|
}
|
|
113
119
|
}
|
|
@@ -125,7 +131,10 @@ function createAI(adapter, options) {
|
|
|
125
131
|
try {
|
|
126
132
|
finalResponse = await withRetry(() => adapter.chat(req), retry);
|
|
127
133
|
} catch (err) {
|
|
128
|
-
hooks?.onError?.(err
|
|
134
|
+
hooks?.onError?.(err instanceof Error ? err : new Error(String(err)), {
|
|
135
|
+
method: "run",
|
|
136
|
+
request: req
|
|
137
|
+
});
|
|
129
138
|
throw err;
|
|
130
139
|
}
|
|
131
140
|
if (hooks?.onAfterChat) {
|
|
@@ -144,8 +153,8 @@ async function withRetry(fn, config) {
|
|
|
144
153
|
try {
|
|
145
154
|
return await fn();
|
|
146
155
|
} catch (err) {
|
|
147
|
-
lastError = err;
|
|
148
|
-
if (attempt < maxRetries && shouldRetry(
|
|
156
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
157
|
+
if (attempt < maxRetries && shouldRetry(lastError)) {
|
|
149
158
|
await sleep(baseDelay * Math.pow(2, attempt));
|
|
150
159
|
continue;
|
|
151
160
|
}
|
|
@@ -175,17 +184,20 @@ async function streamSSE(res, stream, options) {
|
|
|
175
184
|
res.writeHead(200, {
|
|
176
185
|
"Content-Type": "text/event-stream",
|
|
177
186
|
"Cache-Control": "no-cache",
|
|
178
|
-
|
|
187
|
+
Connection: "keep-alive",
|
|
179
188
|
"X-Accel-Buffering": "no"
|
|
180
189
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
try {
|
|
191
|
+
for await (const chunk of stream) {
|
|
192
|
+
options?.onChunk?.(chunk);
|
|
193
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
184
194
|
|
|
185
195
|
`);
|
|
196
|
+
}
|
|
197
|
+
res.write("data: [DONE]\n\n");
|
|
198
|
+
} finally {
|
|
199
|
+
res.end();
|
|
186
200
|
}
|
|
187
|
-
res.write("data: [DONE]\n\n");
|
|
188
|
-
res.end();
|
|
189
201
|
}
|
|
190
202
|
async function collectStream(stream) {
|
|
191
203
|
let result = "";
|
|
@@ -195,25 +207,33 @@ async function collectStream(stream) {
|
|
|
195
207
|
return result;
|
|
196
208
|
}
|
|
197
209
|
async function* parseSSEStream(response) {
|
|
210
|
+
if (!response.body) {
|
|
211
|
+
throw new Error("Response body is null \u2014 streaming not supported");
|
|
212
|
+
}
|
|
198
213
|
const reader = response.body.getReader();
|
|
199
214
|
const decoder = new TextDecoder();
|
|
200
215
|
let buffer = "";
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
try {
|
|
217
|
+
while (true) {
|
|
218
|
+
const { done, value } = await reader.read();
|
|
219
|
+
if (done) break;
|
|
220
|
+
buffer += decoder.decode(value, { stream: true });
|
|
221
|
+
const lines = buffer.split("\n");
|
|
222
|
+
buffer = lines.pop() ?? "";
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
if (line.startsWith("data: ")) {
|
|
225
|
+
const data = line.slice(6).trim();
|
|
226
|
+
if (data === "[DONE]") return;
|
|
227
|
+
try {
|
|
228
|
+
yield JSON.parse(data);
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
214
231
|
}
|
|
215
232
|
}
|
|
216
233
|
}
|
|
234
|
+
} finally {
|
|
235
|
+
reader.cancel().catch(() => {
|
|
236
|
+
});
|
|
217
237
|
}
|
|
218
238
|
}
|
|
219
239
|
export {
|
package/dist/mcp/index.cjs
CHANGED
|
@@ -47,13 +47,14 @@ function createMCPServer(config) {
|
|
|
47
47
|
const result = await dispatch(request);
|
|
48
48
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
49
49
|
} catch (err) {
|
|
50
|
+
const rpcErr = err;
|
|
50
51
|
return {
|
|
51
52
|
jsonrpc: "2.0",
|
|
52
53
|
id: request.id,
|
|
53
54
|
error: {
|
|
54
|
-
code:
|
|
55
|
-
message:
|
|
56
|
-
data:
|
|
55
|
+
code: rpcErr.code ?? -32603,
|
|
56
|
+
message: rpcErr.message ?? "Internal error",
|
|
57
|
+
data: rpcErr.data
|
|
57
58
|
}
|
|
58
59
|
};
|
|
59
60
|
}
|
|
@@ -139,7 +140,8 @@ function createMCPServer(config) {
|
|
|
139
140
|
return { handleRequest, info: () => serverInfo };
|
|
140
141
|
}
|
|
141
142
|
function matchesTemplate(pattern, uri) {
|
|
142
|
-
const
|
|
143
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
144
|
+
const regex = escaped.replace(/\\\{[^\\}]+\\\}/g, "([^/]+)");
|
|
143
145
|
return new RegExp(`^${regex}$`).test(uri);
|
|
144
146
|
}
|
|
145
147
|
function makeError(code, message) {
|
|
@@ -162,7 +164,8 @@ function createMCPClient(config) {
|
|
|
162
164
|
"Content-Type": "application/json",
|
|
163
165
|
...config.headers
|
|
164
166
|
},
|
|
165
|
-
body: JSON.stringify(request)
|
|
167
|
+
body: JSON.stringify(request),
|
|
168
|
+
signal: AbortSignal.timeout(3e4)
|
|
166
169
|
});
|
|
167
170
|
if (!response.ok) {
|
|
168
171
|
const text = await response.text();
|
|
@@ -170,29 +173,28 @@ function createMCPClient(config) {
|
|
|
170
173
|
}
|
|
171
174
|
const result = await response.json();
|
|
172
175
|
if (result.error) {
|
|
173
|
-
const err = new Error(result.error.message)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const err = Object.assign(new Error(result.error.message), {
|
|
177
|
+
code: result.error.code,
|
|
178
|
+
data: result.error.data
|
|
179
|
+
});
|
|
176
180
|
throw err;
|
|
177
181
|
}
|
|
178
182
|
return result.result;
|
|
179
183
|
}
|
|
180
184
|
return {
|
|
181
185
|
async initialize() {
|
|
182
|
-
|
|
186
|
+
return rpc("initialize", {
|
|
183
187
|
protocolVersion: "2024-11-05",
|
|
184
188
|
capabilities: {},
|
|
185
189
|
clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
|
|
186
190
|
});
|
|
187
|
-
return result;
|
|
188
191
|
},
|
|
189
192
|
async listTools() {
|
|
190
193
|
const result = await rpc("tools/list");
|
|
191
194
|
return result.tools ?? [];
|
|
192
195
|
},
|
|
193
196
|
async callTool(name, args) {
|
|
194
|
-
|
|
195
|
-
return result;
|
|
197
|
+
return rpc("tools/call", { name, arguments: args });
|
|
196
198
|
},
|
|
197
199
|
async listResources() {
|
|
198
200
|
const result = await rpc("resources/list");
|
|
@@ -207,27 +209,28 @@ function createMCPClient(config) {
|
|
|
207
209
|
return result.prompts ?? [];
|
|
208
210
|
},
|
|
209
211
|
async getPrompt(name, args) {
|
|
210
|
-
|
|
211
|
-
return result;
|
|
212
|
+
return rpc("prompts/get", { name, arguments: args });
|
|
212
213
|
},
|
|
213
214
|
async toToolHandlers() {
|
|
214
215
|
const tools = await this.listTools();
|
|
215
|
-
return tools.map(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
216
|
+
return tools.map(
|
|
217
|
+
(tool) => ({
|
|
218
|
+
definition: {
|
|
219
|
+
name: tool.name,
|
|
220
|
+
description: tool.description,
|
|
221
|
+
parameters: tool.inputSchema
|
|
222
|
+
},
|
|
223
|
+
handler: async (args) => {
|
|
224
|
+
const result = await this.callTool(tool.name, args);
|
|
225
|
+
if (result.isError) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return result.content.map((c) => c.text ?? "").join("\n");
|
|
227
231
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}));
|
|
232
|
+
})
|
|
233
|
+
);
|
|
231
234
|
}
|
|
232
235
|
};
|
|
233
236
|
}
|
|
@@ -265,18 +268,24 @@ async function handlePost(server, req, res) {
|
|
|
265
268
|
res.end(JSON.stringify(response));
|
|
266
269
|
} catch (err) {
|
|
267
270
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
268
|
-
res.end(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
res.end(
|
|
272
|
+
JSON.stringify({
|
|
273
|
+
jsonrpc: "2.0",
|
|
274
|
+
id: null,
|
|
275
|
+
error: {
|
|
276
|
+
code: -32700,
|
|
277
|
+
message: "Parse error",
|
|
278
|
+
data: err instanceof Error ? err.message : String(err)
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
);
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
284
|
function handleSSE(server, _req, res) {
|
|
276
285
|
res.writeHead(200, {
|
|
277
286
|
"Content-Type": "text/event-stream",
|
|
278
287
|
"Cache-Control": "no-cache",
|
|
279
|
-
|
|
288
|
+
Connection: "keep-alive"
|
|
280
289
|
});
|
|
281
290
|
const endpointUrl = "./";
|
|
282
291
|
res.write(`event: endpoint
|
package/dist/mcp/index.d.cts
CHANGED
|
@@ -170,27 +170,6 @@ interface MCPClient {
|
|
|
170
170
|
*/
|
|
171
171
|
toToolHandlers(): Promise<ToolHandler[]>;
|
|
172
172
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Create an MCP client that connects to an MCP server over HTTP.
|
|
175
|
-
*
|
|
176
|
-
* ```ts
|
|
177
|
-
* import { createMCPClient } from '@matthesketh/utopia-ai/mcp';
|
|
178
|
-
*
|
|
179
|
-
* const client = createMCPClient({ url: 'http://localhost:3001/mcp' });
|
|
180
|
-
* await client.initialize();
|
|
181
|
-
*
|
|
182
|
-
* const tools = await client.listTools();
|
|
183
|
-
* const result = await client.callTool('get_weather', { city: 'NYC' });
|
|
184
|
-
*
|
|
185
|
-
* // Bridge MCP tools into AI tool loop
|
|
186
|
-
* const ai = createAI(openaiAdapter({ apiKey: '...' }));
|
|
187
|
-
* const toolHandlers = await client.toToolHandlers();
|
|
188
|
-
* const response = await ai.run({
|
|
189
|
-
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
190
|
-
* tools: toolHandlers,
|
|
191
|
-
* });
|
|
192
|
-
* ```
|
|
193
|
-
*/
|
|
194
173
|
declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
195
174
|
|
|
196
175
|
/**
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -170,27 +170,6 @@ interface MCPClient {
|
|
|
170
170
|
*/
|
|
171
171
|
toToolHandlers(): Promise<ToolHandler[]>;
|
|
172
172
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Create an MCP client that connects to an MCP server over HTTP.
|
|
175
|
-
*
|
|
176
|
-
* ```ts
|
|
177
|
-
* import { createMCPClient } from '@matthesketh/utopia-ai/mcp';
|
|
178
|
-
*
|
|
179
|
-
* const client = createMCPClient({ url: 'http://localhost:3001/mcp' });
|
|
180
|
-
* await client.initialize();
|
|
181
|
-
*
|
|
182
|
-
* const tools = await client.listTools();
|
|
183
|
-
* const result = await client.callTool('get_weather', { city: 'NYC' });
|
|
184
|
-
*
|
|
185
|
-
* // Bridge MCP tools into AI tool loop
|
|
186
|
-
* const ai = createAI(openaiAdapter({ apiKey: '...' }));
|
|
187
|
-
* const toolHandlers = await client.toToolHandlers();
|
|
188
|
-
* const response = await ai.run({
|
|
189
|
-
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
190
|
-
* tools: toolHandlers,
|
|
191
|
-
* });
|
|
192
|
-
* ```
|
|
193
|
-
*/
|
|
194
173
|
declare function createMCPClient(config: MCPClientConfig): MCPClient;
|
|
195
174
|
|
|
196
175
|
/**
|
package/dist/mcp/index.js
CHANGED
|
@@ -19,13 +19,14 @@ function createMCPServer(config) {
|
|
|
19
19
|
const result = await dispatch(request);
|
|
20
20
|
return { jsonrpc: "2.0", id: request.id, result };
|
|
21
21
|
} catch (err) {
|
|
22
|
+
const rpcErr = err;
|
|
22
23
|
return {
|
|
23
24
|
jsonrpc: "2.0",
|
|
24
25
|
id: request.id,
|
|
25
26
|
error: {
|
|
26
|
-
code:
|
|
27
|
-
message:
|
|
28
|
-
data:
|
|
27
|
+
code: rpcErr.code ?? -32603,
|
|
28
|
+
message: rpcErr.message ?? "Internal error",
|
|
29
|
+
data: rpcErr.data
|
|
29
30
|
}
|
|
30
31
|
};
|
|
31
32
|
}
|
|
@@ -111,7 +112,8 @@ function createMCPServer(config) {
|
|
|
111
112
|
return { handleRequest, info: () => serverInfo };
|
|
112
113
|
}
|
|
113
114
|
function matchesTemplate(pattern, uri) {
|
|
114
|
-
const
|
|
115
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
116
|
+
const regex = escaped.replace(/\\\{[^\\}]+\\\}/g, "([^/]+)");
|
|
115
117
|
return new RegExp(`^${regex}$`).test(uri);
|
|
116
118
|
}
|
|
117
119
|
function makeError(code, message) {
|
|
@@ -134,7 +136,8 @@ function createMCPClient(config) {
|
|
|
134
136
|
"Content-Type": "application/json",
|
|
135
137
|
...config.headers
|
|
136
138
|
},
|
|
137
|
-
body: JSON.stringify(request)
|
|
139
|
+
body: JSON.stringify(request),
|
|
140
|
+
signal: AbortSignal.timeout(3e4)
|
|
138
141
|
});
|
|
139
142
|
if (!response.ok) {
|
|
140
143
|
const text = await response.text();
|
|
@@ -142,29 +145,28 @@ function createMCPClient(config) {
|
|
|
142
145
|
}
|
|
143
146
|
const result = await response.json();
|
|
144
147
|
if (result.error) {
|
|
145
|
-
const err = new Error(result.error.message)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
const err = Object.assign(new Error(result.error.message), {
|
|
149
|
+
code: result.error.code,
|
|
150
|
+
data: result.error.data
|
|
151
|
+
});
|
|
148
152
|
throw err;
|
|
149
153
|
}
|
|
150
154
|
return result.result;
|
|
151
155
|
}
|
|
152
156
|
return {
|
|
153
157
|
async initialize() {
|
|
154
|
-
|
|
158
|
+
return rpc("initialize", {
|
|
155
159
|
protocolVersion: "2024-11-05",
|
|
156
160
|
capabilities: {},
|
|
157
161
|
clientInfo: { name: "utopia-mcp-client", version: "1.0.0" }
|
|
158
162
|
});
|
|
159
|
-
return result;
|
|
160
163
|
},
|
|
161
164
|
async listTools() {
|
|
162
165
|
const result = await rpc("tools/list");
|
|
163
166
|
return result.tools ?? [];
|
|
164
167
|
},
|
|
165
168
|
async callTool(name, args) {
|
|
166
|
-
|
|
167
|
-
return result;
|
|
169
|
+
return rpc("tools/call", { name, arguments: args });
|
|
168
170
|
},
|
|
169
171
|
async listResources() {
|
|
170
172
|
const result = await rpc("resources/list");
|
|
@@ -179,27 +181,28 @@ function createMCPClient(config) {
|
|
|
179
181
|
return result.prompts ?? [];
|
|
180
182
|
},
|
|
181
183
|
async getPrompt(name, args) {
|
|
182
|
-
|
|
183
|
-
return result;
|
|
184
|
+
return rpc("prompts/get", { name, arguments: args });
|
|
184
185
|
},
|
|
185
186
|
async toToolHandlers() {
|
|
186
187
|
const tools = await this.listTools();
|
|
187
|
-
return tools.map(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
188
|
+
return tools.map(
|
|
189
|
+
(tool) => ({
|
|
190
|
+
definition: {
|
|
191
|
+
name: tool.name,
|
|
192
|
+
description: tool.description,
|
|
193
|
+
parameters: tool.inputSchema
|
|
194
|
+
},
|
|
195
|
+
handler: async (args) => {
|
|
196
|
+
const result = await this.callTool(tool.name, args);
|
|
197
|
+
if (result.isError) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
result.content.map((c) => c.text ?? "").join("\n") || "Tool call failed"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return result.content.map((c) => c.text ?? "").join("\n");
|
|
199
203
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}));
|
|
204
|
+
})
|
|
205
|
+
);
|
|
203
206
|
}
|
|
204
207
|
};
|
|
205
208
|
}
|
|
@@ -237,18 +240,24 @@ async function handlePost(server, req, res) {
|
|
|
237
240
|
res.end(JSON.stringify(response));
|
|
238
241
|
} catch (err) {
|
|
239
242
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
240
|
-
res.end(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
res.end(
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
jsonrpc: "2.0",
|
|
246
|
+
id: null,
|
|
247
|
+
error: {
|
|
248
|
+
code: -32700,
|
|
249
|
+
message: "Parse error",
|
|
250
|
+
data: err instanceof Error ? err.message : String(err)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
);
|
|
245
254
|
}
|
|
246
255
|
}
|
|
247
256
|
function handleSSE(server, _req, res) {
|
|
248
257
|
res.writeHead(200, {
|
|
249
258
|
"Content-Type": "text/event-stream",
|
|
250
259
|
"Cache-Control": "no-cache",
|
|
251
|
-
|
|
260
|
+
Connection: "keep-alive"
|
|
252
261
|
});
|
|
253
262
|
const endpointUrl = "./";
|
|
254
263
|
res.write(`event: endpoint
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthesketh/utopia-ai",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI adapters and MCP support for UtopiaJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"dist"
|
|
68
68
|
],
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@anthropic-ai/sdk": "^0.30.0",
|
|
71
|
-
"@google/generative-ai": "^0.21.0",
|
|
72
|
-
"openai": "^4.0.0"
|
|
70
|
+
"@anthropic-ai/sdk": "^0.30.0 || ^0.74.0",
|
|
71
|
+
"@google/generative-ai": "^0.21.0 || ^0.24.0",
|
|
72
|
+
"openai": "^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|
|
75
75
|
"openai": {
|