implement modular web search capabilities with provider support, content fetching, and file-based caching.
This commit is contained in:
1 parent
ffec37fd0a
commit
c280683a9d
8 files changed
+580
-5
No files matched your search
@@ -10,6 +10,7 @@ const net = require("net");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawn, spawnSync, execSync, exec, execFile } = require("child_process");
|
||||
const { comprehensiveWebSearch } = require("../search/core");
|
||||
|
||||
// HTTP keep-alive agent for llama-server (eliminates TCP handshake per request)
|
||||
const llmHttpAgent = new http.Agent({
|
||||
@@ -129,6 +130,10 @@ const TTS_CACHE = path.join(ROOT, "app", "tts-cache");
|
||||
if (!fs.existsSync(TTS_CACHE)) {
|
||||
fs.mkdirSync(TTS_CACHE, { recursive: true });
|
||||
}
|
||||
const WEB_SEARCH_CACHE = path.join(ROOT, "app", "cache", "search");
|
||||
if (!fs.existsSync(WEB_SEARCH_CACHE)) {
|
||||
fs.mkdirSync(WEB_SEARCH_CACHE, { recursive: true });
|
||||
}
|
||||
const TTS_RUNTIME = path.join(ROOT, "app", "tts-runtime");
|
||||
const TTS_WORKER = path.join(ROOT, "scripts", "workers", "tts-kokoro-worker.mjs");
|
||||
const SPEECH_BACKEND_ROOT = path.join(ROOT, "app", "speech-backend");
|
||||
@@ -5404,12 +5409,72 @@ async function retryLowerContext() {
|
||||
await runExclusiveLlmOperation(() => startLlm(newSettings));
|
||||
}
|
||||
|
||||
function getMessageText(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.map((item) => {
|
||||
if (typeof item === "string") return item;
|
||||
if (typeof item?.text === "string") return item.text;
|
||||
if (typeof item?.content === "string") return item.content;
|
||||
return "";
|
||||
}).join("\n");
|
||||
}
|
||||
if (content && typeof content === "object") {
|
||||
if (typeof content.text === "string") return content.text;
|
||||
if (typeof content.content === "string") return content.content;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getLastUserQuery(messages) {
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
if (messages[i]?.role === "user") {
|
||||
return getMessageText(messages[i].content).trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function augmentMessagesWithWebSearch(messages, body) {
|
||||
if (body.useWeb !== true && body.use_web !== true) {
|
||||
return { messages, webSources: [], webContext: "" };
|
||||
}
|
||||
const query = String(body.webQuery || body.query || getLastUserQuery(messages) || "").trim();
|
||||
if (!query) return { messages, webSources: [], webContext: "" };
|
||||
|
||||
const result = await comprehensiveWebSearch(query, {
|
||||
timeFilter: body.timeFilter || body.time_filter || "any",
|
||||
resultLimit: body.webResultLimit || 5,
|
||||
fetchLimit: body.webFetchLimit || 3,
|
||||
cacheDir: WEB_SEARCH_CACHE,
|
||||
});
|
||||
if (!result.context || !result.sources.length) {
|
||||
return { messages, webSources: [], webContext: "" };
|
||||
}
|
||||
let insertAt = 0;
|
||||
while (insertAt < messages.length && messages[insertAt]?.role === "system") {
|
||||
insertAt += 1;
|
||||
}
|
||||
const augmentedMessages = [
|
||||
...messages.slice(0, insertAt),
|
||||
{ role: "user", content: result.context },
|
||||
...messages.slice(insertAt),
|
||||
];
|
||||
return {
|
||||
messages: augmentedMessages,
|
||||
webSources: result.sources,
|
||||
webContext: result.context,
|
||||
};
|
||||
}
|
||||
|
||||
async function doLlmChat(req, res, body, retryCount = 0) {
|
||||
try {
|
||||
const isStream = body.stream === true;
|
||||
const rawMessages = Array.isArray(body.messages) ? body.messages : [];
|
||||
const webAugmentation = await augmentMessagesWithWebSearch(rawMessages, body);
|
||||
const requestData = JSON.stringify({
|
||||
model: llmSettings.model || "local-model",
|
||||
messages: Array.isArray(body.messages) ? body.messages : [],
|
||||
messages: webAugmentation.messages,
|
||||
temperature: Number.isFinite(Number(body.temperature)) ? Number(body.temperature) : 0.7,
|
||||
max_tokens: Math.max(1, Math.min(4096, Number(body.max_tokens) || Number(body.maxTokens) || 1024)),
|
||||
stream: isStream,
|
||||
@@ -5468,6 +5533,9 @@ async function doLlmChat(req, res, body, retryCount = 0) {
|
||||
const socket = res.socket || res.connection;
|
||||
socket?.setNoDelay?.(true);
|
||||
res.flushHeaders?.();
|
||||
if (webAugmentation.webSources.length) {
|
||||
res.write(`event: web_sources\ndata: ${JSON.stringify({ sources: webAugmentation.webSources })}\n\n`);
|
||||
}
|
||||
clientRes.on("data", (chunk) => res.write(chunk));
|
||||
clientRes.on("end", () => res.end());
|
||||
clientRes.on("error", (err) => res.destroy(err));
|
||||
@@ -5500,6 +5568,9 @@ async function doLlmChat(req, res, body, retryCount = 0) {
|
||||
} else {
|
||||
try {
|
||||
const result = await requestJson(`http://127.0.0.1:${PORT_LLM}/v1/chat/completions`, JSON.parse(requestData), 300000);
|
||||
if (webAugmentation.webSources.length) {
|
||||
result.web_sources = webAugmentation.webSources;
|
||||
}
|
||||
return json(res, 200, result);
|
||||
} catch (err) {
|
||||
if (isOomError(err.message) && retryCount === 0) {
|
||||
@@ -5701,6 +5772,32 @@ async function getLlmfitRecommendations(useCase = "chat", limit = 10) {
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === "/api/web-search/config" && req.method === "GET") {
|
||||
return json(res, 200, {
|
||||
ok: true,
|
||||
provider: "duckduckgo",
|
||||
providers: [{ key: "duckduckgo", name: "DuckDuckGo", requiresApiKey: false, active: true }],
|
||||
cachePath: WEB_SEARCH_CACHE,
|
||||
defaults: { resultLimit: 5, fetchLimit: 3, timeFilter: "any" },
|
||||
});
|
||||
}
|
||||
|
||||
if (req.url === "/api/web-search" && req.method === "POST") {
|
||||
const body = await readJsonBody(req, res);
|
||||
if (!body) return;
|
||||
try {
|
||||
const result = await comprehensiveWebSearch(body.query || body.q || "", {
|
||||
timeFilter: body.timeFilter || body.time_filter || "any",
|
||||
resultLimit: body.resultLimit || 5,
|
||||
fetchLimit: body.fetchLimit || 3,
|
||||
cacheDir: WEB_SEARCH_CACHE,
|
||||
});
|
||||
return json(res, 200, { ok: true, ...result });
|
||||
} catch (err) {
|
||||
return json(res, 500, { ok: false, error: err.message || String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === "/api/llm/chat" && req.method === "POST") {
|
||||
const body = await readJsonBody(req, res);
|
||||
if (!body) return;
|
||||
|
||||
Reference in new issue
Block a user