feat: configure deep search llm models
This commit is contained in:
+113
-9
@@ -291,6 +291,69 @@ export function createOpenAiCompatibleResearchLlm({
|
||||
};
|
||||
}
|
||||
|
||||
export function createPortalGatewayResearchLlmResolver({
|
||||
endpoint = process.env.TKMIND_DEEP_SEARCH_LLM_GATEWAY_URL
|
||||
|| 'http://127.0.0.1:8081/api/internal/deep-search/llm',
|
||||
secret = process.env.TKMIND_DEEP_SEARCH_LLM_GATEWAY_SECRET
|
||||
|| process.env.TKMIND_DEEP_SEARCH_SECRET,
|
||||
fetchImpl = fetch,
|
||||
timeoutMs = 90_000,
|
||||
} = {}) {
|
||||
const resolver = async ({ providerKeyId, model }) => {
|
||||
if (!providerKeyId || !model) throw new Error('Deep Search LLM provider and model are required');
|
||||
if (!secret) throw new Error('Deep Search LLM gateway secret is not configured');
|
||||
async function complete(messages, { json = false } = {}) {
|
||||
const response = await fetchImpl(endpoint, {
|
||||
method: 'POST',
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
authorization: `Bearer ${secret}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
providerKeyId,
|
||||
model,
|
||||
messages,
|
||||
temperature: 0.2,
|
||||
json,
|
||||
}),
|
||||
});
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !body.ok || !body.reply) {
|
||||
throw new Error(body.message || `Deep Search LLM gateway returned ${response.status}`);
|
||||
}
|
||||
return String(body.reply).trim();
|
||||
}
|
||||
return {
|
||||
async plan(question, depth) {
|
||||
const content = await complete([
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a research planner. Return JSON {"research_plan":[{"goal":"","queries":[""],"sources":["web"]}]}. Create distinct, verifiable goals and search queries.',
|
||||
},
|
||||
{ role: 'user', content: `Depth: ${depth}\nQuestion: ${question}` },
|
||||
], { json: true });
|
||||
return JSON.parse(content);
|
||||
},
|
||||
async synthesize({ question, plan, evidence }) {
|
||||
return complete([
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Write a rigorous Markdown research report using only the supplied evidence. Cite every factual claim with [n]. Include executive summary, findings by research goal, uncertainties, and sources. Never invent citations.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: JSON.stringify({ question, plan, evidence }, null, 2).slice(0, 120_000),
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
resolver.configured = Boolean(endpoint && secret);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
function sourceAuthority(url) {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
@@ -446,6 +509,7 @@ export function createDeepSearchEngine({
|
||||
searchProvider = createSearxngResearchProvider(),
|
||||
reader = readResearchSource,
|
||||
llm = createOpenAiCompatibleResearchLlm(),
|
||||
llmResolver = null,
|
||||
memorySink = null,
|
||||
idFactory = () => randomUUID(),
|
||||
now = () => Date.now(),
|
||||
@@ -460,10 +524,10 @@ export function createDeepSearchEngine({
|
||||
emitter.emit(taskId, { type, payload, createdAt: now() });
|
||||
}
|
||||
|
||||
async function planResearch(question, depth) {
|
||||
if (llm?.plan) {
|
||||
async function planResearch(question, depth, activeLlm) {
|
||||
if (activeLlm?.plan) {
|
||||
try {
|
||||
return normalizePlan(await llm.plan(question, depth), question, depth);
|
||||
return normalizePlan(await activeLlm.plan(question, depth), question, depth);
|
||||
} catch {
|
||||
// The deterministic planner keeps Deep Search available when the LLM is unavailable.
|
||||
}
|
||||
@@ -477,7 +541,23 @@ export function createDeepSearchEngine({
|
||||
try {
|
||||
store.updateTask(taskId, { status: 'researching', phase: 'planning', progress: 5 });
|
||||
publish(taskId, 'phase', { phase: 'planning', progress: 5 });
|
||||
const plan = await planResearch(task.question, task.depth);
|
||||
let activeLlm = llm;
|
||||
if (task.llmProviderKeyId && typeof llmResolver === 'function') {
|
||||
try {
|
||||
activeLlm = await llmResolver({
|
||||
providerKeyId: task.llmProviderKeyId,
|
||||
model: task.llmModel,
|
||||
});
|
||||
publish(taskId, 'llm_selected', {
|
||||
providerKeyId: task.llmProviderKeyId,
|
||||
model: task.llmModel,
|
||||
});
|
||||
} catch (error) {
|
||||
activeLlm = null;
|
||||
publish(taskId, 'llm_error', { message: String(error?.message ?? error) });
|
||||
}
|
||||
}
|
||||
const plan = await planResearch(task.question, task.depth, activeLlm);
|
||||
throwIfAborted(signal);
|
||||
store.updateTask(taskId, { plan, phase: 'searching', progress: 12 });
|
||||
publish(taskId, 'plan', { plan });
|
||||
@@ -561,13 +641,17 @@ export function createDeepSearchEngine({
|
||||
store.updateTask(taskId, { phase: 'synthesizing', progress: 78 });
|
||||
publish(taskId, 'phase', { phase: 'synthesizing', progress: 78, evidence: evidence.length });
|
||||
let report = '';
|
||||
if (llm?.synthesize && finalSources.length) {
|
||||
if (activeLlm?.synthesize && finalSources.length) {
|
||||
try {
|
||||
const numberedEvidence = evidence.map((item) => ({
|
||||
...item,
|
||||
citation: finalSources.findIndex((source) => source.url === item.url) + 1,
|
||||
}));
|
||||
const candidate = await llm.synthesize({ question: task.question, plan, evidence: numberedEvidence });
|
||||
const candidate = await activeLlm.synthesize({
|
||||
question: task.question,
|
||||
plan,
|
||||
evidence: numberedEvidence,
|
||||
});
|
||||
if (validLlmReport(candidate, finalSources.length)) report = candidate;
|
||||
} catch {
|
||||
// Fall through to the citation-safe deterministic report.
|
||||
@@ -637,14 +721,33 @@ export function createDeepSearchEngine({
|
||||
}
|
||||
|
||||
return {
|
||||
start({ question, depth = 'standard', userId = null } = {}) {
|
||||
start({
|
||||
question,
|
||||
depth = 'standard',
|
||||
userId = null,
|
||||
llmProviderKeyId = '',
|
||||
llmModel = '',
|
||||
} = {}) {
|
||||
const normalizedQuestion = String(question ?? '').trim();
|
||||
if (!normalizedQuestion || normalizedQuestion.length > 2000) {
|
||||
throw new Error('question must be 1-2000 characters');
|
||||
}
|
||||
const normalizedDepth = Object.hasOwn(DEPTH_PROFILES, depth) ? depth : 'standard';
|
||||
const normalizedProviderKeyId = /^[a-zA-Z0-9._:-]{1,128}$/.test(String(llmProviderKeyId))
|
||||
? String(llmProviderKeyId)
|
||||
: '';
|
||||
const normalizedModel = normalizedProviderKeyId
|
||||
? String(llmModel ?? '').trim().slice(0, 200)
|
||||
: '';
|
||||
const taskId = idFactory();
|
||||
store.createTask({ id: taskId, userId, question: normalizedQuestion, depth: normalizedDepth });
|
||||
store.createTask({
|
||||
id: taskId,
|
||||
userId,
|
||||
question: normalizedQuestion,
|
||||
depth: normalizedDepth,
|
||||
llmProviderKeyId: normalizedProviderKeyId,
|
||||
llmModel: normalizedModel,
|
||||
});
|
||||
const controller = new AbortController();
|
||||
running.set(taskId, controller);
|
||||
queueMicrotask(() => executeTask(taskId, controller.signal));
|
||||
@@ -690,7 +793,8 @@ export function createDeepSearchEngine({
|
||||
service: 'tkmind-deep-search',
|
||||
running: running.size,
|
||||
tasks: store.getStats(),
|
||||
llmEnabled: Boolean(llm),
|
||||
llmEnabled: Boolean(llm || llmResolver?.configured),
|
||||
llmMode: llmResolver?.configured ? 'portal-gateway' : (llm ? 'direct' : 'deterministic'),
|
||||
provider: searchProvider.name ?? 'custom',
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user