From 46e384c3418033a2f85d7a124bea14eaebe7017e Mon Sep 17 00:00:00 2001 From: Manus Date: Fri, 20 Mar 2026 18:20:37 -0400 Subject: [PATCH] Checkpoint: Phase 8 Complete: Fix Orchestrator Chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Исправлено: - Chat.tsx: убрана хардкодированная модель "qwen2.5:7b" из мутации — теперь оркестратор использует модель из конфига БД (minimax-m2.7) - Chat.tsx: добавлен Streamdown для markdown рендеринга ответов оркестратора - Подтверждено: tool calling работает — команда "Покажи файлы проекта" вызывает file_list и возвращает структуру проекта - Подтверждено: model в header показывает "minimax-m2.7" из БД - TypeScript: 0 ошибок (pnpm tsc --noEmit) - Тесты: 24/24 passed --- .manus/db/db-query-1774044902126.json | 34 ++++ .manus/db/db-query-1774044910065.json | 202 ++++++++++++++++++++ .manus/db/db-query-1774044914301.json | 82 ++++++++ .manus/db/db-query-1774044919559.json | 22 +++ .manus/db/db-query-1774044946048.json | 46 +++++ .manus/db/db-query-error-1774044906729.json | 8 + client/src/pages/Chat.tsx | 11 +- server/ollama.ts | 58 +++++- server/orchestrator.ts | 19 +- todo.md | 24 ++- 10 files changed, 477 insertions(+), 29 deletions(-) create mode 100644 .manus/db/db-query-1774044902126.json create mode 100644 .manus/db/db-query-1774044910065.json create mode 100644 .manus/db/db-query-1774044914301.json create mode 100644 .manus/db/db-query-1774044919559.json create mode 100644 .manus/db/db-query-1774044946048.json create mode 100644 .manus/db/db-query-error-1774044906729.json diff --git a/.manus/db/db-query-1774044902126.json b/.manus/db/db-query-1774044902126.json new file mode 100644 index 0000000..4b91eef --- /dev/null +++ b/.manus/db/db-query-1774044902126.json @@ -0,0 +1,34 @@ +{ + "query": "SHOW TABLES;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Zh4qRMAq8LtV1c.6374c5e14c69 --database ZEGAT83geRq9CNvryykaQv --execute SHOW TABLES;", + "rows": [ + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "__drizzle_migrations" + }, + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "agentAccessControl" + }, + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "agentHistory" + }, + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "agentMetrics" + }, + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "agents" + }, + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "browserSessions" + }, + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "toolDefinitions" + }, + { + "Tables_in_ZEGAT83geRq9CNvryykaQv": "users" + } + ], + "messages": [], + "stdout": "Tables_in_ZEGAT83geRq9CNvryykaQv\n__drizzle_migrations\nagentAccessControl\nagentHistory\nagentMetrics\nagents\nbrowserSessions\ntoolDefinitions\nusers\n", + "stderr": "", + "execution_time_ms": 605 +} \ No newline at end of file diff --git a/.manus/db/db-query-1774044910065.json b/.manus/db/db-query-1774044910065.json new file mode 100644 index 0000000..7270736 --- /dev/null +++ b/.manus/db/db-query-1774044910065.json @@ -0,0 +1,202 @@ +{ + "query": "DESCRIBE agents;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Zh4qRMAq8LtV1c.6374c5e14c69 --database ZEGAT83geRq9CNvryykaQv --execute DESCRIBE agents;", + "rows": [ + { + "Field": "id", + "Type": "int", + "Null": "NO", + "Key": "PRI", + "Default": "NULL", + "Extra": "auto_increment" + }, + { + "Field": "userId", + "Type": "int", + "Null": "NO", + "Key": "MUL", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "name", + "Type": "varchar(255)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "description", + "Type": "text", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "role", + "Type": "varchar(100)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "model", + "Type": "varchar(100)", + "Null": "NO", + "Key": "MUL", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "provider", + "Type": "varchar(50)", + "Null": "NO", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "temperature", + "Type": "decimal(3,2)", + "Null": "YES", + "Key": "", + "Default": "0.7", + "Extra": "" + }, + { + "Field": "maxTokens", + "Type": "int", + "Null": "YES", + "Key": "", + "Default": "2048", + "Extra": "" + }, + { + "Field": "topP", + "Type": "decimal(3,2)", + "Null": "YES", + "Key": "", + "Default": "1.0", + "Extra": "" + }, + { + "Field": "frequencyPenalty", + "Type": "decimal(3,2)", + "Null": "YES", + "Key": "", + "Default": "0.0", + "Extra": "" + }, + { + "Field": "presencePenalty", + "Type": "decimal(3,2)", + "Null": "YES", + "Key": "", + "Default": "0.0", + "Extra": "" + }, + { + "Field": "systemPrompt", + "Type": "text", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "allowedTools", + "Type": "json", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "allowedDomains", + "Type": "json", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "maxRequestsPerHour", + "Type": "int", + "Null": "YES", + "Key": "", + "Default": "100", + "Extra": "" + }, + { + "Field": "isActive", + "Type": "tinyint(1)", + "Null": "YES", + "Key": "", + "Default": "1", + "Extra": "" + }, + { + "Field": "isPublic", + "Type": "tinyint(1)", + "Null": "YES", + "Key": "", + "Default": "0", + "Extra": "" + }, + { + "Field": "tags", + "Type": "json", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "metadata", + "Type": "json", + "Null": "YES", + "Key": "", + "Default": "NULL", + "Extra": "" + }, + { + "Field": "createdAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "" + }, + { + "Field": "updatedAt", + "Type": "timestamp", + "Null": "NO", + "Key": "", + "Default": "CURRENT_TIMESTAMP", + "Extra": "DEFAULT_GENERATED on update CURRENT_TIMESTAMP" + }, + { + "Field": "isSystem", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "0", + "Extra": "" + }, + { + "Field": "isOrchestrator", + "Type": "tinyint(1)", + "Null": "NO", + "Key": "", + "Default": "0", + "Extra": "" + } + ], + "messages": [], + "stdout": "Field\tType\tNull\tKey\tDefault\tExtra\nid\tint\tNO\tPRI\tNULL\tauto_increment\nuserId\tint\tNO\tMUL\tNULL\t\nname\tvarchar(255)\tNO\t\tNULL\t\ndescription\ttext\tYES\t\tNULL\t\nrole\tvarchar(100)\tNO\t\tNULL\t\nmodel\tvarchar(100)\tNO\tMUL\tNULL\t\nprovider\tvarchar(50)\tNO\t\tNULL\t\ntemperature\tdecimal(3,2)\tYES\t\t0.7\t\nmaxTokens\tint\tYES\t\t2048\t\ntopP\tdecimal(3,2)\tYES\t\t1.0\t\nfrequencyPenalty\tdecimal(3,2)\tYES\t\t0.0\t\npresencePenalty\tdecimal(3,2)\tYES\t\t0.0\t\nsystemPrompt\ttext\tYES\t\tNULL\t\nallowedTools\tjson\tYES\t\tNULL\t\nallowedDomains\tjson\tYES\t\tNULL\t\nmaxRequestsPerHour\tint\tYES\t\t100\t\nisActive\ttinyint(1)\tYES\t\t1\t\nisPublic\ttinyint(1)\tYES\t\t0\t\ntags\tjson\tYES\t\tNULL\t\nmetadata\tjson\tYES\t\tNULL\t\ncreatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\t\nupdatedAt\ttimestamp\tNO\t\tCURRENT_TIMESTAMP\tDEFAULT_GENERATED on update CURRENT_TIMESTAMP\nisSystem\ttinyint(1)\tNO\t\t0\t\nisOrchestrator\ttinyint(1)\tNO\t\t0\t\n", + "stderr": "", + "execution_time_ms": 523 +} \ No newline at end of file diff --git a/.manus/db/db-query-1774044914301.json b/.manus/db/db-query-1774044914301.json new file mode 100644 index 0000000..7e5df6e --- /dev/null +++ b/.manus/db/db-query-1774044914301.json @@ -0,0 +1,82 @@ +{ + "query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'agents' AND TABLE_SCHEMA = DATABASE() ORDER BY ORDINAL_POSITION;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Zh4qRMAq8LtV1c.6374c5e14c69 --database ZEGAT83geRq9CNvryykaQv --execute SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'agents' AND TABLE_SCHEMA = DATABASE() ORDER BY ORDINAL_POSITION;", + "rows": [ + { + "COLUMN_NAME": "id" + }, + { + "COLUMN_NAME": "userId" + }, + { + "COLUMN_NAME": "name" + }, + { + "COLUMN_NAME": "description" + }, + { + "COLUMN_NAME": "role" + }, + { + "COLUMN_NAME": "model" + }, + { + "COLUMN_NAME": "provider" + }, + { + "COLUMN_NAME": "temperature" + }, + { + "COLUMN_NAME": "maxTokens" + }, + { + "COLUMN_NAME": "topP" + }, + { + "COLUMN_NAME": "frequencyPenalty" + }, + { + "COLUMN_NAME": "presencePenalty" + }, + { + "COLUMN_NAME": "systemPrompt" + }, + { + "COLUMN_NAME": "allowedTools" + }, + { + "COLUMN_NAME": "allowedDomains" + }, + { + "COLUMN_NAME": "maxRequestsPerHour" + }, + { + "COLUMN_NAME": "isActive" + }, + { + "COLUMN_NAME": "isPublic" + }, + { + "COLUMN_NAME": "tags" + }, + { + "COLUMN_NAME": "metadata" + }, + { + "COLUMN_NAME": "createdAt" + }, + { + "COLUMN_NAME": "updatedAt" + }, + { + "COLUMN_NAME": "isSystem" + }, + { + "COLUMN_NAME": "isOrchestrator" + } + ], + "messages": [], + "stdout": "COLUMN_NAME\nid\nuserId\nname\ndescription\nrole\nmodel\nprovider\ntemperature\nmaxTokens\ntopP\nfrequencyPenalty\npresencePenalty\nsystemPrompt\nallowedTools\nallowedDomains\nmaxRequestsPerHour\nisActive\nisPublic\ntags\nmetadata\ncreatedAt\nupdatedAt\nisSystem\nisOrchestrator\n", + "stderr": "", + "execution_time_ms": 383 +} \ No newline at end of file diff --git a/.manus/db/db-query-1774044919559.json b/.manus/db/db-query-1774044919559.json new file mode 100644 index 0000000..51ed9f7 --- /dev/null +++ b/.manus/db/db-query-1774044919559.json @@ -0,0 +1,22 @@ +{ + "query": "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'agents' AND TABLE_SCHEMA = DATABASE() AND ORDINAL_POSITION > 20 ORDER BY ORDINAL_POSITION;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Zh4qRMAq8LtV1c.6374c5e14c69 --database ZEGAT83geRq9CNvryykaQv --execute SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'agents' AND TABLE_SCHEMA = DATABASE() AND ORDINAL_POSITION > 20 ORDER BY ORDINAL_POSITION;", + "rows": [ + { + "COLUMN_NAME": "createdAt" + }, + { + "COLUMN_NAME": "updatedAt" + }, + { + "COLUMN_NAME": "isSystem" + }, + { + "COLUMN_NAME": "isOrchestrator" + } + ], + "messages": [], + "stdout": "COLUMN_NAME\ncreatedAt\nupdatedAt\nisSystem\nisOrchestrator\n", + "stderr": "", + "execution_time_ms": 370 +} \ No newline at end of file diff --git a/.manus/db/db-query-1774044946048.json b/.manus/db/db-query-1774044946048.json new file mode 100644 index 0000000..fb76c80 --- /dev/null +++ b/.manus/db/db-query-1774044946048.json @@ -0,0 +1,46 @@ +{ + "query": "SELECT id, name, role, model, isSystem, isOrchestrator, isActive FROM agents;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Zh4qRMAq8LtV1c.6374c5e14c69 --database ZEGAT83geRq9CNvryykaQv --execute SELECT id, name, role, model, isSystem, isOrchestrator, isActive FROM agents;", + "rows": [ + { + "id": "8", + "name": "Orchestrator", + "role": "orchestrator", + "model": "minimax-m2.7", + "isSystem": "1", + "isOrchestrator": "1", + "isActive": "1" + }, + { + "id": "9", + "name": "Browser Agent", + "role": "browser", + "model": "qwen2.5:7b", + "isSystem": "1", + "isOrchestrator": "0", + "isActive": "1" + }, + { + "id": "10", + "name": "Tool Builder", + "role": "tool_builder", + "model": "qwen2.5-coder:7b", + "isSystem": "1", + "isOrchestrator": "0", + "isActive": "1" + }, + { + "id": "11", + "name": "Agent Compiler", + "role": "agent_compiler", + "model": "qwen2.5:14b", + "isSystem": "1", + "isOrchestrator": "0", + "isActive": "1" + } + ], + "messages": [], + "stdout": "id\tname\trole\tmodel\tisSystem\tisOrchestrator\tisActive\n8\tOrchestrator\torchestrator\tminimax-m2.7\t1\t1\t1\n9\tBrowser Agent\tbrowser\tqwen2.5:7b\t1\t0\t1\n10\tTool Builder\ttool_builder\tqwen2.5-coder:7b\t1\t0\t1\n11\tAgent Compiler\tagent_compiler\tqwen2.5:14b\t1\t0\t1\n", + "stderr": "", + "execution_time_ms": 538 +} \ No newline at end of file diff --git a/.manus/db/db-query-error-1774044906729.json b/.manus/db/db-query-error-1774044906729.json new file mode 100644 index 0000000..4798e46 --- /dev/null +++ b/.manus/db/db-query-error-1774044906729.json @@ -0,0 +1,8 @@ +{ + "query": "SELECT id, name, role, activeModel, isOrchestrator, isSystem, status FROM agents;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Zh4qRMAq8LtV1c.6374c5e14c69 --database ZEGAT83geRq9CNvryykaQv --execute SELECT id, name, role, activeModel, isOrchestrator, isSystem, status FROM agents;", + "returncode": 1, + "logs": [ + "ERROR 1054 (42S22) at line 1: Unknown column 'activemodel' in 'field list'" + ] +} \ No newline at end of file diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index fb44e39..ca85cc3 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect } from "react"; +import { Streamdown } from "streamdown"; import { motion, AnimatePresence } from "framer-motion"; import { trpc } from "@/lib/trpc"; import { Card, CardContent } from "@/components/ui/card"; @@ -226,7 +227,13 @@ function MessageBubble({ msg }: { msg: ChatMessage }) { : "bg-secondary/50 border border-border/40 text-foreground" }`} > -
{msg.content}
+ {isUser || isSystem || msg.isError ? ( +
{msg.content}
+ ) : ( +
+ {msg.content} +
+ )} )} @@ -312,7 +319,7 @@ export default function Chat() { try { const result = await orchestratorMutation.mutateAsync({ messages: newHistory, - model: "qwen2.5:7b", + // model is loaded from DB config — do not override here maxIterations: 10, }); diff --git a/server/ollama.ts b/server/ollama.ts index 48189f8..c6289a0 100644 --- a/server/ollama.ts +++ b/server/ollama.ts @@ -19,13 +19,42 @@ interface OllamaModelsResponse { } interface ChatMessage { - role: "system" | "user" | "assistant"; - content: string; + role: "system" | "user" | "assistant" | "tool" | "function"; + content: string | null; + tool_call_id?: string; + name?: string; + tool_calls?: ToolCall[]; +} + +interface ToolFunction { + name: string; + description?: string; + parameters?: Record; +} + +interface Tool { + type: "function"; + function: ToolFunction; +} + +interface ToolCallFunction { + name: string; + arguments: string; +} + +interface ToolCall { + id: string; + type: "function"; + function: ToolCallFunction; } interface ChatChoice { index: number; - message: ChatMessage; + message: { + role: string; + content: string | null; + tool_calls?: ToolCall[]; + }; finish_reason: string; } @@ -100,6 +129,7 @@ export async function listModels(): Promise { /** * Отправка сообщения в чат (OpenAI-совместимый формат, без стриминга) + * Поддерживает tool calling для оркестратора. */ export async function chatCompletion( model: string, @@ -107,18 +137,26 @@ export async function chatCompletion( options?: { temperature?: number; max_tokens?: number; + tools?: Tool[]; + tool_choice?: "auto" | "none" | "required"; } ): Promise { + const body: Record = { + model, + messages, + stream: false, + }; + if (options?.temperature !== undefined) body.temperature = options.temperature; + if (options?.max_tokens !== undefined) body.max_tokens = options.max_tokens; + if (options?.tools?.length) { + body.tools = options.tools; + body.tool_choice = options.tool_choice ?? "auto"; + } + const res = await fetch(`${getBaseUrl()}/chat/completions`, { method: "POST", headers: getHeaders(), - body: JSON.stringify({ - model, - messages, - stream: false, - ...(options?.temperature !== undefined && { temperature: options.temperature }), - ...(options?.max_tokens !== undefined && { max_tokens: options.max_tokens }), - }), + body: JSON.stringify(body), signal: AbortSignal.timeout(120_000), // LLM может думать долго }); if (!res.ok) { diff --git a/server/orchestrator.ts b/server/orchestrator.ts index 423e375..a4ac186 100644 --- a/server/orchestrator.ts +++ b/server/orchestrator.ts @@ -522,31 +522,32 @@ export async function orchestratorChat( while (iterations < maxToolIterations) { iterations++; - // Call LLM with tools + // Call LLM with tools using the model from DB config let llmResult: any; try { - llmResult = await invokeLLM({ - messages: conversation as any, - tools: ORCHESTRATOR_TOOLS, + llmResult = await chatCompletion(activeModel, conversation as any, { + temperature: config.temperature, + max_tokens: config.maxTokens, + tools: ORCHESTRATOR_TOOLS as any, tool_choice: "auto", }); } catch (err: any) { - // Fallback: try without tools if LLM doesn't support them + // Fallback: try without tools if model doesn't support them try { const fallbackResult = await chatCompletion(activeModel, conversation as any, { - temperature: 0.7, - max_tokens: 4096, + temperature: config.temperature, + max_tokens: config.maxTokens, }); finalResponse = fallbackResult.choices[0]?.message?.content ?? ""; lastUsage = fallbackResult.usage; - lastModel = fallbackResult.model; + lastModel = fallbackResult.model ?? activeModel; break; } catch (fallbackErr: any) { return { success: false, response: "", toolCalls, - error: `LLM error: ${fallbackErr.message}`, + error: `LLM error (model: ${activeModel}): ${fallbackErr.message}`, }; } } diff --git a/todo.md b/todo.md index 771b86d..4cc49ac 100644 --- a/todo.md +++ b/todo.md @@ -107,11 +107,19 @@ - [ ] Write tests for orchestrator ## Phase 7: Orchestrator as Configurable System Agent -- [ ] Add isSystem + isOrchestrator fields to agents table (DB migration) -- [ ] Seed Orchestrator as system agent in DB (role=orchestrator, isSystem=true) -- [ ] Update orchestrator.ts to load model/systemPrompt/allowedTools from DB -- [ ] Update /chat to read orchestrator config from DB, show active model in header -- [ ] Update /agents to show Orchestrator with SYSTEM badge, Configure button, no delete -- [ ] AgentDetailModal: orchestrator gets extra tab with system tools (shell, docker, agents mgmt) -- [ ] Add system tools to orchestrator: docker_ps, docker_restart, manage_agents, read_logs -- [ ] /chat header: show current model name + link to Configure Orchestrator +- [x] Add isSystem + isOrchestrator fields to agents table (DB migration) +- [x] Seed Orchestrator as system agent in DB (role=orchestrator, isSystem=true) +- [x] Update orchestrator.ts to load model/systemPrompt/allowedTools from DB +- [x] Update /chat to read orchestrator config from DB, show active model in header +- [x] Update /agents to show Orchestrator with SYSTEM badge, Configure button, no delete +- [x] AgentDetailModal: orchestrator gets extra tab with system tools (shell, docker, agents mgmt) +- [x] Add system tools to orchestrator: docker_ps, docker_restart, manage_agents, read_logs +- [x] /chat header: show current model name + link to Configure Orchestrator + +## Phase 8: Fix Orchestrator Chat +- [x] Fix: orchestrator uses model from DB config (minimax-m2.7, not hardcoded fallback) +- [x] Fix: real tool-use loop — execute shell_exec, file_read, file_list tools +- [x] Fix: show tool call steps in Chat UI (tool name, args, result, duration) +- [x] Fix: Chat.tsx shows which model is being used from orchestrator config +- [x] Fix: Streamdown markdown rendering for assistant responses +- [ ] Add: streaming/SSE for real-time response display