Deploy: migrate k3s → Docker; search logic → master-agents-svc
- deploy/k3s удалён, deploy/docker добавлен (Caddyfile, docker-compose, searxng) - chat-svc: agents/models/prompts удалены, использует llm-svc (LLMClient, EmbeddingClient) - master-agents-svc: SearchOrchestrator, classifier, researcher, actions, widgets - web-svc: ChatModelSelector, Optimization, Sources удалены; InputBarPlus; UnregisterSW - geo-device-svc, localization-svc: Dockerfiles - docs: 02-k3s-services-spec.md, RUNBOOK/TELEMETRY/WORKING удалены Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
156
services/master-agents-svc/src/lib/agent/searchOrchestrator.ts
Normal file
156
services/master-agents-svc/src/lib/agent/searchOrchestrator.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { LlmClient } from '../llm-client.js';
|
||||
import SessionManager from '../session.js';
|
||||
import type { TextBlock } from '../types.js';
|
||||
import type { ClassifierOutput } from '../actions/types.js';
|
||||
import { getClassifierPrompt } from '../prompts/classifier.js';
|
||||
import { getWriterPrompt } from '../prompts/writer.js';
|
||||
import { classify } from './classifier.js';
|
||||
import { research } from './researcher.js';
|
||||
import { executeAllWidgets } from '../widgets/index.js';
|
||||
|
||||
export type SearchOrchestratorConfig = {
|
||||
llm: LlmClient;
|
||||
mode: 'speed' | 'balanced' | 'quality';
|
||||
sources: ('web' | 'discussions' | 'academic')[];
|
||||
fileIds: string[];
|
||||
systemInstructions: string;
|
||||
locale?: string;
|
||||
memoryContext?: string;
|
||||
answerMode?: import('../prompts/writer.js').AnswerMode;
|
||||
responsePrefs?: { format?: string; length?: string; tone?: string };
|
||||
learningMode?: boolean;
|
||||
};
|
||||
|
||||
export type SearchOrchestratorInput = {
|
||||
chatHistory: { role: string; content: string }[];
|
||||
followUp: string;
|
||||
config: SearchOrchestratorConfig;
|
||||
};
|
||||
|
||||
export async function runSearchOrchestrator(
|
||||
session: SessionManager,
|
||||
input: SearchOrchestratorInput,
|
||||
): Promise<void> {
|
||||
const { chatHistory, followUp, config } = input;
|
||||
|
||||
const classification = await classify({
|
||||
chatHistory,
|
||||
query: followUp,
|
||||
llm: config.llm,
|
||||
locale: config.locale,
|
||||
enabledSources: config.sources,
|
||||
});
|
||||
|
||||
const widgetPromise = executeAllWidgets({
|
||||
chatHistory,
|
||||
followUp,
|
||||
classification,
|
||||
llm: config.llm,
|
||||
}).then((outputs) => {
|
||||
for (const o of outputs) {
|
||||
session.emitBlock({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'widget',
|
||||
data: { widgetType: o.type, params: o.data ?? {} },
|
||||
});
|
||||
}
|
||||
return outputs;
|
||||
});
|
||||
|
||||
let searchPromise: Promise<{ searchFindings: import('../types.js').Chunk[] }> | null = null;
|
||||
if (!classification.classification.skipSearch) {
|
||||
searchPromise = research(session, config.llm, {
|
||||
chatHistory,
|
||||
followUp,
|
||||
classification,
|
||||
config: {
|
||||
mode: config.mode,
|
||||
sources: config.sources,
|
||||
fileIds: config.fileIds,
|
||||
locale: config.locale,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [widgetOutputs, searchResults] = await Promise.all([widgetPromise, searchPromise ?? Promise.resolve({ searchFindings: [] })]);
|
||||
|
||||
session.emit('data', { type: 'researchComplete' });
|
||||
|
||||
const MAX_RESULTS_FOR_WRITER = 15;
|
||||
const MAX_CONTENT_PER_RESULT = 180;
|
||||
const findingsForWriter = (searchResults?.searchFindings ?? []).slice(0, MAX_RESULTS_FOR_WRITER);
|
||||
const finalContext =
|
||||
findingsForWriter
|
||||
.map((f, index) => {
|
||||
const content = f.content.length > MAX_CONTENT_PER_RESULT ? f.content.slice(0, MAX_CONTENT_PER_RESULT) + '…' : f.content;
|
||||
return `<result index=${index + 1} title="${String(f.metadata?.title ?? '').replace(/"/g, "'")}">${content}</result>`;
|
||||
})
|
||||
.join('\n') || '';
|
||||
|
||||
const widgetContext = widgetOutputs
|
||||
.map((o) => `<result>${o.llmContext}</result>`)
|
||||
.join('\n-------------\n');
|
||||
|
||||
const finalContextWithWidgets =
|
||||
`<search_results note="These are the search results and assistant can cite these">\n${finalContext}\n</search_results>\n` +
|
||||
`<widgets_result noteForAssistant="Its output is already showed to the user, assistant can use this information to answer the query but do not CITE this as a source">\n${widgetContext}\n</widgets_result>`;
|
||||
|
||||
const writerPrompt = getWriterPrompt(
|
||||
finalContextWithWidgets,
|
||||
config.systemInstructions,
|
||||
config.mode,
|
||||
config.locale,
|
||||
config.memoryContext,
|
||||
config.answerMode,
|
||||
config.responsePrefs,
|
||||
config.learningMode,
|
||||
);
|
||||
|
||||
const answerStream = config.llm.streamText({
|
||||
messages: [
|
||||
{ role: 'system', content: writerPrompt },
|
||||
...chatHistory,
|
||||
{ role: 'user', content: followUp },
|
||||
],
|
||||
options: { maxTokens: 4096 },
|
||||
});
|
||||
|
||||
let responseBlockId = '';
|
||||
let hasContent = false;
|
||||
|
||||
for await (const chunk of answerStream) {
|
||||
if (!chunk.contentChunk && !responseBlockId) continue;
|
||||
if (!responseBlockId) {
|
||||
const block: TextBlock = {
|
||||
id: crypto.randomUUID(),
|
||||
type: 'text',
|
||||
data: chunk.contentChunk ?? '',
|
||||
};
|
||||
session.emitBlock(block);
|
||||
responseBlockId = block.id;
|
||||
if (chunk.contentChunk) hasContent = true;
|
||||
} else {
|
||||
const block = session.getBlock(responseBlockId) as TextBlock | null;
|
||||
if (block) {
|
||||
block.data += chunk.contentChunk ?? '';
|
||||
if (chunk.contentChunk) hasContent = true;
|
||||
session.updateBlock(block.id, [{ op: 'replace', path: '/data', value: block.data }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasContent && findingsForWriter.length > 0) {
|
||||
const lines = findingsForWriter.slice(0, 10).map((f, i) => {
|
||||
const title = (f.metadata?.title as string) ?? 'Без названия';
|
||||
const excerpt = f.content.length > 120 ? f.content.slice(0, 120) + '…' : f.content;
|
||||
return `${i + 1}. **${title}** — ${excerpt}`;
|
||||
});
|
||||
session.emitBlock({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'text',
|
||||
data: `## По найденным источникам\n\n${lines.join('\n\n')}\n\n*Ответ LLM недоступен. Проверьте модель в Settings.*`,
|
||||
});
|
||||
}
|
||||
|
||||
session.emit('end', {});
|
||||
}
|
||||
Reference in New Issue
Block a user