Compare commits

..

2 Commits
1.6.0 ... 1.6.2

Author SHA1 Message Date
85c01d3ef1 Added official file support
All checks were successful
Publish Library / Build NPM Project (push) Successful in 30s
Publish Library / Tag Version (push) Successful in 10s
2026-08-17 15:50:48 -04:00
5826573d5c Added official file support
All checks were successful
Publish Library / Build NPM Project (push) Successful in 50s
Publish Library / Tag Version (push) Successful in 13s
2026-08-17 15:16:32 -04:00
2 changed files with 17 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ztimson/ai-utils", "name": "@ztimson/ai-utils",
"version": "1.6.0", "version": "1.6.2",
"description": "AI Utility library", "description": "AI Utility library",
"author": "Zak Timson", "author": "Zak Timson",
"license": "MIT", "license": "MIT",

View File

@@ -50,6 +50,8 @@ export type LLMMessage = {
role: 'assistant' | 'system' | 'user'; role: 'assistant' | 'system' | 'user';
/** Message content */ /** Message content */
content: string | any; content: string | any;
/** Files attached to request */
files?: LLMFile[];
/** Timestamp */ /** Timestamp */
timestamp?: number; timestamp?: number;
/** Response duration in ms */ /** Response duration in ms */
@@ -241,10 +243,8 @@ class LLM {
} else if(isText) { } else if(isText) {
text = (await this.loadBuffer(file, true)).toString('utf-8'); text = (await this.loadBuffer(file, true)).toString('utf-8');
} else { } else {
text = `Unsupported file type: ${ext || mime}`; text = typeof file.content === 'string' ? file.content : `[Binary file, unable to extract: ${name}]`;
} }
// Cache result, skip re-extraction on future turns of the same conversation
file.content = text; file.content = text;
file.extracted = true; file.extracted = true;
delete file.path; delete file.path;
@@ -411,7 +411,8 @@ ${a.system}`,
let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || []; let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || [];
const prompts: string[] = []; const prompts: string[] = [];
let history = options.history || []; let history = options.history || [];
if(message) history.push({role: 'user', content: message, timestamp: Date.now()}); const files = options.files || [];
if(message || files.length) history.push({role: 'user', content: message || '', timestamp: Date.now()});
// MCP // MCP
const mcp = options.mcp || this.ai.options?.llm?.mcp; const mcp = options.mcp || this.ai.options?.llm?.mcp;
@@ -482,14 +483,16 @@ Linked: ${makeUnique([...r.links, ...r.backlinks]).join(', ')}
if(aborted) throw Object.assign(new Error('Aborted'), {name: 'AbortError'}); if(aborted) throw Object.assign(new Error('Aborted'), {name: 'AbortError'});
// Files
const files = options.files || [];
const lastMsg = history[history.length - 1]; const lastMsg = history[history.length - 1];
const originalContent = lastMsg?.content; if(files.length && lastMsg?.role === 'user') lastMsg.files = files;
if(files.length && lastMsg?.role === 'user') { const restores: {msg: LLMMessage, content: any}[] = [];
const {text, images} = await this.resolveFiles(files); for(const msg of history) {
const merged = text ? `${originalContent}\n\n${text}` : originalContent; if(msg.role !== 'user' || !msg.files?.length) continue;
lastMsg.content = images.length const {text, images} = await this.resolveFiles(msg.files);
if(!text && !images.length) continue;
restores.push({msg, content: msg.content});
const merged = text ? [msg.content, text].filter(Boolean).join('\n\n') : msg.content;
msg.content = images.length
? [...images.map(i => ({type: 'image', mime: i.mime, data: i.data})), {type: 'text', text: merged}] ? [...images.map(i => ({type: 'image', mime: i.mime, data: i.data})), {type: 'text', text: merged}]
: merged; : merged;
} }
@@ -503,7 +506,8 @@ Linked: ${makeUnique([...r.links, ...r.backlinks]).join(', ')}
request = this.models[m].ask('', {...options, tools, system: prompts.filter(Boolean).join('\n\n')}); request = this.models[m].ask('', {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
let resp = await request; let resp = await request;
if(files.length && lastMsg?.role === 'user') lastMsg.content = originalContent; // Strip the file injection shim
restores.forEach(({msg, content}) => msg.content = content);
// Capture meta (duration / tps) // Capture meta (duration / tps)
for(const h of history) { for(const h of history) {