Перейти к содержимому

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


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"

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"

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.ts
git commit -m "feat(project-api): wire ProjectModule into AppModule"

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.json
git commit -m "feat(config): add RabbitMQ configuration 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.json
git commit -m "feat(rabbit-client): add RabbitMQ client library"

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.json
git commit -m "feat(ai-agent-service): scaffold NX application"

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.json
git commit -m "feat(ai-agent-service): add data-access library with DTOs and interfaces"

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.json
git commit -m "feat(ai-agent-service): add agent-chat feature with WS gateway, RabbitMQ, and HTTP endpoints"

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.ts
git commit -m "feat(ai-agent-service): wire AgentChatModule into AppModule"

Files:

  • Delete: libs/apis/providers/user-api/features/walrider/ (entire directory)
  • Modify: tsconfig.base.json (remove @crewsforge-back/walrider alias)
  • 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 -A
git commit -m "refactor: remove old walrider feature from user-api, moved to ai-agent-service"

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 -A
git commit -m "fix: address build and lint issues"