shitton of the ai changes

This commit is contained in:
2026-05-01 04:54:11 +03:00
parent d95c37a322
commit 8cff086a8e
194 changed files with 29409 additions and 8841 deletions
+60
View File
@@ -0,0 +1,60 @@
import test from "node:test";
import assert from "node:assert/strict";
const {
DEFAULT_PIPELINE_FALLBACK_POLICIES,
} = await import("../dist/ai/user-request-pipeline/blueprint.js");
const {
decidePipelineFallback,
fallbackReasonFromStageStatus,
resolvePipelineFallbackAction,
} = await import("../dist/ai/user-request-pipeline/fallback-executor.js");
test("fallback executor resolves configured failed action", () => {
assert.equal(
resolvePipelineFallbackAction({
stage: "input_size_gate",
reason: "failed",
policies: DEFAULT_PIPELINE_FALLBACK_POLICIES,
}),
"notify_user",
);
});
test("fallback executor uses default action for missing policy", () => {
assert.equal(
resolvePipelineFallbackAction({
stage: "send_response",
reason: "failed",
policies: [],
}),
"fail_request",
);
assert.equal(
resolvePipelineFallbackAction({
stage: "send_response",
reason: "unavailable",
policies: [],
}),
"continue_without_stage",
);
});
test("fallback decision exposes notify and continuation flags", () => {
const decision = decidePipelineFallback({
stage: "document_rag",
reason: "failed",
policies: DEFAULT_PIPELINE_FALLBACK_POLICIES,
});
assert.equal(decision.action, "notify_user");
assert.equal(decision.shouldNotifyUser, true);
assert.equal(decision.shouldContinue, true);
assert.equal(decision.shouldFailRequest, false);
});
test("fallback reason maps only failed and skipped statuses", () => {
assert.equal(fallbackReasonFromStageStatus("failed"), "failed");
assert.equal(fallbackReasonFromStageStatus("skipped"), "unavailable");
assert.equal(fallbackReasonFromStageStatus("succeeded"), undefined);
});
+178
View File
@@ -0,0 +1,178 @@
import test from "node:test";
import assert from "node:assert/strict";
const {UserRequestPipeline} = await import("../dist/ai/user-request-pipeline/pipeline.js");
const {splitAttachmentsBySize} = await import("../dist/ai/user-request-pipeline/size-gate.js");
const {PIPELINE_ATTACHMENT_LIMIT_BYTES} = await import("../dist/ai/user-request-pipeline/types.js");
function baseState() {
return {
requestId: "test-request",
chatId: 1,
messageId: 2,
fromId: 3,
receivedAt: new Date(0).toISOString(),
text: "hello",
settings: {
provider: "OLLAMA",
responseLanguage: "default",
voiceMode: "execute",
imageOutputMode: "photo",
},
inputAttachments: [],
outputAttachments: [],
artifacts: [],
toolRankDecisions: [],
audit: [],
};
}
test("pipeline runs only requested stage slice", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["input_size_gate", "download_attachments"],
stages: [{
name: "input_size_gate",
async run() {
return {
stage: "input_size_gate",
status: "succeeded",
details: {checked: true},
};
},
}],
});
await pipeline.run(state, new AbortController().signal);
assert.equal(state.audit.length, 3);
assert.equal(state.audit[0].stage, "input_size_gate");
assert.equal(state.audit[0].status, "running");
assert.equal(state.audit[1].stage, "input_size_gate");
assert.equal(state.audit[1].status, "succeeded");
assert.deepEqual(state.audit[1].details, {checked: true});
assert.equal(state.audit[2].stage, "download_attachments");
assert.equal(state.audit[2].status, "skipped");
assert.deepEqual(state.audit[2].details, {
reason: "stage_not_registered",
fallbackAction: "continue_without_stage",
});
});
test("pipeline stops when fallback decision is fail_request", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["send_response"],
stages: [{
name: "send_response",
async run() {
throw new Error("send failed");
},
}],
fallbackPolicies: [{
stage: "send_response",
onUnavailable: "fail_request",
onFailed: "fail_request",
}],
});
await assert.rejects(() => pipeline.run(state, new AbortController().signal), /send failed/);
assert.equal(state.audit.at(-1).stage, "send_response");
assert.equal(state.audit.at(-1).status, "failed");
assert.equal(state.audit.at(-1).details.fallbackAction, "fail_request");
});
test("pipeline continues when fallback decision allows continuation", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["document_rag", "send_response"],
stages: [
{
name: "document_rag",
async run() {
throw new Error("rag failed");
},
},
{
name: "send_response",
async run() {
return {
stage: "send_response",
status: "succeeded",
};
},
},
],
});
await pipeline.run(state, new AbortController().signal);
assert.equal(state.audit.some(event => event.stage === "document_rag" && event.status === "failed"), true);
assert.equal(state.audit.at(-1).stage, "send_response");
assert.equal(state.audit.at(-1).status, "succeeded");
});
test("pipeline persists stage artifacts and direction-aware attachments", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["persist_output_artifacts"],
stages: [{
name: "persist_output_artifacts",
async run() {
return {
stage: "persist_output_artifacts",
status: "succeeded",
artifacts: [{
kind: "final_text",
stage: "persist_output_artifacts",
createdAt: new Date(0).toISOString(),
text: "answer",
}],
attachments: [
{
direction: "input",
kind: "document",
fileName: "input.txt",
sizeBytes: 10,
},
{
direction: "output",
kind: "document",
fileName: "output.txt",
sizeBytes: 20,
},
],
};
},
}],
});
await pipeline.run(state, new AbortController().signal);
assert.equal(state.artifacts.length, 1);
assert.equal(state.artifacts[0].kind, "final_text");
assert.equal(state.inputAttachments.length, 1);
assert.equal(state.inputAttachments[0].fileName, "input.txt");
assert.equal(state.outputAttachments.length, 1);
assert.equal(state.outputAttachments[0].fileName, "output.txt");
});
test("size gate splits accepted and rejected attachments", () => {
const result = splitAttachmentsBySize([
{
direction: "input",
kind: "document",
fileName: "small.txt",
sizeBytes: PIPELINE_ATTACHMENT_LIMIT_BYTES,
},
{
direction: "input",
kind: "document",
fileName: "large.txt",
sizeBytes: PIPELINE_ATTACHMENT_LIMIT_BYTES + 1,
},
]);
assert.deepEqual(result.accepted.map(attachment => attachment.fileName), ["small.txt"]);
assert.equal(result.rejected.length, 1);
assert.equal(result.rejected[0].attachment.fileName, "large.txt");
});
+154
View File
@@ -0,0 +1,154 @@
import test, {after} from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tg-chat-bot-rag-"));
process.env.BOT_TOKEN = process.env.BOT_TOKEN ?? "test-token";
process.env.CREATOR_ID = process.env.CREATOR_ID ?? "1";
process.env.DATA_PATH = tempRoot;
process.env.DB_PATH = `file:${path.join(tempRoot, "test.sqlite")}`;
process.env.TEST_ENVIRONMENT = "true";
const {Environment} = await import("../dist/common/environment.js");
Environment.load();
const {DatabaseManager} = await import("../dist/db/database-manager.js");
DatabaseManager.init();
await DatabaseManager.ready;
const {ArtifactStore} = await import("../dist/common/artifact-store.js");
const {filterUserVisibleStoredAttachments} = await import("../dist/common/stored-attachment-utils.js");
const {AiProvider} = await import("../dist/model/ai-provider.js");
const {persistRagArtifactAttachment} = await import("../dist/ai/rag-artifact-store.js");
after(async () => {
await DatabaseManager.close().catch(() => undefined);
fs.rmSync(tempRoot, {recursive: true, force: true});
});
test("internal artifacts are not treated as user-visible attachments", () => {
const visible = filterUserVisibleStoredAttachments([
{
kind: "document",
fileId: "visible",
fileName: "visible.txt",
cachePath: "/tmp/visible.txt",
},
{
kind: "document",
fileId: "internal",
fileName: "rag.json",
cachePath: "/tmp/rag.json",
scope: "internal_artifact",
artifactKind: "rag",
},
]);
assert.equal(visible.length, 1);
assert.equal(visible[0].fileId, "visible");
});
test("RAG artifacts persist structured ollama metadata", async () => {
const chatId = 42;
const messageId = 7;
const attachment = await persistRagArtifactAttachment({
provider: AiProvider.OLLAMA,
prepared: {
provider: AiProvider.OLLAMA,
prepared: true,
cleanup: async () => undefined,
artifact: {
query: "What is in the file?",
extractedDocuments: [
{documentIndex: 0, fileName: "report.txt", textChars: 120},
],
selectedChunks: [
{
sourceId: "doc1-1",
documentIndex: 0,
documentName: "report.txt",
chunkIndex: 0,
chunkCount: 1,
textChars: 120,
score: 0.91,
},
],
skippedDocuments: [
{documentIndex: 1, fileName: "ignored.bin", reason: "unsupported format"},
],
providerState: {
embeddingModel: "nomic-embed-text:latest",
topK: 8,
chunkSize: 1400,
chunkOverlap: 220,
maxContextChars: 14000,
minScore: 0.12,
maxArchiveFiles: 200,
maxArchiveBytes: 50 * 1024 * 1024,
maxArchiveDepth: 2,
},
},
},
downloads: [{
kind: "document",
fileId: "file-1",
fileName: "report.txt",
buffer: Buffer.from("hello world"),
path: path.join(tempRoot, "report.txt"),
}],
chatId,
messageId,
details: {
embeddingModel: "nomic-embed-text:latest",
topK: 8,
chunkSize: 1400,
chunkOverlap: 220,
maxContextChars: 14000,
artifact: {
query: "What is in the file?",
extractedDocuments: [
{documentIndex: 0, fileName: "report.txt", textChars: 120},
],
selectedChunks: [
{
sourceId: "doc1-1",
documentIndex: 0,
documentName: "report.txt",
chunkIndex: 0,
chunkCount: 1,
textChars: 120,
score: 0.91,
},
],
skippedDocuments: [
{documentIndex: 1, fileName: "ignored.bin", reason: "unsupported format"},
],
providerState: {
embeddingModel: "nomic-embed-text:latest",
topK: 8,
chunkSize: 1400,
chunkOverlap: 220,
maxContextChars: 14000,
minScore: 0.12,
maxArchiveFiles: 200,
maxArchiveBytes: 50 * 1024 * 1024,
maxArchiveDepth: 2,
},
},
},
});
assert.equal(attachment?.artifactKind, "rag");
assert.equal(fs.existsSync(attachment.cachePath), true);
const stored = await ArtifactStore.getByMessage(chatId, messageId);
assert.equal(stored.length, 1);
assert.equal(stored[0].kind, "rag");
assert.equal(stored[0].payload.providerState.query, "What is in the file?");
assert.equal(stored[0].payload.providerState.selectedChunks[0].score, 0.91);
assert.equal(stored[0].payload.providerState.skippedDocuments[0].reason, "unsupported format");
assert.equal(stored[0].payload.providerState.ollama.embeddingModel, "nomic-embed-text:latest");
});
+154
View File
@@ -0,0 +1,154 @@
import test from "node:test";
import assert from "node:assert/strict";
const {
buildToolRankerSystemPrompt,
getToolRankerAvailableToolInfos,
sanitizeToolRankerResult,
} = await import("../dist/ai/tool-ranker-metadata.js");
function toolInfos(...toolTypes) {
return getToolRankerAvailableToolInfos(toolTypes.map(type => ({type})));
}
function promptFor(...toolTypes) {
return buildToolRankerSystemPrompt({
availableTools: toolInfos(...toolTypes),
includeExamples: true,
maxExamplesPerTool: 1,
compact: true,
});
}
test("prompt contains only available tools", () => {
const prompt = promptFor("no_tool", "get_datetime", "image_generation", "code_interpreter", "file_search");
assert.ok(prompt.includes("no_tool"));
assert.ok(prompt.includes("get_datetime"));
assert.ok(prompt.includes("image_generation"));
assert.ok(prompt.includes("code_interpreter"));
assert.ok(prompt.includes("file_search"));
assert.ok(!prompt.includes("get_weather"));
assert.ok(!prompt.includes("python_interpreter"));
});
test("prompt does not contain disabled tools", () => {
const prompt = promptFor("no_tool", "read_file", "search_files");
assert.ok(prompt.includes("read_file"));
assert.ok(prompt.includes("search_files"));
assert.ok(!prompt.includes("get_weather"));
assert.ok(!prompt.includes("shell_execute"));
assert.ok(!prompt.includes("python_interpreter"));
});
test("examples are filtered when tools are unavailable", () => {
const prompt = promptFor("no_tool", "read_file", "search_files");
assert.ok(prompt.includes("прочитай src/index.ts"));
assert.ok(prompt.includes("найди где используется sendMessage"));
assert.ok(!prompt.includes("погода завтра"));
assert.ok(!prompt.includes("выполни этот python код"));
});
test("prompt includes image generation routing example", () => {
const prompt = promptFor("no_tool", "image_generation");
assert.ok(prompt.includes("сделай его лысым"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["image_generation"]})));
});
test("prompt includes weather routing example", () => {
const prompt = promptFor("no_tool", "get_weather");
assert.ok(prompt.includes("погода завтра"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["get_weather"]})));
});
test("prompt includes web search routing example for current information", () => {
const prompt = promptFor("no_tool", "web_search");
assert.ok(prompt.includes("найди актуальную документацию OpenAI API"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["web_search"]})));
});
test("prompt includes read file routing example for known file paths", () => {
const prompt = promptFor("no_tool", "read_file");
assert.ok(prompt.includes("прочитай src/index.ts"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["read_file"]})));
});
test("prompt includes search files routing example for usage search", () => {
const prompt = promptFor("no_tool", "search_files");
assert.ok(prompt.includes("найди где используется sendMessage"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["search_files"]})));
});
test("prompt includes edit file patch routing example for targeted edits", () => {
const prompt = promptFor("no_tool", "edit_file_patch");
assert.ok(prompt.includes("исправь этот баг патчем"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["edit_file_patch"]})));
});
test("prompt includes update file routing example for full overwrite", () => {
const prompt = promptFor("no_tool", "update_file");
assert.ok(prompt.includes("полностью перезапиши config.json"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["update_file"]})));
});
test("prompt includes delete path caution for explicit deletion only", () => {
const prompt = promptFor("no_tool", "delete_path");
assert.ok(prompt.includes("delete_path only when the user clearly asks to delete or remove something."));
assert.ok(prompt.includes("удали папку dist"));
assert.ok(prompt.includes(JSON.stringify({toolNames: ["delete_path"]})));
});
test("sanitizer returns no_tool for normal explanation", () => {
const result = sanitizeToolRankerResult({
raw: "объясни docker volumes",
availableToolNames: ["read_file", "search_files"],
});
assert.deepEqual(result, ["no_tool"]);
});
test("sanitizer removes unavailable tools", () => {
const result = sanitizeToolRankerResult({
raw: JSON.stringify({toolNames: ["read_file", "missing_tool"]}),
availableToolNames: ["read_file"],
});
assert.deepEqual(result, ["read_file"]);
});
test("sanitizer deduplicates tools", () => {
const result = sanitizeToolRankerResult({
raw: JSON.stringify({toolNames: ["read_file", "read_file", "search_files"]}),
availableToolNames: ["read_file", "search_files"],
});
assert.deepEqual(result, ["read_file", "search_files"]);
});
test("sanitizer handles malformed output", () => {
const result = sanitizeToolRankerResult({
raw: "```json\nnot json\n```",
availableToolNames: ["read_file"],
});
assert.deepEqual(result, ["no_tool"]);
});
test("sanitizer removes no_tool when mixed with real tools", () => {
const result = sanitizeToolRankerResult({
raw: JSON.stringify({toolNames: ["no_tool", "read_file"]}),
availableToolNames: ["read_file"],
});
assert.deepEqual(result, ["read_file"]);
});