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

Customer-Agent Interaction Design

Enable customers to communicate with an AI agent (walrider) through WebSocket, with messages routed via RabbitMQ. Each project automatically gets an agent thread on creation.

Two branches of work:

  1. feat/project-service — auto-create agent thread on project creation, link Project ↔ WalriderThread in DB
  2. feat/ai-agent-service — new NX service with WS gateway, RabbitMQ integration, HTTP endpoints for history/state

Additionally: remove obsolete walrider HTTP integration from user-api, move repositories to ai-agent-service.

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")
}
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")
}
model Project {
// ... existing fields
walriderThread WalriderThread?
}

Following the redis-client pattern:

  • RabbitClientModule — dynamic module with forRoot(options)
  • RabbitClientService — wraps amqplib connection with publish/subscribe/ack methods
  • Connection management and reconnection

Environment variables:

RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USERNAME, RABBITMQ_PASSWORD, RABBITMQ_PREFETCH
RABBITMQ_REQUEST_QUEUE, RABBITMQ_REQUEST_EXCHANGE, RABBITMQ_REQUEST_EXCHANGE_TYPE
RABBITMQ_RESPONSES_QUEUE, RABBITMQ_RESPONSES_EXCHANGE, RABBITMQ_RESPONSES_EXCHANGE_TYPE

Request (ai.requests):

{ "messageId": "int", "threadId": "string", "content": "string" }

Response (ai.responses):

{
"messageId": "int", "threadId": "string", "response": "string",
"phase": "string", "isComplete": false, "plan": {}
}

On POST /customers/projects:

  1. Create Project in DB (existing)
  2. Create WalriderThread with { customerId, projectId } in the same transaction
  3. Thread’s id (UUID) serves as threadId in RabbitMQ messages

Implementation: import WalriderThreadRepository into ProjectModule, extend createProject.

Standard NestJS bootstrap with CORS, validation pipe, Prisma filter, Swagger.

features/
agent-chat/ — WS gateway + RabbitMQ handler + HTTP controller
data-access/ — DTOs for WS events and HTTP responses
  • Socket.IO based (@WebSocketGateway)
  • Connection: client sends JWT token (query param) + projectId
  • Gateway validates JWT, verifies customer owns the project
  • Stores socket mapping in Redis: ws:project:{projectId} → socketId
  • Client events: sendMessage { content } → save to DB, publish to RabbitMQ
  • Server events: message { messageId, response, phase, isComplete, plan }
  • Producer: on WS sendMessage, publish to ai.requests exchange
  • Consumer: subscribe to ai.responses queue
    1. Save ASSISTANT message to DB
    2. Save WalriderThreadState to DB
    3. If active socket → emit via WS
    4. If no socket → persisted in DB for later retrieval
  • GET /agents/:projectId/history — paginated chat history
  • GET /agents/:projectId/state — current agent state (latest thread state)

Both protected by CustomerGuard with project ownership validation.

Remove libs/apis/providers/user-api/features/walrider/ entirely:

  • Move repositories (thread, message, state) to ai-agent-service libs
  • Remove walrider-http.service.ts (replaced by RabbitMQ)
  • Remove walrider controller and service
  • Remove walrider module import from user-api
Customer creates project
→ project-api: create Project + WalriderThread (same transaction)
Customer opens chat
→ WS connect to ai-agent-service with JWT + projectId
→ Validate auth + ownership, store session in Redis
Customer sends message
→ WS "sendMessage" → save USER message to DB → publish to ai.requests
Walrider responds
→ Consumer reads ai.responses → save ASSISTANT message + state to DB
→ If active socket → emit "message" to client
Customer fetches history
→ GET /agents/:projectId/history → paginated messages from DB
DecisionChoiceRationale
ArchitectureSingle ai-agent-serviceMatches requirements, follows monorepo patterns, minimal complexity
Project-Thread linkprojectId on WalriderThread (unique 1:1)Simple, each project gets exactly one agent thread
WS authJWT token in query paramReuses existing JWT infrastructure (CustomerGuard pattern)
RabbitMQ threadIdWalriderThread.id (UUID)No extra fields, direct mapping
RabbitMQ messageIdDB autoincrement int on WalriderMessageGuaranteed unique, simple
Old walrider cleanupMove repos to ai-agent-service, remove from user-apiClean separation, no dead code