diff --git a/apps/api/src/chat-proxy/chat-proxy.controller.ts b/apps/api/src/chat-proxy/chat-proxy.controller.ts index ad38272..a5f7c4f 100644 --- a/apps/api/src/chat-proxy/chat-proxy.controller.ts +++ b/apps/api/src/chat-proxy/chat-proxy.controller.ts @@ -99,7 +99,8 @@ export class ChatProxyController { const upstreamResponse = await this.chatProxyService.proxyChat( userId, body.messages, - abortController.signal + abortController.signal, + body.agent ); const upstreamContentType = upstreamResponse.headers.get("content-type"); diff --git a/apps/api/src/chat-proxy/chat-proxy.dto.ts b/apps/api/src/chat-proxy/chat-proxy.dto.ts index 1c77cd9..830e48f 100644 --- a/apps/api/src/chat-proxy/chat-proxy.dto.ts +++ b/apps/api/src/chat-proxy/chat-proxy.dto.ts @@ -1,5 +1,12 @@ import { Type } from "class-transformer"; -import { ArrayMinSize, IsArray, IsNotEmpty, IsString, ValidateNested } from "class-validator"; +import { + ArrayMinSize, + IsArray, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; export interface ChatMessage { role: string; @@ -22,4 +29,8 @@ export class ChatStreamDto { @ValidateNested({ each: true }) @Type(() => ChatMessageDto) messages!: ChatMessageDto[]; + + @IsString({ message: "agent must be a string" }) + @IsOptional() + agent?: string; } diff --git a/apps/api/src/chat-proxy/chat-proxy.service.ts b/apps/api/src/chat-proxy/chat-proxy.service.ts index 731fb58..0015fd0 100644 --- a/apps/api/src/chat-proxy/chat-proxy.service.ts +++ b/apps/api/src/chat-proxy/chat-proxy.service.ts @@ -2,6 +2,7 @@ import { BadGatewayException, Injectable, Logger, + NotFoundException, ServiceUnavailableException, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; @@ -18,6 +19,13 @@ interface ContainerConnection { token: string; } +interface AgentConfig { + name: string; + displayName: string; + personality: string; + primaryModel: string | null; +} + @Injectable() export class ChatProxyService { private readonly logger = new Logger(ChatProxyService.name); @@ -38,21 +46,38 @@ export class ChatProxyService { async proxyChat( userId: string, messages: ChatMessage[], - signal?: AbortSignal + signal?: AbortSignal, + agentName?: string ): Promise { const { url: containerUrl, token: gatewayToken } = await this.getContainerConnection(userId); - const model = await this.getPreferredModel(userId); + + // Get agent config if specified + let agentConfig: AgentConfig | null = null; + if (agentName) { + agentConfig = await this.getAgentConfig(userId, agentName); + } + + const model = agentConfig?.primaryModel ?? (await this.getPreferredModel(userId)); + + const requestBody: Record = { + messages, + model, + stream: true, + }; + + // Add agent config if available + if (agentConfig) { + requestBody.agent = agentConfig.name; + requestBody.agent_personality = agentConfig.personality; + } + const requestInit: RequestInit = { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${gatewayToken}`, }, - body: JSON.stringify({ - messages, - model, - stream: true, - }), + body: JSON.stringify(requestBody), }; if (signal) { @@ -170,4 +195,32 @@ export class ChatProxyService { return null; } } + + private async getAgentConfig(userId: string, agentName: string): Promise { + const agent = await this.prisma.userAgent.findUnique({ + where: { userId_name: { userId, name: agentName } }, + select: { + name: true, + displayName: true, + personality: true, + primaryModel: true, + isActive: true, + }, + }); + + if (!agent) { + throw new NotFoundException(`Agent "${agentName}" not found for user`); + } + + if (!agent.isActive) { + throw new NotFoundException(`Agent "${agentName}" is not active`); + } + + return { + name: agent.name, + displayName: agent.displayName, + personality: agent.personality, + primaryModel: agent.primaryModel, + }; + } } diff --git a/apps/api/src/user-agent/user-agent.controller.ts b/apps/api/src/user-agent/user-agent.controller.ts index d5923de..c2ecf76 100644 --- a/apps/api/src/user-agent/user-agent.controller.ts +++ b/apps/api/src/user-agent/user-agent.controller.ts @@ -26,11 +26,21 @@ export class UserAgentController { return this.userAgentService.findAll(user.id); } + @Get("status") + getAllStatuses(@CurrentUser() user: AuthUser) { + return this.userAgentService.getAllStatuses(user.id); + } + @Get(":id") findOne(@CurrentUser() user: AuthUser, @Param("id", ParseUUIDPipe) id: string) { return this.userAgentService.findOne(user.id, id); } + @Get(":id/status") + getStatus(@CurrentUser() user: AuthUser, @Param("id", ParseUUIDPipe) id: string) { + return this.userAgentService.getStatus(user.id, id); + } + @Post() create(@CurrentUser() user: AuthUser, @Body() dto: CreateUserAgentDto) { return this.userAgentService.create(user.id, dto); diff --git a/apps/api/src/user-agent/user-agent.service.ts b/apps/api/src/user-agent/user-agent.service.ts index ca88c92..9e8806a 100644 --- a/apps/api/src/user-agent/user-agent.service.ts +++ b/apps/api/src/user-agent/user-agent.service.ts @@ -8,6 +8,15 @@ import { PrismaService } from "../prisma/prisma.service"; import { CreateUserAgentDto } from "./dto/create-user-agent.dto"; import { UpdateUserAgentDto } from "./dto/update-user-agent.dto"; +export interface AgentStatusResponse { + id: string; + name: string; + displayName: string; + role: string; + isActive: boolean; + containerStatus?: "running" | "stopped" | "unknown"; +} + @Injectable() export class UserAgentService { constructor(private readonly prisma: PrismaService) {} @@ -119,4 +128,26 @@ export class UserAgentService { await this.findOne(userId, id); return this.prisma.userAgent.delete({ where: { id } }); } + + async getStatus(userId: string, id: string): Promise { + const agent = await this.findOne(userId, id); + return { + id: agent.id, + name: agent.name, + displayName: agent.displayName, + role: agent.role, + isActive: agent.isActive, + }; + } + + async getAllStatuses(userId: string): Promise { + const agents = await this.findAll(userId); + return agents.map((agent) => ({ + id: agent.id, + name: agent.name, + displayName: agent.displayName, + role: agent.role, + isActive: agent.isActive, + })); + } } diff --git a/docs/TASKS.md b/docs/TASKS.md index 5972db0..6b8149c 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -100,7 +100,7 @@ PRD: `docs/PRD-MS22-P2-AGENT-FLEET.md` | MS22-P2-002 | done | p2-fleet | Seed default agents (jarvis, builder, medic) | TASKS:P2 | api | feat/ms22-p2-agent-seed | P2-001 | P2-004 | orchestrator | 2026-03-04 | 2026-03-04 | 5K | 2K | PR #677 merged | | MS22-P2-003 | done | p2-fleet | Agent template CRUD endpoints (admin) | TASKS:P2 | api | feat/ms22-p2-agent-crud | P2-001 | P2-005 | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 5K | PR #678 merged | | MS22-P2-004 | done | p2-fleet | User agent CRUD endpoints | TASKS:P2 | api | feat/ms22-p2-user-agents | P2-002,P2-003 | P2-006 | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 8K | PR #682 merged | -| MS22-P2-005 | not-started | p2-fleet | Agent status endpoints | TASKS:P2 | api | feat/ms22-p2-agent-api | P2-003 | P2-008 | — | — | — | 10K | — | | +| MS22-P2-005 | in-progress | p2-fleet | Agent status endpoints | TASKS:P2 | api | feat/ms22-p2-agent-status | P2-003 | P2-008 | orchestrator | 2026-03-04 | — | 10K | — | | | MS22-P2-006 | not-started | p2-fleet | Agent chat routing (select agent by name) | TASKS:P2 | api | feat/ms22-p2-agent-routing | P2-004 | P2-007 | — | — | — | 15K | — | | | MS22-P2-007 | not-started | p2-fleet | Discord channel → agent routing | TASKS:P2 | api | feat/ms22-p2-discord-router | P2-006 | P2-009 | — | — | — | 15K | — | | | MS22-P2-008 | not-started | p2-fleet | Agent list/selector UI in WebUI | TASKS:P2 | web | feat/ms22-p2-agent-ui | P2-005 | — | — | — | — | 15K | — | |