neural-ai-sdk 0.1.4 → 0.1.5
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/models/ollama-model.d.ts +4 -0
- package/dist/models/ollama-model.js +231 -86
- package/package.json +1 -1
@@ -10,6 +10,10 @@ export declare class OllamaModel extends BaseModel {
|
|
10
10
|
* Extract function calls from text using various patterns
|
11
11
|
*/
|
12
12
|
private extractFunctionCallsFromText;
|
13
|
+
/**
|
14
|
+
* Tries to fix incomplete JSON strings by adding missing closing braces
|
15
|
+
*/
|
16
|
+
private tryFixIncompleteJSON;
|
13
17
|
/**
|
14
18
|
* Creates the request payload for Ollama, handling multimodal content if provided
|
15
19
|
*/
|
@@ -18,30 +18,70 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
18
18
|
async generate(request) {
|
19
19
|
const config = this.mergeConfig(request.options);
|
20
20
|
try {
|
21
|
-
// Create
|
21
|
+
// Create the payload for the request
|
22
22
|
const payload = await this.createRequestPayload(request, config);
|
23
|
-
|
23
|
+
// Determine which endpoint to use based on the payload format
|
24
|
+
const endpoint = payload.messages ? "chat" : "generate";
|
25
|
+
console.log(`Using Ollama ${endpoint} endpoint with model: ${payload.model || "default"}`);
|
26
|
+
// Set stream to true to handle responses as a stream
|
27
|
+
payload.stream = true;
|
28
|
+
const response = await axios_1.default.post(`${this.baseURL}/${endpoint}`, payload, {
|
29
|
+
responseType: "stream",
|
24
30
|
headers: {
|
25
31
|
"Content-Type": "application/json",
|
26
32
|
},
|
27
33
|
});
|
28
|
-
|
29
|
-
|
34
|
+
// Accumulate the complete response
|
35
|
+
let responseText = "";
|
36
|
+
let promptTokens = 0;
|
37
|
+
let completionTokens = 0;
|
38
|
+
// Process the stream
|
39
|
+
const reader = response.data;
|
40
|
+
for await (const chunk of reader) {
|
41
|
+
const lines = chunk.toString().split("\n").filter(Boolean);
|
42
|
+
for (const line of lines) {
|
43
|
+
try {
|
44
|
+
const parsed = JSON.parse(line);
|
45
|
+
// Handle different response formats
|
46
|
+
if (endpoint === "chat") {
|
47
|
+
if (parsed.message && parsed.message.content) {
|
48
|
+
responseText += parsed.message.content;
|
49
|
+
}
|
50
|
+
}
|
51
|
+
else if (parsed.response) {
|
52
|
+
responseText += parsed.response;
|
53
|
+
}
|
54
|
+
// Extract token usage from the final message
|
55
|
+
if (parsed.done) {
|
56
|
+
promptTokens = parsed.prompt_eval_count || 0;
|
57
|
+
completionTokens = parsed.eval_count || 0;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
catch (error) {
|
61
|
+
console.error("Error parsing Ollama stream data:", line, error);
|
62
|
+
}
|
63
|
+
}
|
30
64
|
}
|
65
|
+
console.log(`Extracted response text: "${responseText}"`);
|
31
66
|
// Try to extract function calls from the response
|
32
|
-
const functionCalls = this.extractFunctionCallsFromText(
|
67
|
+
const functionCalls = this.extractFunctionCallsFromText(responseText, request);
|
33
68
|
return {
|
34
|
-
text:
|
69
|
+
text: responseText,
|
35
70
|
usage: {
|
36
|
-
promptTokens:
|
37
|
-
completionTokens:
|
38
|
-
totalTokens:
|
71
|
+
promptTokens: promptTokens,
|
72
|
+
completionTokens: completionTokens,
|
73
|
+
totalTokens: promptTokens + completionTokens,
|
39
74
|
},
|
40
75
|
functionCalls,
|
41
|
-
raw: response
|
76
|
+
raw: { response: responseText }, // We don't have the original raw response, so create one
|
42
77
|
};
|
43
78
|
}
|
44
79
|
catch (error) {
|
80
|
+
console.error("Ollama API error details:", error);
|
81
|
+
if (error.response) {
|
82
|
+
console.error("Response status:", error.response.status);
|
83
|
+
console.error("Response data:", error.response.data);
|
84
|
+
}
|
45
85
|
// Enhance error message if it appears to be related to multimodal support
|
46
86
|
if (error.response?.status === 400 &&
|
47
87
|
(request.image || request.content) &&
|
@@ -50,6 +90,16 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
50
90
|
error.response?.data?.error?.includes("vision"))) {
|
51
91
|
throw new Error(`The model "${config.model || "default"}" doesn't support multimodal inputs. Try a vision-capable model like "llama-3.2-vision" or "llava". Original error: ${error.message}`);
|
52
92
|
}
|
93
|
+
// Check if the error is about the model not being found or loaded
|
94
|
+
if (error.response?.status === 404 ||
|
95
|
+
(error.response?.data &&
|
96
|
+
typeof error.response.data.error === "string" &&
|
97
|
+
error.response.data.error.toLowerCase().includes("model") &&
|
98
|
+
error.response.data.error.toLowerCase().includes("not"))) {
|
99
|
+
throw new Error(`Model "${config.model || "default"}" not found or not loaded in Ollama. ` +
|
100
|
+
`Make sure the model is installed with 'ollama pull ${config.model || "llama2"}' ` +
|
101
|
+
`Original error: ${error.response?.data?.error || error.message}`);
|
102
|
+
}
|
53
103
|
throw error;
|
54
104
|
}
|
55
105
|
}
|
@@ -57,7 +107,9 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
57
107
|
const config = this.mergeConfig(request.options);
|
58
108
|
try {
|
59
109
|
const payload = await this.createRequestPayload(request, config, true);
|
60
|
-
|
110
|
+
// Determine which endpoint to use based on the payload format
|
111
|
+
const endpoint = payload.messages ? "chat" : "generate";
|
112
|
+
const response = await axios_1.default.post(`${this.baseURL}/${endpoint}`, payload, {
|
61
113
|
responseType: "stream",
|
62
114
|
headers: {
|
63
115
|
"Content-Type": "application/json",
|
@@ -69,7 +121,13 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
69
121
|
for (const line of lines) {
|
70
122
|
try {
|
71
123
|
const parsed = JSON.parse(line);
|
72
|
-
|
124
|
+
// Handle different response formats
|
125
|
+
if (endpoint === "chat") {
|
126
|
+
if (parsed.message && parsed.message.content) {
|
127
|
+
yield parsed.message.content;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
else if (parsed.response) {
|
73
131
|
yield parsed.response;
|
74
132
|
}
|
75
133
|
}
|
@@ -98,16 +156,20 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
98
156
|
if (!text)
|
99
157
|
return undefined;
|
100
158
|
try {
|
101
|
-
//
|
159
|
+
// Fix incomplete JSON - look for patterns where JSON might be incomplete
|
160
|
+
// First, let's try to fix a common issue where the closing brace is missing
|
161
|
+
const fixedText = this.tryFixIncompleteJSON(text);
|
102
162
|
// Pattern 1: JSON format with name and arguments
|
103
163
|
// E.g., {"name": "getWeather", "arguments": {"location": "Tokyo"}}
|
104
164
|
const jsonRegex = /\{[\s\n]*"name"[\s\n]*:[\s\n]*"([^"]+)"[\s\n]*,[\s\n]*"arguments"[\s\n]*:[\s\n]*([\s\S]*?)\}/g;
|
105
|
-
const jsonMatches = [...
|
165
|
+
const jsonMatches = [...fixedText.matchAll(jsonRegex)];
|
106
166
|
if (jsonMatches.length > 0) {
|
107
167
|
return jsonMatches.map((match) => {
|
108
168
|
try {
|
109
169
|
// Try to parse the arguments as JSON
|
110
|
-
|
170
|
+
let argsText = match[2];
|
171
|
+
// Fix potential incomplete JSON in arguments
|
172
|
+
argsText = this.tryFixIncompleteJSON(argsText);
|
111
173
|
let args;
|
112
174
|
try {
|
113
175
|
args = JSON.parse(argsText);
|
@@ -117,10 +179,11 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
117
179
|
};
|
118
180
|
}
|
119
181
|
catch (e) {
|
120
|
-
// If parsing fails,
|
182
|
+
// If parsing fails, try to fix the JSON before returning
|
183
|
+
console.warn("Error parsing function arguments, trying to fix:", e);
|
121
184
|
return {
|
122
185
|
name: match[1],
|
123
|
-
arguments: argsText,
|
186
|
+
arguments: this.tryFixIncompleteJSON(argsText, true),
|
124
187
|
};
|
125
188
|
}
|
126
189
|
}
|
@@ -135,46 +198,88 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
135
198
|
}
|
136
199
|
// Pattern 2: Function call pattern: functionName({"key": "value"})
|
137
200
|
const functionRegex = /([a-zA-Z0-9_]+)\s*\(\s*(\{[\s\S]*?\})\s*\)/g;
|
138
|
-
const functionMatches = [...
|
201
|
+
const functionMatches = [...fixedText.matchAll(functionRegex)];
|
139
202
|
if (functionMatches.length > 0) {
|
140
|
-
return functionMatches.map((match) =>
|
141
|
-
|
142
|
-
|
143
|
-
|
203
|
+
return functionMatches.map((match) => {
|
204
|
+
const argsText = this.tryFixIncompleteJSON(match[2]);
|
205
|
+
return {
|
206
|
+
name: match[1],
|
207
|
+
arguments: argsText,
|
208
|
+
};
|
209
|
+
});
|
144
210
|
}
|
145
|
-
// Pattern 3:
|
211
|
+
// Pattern 3: Looking for direct JSON objects - for function specific forced calls
|
146
212
|
if (currentRequest.functionCall &&
|
147
|
-
typeof currentRequest.functionCall === "object"
|
148
|
-
currentRequest.functionCall.name
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
213
|
+
typeof currentRequest.functionCall === "object") {
|
214
|
+
const forcedFunctionName = currentRequest.functionCall.name;
|
215
|
+
// For getWeather function
|
216
|
+
if (forcedFunctionName === "getWeather") {
|
217
|
+
const weatherMatch = fixedText.match(/\{[\s\n]*"location"[\s\n]*:[\s\n]*"([^"]*)"(?:[\s\n]*,[\s\n]*"unit"[\s\n]*:[\s\n]*"([^"]*)"|)(.*?)\}/s);
|
218
|
+
if (weatherMatch) {
|
219
|
+
const location = weatherMatch[1];
|
220
|
+
const unit = weatherMatch[2] || "celsius";
|
221
|
+
return [
|
222
|
+
{
|
223
|
+
name: "getWeather",
|
224
|
+
arguments: JSON.stringify({ location, unit }),
|
225
|
+
},
|
226
|
+
];
|
227
|
+
}
|
228
|
+
}
|
229
|
+
// For calculator function
|
230
|
+
if (forcedFunctionName === "calculator") {
|
231
|
+
const calculatorMatch = fixedText.match(/\{[\s\n]*"operation"[\s\n]*:[\s\n]*"([^"]*)"[\s\n]*,[\s\n]*"a"[\s\n]*:[\s\n]*(\d+)[\s\n]*,[\s\n]*"b"[\s\n]*:[\s\n]*(\d+)[\s\S]*?\}/s);
|
232
|
+
if (calculatorMatch) {
|
233
|
+
const operation = calculatorMatch[1];
|
234
|
+
const a = parseInt(calculatorMatch[2]);
|
235
|
+
const b = parseInt(calculatorMatch[3]);
|
236
|
+
return [
|
237
|
+
{
|
238
|
+
name: "calculator",
|
239
|
+
arguments: JSON.stringify({ operation, a, b }),
|
240
|
+
},
|
241
|
+
];
|
242
|
+
}
|
161
243
|
}
|
162
244
|
}
|
163
|
-
//
|
164
|
-
if (currentRequest.functionCall
|
165
|
-
|
166
|
-
currentRequest.functionCall
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
245
|
+
// If no matches found and we have a functionCall request, try one more pattern matching approach
|
246
|
+
if (currentRequest.functionCall) {
|
247
|
+
// Try to extract JSON-like structures even if they're not complete
|
248
|
+
const namedFunction = typeof currentRequest.functionCall === "object"
|
249
|
+
? currentRequest.functionCall.name
|
250
|
+
: null;
|
251
|
+
// Look for the function name in the text followed by arguments
|
252
|
+
const functionNamePattern = namedFunction
|
253
|
+
? new RegExp(`"name"\\s*:\\s*"${namedFunction}"\\s*,\\s*"arguments"\\s*:\\s*(\\{[\\s\\S]*?)(?:\\}|$)`, "s")
|
254
|
+
: null;
|
255
|
+
if (functionNamePattern) {
|
256
|
+
const extractedMatch = fixedText.match(functionNamePattern);
|
257
|
+
if (extractedMatch && extractedMatch[1]) {
|
258
|
+
let argsText = extractedMatch[1];
|
259
|
+
// Make sure the JSON is complete
|
260
|
+
if (!argsText.endsWith("}")) {
|
261
|
+
argsText += "}";
|
262
|
+
}
|
263
|
+
try {
|
264
|
+
// Try to parse the fixed arguments
|
265
|
+
const args = JSON.parse(argsText);
|
266
|
+
return [
|
267
|
+
{
|
268
|
+
name: namedFunction,
|
269
|
+
arguments: JSON.stringify(args),
|
270
|
+
},
|
271
|
+
];
|
272
|
+
}
|
273
|
+
catch (e) {
|
274
|
+
console.warn("Failed to parse extracted arguments:", e);
|
275
|
+
return [
|
276
|
+
{
|
277
|
+
name: namedFunction,
|
278
|
+
arguments: this.tryFixIncompleteJSON(argsText, true),
|
279
|
+
},
|
280
|
+
];
|
281
|
+
}
|
282
|
+
}
|
178
283
|
}
|
179
284
|
}
|
180
285
|
}
|
@@ -183,6 +288,40 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
183
288
|
}
|
184
289
|
return undefined;
|
185
290
|
}
|
291
|
+
/**
|
292
|
+
* Tries to fix incomplete JSON strings by adding missing closing braces
|
293
|
+
*/
|
294
|
+
tryFixIncompleteJSON(text, returnAsString = false) {
|
295
|
+
// Skip if the string is already valid JSON
|
296
|
+
try {
|
297
|
+
JSON.parse(text);
|
298
|
+
return text; // Already valid
|
299
|
+
}
|
300
|
+
catch (e) {
|
301
|
+
// Not valid JSON, try to fix
|
302
|
+
}
|
303
|
+
// Count opening and closing braces
|
304
|
+
const openBraces = (text.match(/\{/g) || []).length;
|
305
|
+
const closeBraces = (text.match(/\}/g) || []).length;
|
306
|
+
// If we have more opening braces than closing, add the missing closing braces
|
307
|
+
if (openBraces > closeBraces) {
|
308
|
+
const missingBraces = openBraces - closeBraces;
|
309
|
+
let fixedText = text + "}".repeat(missingBraces);
|
310
|
+
// Try to parse it to see if it's valid now
|
311
|
+
try {
|
312
|
+
if (!returnAsString) {
|
313
|
+
JSON.parse(fixedText);
|
314
|
+
}
|
315
|
+
return fixedText;
|
316
|
+
}
|
317
|
+
catch (e) {
|
318
|
+
// Still not valid, return the original
|
319
|
+
console.warn("Failed to fix JSON even after adding braces", e);
|
320
|
+
return text;
|
321
|
+
}
|
322
|
+
}
|
323
|
+
return text;
|
324
|
+
}
|
186
325
|
/**
|
187
326
|
* Creates the request payload for Ollama, handling multimodal content if provided
|
188
327
|
*/
|
@@ -191,9 +330,12 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
191
330
|
const payload = {
|
192
331
|
model: config.model || "llama3", // Updated default to a model that better supports function calling
|
193
332
|
temperature: config.temperature,
|
194
|
-
num_predict: config.maxTokens,
|
195
333
|
top_p: config.topP,
|
196
334
|
};
|
335
|
+
// Add max tokens for the generate endpoint
|
336
|
+
if (config.maxTokens) {
|
337
|
+
payload.num_predict = config.maxTokens;
|
338
|
+
}
|
197
339
|
// Handle streaming
|
198
340
|
if (isStream) {
|
199
341
|
payload.stream = true;
|
@@ -202,9 +344,10 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
202
344
|
const useMessagesFormat = request.image ||
|
203
345
|
(request.content &&
|
204
346
|
request.content.some((item) => item.type === "image")) ||
|
205
|
-
(request.functions && request.functions.length > 0)
|
347
|
+
(request.functions && request.functions.length > 0) ||
|
348
|
+
request.systemPrompt; // Always use messages format when system prompt is provided
|
206
349
|
if (useMessagesFormat) {
|
207
|
-
// Modern message-based format for Ollama
|
350
|
+
// Modern message-based format for Ollama (chat endpoint)
|
208
351
|
const messages = [];
|
209
352
|
// Add system prompt if provided
|
210
353
|
if (request.systemPrompt) {
|
@@ -213,6 +356,27 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
213
356
|
content: request.systemPrompt,
|
214
357
|
});
|
215
358
|
}
|
359
|
+
else if (request.functions && request.functions.length > 0) {
|
360
|
+
// Add function calling guidance in system prompt if none provided
|
361
|
+
let functionSystemPrompt = "You are a helpful AI assistant with access to functions.";
|
362
|
+
// Add function definitions as JSON
|
363
|
+
functionSystemPrompt += `\n\nAvailable functions:\n\`\`\`json\n${JSON.stringify(request.functions, null, 2)}\n\`\`\`\n\n`;
|
364
|
+
// Add instruction based on functionCall setting
|
365
|
+
if (typeof request.functionCall === "object") {
|
366
|
+
functionSystemPrompt += `You must call the function: ${request.functionCall.name}.\n`;
|
367
|
+
functionSystemPrompt += `Format your response as a function call using this exact format:\n`;
|
368
|
+
functionSystemPrompt += `{"name": "${request.functionCall.name}", "arguments": {...}}\n`;
|
369
|
+
}
|
370
|
+
else if (request.functionCall === "auto") {
|
371
|
+
functionSystemPrompt += `Call one of these functions if appropriate for the user's request.\n`;
|
372
|
+
functionSystemPrompt += `Format your response as a function call using this exact format:\n`;
|
373
|
+
functionSystemPrompt += `{"name": "functionName", "arguments": {...}}\n`;
|
374
|
+
}
|
375
|
+
messages.push({
|
376
|
+
role: "system",
|
377
|
+
content: functionSystemPrompt,
|
378
|
+
});
|
379
|
+
}
|
216
380
|
// Create user message content parts
|
217
381
|
let userContent = [];
|
218
382
|
// Add main text content
|
@@ -262,43 +426,24 @@ class OllamaModel extends base_model_1.BaseModel {
|
|
262
426
|
if (userContent.length === 1 && userContent[0].type === "text") {
|
263
427
|
userMessage.content = userContent[0].text;
|
264
428
|
}
|
265
|
-
else {
|
429
|
+
else if (userContent.length > 0) {
|
266
430
|
userMessage.content = userContent;
|
267
431
|
}
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
// Create a system prompt for function calling
|
272
|
-
let functionSystemPrompt = request.systemPrompt || "";
|
273
|
-
// Add function definitions as JSON
|
274
|
-
functionSystemPrompt += `\n\nAvailable functions:\n\`\`\`json\n${JSON.stringify(request.functions, null, 2)}\n\`\`\`\n\n`;
|
275
|
-
// Add instruction based on functionCall setting
|
276
|
-
if (typeof request.functionCall === "object") {
|
277
|
-
functionSystemPrompt += `You must call the function: ${request.functionCall.name}.\n`;
|
278
|
-
functionSystemPrompt += `Format your response as a function call using this exact format:\n`;
|
279
|
-
functionSystemPrompt += `{"name": "${request.functionCall.name}", "arguments": {...}}\n`;
|
280
|
-
}
|
281
|
-
else if (request.functionCall === "auto") {
|
282
|
-
functionSystemPrompt += `Call one of these functions if appropriate for the user's request.\n`;
|
283
|
-
functionSystemPrompt += `Format your response as a function call using this exact format:\n`;
|
284
|
-
functionSystemPrompt += `{"name": "functionName", "arguments": {...}}\n`;
|
285
|
-
}
|
286
|
-
// Replace or add the system message
|
287
|
-
if (messages.length > 0 && messages[0].role === "system") {
|
288
|
-
messages[0].content = functionSystemPrompt;
|
289
|
-
}
|
290
|
-
else {
|
291
|
-
messages.unshift({
|
292
|
-
role: "system",
|
293
|
-
content: functionSystemPrompt,
|
294
|
-
});
|
295
|
-
}
|
432
|
+
else {
|
433
|
+
// Add empty string if no content provided to avoid invalid request
|
434
|
+
userMessage.content = "";
|
296
435
|
}
|
436
|
+
messages.push(userMessage);
|
297
437
|
payload.messages = messages;
|
438
|
+
// Remove any fields specific to the generate endpoint
|
439
|
+
// that might cause issues with the chat endpoint
|
440
|
+
if (payload.hasOwnProperty("num_predict")) {
|
441
|
+
delete payload.num_predict;
|
442
|
+
}
|
298
443
|
}
|
299
444
|
else {
|
300
|
-
// Traditional text-only format
|
301
|
-
let prompt = request.prompt;
|
445
|
+
// Traditional text-only format (generate endpoint)
|
446
|
+
let prompt = request.prompt || "";
|
302
447
|
// Add system prompt if provided
|
303
448
|
if (request.systemPrompt) {
|
304
449
|
prompt = `${request.systemPrompt}\n\n${prompt}`;
|