Customer-Agent Interaction Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Enable customers to chat with an AI agent (walrider) via WebSocket, with messages routed through RabbitMQ, and auto-create an agent thread when a project is created.
Architecture: Single ai-agent-service NX app with WS gateway, RabbitMQ consumer/producer, and HTTP endpoints. The project-api auto-creates a WalriderThread on project creation. A shared rabbit-client lib and rabbit config follow existing redis patterns.
Tech Stack: NestJS, Socket.IO, amqplib, Prisma, Redis, RabbitMQ, NX monorepo
Branch: feat/project-service
Заголовок раздела «Branch: feat/project-service»Task 1: Update Prisma Schema
Заголовок раздела «Task 1: Update Prisma Schema»Files:
- Modify:
apps/core-api/prisma/schema.prisma
Step 1: Add projectId to WalriderThread and messageId to WalriderMessage
In apps/core-api/prisma/schema.prisma, update the WalriderThread model to add projectId:
model WalriderThread { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid customerId String @map("customer_id") @db.Uuid projectId String @unique @map("project_id") @db.Uuid externalThreadId String? @map("external_thread_id") @db.Text
createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at")
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) messages WalriderMessage[] states WalriderThreadState[]
@@index([customerId]) @@map("walrider_threads")}Update WalriderMessage to add autoincrement messageId:
model WalriderMessage { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid messageId Int @default(autoincrement()) @map("message_id") threadId String @map("thread_id") @db.Uuid role WalriderMessageRole content String @db.Text externalMessageId String? @map("external_message_id") @db.Text metadata Json? @db.Json
createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at")
thread WalriderThread @relation(fields: [threadId], references: [id], onDelete: Cascade) states WalriderThreadState[]
@@index([threadId]) @@map("walrider_messages")}Add relation to Project model (add this line inside the Project model, after the customer relation):
walriderThread WalriderThread?Step 2: Generate and apply migration
Run: npx prisma migrate dev --name add-project-thread-link --schema apps/core-api/prisma/schema.prisma
Step 3: Regenerate Prisma client
Run: npx prisma generate --schema apps/core-api/prisma/schema.prisma
Step 4: Commit
git add apps/core-api/prisma/git commit -m "feat(prisma): add projectId to WalriderThread and messageId to WalriderMessage"Task 2: Add WalriderThread Auto-Creation on Project Creation
Заголовок раздела «Task 2: Add WalriderThread Auto-Creation on Project Creation»Files:
- Modify:
libs/apis/providers/project-api/features/project/src/lib/project.module.ts - Modify:
libs/apis/providers/project-api/features/project/src/lib/project.service.ts - Create:
libs/apis/providers/project-api/features/project/src/lib/walrider-thread.repository.ts
Step 1: Create WalriderThreadRepository in project feature
Create libs/apis/providers/project-api/features/project/src/lib/walrider-thread.repository.ts:
import { Injectable } from '@nestjs/common';import { Prisma } from '@prisma/client';import { TransactionHost } from '@nestjs-cls/transactional';import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';
@Injectable()export class WalriderThreadRepository { constructor(private readonly txHost: TransactionHost<TransactionalAdapterPrisma>) {}
create(args: Prisma.WalriderThreadCreateArgs) { return this.txHost.tx.walriderThread.create(args); }
findUnique(args: Prisma.WalriderThreadFindUniqueArgs) { return this.txHost.tx.walriderThread.findUnique(args); }}Step 2: Update ProjectModule to include WalriderThreadRepository
In libs/apis/providers/project-api/features/project/src/lib/project.module.ts, add the import and provider:
import { Module } from '@nestjs/common';import { JwtModule } from '@nestjs/jwt';import { MapperService, CustomerGuard } from '@crewsforge-back/apis/shared';import { JwtCustomerConfigModule } from '@crewsforge-back/apis/configs/auth-api/jwt-customer';import { PrismaClientModule } from '@crewsforge-back/apis/utils/prisma-client';
import { ProjectController } from './project.controller';import { ProjectService } from './project.service';import { ProjectRepository } from './project.repository';import { WalriderThreadRepository } from './walrider-thread.repository';
@Module({ imports: [PrismaClientModule, JwtModule.register({}), JwtCustomerConfigModule], controllers: [ProjectController], providers: [ProjectService, ProjectRepository, WalriderThreadRepository, MapperService, CustomerGuard], exports: [ProjectService],})export class ProjectModule {}Step 3: Update ProjectService.create to auto-create WalriderThread
In libs/apis/providers/project-api/features/project/src/lib/project.service.ts, inject WalriderThreadRepository and update the create method:
Add import:
import { WalriderThreadRepository } from './walrider-thread.repository';Update constructor:
constructor( private readonly projectRepository: ProjectRepository, private readonly walriderThreadRepository: WalriderThreadRepository,) {}Update the create method:
@Transactional()async create(customerId: string, dto: ProjectCreateDto) { const data: Prisma.ProjectCreateInput = { title: dto.title, description: dto.description, category: dto.category, budgetType: dto.budgetType, budgetMin: dto.budgetMin ?? undefined, budgetMax: dto.budgetMax ?? undefined, currency: dto.currency ?? 'USD', deadline: dto.deadline ?? undefined, customer: { connect: { id: customerId } }, };
const project = await this.projectRepository.create({ data });
await this.walriderThreadRepository.create({ data: { customer: { connect: { id: customerId } }, project: { connect: { id: project.id } }, }, });
return project;}Step 4: Verify build compiles
Run: npx nx build project-api
Step 5: Commit
git add libs/apis/providers/project-api/features/project/src/lib/git commit -m "feat(project-api): auto-create WalriderThread on project creation"Task 3: Wire ProjectModule into project-api AppModule
Заголовок раздела «Task 3: Wire ProjectModule into project-api AppModule»Files:
- Modify:
apps/project-api/src/app/app.module.ts
Step 1: Import ProjectModule
Update apps/project-api/src/app/app.module.ts:
import { Module } from '@nestjs/common';import { AppConfigModule } from '@crewsforge-back/apis/configs/shared/app';import { ClsModule } from 'nestjs-cls';import { v4 as uuid } from 'uuid';import { ClsPluginTransactional } from '@nestjs-cls/transactional';import { PrismaClientModule, PrismaClientService } from '@crewsforge-back/apis/utils/prisma-client';import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';import { ProjectModule } from '@crewsforge-back/apis/providers/project-api/features/project';
@Module({ imports: [ AppConfigModule,
ClsModule.forRoot({ global: true, middleware: { mount: true, generateId: true, idGenerator: (req: Request) => req.headers['X-Request-Id'] ?? uuid(), }, plugins: [ new ClsPluginTransactional({ imports: [PrismaClientModule], adapter: new TransactionalAdapterPrisma({ prismaInjectionToken: PrismaClientService }), }), ], }),
ProjectModule, ], controllers: [], providers: [],})export class AppModule {}Step 2: Verify build
Run: npx nx build project-api
Step 3: Commit
git add apps/project-api/src/app/app.module.tsgit commit -m "feat(project-api): wire ProjectModule into AppModule"Branch: feat/ai-agent-service (infrastructure)
Заголовок раздела «Branch: feat/ai-agent-service (infrastructure)»Task 4: Create RabbitMQ Config Library
Заголовок раздела «Task 4: Create RabbitMQ Config Library»Files:
- Create:
libs/apis/configs/shared/rabbit/project.json - Create:
libs/apis/configs/shared/rabbit/tsconfig.json - Create:
libs/apis/configs/shared/rabbit/tsconfig.lib.json - Create:
libs/apis/configs/shared/rabbit/tsconfig.spec.json - Create:
libs/apis/configs/shared/rabbit/eslint.config.mjs - Create:
libs/apis/configs/shared/rabbit/jest.config.ts - Create:
libs/apis/configs/shared/rabbit/src/index.ts - Create:
libs/apis/configs/shared/rabbit/src/lib/rabbit.env.ts - Create:
libs/apis/configs/shared/rabbit/src/lib/rabbit.config.ts - Create:
libs/apis/configs/shared/rabbit/src/lib/rabbit-config.validation.ts - Create:
libs/apis/configs/shared/rabbit/src/lib/rabbit-config.module.ts - Create:
libs/apis/configs/shared/rabbit/src/lib/rabbit-config.service.ts - Modify:
tsconfig.base.json(add path alias)
Step 1: Create project.json
Create libs/apis/configs/shared/rabbit/project.json:
{ "name": "config-shared-rabbit", "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/apis/configs/shared/rabbit/src", "projectType": "library", "tags": [], "targets": {}}Step 2: Create tsconfig files
Create libs/apis/configs/shared/rabbit/tsconfig.json:
{ "extends": "../../../../../tsconfig.base.json", "compilerOptions": { "module": "commonjs", "forceConsistentCasingInFileNames": true, "strict": true, "importHelpers": true, "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], "references": [ { "path": "./tsconfig.lib.json" }, { "path": "./tsconfig.spec.json" } ]}Create libs/apis/configs/shared/rabbit/tsconfig.lib.json:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../../../dist/out-tsc", "declaration": true, "types": ["node"], "target": "es2021", "experimentalDecorators": true, "emitDecoratorMetadata": true, "strictNullChecks": true, "noImplicitAny": true, "strictBindCallApply": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true }, "include": ["src/**/*.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]}Create libs/apis/configs/shared/rabbit/tsconfig.spec.json:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../../../dist/out-tsc", "module": "commonjs", "types": ["jest", "node"], "experimentalDecorators": true, "emitDecoratorMetadata": true }, "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]}Step 3: Create rabbit.env.ts
Create libs/apis/configs/shared/rabbit/src/lib/rabbit.env.ts:
import * as process from 'process';
export interface RabbitEnvironmentVariables { RABBITMQ_HOST: string; RABBITMQ_PORT: number; RABBITMQ_USERNAME: string; RABBITMQ_PASSWORD: string; RABBITMQ_PREFETCH: number; RABBITMQ_REQUEST_QUEUE: string; RABBITMQ_REQUEST_EXCHANGE: string; RABBITMQ_REQUEST_EXCHANGE_TYPE: string; RABBITMQ_RESPONSES_QUEUE: string; RABBITMQ_RESPONSES_EXCHANGE: string; RABBITMQ_RESPONSES_EXCHANGE_TYPE: string;}
export const getRabbitEnvironment = (): RabbitEnvironmentVariables => { const env = process.env as Record<string, string>;
return { RABBITMQ_HOST: env.RABBITMQ_HOST, RABBITMQ_PORT: parseInt(env.RABBITMQ_PORT, 10) || 5672, RABBITMQ_USERNAME: env.RABBITMQ_USERNAME, RABBITMQ_PASSWORD: env.RABBITMQ_PASSWORD, RABBITMQ_PREFETCH: parseInt(env.RABBITMQ_PREFETCH, 10) || 50, RABBITMQ_REQUEST_QUEUE: env.RABBITMQ_REQUEST_QUEUE, RABBITMQ_REQUEST_EXCHANGE: env.RABBITMQ_REQUEST_EXCHANGE, RABBITMQ_REQUEST_EXCHANGE_TYPE: env.RABBITMQ_REQUEST_EXCHANGE_TYPE, RABBITMQ_RESPONSES_QUEUE: env.RABBITMQ_RESPONSES_QUEUE, RABBITMQ_RESPONSES_EXCHANGE: env.RABBITMQ_RESPONSES_EXCHANGE, RABBITMQ_RESPONSES_EXCHANGE_TYPE: env.RABBITMQ_RESPONSES_EXCHANGE_TYPE, };};Step 4: Create rabbit.config.ts
Create libs/apis/configs/shared/rabbit/src/lib/rabbit.config.ts:
import { registerAs } from '@nestjs/config';
import { getRabbitEnvironment } from './rabbit.env';
export const rabbitConfig = registerAs('rabbit', () => { const env = getRabbitEnvironment();
return { host: env.RABBITMQ_HOST, port: env.RABBITMQ_PORT, username: env.RABBITMQ_USERNAME, password: env.RABBITMQ_PASSWORD, prefetch: env.RABBITMQ_PREFETCH, requestQueue: env.RABBITMQ_REQUEST_QUEUE, requestExchange: env.RABBITMQ_REQUEST_EXCHANGE, requestExchangeType: env.RABBITMQ_REQUEST_EXCHANGE_TYPE, responsesQueue: env.RABBITMQ_RESPONSES_QUEUE, responsesExchange: env.RABBITMQ_RESPONSES_EXCHANGE, responsesExchangeType: env.RABBITMQ_RESPONSES_EXCHANGE_TYPE, };});Step 5: Create rabbit-config.validation.ts
Create libs/apis/configs/shared/rabbit/src/lib/rabbit-config.validation.ts:
import * as Joi from 'joi';
export const validationSchema = Joi.object({ RABBITMQ_HOST: Joi.string().required(), RABBITMQ_PORT: Joi.number().required(), RABBITMQ_USERNAME: Joi.string().required(), RABBITMQ_PASSWORD: Joi.string().allow('').required(), RABBITMQ_PREFETCH: Joi.number().default(50), RABBITMQ_REQUEST_QUEUE: Joi.string().required(), RABBITMQ_REQUEST_EXCHANGE: Joi.string().required(), RABBITMQ_REQUEST_EXCHANGE_TYPE: Joi.string().required(), RABBITMQ_RESPONSES_QUEUE: Joi.string().required(), RABBITMQ_RESPONSES_EXCHANGE: Joi.string().required(), RABBITMQ_RESPONSES_EXCHANGE_TYPE: Joi.string().required(),});Step 6: Create rabbit-config.service.ts
Create libs/apis/configs/shared/rabbit/src/lib/rabbit-config.service.ts:
import { Injectable } from '@nestjs/common';import { ConfigService } from '@nestjs/config';
@Injectable()export class RabbitConfigService { constructor(private configService: ConfigService<Record<string, unknown>, true>) {}
get host(): string { return this.configService.get<string>('rabbit.host'); }
get port(): number { return this.configService.get<number>('rabbit.port'); }
get username(): string { return this.configService.get<string>('rabbit.username'); }
get password(): string { return this.configService.get<string>('rabbit.password'); }
get prefetch(): number { return this.configService.get<number>('rabbit.prefetch'); }
get requestQueue(): string { return this.configService.get<string>('rabbit.requestQueue'); }
get requestExchange(): string { return this.configService.get<string>('rabbit.requestExchange'); }
get requestExchangeType(): string { return this.configService.get<string>('rabbit.requestExchangeType'); }
get responsesQueue(): string { return this.configService.get<string>('rabbit.responsesQueue'); }
get responsesExchange(): string { return this.configService.get<string>('rabbit.responsesExchange'); }
get responsesExchangeType(): string { return this.configService.get<string>('rabbit.responsesExchangeType'); }}Step 7: Create rabbit-config.module.ts
Create libs/apis/configs/shared/rabbit/src/lib/rabbit-config.module.ts:
import { Module } from '@nestjs/common';import { ConfigModule, ConfigService } from '@nestjs/config';
import { rabbitConfig } from './rabbit.config';import { RabbitConfigService } from './rabbit-config.service';import { validationSchema } from './rabbit-config.validation';
@Module({ imports: [ConfigModule.forRoot({ load: [rabbitConfig], validationSchema })], providers: [ConfigService, RabbitConfigService], exports: [ConfigService, RabbitConfigService],})export class RabbitConfigModule {}Step 8: Create index.ts
Create libs/apis/configs/shared/rabbit/src/index.ts:
export * from './lib/rabbit-config.service';export * from './lib/rabbit-config.module';Step 9: Add path alias to tsconfig.base.json
Add to tsconfig.base.json paths:
"@crewsforge-back/config-shared-rabbit": ["libs/apis/configs/shared/rabbit/src/index.ts"]Step 10: Commit
git add libs/apis/configs/shared/rabbit/ tsconfig.base.jsongit commit -m "feat(config): add RabbitMQ configuration library"Task 5: Create RabbitMQ Client Library
Заголовок раздела «Task 5: Create RabbitMQ Client Library»Files:
- Create:
libs/apis/utils/rabbit-client/project.json - Create:
libs/apis/utils/rabbit-client/tsconfig.json - Create:
libs/apis/utils/rabbit-client/tsconfig.lib.json - Create:
libs/apis/utils/rabbit-client/tsconfig.spec.json - Create:
libs/apis/utils/rabbit-client/src/index.ts - Create:
libs/apis/utils/rabbit-client/src/lib/rabbit-client.constants.ts - Create:
libs/apis/utils/rabbit-client/src/lib/rabbit-client.service.ts - Create:
libs/apis/utils/rabbit-client/src/lib/rabbit-client.module.ts - Modify:
tsconfig.base.json(add path alias)
Step 1: Create project.json
Create libs/apis/utils/rabbit-client/project.json:
{ "name": "rabbit-client", "$schema": "../../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/apis/utils/rabbit-client/src", "projectType": "library", "tags": [], "targets": {}}Step 2: Create tsconfig files
Create libs/apis/utils/rabbit-client/tsconfig.json:
{ "extends": "../../../../tsconfig.base.json", "compilerOptions": { "module": "commonjs", "forceConsistentCasingInFileNames": true, "strict": true, "importHelpers": true, "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], "references": [ { "path": "./tsconfig.lib.json" }, { "path": "./tsconfig.spec.json" } ]}Create libs/apis/utils/rabbit-client/tsconfig.lib.json:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../../dist/out-tsc", "declaration": true, "types": ["node"], "target": "es2021", "experimentalDecorators": true, "emitDecoratorMetadata": true, "strictNullChecks": true, "noImplicitAny": true, "strictBindCallApply": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true }, "include": ["src/**/*.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]}Create libs/apis/utils/rabbit-client/tsconfig.spec.json:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../../dist/out-tsc", "module": "commonjs", "types": ["jest", "node"], "experimentalDecorators": true, "emitDecoratorMetadata": true }, "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]}Step 3: Create rabbit-client.constants.ts
Create libs/apis/utils/rabbit-client/src/lib/rabbit-client.constants.ts:
export const RABBIT_CLIENT = Symbol('RABBIT_CLIENT');Step 4: Create rabbit-client.service.ts
Create libs/apis/utils/rabbit-client/src/lib/rabbit-client.service.ts:
import { Injectable, Logger, OnApplicationShutdown, OnModuleDestroy } from '@nestjs/common';import { RabbitConfigService } from '@crewsforge-back/config-shared-rabbit';import * as amqplib from 'amqplib';
@Injectable()export class RabbitClientService implements OnModuleDestroy, OnApplicationShutdown { private readonly logger = new Logger(RabbitClientService.name); private connection: amqplib.Connection | null = null; private channel: amqplib.Channel | null = null;
constructor(private readonly rabbitConfig: RabbitConfigService) {}
async connect(): Promise<void> { const { host, port, username, password } = this.rabbitConfig; const url = `amqp://${username}:${password}@${host}:${port}`;
this.connection = await amqplib.connect(url); this.channel = await this.connection.createChannel(); await this.channel.prefetch(this.rabbitConfig.prefetch);
this.connection.on('error', (err) => { this.logger.error('[RabbitMQ] Connection error', err); });
this.connection.on('close', () => { this.logger.warn('[RabbitMQ] Connection closed'); });
this.logger.log('[RabbitMQ] Connected successfully'); }
async assertExchange(exchange: string, type: string): Promise<void> { if (!this.channel) throw new Error('RabbitMQ channel not initialized'); await this.channel.assertExchange(exchange, type, { durable: true }); }
async assertQueue(queue: string): Promise<void> { if (!this.channel) throw new Error('RabbitMQ channel not initialized'); await this.channel.assertQueue(queue, { durable: true }); }
async bindQueue(queue: string, exchange: string, routingKey = ''): Promise<void> { if (!this.channel) throw new Error('RabbitMQ channel not initialized'); await this.channel.bindQueue(queue, exchange, routingKey); }
publish(exchange: string, routingKey: string, message: Record<string, unknown>): boolean { if (!this.channel) throw new Error('RabbitMQ channel not initialized'); return this.channel.publish( exchange, routingKey, Buffer.from(JSON.stringify(message)), { persistent: true, contentType: 'application/json' }, ); }
async consume( queue: string, handler: (msg: amqplib.ConsumeMessage) => Promise<void>, ): Promise<void> { if (!this.channel) throw new Error('RabbitMQ channel not initialized'); await this.channel.consume(queue, async (msg) => { if (!msg) return; try { await handler(msg); this.channel?.ack(msg); } catch (error) { this.logger.error('[RabbitMQ] Message processing error', error); this.channel?.nack(msg, false, false); } }); }
async onModuleDestroy() { await this.disconnect(); }
async onApplicationShutdown() { await this.disconnect(); }
private async disconnect(): Promise<void> { try { await this.channel?.close(); await this.connection?.close(); this.logger.log('[RabbitMQ] Disconnected successfully'); } catch (error) { this.logger.error('[RabbitMQ] Error during disconnect', error); } }}Step 5: Create rabbit-client.module.ts
Create libs/apis/utils/rabbit-client/src/lib/rabbit-client.module.ts:
import { Module } from '@nestjs/common';
import { RabbitConfigModule } from '@crewsforge-back/config-shared-rabbit';
import { RabbitClientService } from './rabbit-client.service';
@Module({ imports: [RabbitConfigModule], providers: [RabbitClientService], exports: [RabbitClientService],})export class RabbitClientModule {}Step 6: Create index.ts
Create libs/apis/utils/rabbit-client/src/index.ts:
export * from './lib/rabbit-client.module';export * from './lib/rabbit-client.service';Step 7: Add path alias to tsconfig.base.json
Add to tsconfig.base.json paths:
"@crewsforge-back/rabbit-client": ["libs/apis/utils/rabbit-client/src/index.ts"]Step 8: Install amqplib
Run: npm install amqplib && npm install -D @types/amqplib
Step 9: Commit
git add libs/apis/utils/rabbit-client/ tsconfig.base.json package.json package-lock.jsongit commit -m "feat(rabbit-client): add RabbitMQ client library"Task 6: Scaffold ai-agent-service NX App
Заголовок раздела «Task 6: Scaffold ai-agent-service NX App»Files:
- Create:
apps/ai-agent-service/project.json - Create:
apps/ai-agent-service/tsconfig.json - Create:
apps/ai-agent-service/tsconfig.app.json - Create:
apps/ai-agent-service/webpack.config.js - Create:
apps/ai-agent-service/src/main.ts - Create:
apps/ai-agent-service/src/app/app.module.ts
Step 1: Create project.json
Create apps/ai-agent-service/project.json:
{ "name": "ai-agent-service", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/ai-agent-service/src", "projectType": "application", "targets": { "build": { "executor": "nx:run-commands", "options": { "command": "webpack-cli build", "args": ["--node-env=production"], "cwd": "apps/ai-agent-service" }, "configurations": { "development": { "args": ["--node-env=development"] } } }, "serve": { "continuous": true, "executor": "@nx/js:node", "defaultConfiguration": "development", "dependsOn": ["build"], "options": { "buildTarget": "ai-agent-service:build", "runBuildTargetDependencies": false }, "configurations": { "development": { "buildTarget": "ai-agent-service:build:development" }, "production": { "buildTarget": "ai-agent-service:build:production" } } } }, "tags": []}Step 2: Create tsconfig files
Create apps/ai-agent-service/tsconfig.json:
{ "extends": "../../tsconfig.base.json", "files": [], "include": [], "references": [ { "path": "./tsconfig.app.json" } ], "compilerOptions": { "esModuleInterop": true }}Create apps/ai-agent-service/tsconfig.app.json:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", "types": ["node"], "experimentalDecorators": true, "emitDecoratorMetadata": true, "target": "es2021" }, "include": ["src/**/*.ts"]}Step 3: Create webpack.config.js
Create apps/ai-agent-service/webpack.config.js:
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');const { join } = require('path');
module.exports = { output: { path: join(__dirname, '../../dist/apps/ai-agent-service'), clean: true, ...(process.env.NODE_ENV !== 'production' && { devtoolModuleFilenameTemplate: '[absolute-resource-path]', }), }, plugins: [ new NxAppWebpackPlugin({ target: 'node', compiler: 'tsc', main: './src/main.ts', tsConfig: './tsconfig.app.json', assets: ['./src/assets'], optimization: false, outputHashing: 'none', generatePackageJson: true, sourceMaps: true, }), ],};Step 4: Create app.module.ts
Create apps/ai-agent-service/src/app/app.module.ts:
import { Module } from '@nestjs/common';import { AppConfigModule } from '@crewsforge-back/apis/configs/shared/app';import { ClsModule } from 'nestjs-cls';import { v4 as uuid } from 'uuid';import { ClsPluginTransactional } from '@nestjs-cls/transactional';import { PrismaClientModule, PrismaClientService } from '@crewsforge-back/apis/utils/prisma-client';import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';
@Module({ imports: [ AppConfigModule,
ClsModule.forRoot({ global: true, middleware: { mount: true, generateId: true, idGenerator: (req: Request) => req.headers['X-Request-Id'] ?? uuid(), }, plugins: [ new ClsPluginTransactional({ imports: [PrismaClientModule], adapter: new TransactionalAdapterPrisma({ prismaInjectionToken: PrismaClientService }), }), ], }), ], controllers: [], providers: [],})export class AppModule {}Step 5: Create main.ts
Create apps/ai-agent-service/src/main.ts:
import { ValidationPipe, VersioningType, Logger } from '@nestjs/common';import { HttpAdapterHost, NestFactory } from '@nestjs/core';import { NestExpressApplication } from '@nestjs/platform-express';import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';import { CUSTOMER_ACCESS_TOKEN_STRATEGY_NAME, PrismaExceptionFilter,} from '@crewsforge-back/apis/shared';import { AppConfigService } from '@crewsforge-back/apis/configs/shared/app';
import { AppModule } from './app/app.module';
async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule, { bufferLogs: true, });
app.enableCors({ credentials: true, origin: true, exposedHeaders: ['set-cookie'], }); app.enableVersioning({ type: VersioningType.URI });
const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix);
const { httpAdapter } = app.get(HttpAdapterHost); app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, transformOptions: { strategy: 'excludeAll' }, }) ); app.useGlobalFilters(new PrismaExceptionFilter(httpAdapter));
const documentBuilder = new DocumentBuilder().setTitle('AI Agent Service').setVersion('1.0'); documentBuilder.addBearerAuth(undefined, CUSTOMER_ACCESS_TOKEN_STRATEGY_NAME);
const config = documentBuilder.build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document);
const { port } = app.get(AppConfigService); await app.listen(port);
Logger.log(`App is running on: http://localhost:${port}/${globalPrefix}`);}
bootstrap();Step 6: Create assets directory
Run: mkdir -p apps/ai-agent-service/src/assets
Step 7: Install WebSocket dependencies
Run: npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
Step 8: Verify build
Run: npx nx build ai-agent-service
Step 9: Commit
git add apps/ai-agent-service/ package.json package-lock.jsongit commit -m "feat(ai-agent-service): scaffold NX application"Task 7: Create ai-agent-service Data Access Library
Заголовок раздела «Task 7: Create ai-agent-service Data Access Library»Files:
- Create:
libs/apis/providers/ai-agent-service/data-access/project.json - Create:
libs/apis/providers/ai-agent-service/data-access/tsconfig.json - Create:
libs/apis/providers/ai-agent-service/data-access/tsconfig.lib.json - Create:
libs/apis/providers/ai-agent-service/data-access/tsconfig.spec.json - Create:
libs/apis/providers/ai-agent-service/data-access/src/index.ts - Create:
libs/apis/providers/ai-agent-service/data-access/src/lib/index.ts - Create:
libs/apis/providers/ai-agent-service/data-access/src/lib/dtos/index.ts - Create:
libs/apis/providers/ai-agent-service/data-access/src/lib/dtos/agent-chat.dto.ts - Create:
libs/apis/providers/ai-agent-service/data-access/src/lib/interfaces/rabbit-messages.interface.ts - Modify:
tsconfig.base.json(add path alias)
Step 1: Create project.json
Create libs/apis/providers/ai-agent-service/data-access/project.json:
{ "name": "ai-agent-service-data-access", "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/apis/providers/ai-agent-service/data-access/src", "projectType": "library", "tags": [], "targets": {}}Step 2: Create tsconfig files
(Same pattern as redis-client/project-api tsconfigs — use ../../../../../ for extends in tsconfig.json, adjust outDir accordingly.)
Create libs/apis/providers/ai-agent-service/data-access/tsconfig.json:
{ "extends": "../../../../../tsconfig.base.json", "compilerOptions": { "module": "commonjs", "forceConsistentCasingInFileNames": true, "strict": true, "importHelpers": true, "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], "references": [ { "path": "./tsconfig.lib.json" }, { "path": "./tsconfig.spec.json" } ]}Create libs/apis/providers/ai-agent-service/data-access/tsconfig.lib.json:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../../../dist/out-tsc", "declaration": true, "types": ["node"], "target": "es2021", "experimentalDecorators": true, "emitDecoratorMetadata": true, "strictNullChecks": true, "noImplicitAny": true, "strictBindCallApply": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true }, "include": ["src/**/*.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]}Create libs/apis/providers/ai-agent-service/data-access/tsconfig.spec.json:
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../../../dist/out-tsc", "module": "commonjs", "types": ["jest", "node"], "experimentalDecorators": true, "emitDecoratorMetadata": true }, "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]}Step 3: Create RabbitMQ message interfaces
Create libs/apis/providers/ai-agent-service/data-access/src/lib/interfaces/rabbit-messages.interface.ts:
export interface RabbitRequestMessage { messageId: number; threadId: string; content: string;}
export interface RabbitResponseMessage { messageId: number; threadId: string; response: string; phase: string; isComplete: boolean; plan: Record<string, unknown> | null;}Step 4: Create DTOs
Create libs/apis/providers/ai-agent-service/data-access/src/lib/dtos/agent-chat.dto.ts:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';import { Expose, Type } from 'class-transformer';import { IsDate, IsEnum, IsInt, IsOptional, IsString, IsUUID, Min, ValidateNested } from 'class-validator';import { WalriderMessageRole } from '@prisma/client';
// WS event DTOs
export class WsSendMessageDto { @IsString() content!: string;}
// HTTP response DTOs
export class AgentMessageDto { @ApiProperty() @Expose() @IsUUID() id!: string;
@ApiProperty() @Expose() @IsInt() messageId!: number;
@ApiProperty({ enum: WalriderMessageRole }) @Expose() @IsEnum(WalriderMessageRole) role!: WalriderMessageRole;
@ApiProperty() @Expose() @IsString() content!: string;
@ApiPropertyOptional() @Expose() @IsOptional() metadata?: Record<string, unknown> | null;
@ApiProperty() @Expose() @IsDate() createdAt!: Date;}
export class AgentStateDto { @ApiProperty() @Expose() @IsUUID() id!: string;
@ApiPropertyOptional() @Expose() @IsOptional() @IsString() status?: string | null;
@ApiPropertyOptional() @Expose() @IsOptional() data?: Record<string, unknown> | null;
@ApiProperty() @Expose() @IsDate() createdAt!: Date;}
export class AgentHistoryQueryDto { @ApiProperty({ default: 1, required: false }) @Expose() @IsOptional() @IsInt() @Min(1) @Type(() => Number) page?: number;
@ApiProperty({ default: 20, required: false }) @Expose() @IsOptional() @IsInt() @Min(1) @Type(() => Number) perPage?: number;}Create libs/apis/providers/ai-agent-service/data-access/src/lib/dtos/index.ts:
export * from './agent-chat.dto';Step 5: Create barrel exports
Create libs/apis/providers/ai-agent-service/data-access/src/lib/index.ts:
export * from './dtos';export * from './interfaces/rabbit-messages.interface';Create libs/apis/providers/ai-agent-service/data-access/src/index.ts:
export * from './lib';Step 6: Add path alias to tsconfig.base.json
Add to tsconfig.base.json paths:
"@crewsforge-back/apis/providers/ai-agent-service/data-access": [ "libs/apis/providers/ai-agent-service/data-access/src/index.ts"]Step 7: Commit
git add libs/apis/providers/ai-agent-service/data-access/ tsconfig.base.jsongit commit -m "feat(ai-agent-service): add data-access library with DTOs and interfaces"Task 8: Create Agent Chat Feature Library
Заголовок раздела «Task 8: Create Agent Chat Feature Library»Files:
- Create:
libs/apis/providers/ai-agent-service/features/agent-chat/project.json - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/tsconfig.json - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/tsconfig.lib.json - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/tsconfig.spec.json - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/index.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.module.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.gateway.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.controller.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.service.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/rabbit-consumer.service.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/walrider-thread.repository.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/walrider-message.repository.ts - Create:
libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/walrider-state.repository.ts - Modify:
tsconfig.base.json(add path alias)
Step 1: Create project.json and tsconfig files
Create libs/apis/providers/ai-agent-service/features/agent-chat/project.json:
{ "name": "ai-agent-service-feature-agent-chat", "$schema": "../../../../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/apis/providers/ai-agent-service/features/agent-chat/src", "projectType": "library", "tags": [], "targets": {}}Create tsconfig.json, tsconfig.lib.json, tsconfig.spec.json following same pattern (extends ../../../../../../tsconfig.base.json, outDir ../../../../../../dist/out-tsc).
Step 2: Create repositories (moved from walrider)
Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/walrider-thread.repository.ts:
import { Injectable } from '@nestjs/common';import { Prisma } from '@prisma/client';import { TransactionHost } from '@nestjs-cls/transactional';import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';
@Injectable()export class WalriderThreadRepository { constructor(private readonly txHost: TransactionHost<TransactionalAdapterPrisma>) {}
create(args: Prisma.WalriderThreadCreateArgs) { return this.txHost.tx.walriderThread.create(args); }
update(args: Prisma.WalriderThreadUpdateArgs) { return this.txHost.tx.walriderThread.update(args); }
findUnique(args: Prisma.WalriderThreadFindUniqueArgs) { return this.txHost.tx.walriderThread.findUnique(args); }
findFirst(args: Prisma.WalriderThreadFindFirstArgs) { return this.txHost.tx.walriderThread.findFirst(args); }
findMany(args?: Prisma.WalriderThreadFindManyArgs) { return this.txHost.tx.walriderThread.findMany(args); }
delete(args: Prisma.WalriderThreadDeleteArgs) { return this.txHost.tx.walriderThread.delete(args); }
count(args?: Prisma.WalriderThreadCountArgs) { return this.txHost.tx.walriderThread.count(args); }}Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/walrider-message.repository.ts:
import { Injectable } from '@nestjs/common';import { Prisma } from '@prisma/client';import { TransactionHost } from '@nestjs-cls/transactional';import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';import { getPaginate, PaginateInput, toPaginateResponse } from '@crewsforge-back/apis/shared';
export type MessageFindManyAndPaginateArgs = Prisma.WalriderMessageFindManyArgs & { paginate: PaginateInput };
@Injectable()export class WalriderMessageRepository { constructor(private readonly txHost: TransactionHost<TransactionalAdapterPrisma>) {}
create<T extends Prisma.WalriderMessageCreateArgs>(args: T) { return this.txHost.tx.walriderMessage.create(args); }
findMany<T extends Prisma.WalriderMessageFindManyArgs>(args?: T) { return this.txHost.tx.walriderMessage.findMany(args); }
count(args?: Prisma.WalriderMessageCountArgs) { return this.txHost.tx.walriderMessage.count(args); }
findManyAndCount(args: Prisma.WalriderMessageFindManyArgs = {}) { return Promise.all([ this.txHost.tx.walriderMessage.findMany(args), this.txHost.tx.walriderMessage.count({ where: args.where }), ]); }
async findManyAndPaginate({ paginate, ...args }: MessageFindManyAndPaginateArgs) { const { skip, take } = getPaginate(paginate); const [data, count] = await this.findManyAndCount({ ...args, skip, take }); return toPaginateResponse({ data, count, paginate }); }}Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/walrider-state.repository.ts:
import { Injectable } from '@nestjs/common';import { Prisma } from '@prisma/client';import { TransactionHost } from '@nestjs-cls/transactional';import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';
@Injectable()export class WalriderStateRepository { constructor(private readonly txHost: TransactionHost<TransactionalAdapterPrisma>) {}
create(args: Prisma.WalriderThreadStateCreateArgs) { return this.txHost.tx.walriderThreadState.create(args); }
findFirst(args: Prisma.WalriderThreadStateFindFirstArgs) { return this.txHost.tx.walriderThreadState.findFirst(args); }
findMany(args?: Prisma.WalriderThreadStateFindManyArgs) { return this.txHost.tx.walriderThreadState.findMany(args); }}Step 3: Create agent-chat.service.ts
Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.service.ts:
import { ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';import { WalriderMessageRole, Prisma } from '@prisma/client';import { Transactional } from '@nestjs-cls/transactional';import { RabbitClientService } from '@crewsforge-back/rabbit-client';import { RabbitConfigService } from '@crewsforge-back/config-shared-rabbit';import { RedisClientService } from '@crewsforge-back/redis-client';import { RabbitRequestMessage, RabbitResponseMessage,} from '@crewsforge-back/apis/providers/ai-agent-service/data-access';
import { WalriderThreadRepository } from './walrider-thread.repository';import { WalriderMessageRepository } from './walrider-message.repository';import { WalriderStateRepository } from './walrider-state.repository';
@Injectable()export class AgentChatService { private readonly logger = new Logger(AgentChatService.name);
constructor( private readonly threadRepository: WalriderThreadRepository, private readonly messageRepository: WalriderMessageRepository, private readonly stateRepository: WalriderStateRepository, private readonly rabbitClient: RabbitClientService, private readonly rabbitConfig: RabbitConfigService, private readonly redisClient: RedisClientService, ) {}
async getThreadByProjectId(projectId: string, customerId: string) { const thread = await this.threadRepository.findFirst({ where: { projectId }, });
if (!thread) { throw new NotFoundException('Agent thread not found for this project'); }
if (thread.customerId !== customerId) { throw new ForbiddenException('Access denied'); }
return thread; }
@Transactional() async sendMessage(projectId: string, customerId: string, content: string) { const thread = await this.getThreadByProjectId(projectId, customerId);
const message = await this.messageRepository.create({ data: { threadId: thread.id, role: WalriderMessageRole.USER, content, }, });
const rabbitMessage: RabbitRequestMessage = { messageId: message.messageId, threadId: thread.id, content, };
this.rabbitClient.publish( this.rabbitConfig.requestExchange, '', rabbitMessage, );
this.logger.log(`Message ${message.messageId} sent to thread ${thread.id}`);
return message; }
@Transactional() async handleAgentResponse(response: RabbitResponseMessage) { const thread = await this.threadRepository.findUnique({ where: { id: response.threadId }, });
if (!thread) { this.logger.warn(`Thread ${response.threadId} not found for response`); return null; }
const metadata: Prisma.InputJsonValue = { phase: response.phase, isComplete: response.isComplete, plan: response.plan as Prisma.InputJsonValue, };
const assistantMessage = await this.messageRepository.create({ data: { threadId: thread.id, role: WalriderMessageRole.ASSISTANT, content: response.response, metadata, }, });
await this.stateRepository.create({ data: { threadId: thread.id, messageId: assistantMessage.id, status: response.phase, data: response.plan as Prisma.InputJsonValue ?? undefined, }, });
return { thread, assistantMessage, response }; }
async getHistory(projectId: string, customerId: string, page = 1, perPage = 20) { const thread = await this.getThreadByProjectId(projectId, customerId);
return this.messageRepository.findManyAndPaginate({ where: { threadId: thread.id }, orderBy: { createdAt: 'asc' }, paginate: { page, perPage }, }); }
async getState(projectId: string, customerId: string) { const thread = await this.getThreadByProjectId(projectId, customerId);
const state = await this.stateRepository.findFirst({ where: { threadId: thread.id }, orderBy: { createdAt: 'desc' }, });
return state; }
async getActiveSocketId(projectId: string): Promise<string | null> { return this.redisClient.get(`ws:project:${projectId}`); }
async setActiveSocket(projectId: string, socketId: string): Promise<void> { await this.redisClient.set(`ws:project:${projectId}`, socketId); }
async removeActiveSocket(projectId: string): Promise<void> { await this.redisClient.set(`ws:project:${projectId}`, '', 1); }}Step 4: Create agent-chat.gateway.ts
Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.gateway.ts:
import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, ConnectedSocket, MessageBody,} from '@nestjs/websockets';import { Logger, UnauthorizedException } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';import { JwtCustomerConfigService } from '@crewsforge-back/apis/configs/auth-api/jwt-customer';import { PrismaClientService } from '@crewsforge-back/apis/utils/prisma-client';import { Server, Socket } from 'socket.io';
import { AgentChatService } from './agent-chat.service';
interface AuthenticatedSocket extends Socket { data: { customerId: string; projectId: string; };}
@WebSocketGateway({ cors: { origin: true, credentials: true }, namespace: '/agent-chat',})export class AgentChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server!: Server;
private readonly logger = new Logger(AgentChatGateway.name);
constructor( private readonly agentChatService: AgentChatService, private readonly jwtService: JwtService, private readonly jwtCustomerConfig: JwtCustomerConfigService, private readonly prisma: PrismaClientService, ) {}
async handleConnection(client: AuthenticatedSocket) { try { const token = client.handshake.query['token'] as string; const projectId = client.handshake.query['projectId'] as string;
if (!token || !projectId) { throw new UnauthorizedException('Token and projectId are required'); }
const payload = this.jwtService.verify(token, { secret: this.jwtCustomerConfig.accessSecretKey, });
if (!payload?.customerId) { throw new UnauthorizedException(); }
const customer = await this.prisma.customer.findUnique({ where: { id: payload.customerId }, });
if (!customer) { throw new UnauthorizedException(); }
// Verify customer owns the project const project = await this.prisma.project.findUnique({ where: { id: projectId }, });
if (!project || project.customerId !== customer.id) { throw new UnauthorizedException('Access denied to this project'); }
client.data = { customerId: customer.id, projectId }; client.join(`project:${projectId}`);
await this.agentChatService.setActiveSocket(projectId, client.id);
this.logger.log(`Client ${client.id} connected to project ${projectId}`); } catch (error) { this.logger.error(`Connection rejected: ${error}`); client.disconnect(); } }
async handleDisconnect(client: AuthenticatedSocket) { if (client.data?.projectId) { await this.agentChatService.removeActiveSocket(client.data.projectId); this.logger.log(`Client ${client.id} disconnected from project ${client.data.projectId}`); } }
@SubscribeMessage('sendMessage') async handleSendMessage( @ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: { content: string }, ) { const { customerId, projectId } = client.data;
const message = await this.agentChatService.sendMessage( projectId, customerId, data.content, );
return { event: 'messageSent', data: { messageId: message.messageId } }; }
emitToProject(projectId: string, event: string, data: unknown) { this.server.to(`project:${projectId}`).emit(event, data); }}Step 5: Create rabbit-consumer.service.ts
Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/rabbit-consumer.service.ts:
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';import { RabbitClientService } from '@crewsforge-back/rabbit-client';import { RabbitConfigService } from '@crewsforge-back/config-shared-rabbit';import { RabbitResponseMessage } from '@crewsforge-back/apis/providers/ai-agent-service/data-access';
import { AgentChatService } from './agent-chat.service';import { AgentChatGateway } from './agent-chat.gateway';
@Injectable()export class RabbitConsumerService implements OnModuleInit { private readonly logger = new Logger(RabbitConsumerService.name);
constructor( private readonly rabbitClient: RabbitClientService, private readonly rabbitConfig: RabbitConfigService, private readonly agentChatService: AgentChatService, private readonly agentChatGateway: AgentChatGateway, ) {}
async onModuleInit() { await this.rabbitClient.connect();
// Assert exchanges and queues await this.rabbitClient.assertExchange( this.rabbitConfig.requestExchange, this.rabbitConfig.requestExchangeType, ); await this.rabbitClient.assertExchange( this.rabbitConfig.responsesExchange, this.rabbitConfig.responsesExchangeType, ); await this.rabbitClient.assertQueue(this.rabbitConfig.requestQueue); await this.rabbitClient.assertQueue(this.rabbitConfig.responsesQueue); await this.rabbitClient.bindQueue( this.rabbitConfig.requestQueue, this.rabbitConfig.requestExchange, '', ); await this.rabbitClient.bindQueue( this.rabbitConfig.responsesQueue, this.rabbitConfig.responsesExchange, '', );
// Start consuming responses await this.rabbitClient.consume( this.rabbitConfig.responsesQueue, async (msg) => { const content = JSON.parse(msg.content.toString()) as RabbitResponseMessage; await this.handleResponse(content); }, );
this.logger.log('RabbitMQ consumer started'); }
private async handleResponse(response: RabbitResponseMessage) { this.logger.log(`Received response for thread ${response.threadId}, message ${response.messageId}`);
const result = await this.agentChatService.handleAgentResponse(response);
if (!result) return;
const { thread } = result;
// Find project for this thread and emit via WS if connected const socketId = await this.agentChatService.getActiveSocketId(thread.projectId);
if (socketId) { this.agentChatGateway.emitToProject(thread.projectId, 'message', { messageId: response.messageId, response: response.response, phase: response.phase, isComplete: response.isComplete, plan: response.plan, }); } }}Step 6: Create agent-chat.controller.ts
Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.controller.ts:
import { Controller, Get, Param, Query, UseGuards, ParseUUIDPipe } from '@nestjs/common';import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';import { CUSTOMER_ACCESS_TOKEN_STRATEGY_NAME, CustomerGuard, GetTokenPayload, MapperService,} from '@crewsforge-back/apis/shared';import { TokenPayload } from '@crewsforge-back/apis/providers/auth-api/features/token';import { AgentHistoryQueryDto, AgentMessageDto, AgentStateDto,} from '@crewsforge-back/apis/providers/ai-agent-service/data-access';
import { AgentChatService } from './agent-chat.service';
@ApiTags('agents')@ApiBearerAuth(CUSTOMER_ACCESS_TOKEN_STRATEGY_NAME)@UseGuards(CustomerGuard)@Controller({ path: 'agents', version: '1' })export class AgentChatController { constructor( private readonly agentChatService: AgentChatService, private readonly mapperService: MapperService, ) {}
@ApiOkResponse() @Get(':projectId/history') async getHistory( @GetTokenPayload() payload: TokenPayload, @Param('projectId', ParseUUIDPipe) projectId: string, @Query() query: AgentHistoryQueryDto, ) { const result = await this.agentChatService.getHistory( projectId, payload.customerId!, query.page, query.perPage, ); return this.mapperService.toPaginateResponse(result, AgentMessageDto); }
@ApiOkResponse({ type: AgentStateDto }) @Get(':projectId/state') async getState( @GetTokenPayload() payload: TokenPayload, @Param('projectId', ParseUUIDPipe) projectId: string, ) { const state = await this.agentChatService.getState(projectId, payload.customerId!); return state ? this.mapperService.toResponse(state, AgentStateDto) : null; }}Step 7: Create agent-chat.module.ts
Create libs/apis/providers/ai-agent-service/features/agent-chat/src/lib/agent-chat.module.ts:
import { Module } from '@nestjs/common';import { JwtModule } from '@nestjs/jwt';import { MapperService, CustomerGuard } from '@crewsforge-back/apis/shared';import { JwtCustomerConfigModule } from '@crewsforge-back/apis/configs/auth-api/jwt-customer';import { PrismaClientModule } from '@crewsforge-back/apis/utils/prisma-client';import { RabbitClientModule } from '@crewsforge-back/rabbit-client';import { RabbitConfigModule } from '@crewsforge-back/config-shared-rabbit';import { RedisClientModule } from '@crewsforge-back/redis-client';
import { AgentChatController } from './agent-chat.controller';import { AgentChatService } from './agent-chat.service';import { AgentChatGateway } from './agent-chat.gateway';import { RabbitConsumerService } from './rabbit-consumer.service';import { WalriderThreadRepository } from './walrider-thread.repository';import { WalriderMessageRepository } from './walrider-message.repository';import { WalriderStateRepository } from './walrider-state.repository';
@Module({ imports: [ PrismaClientModule, JwtModule.register({}), JwtCustomerConfigModule, RabbitClientModule, RabbitConfigModule, RedisClientModule, ], controllers: [AgentChatController], providers: [ AgentChatService, AgentChatGateway, RabbitConsumerService, WalriderThreadRepository, WalriderMessageRepository, WalriderStateRepository, MapperService, CustomerGuard, ], exports: [AgentChatService],})export class AgentChatModule {}Step 8: Create index.ts
Create libs/apis/providers/ai-agent-service/features/agent-chat/src/index.ts:
export * from './lib/agent-chat.module';Step 9: Add path alias to tsconfig.base.json
Add to tsconfig.base.json paths:
"@crewsforge-back/apis/providers/ai-agent-service/features/agent-chat": [ "libs/apis/providers/ai-agent-service/features/agent-chat/src/index.ts"]Step 10: Commit
git add libs/apis/providers/ai-agent-service/features/agent-chat/ tsconfig.base.jsongit commit -m "feat(ai-agent-service): add agent-chat feature with WS gateway, RabbitMQ, and HTTP endpoints"Task 9: Wire AgentChatModule into ai-agent-service AppModule
Заголовок раздела «Task 9: Wire AgentChatModule into ai-agent-service AppModule»Files:
- Modify:
apps/ai-agent-service/src/app/app.module.ts
Step 1: Import AgentChatModule
Update apps/ai-agent-service/src/app/app.module.ts to add the import:
import { AgentChatModule } from '@crewsforge-back/apis/providers/ai-agent-service/features/agent-chat';Add AgentChatModule to the imports array (after ClsModule.forRoot(...)).
Step 2: Verify build
Run: npx nx build ai-agent-service
Step 3: Commit
git add apps/ai-agent-service/src/app/app.module.tsgit commit -m "feat(ai-agent-service): wire AgentChatModule into AppModule"Task 10: Remove Old Walrider Feature from user-api
Заголовок раздела «Task 10: Remove Old Walrider Feature from user-api»Files:
- Delete:
libs/apis/providers/user-api/features/walrider/(entire directory) - Modify:
tsconfig.base.json(remove@crewsforge-back/walrideralias) - Modify: any user-api app.module.ts that imports WalriderModule
Step 1: Find and remove WalriderModule imports from user-api
Search for WalriderModule imports in user-api app:
Run: grep -r "WalriderModule\|walrider" apps/user-api/src/ --include="*.ts"
Remove any import of WalriderModule from the user-api app module.
Step 2: Delete the walrider feature directory
Run: rm -rf libs/apis/providers/user-api/features/walrider
Step 3: Remove path alias from tsconfig.base.json
Remove this line from tsconfig.base.json paths:
"@crewsforge-back/walrider": ["libs/apis/providers/user-api/features/walrider/src/index.ts"],Step 4: Clean up walrider DTOs from user-api data-access
In libs/apis/providers/user-api/data-access/src/lib/dtos/index.ts, remove the export * from './walrider.dto' line.
Delete libs/apis/providers/user-api/data-access/src/lib/dtos/walrider.dto.ts.
Step 5: Verify build
Run: npx nx build user-api (if it exists) and npx nx build ai-agent-service
Step 6: Commit
git add -Agit commit -m "refactor: remove old walrider feature from user-api, moved to ai-agent-service"Task 11: Final Verification
Заголовок раздела «Task 11: Final Verification»Step 1: Build all affected projects
Run: npx nx build project-api && npx nx build ai-agent-service
Step 2: Run lint
Run: npx nx lint project-api && npx nx lint ai-agent-service
Step 3: Verify Prisma client is up to date
Run: npx prisma generate --schema apps/core-api/prisma/schema.prisma
Step 4: Commit if any fixes were needed
git add -Agit commit -m "fix: address build and lint issues"