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:
home
2026-02-23 22:14:00 +03:00
parent cd6b7857ba
commit 328d968f3f
180 changed files with 3022 additions and 9798 deletions

View 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', {});
}