true message
This commit is contained in:
63
server/agents.test.ts
Normal file
63
server/agents.test.ts
Normal 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
241
server/agents.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user