added scheduler
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { base } from './resolve-user';
|
||||
import fs from 'fs';
|
||||
|
||||
export type schedInp = {
|
||||
name: string
|
||||
when: {
|
||||
cron?: string
|
||||
start?: string
|
||||
}
|
||||
oneShot?: boolean
|
||||
template?: { name: string }
|
||||
parameters: object
|
||||
prompt: string
|
||||
userId: string
|
||||
model: string,
|
||||
tools: string[]
|
||||
features?: Record<string, boolean>
|
||||
chatId?: string
|
||||
files?: { fname: string, fkey?: string, content?: string }[]
|
||||
}
|
||||
|
||||
export type ollamaInp = {
|
||||
name: string
|
||||
displayName: string
|
||||
userId: string
|
||||
schedule: string
|
||||
startAt?: string
|
||||
oneShot: boolean
|
||||
template: any //idk
|
||||
parameters: object
|
||||
prompt: string
|
||||
model: string
|
||||
tools: string[]
|
||||
features: Record<string, boolean>
|
||||
cookie: string
|
||||
chatId?: string
|
||||
files?: { fname: string, fkey: string }[]
|
||||
}
|
||||
|
||||
async function makeRequest(cookie: string, path: string, throwOnErr = false) {
|
||||
if (!cookie) return [];
|
||||
|
||||
const r = await fetch(`${base}/${path}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookie}`,
|
||||
"content-type": "application/json",
|
||||
"accept": "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
if (throwOnErr && !r.ok) throw r;
|
||||
|
||||
return await r.text().then(data => {
|
||||
try { return JSON.parse(data); }
|
||||
catch (_) { return data; }
|
||||
}).catch(async err => {
|
||||
console.error(err);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// NO FOREWARD SLASH NO IDK WHY IG /v1/ DOES IT FUCK ME MAN
|
||||
export const getModels = (cookie: string) => makeRequest(cookie, 'api/models').then((r) => (r.data || []));
|
||||
export const getTools = (cookie: string) => makeRequest(cookie, 'api/v1/tools/');
|
||||
export const authCall = (cookie: string) => makeRequest(cookie, 'api/v1/auths/', true);
|
||||
|
||||
const buildPrompt = async (def: ollamaInp) => {
|
||||
let content = `Please complete this request given the following information:\n`;
|
||||
content += `request: ${def.prompt}\nExtra Context:\n\`\`\`json\n${JSON.stringify(def.parameters || {})}\n\`\`\``;
|
||||
|
||||
if (def.files) {
|
||||
const files = await Promise.all(def.files.map(async f => {
|
||||
const fpath = `/app/data/files/${f.fkey}`,
|
||||
r = await fetch(`https://${base}/api/v1/files`, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'filename': f.fname
|
||||
},
|
||||
method: 'POST',
|
||||
body: await fs.readFileSync(fpath)
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
return { data: err, ok: false };
|
||||
});
|
||||
|
||||
fs.rm(fpath, { recursive: false }, (err: any) => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
|
||||
return {
|
||||
ok: r.ok,
|
||||
data: r.ok ? await (r as Response).json() : (r as any).data
|
||||
};
|
||||
}));
|
||||
|
||||
if (files.find(v => !v.ok)) {
|
||||
throw new Error(`File upload failed for file arr: ${JSON.stringify(files)}`)
|
||||
}
|
||||
|
||||
return {
|
||||
content, files: files.map(({ data: f }) => ({
|
||||
id: f.id,
|
||||
type: 'file',
|
||||
url: `/api/v1/files/${f.id}`,
|
||||
file: f,
|
||||
name: f.filename,
|
||||
status: 'uploaded', // trust, trust
|
||||
size: f.meta.size,
|
||||
error: '',
|
||||
itemId: crypto.randomUUID()
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
return { content };
|
||||
};
|
||||
|
||||
const getSystemSettings = async (cookie: string) => {
|
||||
const r = await makeRequest(cookie, 'api/v1/users/user/settings/'),
|
||||
{ notifications, system } = r?.length || {};
|
||||
return { notifications, system };
|
||||
};
|
||||
|
||||
const showErr = async (r: any) => {
|
||||
console.error('=========================================');
|
||||
console.error(r);
|
||||
console.error(await r.text());
|
||||
console.error('=========================================');
|
||||
}
|
||||
|
||||
// duplicate? Maybe?
|
||||
const toFeatureFlags = (f: Record<string, boolean> = {}) => ({
|
||||
image_generation: !!f.image_generation,
|
||||
code_interpreter: !!f.code_interpreter,
|
||||
web_search: !!f.web_search,
|
||||
memory: !!f.memory
|
||||
});
|
||||
|
||||
const splitThink = (raw: string, startPos: number = 0) => {
|
||||
const open = raw.indexOf('<think>', startPos),
|
||||
close = raw.indexOf('</think>', startPos);
|
||||
|
||||
if (close >= 0) {
|
||||
const start = open >= 0 ? open + '<think>'.length : 0;
|
||||
return {
|
||||
thinking: raw.slice(start, close).trim(),
|
||||
answer: raw.slice(close + '</think>'.length).trim()
|
||||
};
|
||||
}
|
||||
return { thinking: '', answer: raw.trim(), endPos: close };
|
||||
};
|
||||
|
||||
async function newChat(def: ollamaInp, sysMsg?: string) {
|
||||
const userId = crypto.randomUUID(),
|
||||
assistantId = crypto.randomUUID(), // will be reused in completions
|
||||
now = Date.now();
|
||||
|
||||
const userMsg = {
|
||||
id: userId,
|
||||
parentId: null,
|
||||
childrenIds: [assistantId],
|
||||
role: 'user',
|
||||
...(await buildPrompt(def)),
|
||||
timestamp: now,
|
||||
models: [def.model]
|
||||
};
|
||||
|
||||
const assistantPlaceholder = {
|
||||
id: assistantId,
|
||||
parentId: userId,
|
||||
childrenIds: [],
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
model: def.model,
|
||||
modelName: def.model,
|
||||
modelIdx: 0,
|
||||
timestamp: now
|
||||
};
|
||||
|
||||
const reqObj = {
|
||||
user_id: def.userId,
|
||||
title: 'New Chat',
|
||||
chat: {
|
||||
id: '',
|
||||
title: 'New Chat',
|
||||
models: [def.model],
|
||||
system: sysMsg || '',
|
||||
params: {}, // model params???
|
||||
history: {
|
||||
messages: {
|
||||
[userMsg.id]: userMsg,
|
||||
[assistantPlaceholder.id]: assistantPlaceholder
|
||||
},
|
||||
currentId: assistantId // needed for openwebUI stuff
|
||||
},
|
||||
messages: [userMsg, assistantPlaceholder],
|
||||
tags: []
|
||||
},
|
||||
share_id: null,
|
||||
archived: false,
|
||||
pinned: false,
|
||||
meta: {},
|
||||
folder_id: null
|
||||
};
|
||||
|
||||
const r = await fetch(`${base}/api/v1/chats/new`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${def.cookie}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(reqObj)
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
await showErr(r);
|
||||
throw new Error('Failed to create new chat!');
|
||||
}
|
||||
|
||||
const created = await r.json();
|
||||
return { chatId: created.id as string, assistantId };
|
||||
}
|
||||
|
||||
export async function callNewChat(def: ollamaInp) {
|
||||
const { cookie } = def;
|
||||
if (!cookie) throw new Error('Cookie not found!');
|
||||
|
||||
const sysSettings = await getSystemSettings(cookie),
|
||||
{ name: username } = await makeRequest(cookie, 'api/v1/auths/'),
|
||||
models = await getModels(cookie),
|
||||
model = models.find((m: any) => m.id === def.model);
|
||||
|
||||
if (!model) throw new Error(`Model ${def.model} not found!`);
|
||||
|
||||
const { chatId, assistantId } = def.chatId
|
||||
? { chatId: def.chatId, assistantId: crypto.randomUUID() } // TODO: if passing an existing chat, insert a placeholder there first (same shape as above)
|
||||
: await newChat(def, sysSettings.system);
|
||||
|
||||
// mock socket ID
|
||||
const sessionId = crypto.randomUUID(),
|
||||
completeReq = {
|
||||
chat_id: chatId,
|
||||
id: assistantId,
|
||||
stream: false,
|
||||
model: def.model,
|
||||
messages: [
|
||||
{ role: 'system', content: sysSettings.system || '' },
|
||||
{ role: 'user', ...(await buildPrompt(def)) }
|
||||
],
|
||||
features: toFeatureFlags(def.features),
|
||||
variables: {
|
||||
'{{USER_NAME}}': username,
|
||||
'{{USER_LANGUAGE}}': 'en-US'
|
||||
},
|
||||
session_id: sessionId,
|
||||
background_tasks: { title_generation: true, follow_up_generation: true }
|
||||
};
|
||||
|
||||
// run completion
|
||||
let r = await fetch(`${base}/api/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${cookie}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(completeReq)
|
||||
});
|
||||
if (!r.ok) return showErr(r);
|
||||
|
||||
// bc stream:false, read the whole json here bc ui still wants a finalizer call
|
||||
const txt = await r.text();
|
||||
let content = txt;
|
||||
|
||||
try { const j = JSON.parse(txt); content = j?.choices?.[0]?.message?.content ?? txt; } catch { }
|
||||
const thinkRes = splitThink(content),
|
||||
answerArr = [(thinkRes.thinking ? `<details id="__DETAIL_${0}__"/>\n` : '') + thinkRes.answer];
|
||||
|
||||
let counter = thinkRes.thinking ? 1 : 0,
|
||||
{ thinking, endPos } = thinkRes;
|
||||
|
||||
while (thinking && counter < 10) {
|
||||
const { thinking: newThink, answer: newAnswer, endPos: newEndPos } = splitThink(content, endPos);
|
||||
|
||||
thinking = newThink;
|
||||
|
||||
if (thinking) {
|
||||
answerArr.push(`<details id="__DETAIL_${counter}__"/>\n` + newAnswer);
|
||||
endPos = newEndPos;
|
||||
}
|
||||
else break;
|
||||
|
||||
counter++;
|
||||
}
|
||||
|
||||
const answer = answerArr.join('\n')
|
||||
|
||||
// fetch current chat, replace the assistant placeholder content with `answer`
|
||||
const chat = await (await fetch(`${base}/api/v1/chats/${chatId}`, {
|
||||
headers: { Authorization: `Bearer ${def.cookie}` }
|
||||
})).json();
|
||||
|
||||
chat.chat.history.messages[assistantId].content = answer;
|
||||
const idx = chat.chat.messages.findIndex((m: any) => m.id === assistantId);
|
||||
if (idx >= 0) chat.chat.messages[idx].content = answer;
|
||||
|
||||
console.log(JSON.stringify(chat));
|
||||
|
||||
r = await fetch(`${base}/api/v1/chats/${chatId}`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${def.cookie}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ chat: chat.chat })
|
||||
});
|
||||
|
||||
// now finalize
|
||||
r = await fetch(`${base}/api/chat/completed`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${def.cookie}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ chat_id: chatId, id: assistantId, session_id: sessionId, model: def.model })
|
||||
});
|
||||
|
||||
console.log('completed:', await r.text());
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import http from 'http'
|
||||
import { readBodyJson } from '../server';
|
||||
import { getModels, getTools } from './ollamaCalls';
|
||||
|
||||
export const base = 'http://open-webui:8080';
|
||||
type Me = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
profile_image_url: string;
|
||||
token: string;
|
||||
token_type: string;
|
||||
expires_at: string | null;
|
||||
permissions: {
|
||||
workspace: {
|
||||
models: boolean;
|
||||
knowledge: boolean;
|
||||
prompts: boolean;
|
||||
tools: boolean;
|
||||
};
|
||||
|
||||
features: {
|
||||
direct_tool_servers: boolean;
|
||||
web_search: boolean;
|
||||
image_generation: boolean;
|
||||
code_interpreter: boolean;
|
||||
notes: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export default async function loginUser(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const { email, password } = await readBodyJson(req);
|
||||
|
||||
if (!email || !password) {
|
||||
res.writeHead(400).end(JSON.stringify({ error: "email or password not sent" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// login with username/password
|
||||
const loginRes = await fetch(`${base}/api/v1/auths/signin`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!loginRes.ok) {
|
||||
console.error("error logging in", await loginRes.text());
|
||||
res.writeHead(401).end();
|
||||
return
|
||||
}
|
||||
|
||||
const upstreamCookie = loginRes.headers.get("set-cookie"),
|
||||
user = await loginRes.json();
|
||||
|
||||
// forward Set-Cookie to the browser so it stores the cookie
|
||||
const outHeaders: Record<string, string | string[]> = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
|
||||
if (upstreamCookie) {
|
||||
outHeaders["Set-Cookie"] = upstreamCookie;
|
||||
}
|
||||
|
||||
res.writeHead(200, outHeaders).end(JSON.stringify({ user }));
|
||||
}
|
||||
|
||||
export async function getUser(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
if (!req.headers.cookie) {
|
||||
return res.writeHead(401).end("Not logged in");
|
||||
}
|
||||
|
||||
const cookies = Object.fromEntries(req.headers.cookie.split(';').map(c => c.split('=').map(o => o.trim())));
|
||||
if (!('token' in cookies)) {
|
||||
return res.writeHead(401).end("Not logged in");
|
||||
}
|
||||
|
||||
const uRes = await fetch(`${base}/api/v1/auths/`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
'Authorization': `Bearer ${cookies['token']}`
|
||||
},
|
||||
});
|
||||
|
||||
if (!uRes.ok) {
|
||||
console.error("Error getting user", await uRes.text());
|
||||
return res.writeHead(401).end();
|
||||
}
|
||||
|
||||
const uObj = await uRes.json();
|
||||
uObj.models = await getModels(cookies['token']);
|
||||
uObj.tools = await getTools(cookies['token']);
|
||||
|
||||
res.writeHead(200).end(JSON.stringify(uObj));
|
||||
}
|
||||
Reference in New Issue
Block a user