## Phase 1 (Fixed): Agent Management UI - Исправлена авторизация: agents переведены на publicProcedure - AgentDetailModal: 5 вкладок (General, LLM Params, Tools, History, Stats) - Полное редактирование: model, provider, temperature, topP, maxTokens, frequencyPenalty, presencePenalty, systemPrompt - Управление allowedTools и allowedDomains через теги - AgentCreateModal: создание агентов с выбором модели из Ollama API - Кнопка Metrics на каждой карточке агента ## Phase 2+3: Tool Binding System - server/tools.ts: реестр из 10 инструментов (http_get, http_post, shell_exec, file_read, file_write, docker_list, docker_exec, docker_logs, browser_navigate, browser_screenshot) - Безопасное выполнение: проверка allowedTools агента, accessControl из БД - tools.execute tRPC endpoint - Tools.tsx: страница управления инструментами с тест-выполнением - Добавлен пункт "Инструменты" в sidebar навигацию ## Phase 4: Metrics & History - AgentMetrics.tsx: детальная страница метрик по агенту - Request Timeline: bar chart по часам (success/error) - Conversation Log: история диалогов с пагинацией - Raw Metrics Table: все метрики с токенами и временем - Time range selector: 6h/24h/48h/7d - Маршрут /agents/:id/metrics ## Tests: 24/24 passed - server/auth.logout.test.ts (1) - server/agents.test.ts (7) - server/tools.test.ts (13) - server/ollama.test.ts (3)
272 lines
7.5 KiB
TypeScript
272 lines
7.5 KiB
TypeScript
import { eq, and, desc, gte } from "drizzle-orm";
|
|
import { agents, agentMetrics, agentHistory, agentAccessControl, type Agent, type InsertAgent, type AgentMetric, type InsertAgentMetric } from "../drizzle/schema";
|
|
import { getDb } from "./db";
|
|
import { nanoid } from "nanoid";
|
|
|
|
/**
|
|
* Создать нового агента
|
|
*/
|
|
export async function createAgent(userId: number, data: InsertAgent): Promise<Agent | null> {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
try {
|
|
const result = await db.insert(agents).values({
|
|
...data,
|
|
userId,
|
|
});
|
|
|
|
const agentId = result[0].insertId;
|
|
const created = await db.select().from(agents).where(eq(agents.id, Number(agentId))).limit(1);
|
|
return created[0] || null;
|
|
} catch (error) {
|
|
console.error("[DB] Failed to create agent:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить агента по ID
|
|
*/
|
|
export async function getAgentById(agentId: number): Promise<Agent | null> {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
try {
|
|
const result = await db.select().from(agents).where(eq(agents.id, agentId)).limit(1);
|
|
return result[0] || null;
|
|
} catch (error) {
|
|
console.error("[DB] Failed to get agent:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить все агенты пользователя
|
|
*/
|
|
export async function getUserAgents(userId: number): Promise<Agent[]> {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
try {
|
|
return await db.select().from(agents).where(eq(agents.userId, userId));
|
|
} catch (error) {
|
|
console.error("[DB] Failed to get user agents:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Обновить конфигурацию агента
|
|
*/
|
|
export async function updateAgent(agentId: number, updates: Partial<InsertAgent>): Promise<Agent | null> {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
try {
|
|
await db.update(agents).set(updates).where(eq(agents.id, agentId));
|
|
return getAgentById(agentId);
|
|
} catch (error) {
|
|
console.error("[DB] Failed to update agent:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Удалить агента
|
|
*/
|
|
export async function deleteAgent(agentId: number): Promise<boolean> {
|
|
const db = await getDb();
|
|
if (!db) return false;
|
|
|
|
try {
|
|
await db.delete(agents).where(eq(agents.id, agentId));
|
|
return true;
|
|
} catch (error) {
|
|
console.error("[DB] Failed to delete agent:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Сохранить метрику запроса
|
|
*/
|
|
export async function saveMetric(agentId: number, data: Omit<InsertAgentMetric, "agentId">): Promise<AgentMetric | null> {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
try {
|
|
const requestId = nanoid();
|
|
const result = await db.insert(agentMetrics).values({
|
|
...data,
|
|
agentId,
|
|
requestId,
|
|
});
|
|
|
|
const metricId = result[0].insertId;
|
|
const created = await db.select().from(agentMetrics).where(eq(agentMetrics.id, Number(metricId))).limit(1);
|
|
return created[0] || null;
|
|
} catch (error) {
|
|
console.error("[DB] Failed to save metric:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить метрики агента за последние N часов
|
|
*/
|
|
export async function getAgentMetrics(agentId: number, hoursBack: number = 24): Promise<AgentMetric[]> {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
try {
|
|
const since = new Date(Date.now() - hoursBack * 60 * 60 * 1000);
|
|
return await db
|
|
.select()
|
|
.from(agentMetrics)
|
|
.where(and(eq(agentMetrics.agentId, agentId), gte(agentMetrics.createdAt, since)))
|
|
.orderBy(desc(agentMetrics.createdAt));
|
|
} catch (error) {
|
|
console.error("[DB] Failed to get agent metrics:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить статистику агента
|
|
*/
|
|
export async function getAgentStats(agentId: number, hoursBack: number = 24) {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
try {
|
|
const metrics = await getAgentMetrics(agentId, hoursBack);
|
|
|
|
const totalRequests = metrics.length;
|
|
const successRequests = metrics.filter((m) => m.status === "success").length;
|
|
const errorRequests = metrics.filter((m) => m.status === "error").length;
|
|
const avgProcessingTime = metrics.length > 0 ? metrics.reduce((sum, m) => sum + m.processingTimeMs, 0) / metrics.length : 0;
|
|
const totalTokens = metrics.reduce((sum, m) => sum + (m.totalTokens || 0), 0);
|
|
const avgTokensPerRequest = metrics.length > 0 ? totalTokens / metrics.length : 0;
|
|
|
|
return {
|
|
totalRequests,
|
|
successRequests,
|
|
errorRequests,
|
|
successRate: totalRequests > 0 ? (successRequests / totalRequests) * 100 : 0,
|
|
avgProcessingTime: Math.round(avgProcessingTime),
|
|
totalTokens,
|
|
avgTokensPerRequest: Math.round(avgTokensPerRequest),
|
|
period: `${hoursBack}h`,
|
|
};
|
|
} catch (error) {
|
|
console.error("[DB] Failed to get agent stats:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить историю запросов агента
|
|
*/
|
|
export async function getAgentHistory(agentId: number, limit: number = 50) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
try {
|
|
return await db
|
|
.select()
|
|
.from(agentHistory)
|
|
.where(eq(agentHistory.agentId, agentId))
|
|
.orderBy(desc(agentHistory.createdAt))
|
|
.limit(limit);
|
|
} catch (error) {
|
|
console.error("[DB] Failed to get agent history:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить управление доступами для агента
|
|
*/
|
|
export async function getAgentAccessControl(agentId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
try {
|
|
return await db.select().from(agentAccessControl).where(eq(agentAccessControl.agentId, agentId));
|
|
} catch (error) {
|
|
console.error("[DB] Failed to get agent access control:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Обновить управление доступами для инструмента
|
|
*/
|
|
export async function updateToolAccess(agentId: number, tool: string, updates: Partial<typeof agentAccessControl.$inferInsert>) {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
try {
|
|
const existing = await db
|
|
.select()
|
|
.from(agentAccessControl)
|
|
.where(and(eq(agentAccessControl.agentId, agentId), eq(agentAccessControl.tool, tool)))
|
|
.limit(1);
|
|
|
|
if (existing.length > 0) {
|
|
await db
|
|
.update(agentAccessControl)
|
|
.set(updates)
|
|
.where(and(eq(agentAccessControl.agentId, agentId), eq(agentAccessControl.tool, tool)));
|
|
} else {
|
|
await db.insert(agentAccessControl).values({
|
|
agentId,
|
|
tool,
|
|
...updates,
|
|
});
|
|
}
|
|
|
|
const result = await db
|
|
.select()
|
|
.from(agentAccessControl)
|
|
.where(and(eq(agentAccessControl.agentId, agentId), eq(agentAccessControl.tool, tool)))
|
|
.limit(1);
|
|
|
|
return result[0] || null;
|
|
} catch (error) {
|
|
console.error("[DB] Failed to update tool access:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Сохранить запись в историю агента
|
|
*/
|
|
export async function saveHistory(
|
|
agentId: number,
|
|
data: {
|
|
userMessage: string;
|
|
agentResponse: string | null;
|
|
conversationId?: string;
|
|
status: "pending" | "success" | "error";
|
|
}
|
|
) {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
try {
|
|
const result = await db.insert(agentHistory).values({
|
|
agentId,
|
|
userMessage: data.userMessage,
|
|
agentResponse: data.agentResponse ?? undefined,
|
|
conversationId: data.conversationId,
|
|
status: data.status,
|
|
});
|
|
return result[0];
|
|
} catch (error) {
|
|
console.error("[DB] Failed to save history:", error);
|
|
return null;
|
|
}
|
|
}
|