true message

This commit is contained in:
Manus
2026-03-20 16:39:29 -04:00
parent b18e6e244f
commit 159a89a156
17 changed files with 3054 additions and 191 deletions

63
server/agents.test.ts Normal file
View File

@@ -0,0 +1,63 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import * as agentsModule from "./agents";
// Mock getDb
vi.mock("./db", () => ({
getDb: vi.fn(async () => null),
}));
describe("Agents Module", () => {
describe("createAgent", () => {
it("should return null when database is unavailable", async () => {
const result = await agentsModule.createAgent(1, {
name: "Test Agent",
role: "developer",
model: "gpt-4o",
provider: "openai",
});
expect(result).toBeNull();
});
});
describe("getAgentById", () => {
it("should return null when database is unavailable", async () => {
const result = await agentsModule.getAgentById(1);
expect(result).toBeNull();
});
});
describe("getUserAgents", () => {
it("should return empty array when database is unavailable", async () => {
const result = await agentsModule.getUserAgents(1);
expect(result).toEqual([]);
});
});
describe("getAgentStats", () => {
it("should return null when database is unavailable", async () => {
const result = await agentsModule.getAgentStats(1);
expect(result).toBeNull();
});
});
describe("getAgentMetrics", () => {
it("should return empty array when database is unavailable", async () => {
const result = await agentsModule.getAgentMetrics(1);
expect(result).toEqual([]);
});
});
describe("getAgentHistory", () => {
it("should return empty array when database is unavailable", async () => {
const result = await agentsModule.getAgentHistory(1);
expect(result).toEqual([]);
});
});
describe("getAgentAccessControl", () => {
it("should return empty array when database is unavailable", async () => {
const result = await agentsModule.getAgentAccessControl(1);
expect(result).toEqual([]);
});
});
});

241
server/agents.ts Normal file
View File

@@ -0,0 +1,241 @@
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;
}
}

View File

@@ -2,7 +2,7 @@ import { COOKIE_NAME } from "@shared/const";
import { z } from "zod";
import { getSessionCookieOptions } from "./_core/cookies";
import { systemRouter } from "./_core/systemRouter";
import { publicProcedure, router } from "./_core/trpc";
import { publicProcedure, router, protectedProcedure } from "./_core/trpc";
import { checkOllamaHealth, listModels, chatCompletion } from "./ollama";
export const appRouter = router({
@@ -78,6 +78,103 @@ export const appRouter = router({
}
}),
}),
/**
* Agents — управление AI-агентами
*/
agents: router({
list: protectedProcedure.query(async ({ ctx }) => {
const { getUserAgents } = await import("./agents");
return getUserAgents(ctx.user.id);
}),
get: protectedProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
const { getAgentById } = await import("./agents");
return getAgentById(input.id);
}),
create: protectedProcedure
.input(
z.object({
name: z.string().min(1),
description: z.string().optional(),
role: z.string(),
model: z.string(),
provider: z.string(),
temperature: z.number().min(0).max(2).default(0.7),
maxTokens: z.number().default(2048),
systemPrompt: z.string().optional(),
allowedTools: z.array(z.string()).default([]),
})
)
.mutation(async ({ ctx, input }) => {
const { createAgent } = await import("./agents");
return createAgent(ctx.user.id, {
...input,
temperature: input.temperature.toString(),
} as any);
}),
update: protectedProcedure
.input(
z.object({
id: z.number(),
name: z.string().optional(),
description: z.string().optional(),
temperature: z.number().optional(),
maxTokens: z.number().optional(),
systemPrompt: z.string().optional(),
isActive: z.boolean().optional(),
})
)
.mutation(async ({ input }) => {
const { updateAgent } = await import("./agents");
const { id, temperature, ...updates } = input;
const finalUpdates = temperature !== undefined ? { ...updates, temperature: temperature.toString() } : updates;
return updateAgent(id, finalUpdates as any);
}),
delete: protectedProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
const { deleteAgent } = await import("./agents");
return deleteAgent(input.id);
}),
stats: protectedProcedure.input(z.object({ id: z.number(), hoursBack: z.number().default(24) })).query(async ({ input }) => {
const { getAgentStats } = await import("./agents");
return getAgentStats(input.id, input.hoursBack);
}),
metrics: protectedProcedure.input(z.object({ id: z.number(), hoursBack: z.number().default(24) })).query(async ({ input }) => {
const { getAgentMetrics } = await import("./agents");
return getAgentMetrics(input.id, input.hoursBack);
}),
history: protectedProcedure.input(z.object({ id: z.number(), limit: z.number().default(50) })).query(async ({ input }) => {
const { getAgentHistory } = await import("./agents");
return getAgentHistory(input.id, input.limit);
}),
accessControl: protectedProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
const { getAgentAccessControl } = await import("./agents");
return getAgentAccessControl(input.id);
}),
updateToolAccess: protectedProcedure
.input(
z.object({
agentId: z.number(),
tool: z.string(),
isAllowed: z.boolean(),
maxExecutionsPerHour: z.number().optional(),
timeoutSeconds: z.number().optional(),
})
)
.mutation(async ({ input }) => {
const { updateToolAccess } = await import("./agents");
const { agentId, ...updates } = input;
return updateToolAccess(agentId, input.tool, updates);
}),
}),
});
export type AppRouter = typeof appRouter;