Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d022a5ef4d | |||
| a1d438a20a |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/ai-utils",
|
"name": "@ztimson/ai-utils",
|
||||||
"version": "1.2.11",
|
"version": "1.2.13",
|
||||||
"description": "AI Utility library",
|
"description": "AI Utility library",
|
||||||
"author": "Zak Timson",
|
"author": "Zak Timson",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ export class Anthropic extends LLMProvider {
|
|||||||
messages.push(<any>{timestamp, ...h});
|
messages.push(<any>{timestamp, ...h});
|
||||||
} else {
|
} else {
|
||||||
const textContent = h.content?.filter((c: any) => c.type == 'text').map((c: any) => c.text).join('\n\n');
|
const textContent = h.content?.filter((c: any) => c.type == 'text').map((c: any) => c.text).join('\n\n');
|
||||||
if(textContent) messages.push({timestamp, role: h.role, content: textContent});
|
if(textContent) messages.push({role: h.role, content: textContent, timestamp: timestamp});
|
||||||
h.content.forEach((c: any) => {
|
h.content.forEach((c: any) => {
|
||||||
if(c.type == 'tool_use') {
|
if(c.type == 'tool_use') {
|
||||||
messages.push({timestamp, role: 'tool', id: c.id, name: c.name, args: c.input, content: undefined});
|
messages.push({role: 'tool', id: c.id, name: c.name, args: c.input, timestamp: c.timestamp, content: undefined});
|
||||||
} else if(c.type == 'tool_result') {
|
} else if(c.type == 'tool_result') {
|
||||||
const m: any = messages.findLast(m => (<any>m).id == c.tool_use_id);
|
const m: any = messages.findLast(m => (<any>m).id == c.tool_use_id);
|
||||||
if(m) m[c.is_error ? 'error' : 'content'] = c.content;
|
if(m) m[c.is_error ? 'error' : 'content'] = c.content;
|
||||||
@@ -46,7 +46,7 @@ export class Anthropic extends LLMProvider {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return history.map(({timestamp, ...h}) => h);
|
return history;
|
||||||
}
|
}
|
||||||
|
|
||||||
ask(message: string, options: LLMRequest = {}): AbortablePromise<string | any> {
|
ask(message: string, options: LLMRequest = {}): AbortablePromise<string | any> {
|
||||||
@@ -83,8 +83,9 @@ export class Anthropic extends LLMProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp: any, isFirstMessage = true;
|
let resp: any, isFirstMessage = true, terminal = false;
|
||||||
do {
|
do {
|
||||||
|
requestParams.messages = history.map(({timestamp, ...m}) => m);
|
||||||
resp = await this.client.messages.create(requestParams).catch(err => {
|
resp = await this.client.messages.create(requestParams).catch(err => {
|
||||||
err.message += `\n\nMessages:\n${JSON.stringify(history, null, 2)}`;
|
err.message += `\n\nMessages:\n${JSON.stringify(history, null, 2)}`;
|
||||||
throw err;
|
throw err;
|
||||||
@@ -113,7 +114,7 @@ export class Anthropic extends LLMProvider {
|
|||||||
}
|
}
|
||||||
} else if(chunk.type === 'content_block_stop') {
|
} else if(chunk.type === 'content_block_stop') {
|
||||||
const last = resp.content.at(-1);
|
const last = resp.content.at(-1);
|
||||||
if(last.input != null) last.input = last.input ? JSONAttemptParse(last.input, {}) : {};
|
if(last?.input != null) last.input = last.input ? JSONAttemptParse(last.input, {}) : {};
|
||||||
} else if(chunk.type === 'message_stop') {
|
} else if(chunk.type === 'message_stop') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -123,31 +124,35 @@ export class Anthropic extends LLMProvider {
|
|||||||
// Run tools
|
// Run tools
|
||||||
const toolCalls = resp.content.filter((c: any) => c.type === 'tool_use');
|
const toolCalls = resp.content.filter((c: any) => c.type === 'tool_use');
|
||||||
if(toolCalls.length && !controller.signal.aborted) {
|
if(toolCalls.length && !controller.signal.aborted) {
|
||||||
history.push({role: 'assistant', content: resp.content});
|
history.push({role: 'assistant', content: resp.content, timestamp: Date.now()});
|
||||||
const results = await Promise.all(toolCalls.map(async (toolCall: any) => {
|
const results = await Promise.all(toolCalls.map(async (toolCall: any) => {
|
||||||
const tool = tools.find(findByProp('name', toolCall.name));
|
const tool = tools.find(findByProp('name', toolCall.name));
|
||||||
if(options.stream) options.stream({tool: toolCall.name});
|
if(options.stream) options.stream({tool: toolCall.name});
|
||||||
if(!tool) return {tool_use_id: toolCall.id, is_error: true, content: 'Tool not found'};
|
if(!tool) return {tool_use_id: toolCall.id, is_error: true, content: 'Tool not found'};
|
||||||
try {
|
try {
|
||||||
const result = await tool.fn(toolCall.input, options?.stream, this.ai);
|
// Wrap stream so a tool's `done` ends turn gracefully
|
||||||
|
const toolStream = options.stream && ((chunk: any) => {
|
||||||
|
if(chunk.done) { terminal = true; return; }
|
||||||
|
options.stream!(chunk);
|
||||||
|
});
|
||||||
|
const result = await tool.fn(toolCall.input, toolStream, this.ai);
|
||||||
return {type: 'tool_result', tool_use_id: toolCall.id, content: typeof result == 'object' ? JSONSanitize(result) : result};
|
return {type: 'tool_result', tool_use_id: toolCall.id, content: typeof result == 'object' ? JSONSanitize(result) : result};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return {type: 'tool_result', tool_use_id: toolCall.id, is_error: true, content: err?.message || err?.toString() || 'Unknown'};
|
return {type: 'tool_result', tool_use_id: toolCall.id, is_error: true, content: err?.message || err?.toString() || 'Unknown'};
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
history.push({role: 'user', content: results});
|
history.push({role: 'user', content: results, timestamp: Date.now()});
|
||||||
requestParams.messages = history;
|
requestParams.messages = history;
|
||||||
}
|
}
|
||||||
} while (!controller.signal.aborted && resp.content.some((c: any) => c.type === 'tool_use'));
|
} while (!terminal && !controller.signal.aborted && resp.content.some((c: any) => c.type === 'tool_use'));
|
||||||
|
|
||||||
|
if(!terminal) {
|
||||||
const textContent = resp.content.filter((c: any) => c.type == 'text').map((c: any) => c.text).join('\n\n');
|
const textContent = resp.content.filter((c: any) => c.type == 'text').map((c: any) => c.text).join('\n\n');
|
||||||
history.push({role: 'assistant', content: textContent});
|
history.push({role: 'assistant', content: textContent, timestamp: Date.now()});
|
||||||
|
}
|
||||||
history = this.toStandard(history);
|
history = this.toStandard(history);
|
||||||
|
|
||||||
if(options.stream) options.stream({done: true});
|
if(options.stream) options.stream({done: true});
|
||||||
if(options.history) options.history.splice(0, options.history.length, ...history);
|
if(options.history) options.history.splice(0, options.history.length, ...history);
|
||||||
|
|
||||||
// Return parsed JSON if schema provided
|
|
||||||
const finalContent = history.at(-1)?.content;
|
const finalContent = history.at(-1)?.content;
|
||||||
res(options.schema ? JSONAttemptParse(finalContent, finalContent) : finalContent);
|
res(options.schema ? JSONAttemptParse(finalContent, finalContent) : finalContent);
|
||||||
}), {abort: () => controller.abort()});
|
}), {abort: () => controller.abort()});
|
||||||
|
|||||||
36
src/llm.ts
36
src/llm.ts
@@ -397,15 +397,37 @@ class LLM {
|
|||||||
* @param {string} searchTerms Multiple search terms to check against target
|
* @param {string} searchTerms Multiple search terms to check against target
|
||||||
* @returns {{avg: number, max: number, similarities: number[]}} Similarity values 0-1: 0 = unique, 1 = identical
|
* @returns {{avg: number, max: number, similarities: number[]}} Similarity values 0-1: 0 = unique, 1 = identical
|
||||||
*/
|
*/
|
||||||
fuzzyMatch(target: string, ...searchTerms: string[]) {
|
fuzzyMatch(target, ...searchTerms) {
|
||||||
if (searchTerms.length < 2) throw new Error('Requires at least 2 strings to compare');
|
if (searchTerms.length < 2) throw new Error('Requires at least 2 strings to compare');
|
||||||
const vector = (text: string, dimensions: number = 10): number[] => {
|
|
||||||
return text.toLowerCase().split('').map((char, index) =>
|
const levenshtein = (a, b) => {
|
||||||
(char.charCodeAt(0) * (index + 1)) % dimensions / dimensions).slice(0, dimensions);
|
const m = a.length, n = b.length;
|
||||||
|
if (!m) return n;
|
||||||
|
if (!n) return m;
|
||||||
|
const dp = Array.from({length: m + 1}, (_, i) => [i, ...Array(n).fill(0)]);
|
||||||
|
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
dp[i][j] = a[i - 1] === b[j - 1]
|
||||||
|
? dp[i - 1][j - 1]
|
||||||
|
: 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
|
||||||
}
|
}
|
||||||
const v = vector(target);
|
}
|
||||||
const similarities = searchTerms.map(t => vector(t)).map(refVector => this.cosineSimilarity(v, refVector));
|
return dp[m][n];
|
||||||
return {avg: similarities.reduce((acc, s) => acc + s, 0) / similarities.length, max: Math.max(...similarities), similarities};
|
};
|
||||||
|
|
||||||
|
const similarity = (a, b) => {
|
||||||
|
a = a.toLowerCase(); b = b.toLowerCase();
|
||||||
|
const dist = levenshtein(a, b);
|
||||||
|
return 1 - dist / Math.max(a.length, b.length, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const similarities = searchTerms.map(t => similarity(target, t));
|
||||||
|
return {
|
||||||
|
avg: similarities.reduce((acc, s) => acc + s, 0) / similarities.length,
|
||||||
|
max: Math.max(...similarities),
|
||||||
|
similarities
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -51,15 +51,16 @@ export class OpenAi extends LLMProvider {
|
|||||||
content: null,
|
content: null,
|
||||||
tool_calls: [{ id: h.id, type: 'function', function: { name: h.name, arguments: JSON.stringify(h.args) } }],
|
tool_calls: [{ id: h.id, type: 'function', function: { name: h.name, arguments: JSON.stringify(h.args) } }],
|
||||||
refusal: null,
|
refusal: null,
|
||||||
annotations: []
|
annotations: [],
|
||||||
|
timestamp: h.timestamp,
|
||||||
}, {
|
}, {
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
tool_call_id: h.id,
|
tool_call_id: h.id,
|
||||||
content: h.error || h.content
|
content: h.error || h.content,
|
||||||
|
timestamp: h.timestamp,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const {timestamp, ...rest} = h;
|
result.push(h);
|
||||||
result.push(rest);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [] as any[]);
|
}, [] as any[]);
|
||||||
@@ -106,8 +107,9 @@ export class OpenAi extends LLMProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp: any, isFirstMessage = true;
|
let resp: any, isFirstMessage = true, terminal = false;
|
||||||
do {
|
do {
|
||||||
|
requestParams.messages = history.map(({timestamp, ...m}) => m);
|
||||||
resp = await this.client.chat.completions.create(requestParams).catch(err => {
|
resp = await this.client.chat.completions.create(requestParams).catch(err => {
|
||||||
err.message += `\n\nMessages:\n${JSON.stringify(history, null, 2)}`;
|
err.message += `\n\nMessages:\n${JSON.stringify(history, null, 2)}`;
|
||||||
throw err;
|
throw err;
|
||||||
@@ -116,7 +118,7 @@ export class OpenAi extends LLMProvider {
|
|||||||
if(options.stream) {
|
if(options.stream) {
|
||||||
if(!isFirstMessage) options.stream({text: '\n\n'});
|
if(!isFirstMessage) options.stream({text: '\n\n'});
|
||||||
else isFirstMessage = false;
|
else isFirstMessage = false;
|
||||||
resp.choices = [{message: {role: 'assistant', content: '', tool_calls: []}}];
|
resp.choices = [{message: {role: 'assistant', content: '', tool_calls: [], timestamp: Date.now()}}];
|
||||||
for await (const chunk of resp) {
|
for await (const chunk of resp) {
|
||||||
if(controller.signal.aborted) break;
|
if(controller.signal.aborted) break;
|
||||||
if(chunk.choices[0].delta.content) {
|
if(chunk.choices[0].delta.content) {
|
||||||
@@ -158,28 +160,32 @@ export class OpenAi extends LLMProvider {
|
|||||||
const results = await Promise.all(toolCalls.map(async (toolCall: any) => {
|
const results = await Promise.all(toolCalls.map(async (toolCall: any) => {
|
||||||
const tool = tools?.find(findByProp('name', toolCall.function.name));
|
const tool = tools?.find(findByProp('name', toolCall.function.name));
|
||||||
if(options.stream) options.stream({tool: toolCall.function.name});
|
if(options.stream) options.stream({tool: toolCall.function.name});
|
||||||
if(!tool) return {role: 'tool', tool_call_id: toolCall.id, content: '{"error": "Tool not found"}'};
|
if(!tool) return {role: 'tool', tool_call_id: toolCall.id, content: '{"error": "Tool not found"}', timestamp: Date.now()};
|
||||||
try {
|
try {
|
||||||
const args = JSONAttemptParse(toolCall.function.arguments, {});
|
const args = JSONAttemptParse(toolCall.function.arguments, {});
|
||||||
const result = await tool.fn(args, options.stream, this.ai);
|
// Wrap stream so a tool's `done` ends turn gracefully
|
||||||
return {role: 'tool', tool_call_id: toolCall.id, content: typeof result == 'object' ? JSONSanitize(result) : result};
|
const toolStream = options.stream && ((chunk: any) => {
|
||||||
|
if(chunk.done) { terminal = true; return; }
|
||||||
|
options.stream!(chunk);
|
||||||
|
});
|
||||||
|
const result = await tool.fn(args, toolStream, this.ai);
|
||||||
|
return {role: 'tool', tool_call_id: toolCall.id, content: typeof result == 'object' ? JSONSanitize(result) : result, timestamp: Date.now()};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return {role: 'tool', tool_call_id: toolCall.id, content: JSONSanitize({error: err?.message || err?.toString() || 'Unknown'})};
|
return {role: 'tool', tool_call_id: toolCall.id, content: JSONSanitize({error: err?.message || err?.toString() || 'Unknown'}), timestamp: Date.now()};
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
history.push(...results);
|
history.push(...results);
|
||||||
requestParams.messages = history;
|
requestParams.messages = history;
|
||||||
}
|
}
|
||||||
} while (!controller.signal.aborted && resp.choices?.[0]?.message?.tool_calls?.length);
|
} while (!terminal && !controller.signal.aborted && resp.choices?.[0]?.message?.tool_calls?.length);
|
||||||
|
|
||||||
|
if(!terminal) {
|
||||||
const textContent = resp.choices[0].message.content?.trim() || '';
|
const textContent = resp.choices[0].message.content?.trim() || '';
|
||||||
history.push({role: 'assistant', content: textContent});
|
history.push({role: 'assistant', content: textContent, timestamp: Date.now()});
|
||||||
|
}
|
||||||
history = this.toStandard(history);
|
history = this.toStandard(history);
|
||||||
|
|
||||||
if(options.stream) options.stream({done: true});
|
if(options.stream) options.stream({done: true});
|
||||||
if(options.history) options.history.splice(0, options.history.length, ...history);
|
if(options.history) options.history.splice(0, options.history.length, ...history);
|
||||||
|
|
||||||
// Return parsed JSON if schema provided
|
|
||||||
const finalContent = history.at(-1)?.content;
|
const finalContent = history.at(-1)?.content;
|
||||||
res(options.schema ? JSONAttemptParse(finalContent, finalContent) : finalContent);
|
res(options.schema ? JSONAttemptParse(finalContent, finalContent) : finalContent);
|
||||||
}), {abort: () => controller.abort()});
|
}), {abort: () => controller.abort()});
|
||||||
|
|||||||
Reference in New Issue
Block a user