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.
@@ -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 and modify the payload for function calling
21
+ // Create the payload for the request
22
22
  const payload = await this.createRequestPayload(request, config);
23
- const response = await axios_1.default.post(`${this.baseURL}/generate`, payload, {
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
- if (!response.data || !response.data.response) {
29
- throw new Error("Invalid response from Ollama API");
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(response.data.response, request);
67
+ const functionCalls = this.extractFunctionCallsFromText(responseText, request);
33
68
  return {
34
- text: response.data.response,
69
+ text: responseText,
35
70
  usage: {
36
- promptTokens: response.data.prompt_eval_count,
37
- completionTokens: response.data.eval_count,
38
- totalTokens: response.data.prompt_eval_count + response.data.eval_count,
71
+ promptTokens: promptTokens,
72
+ completionTokens: completionTokens,
73
+ totalTokens: promptTokens + completionTokens,
39
74
  },
40
75
  functionCalls,
41
- raw: response.data,
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
- const response = await axios_1.default.post(`${this.baseURL}/generate`, payload, {
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
- if (parsed.response) {
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
- // Try multiple patterns for function calls
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 = [...text.matchAll(jsonRegex)];
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
- const argsText = match[2];
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, use the raw text
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 = [...text.matchAll(functionRegex)];
201
+ const functionMatches = [...fixedText.matchAll(functionRegex)];
139
202
  if (functionMatches.length > 0) {
140
- return functionMatches.map((match) => ({
141
- name: match[1],
142
- arguments: match[2],
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: Look for more specific calculator patterns
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 === "calculator") {
149
- const calculatorRegex = /"?operation"?\s*:\s*"?([^",\s]+)"?,\s*"?a"?\s*:\s*(\d+),\s*"?b"?\s*:\s*(\d+)/;
150
- const calculatorMatch = text.match(calculatorRegex);
151
- if (calculatorMatch) {
152
- const operation = calculatorMatch[1];
153
- const a = parseInt(calculatorMatch[2]);
154
- const b = parseInt(calculatorMatch[3]);
155
- return [
156
- {
157
- name: "calculator",
158
- arguments: JSON.stringify({ operation, a, b }),
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
- // Pattern 4: Look for more specific weather patterns
164
- if (currentRequest.functionCall &&
165
- typeof currentRequest.functionCall === "object" &&
166
- currentRequest.functionCall.name === "getWeather") {
167
- const weatherRegex = /"?location"?\s*:\s*"([^"]+)"(?:,\s*"?unit"?\s*:\s*"([^"]+)")?/;
168
- const weatherMatch = text.match(weatherRegex);
169
- if (weatherMatch) {
170
- const location = weatherMatch[1];
171
- const unit = weatherMatch[2] || "celsius";
172
- return [
173
- {
174
- name: "getWeather",
175
- arguments: JSON.stringify({ location, unit }),
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
- messages.push(userMessage);
269
- // Add function calling data to system prompt
270
- if (request.functions && request.functions.length > 0) {
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}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neural-ai-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Unified SDK for interacting with various AI LLM providers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",