trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/especificaciones/ET-LLM-001-arquitectura-chat.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
ML Engine Updates:
- Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records
- Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence)
- Backtest results: +176.71R profit with aggressive_filter strategy

Documentation Consolidation:
- Created docs/99-analisis/_MAP.md index with 13 new analysis documents
- Consolidated inventories: removed duplicates from orchestration/inventarios/
- Updated ML_INVENTORY.yml with BTCUSD metrics and training results
- Added execution reports: FASE11-BTCUSD, correction issues, alignment validation

Architecture & Integration:
- Updated all module documentation with NEXUS v3.4 frontmatter
- Fixed _MAP.md indexes across all folders
- Updated orchestration plans and traces

Files: 229 changed, 5064 insertions(+), 1872 deletions(-)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:31:29 -06:00

766 lines
23 KiB
Markdown

---
id: "ET-LLM-001"
title: "Arquitectura del Sistema de Chat"
type: "Technical Specification"
status: "Done"
priority: "Alta"
epic: "OQI-007"
project: "trading-platform"
version: "1.0.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-LLM-001: Arquitectura del Sistema de Chat
**Épica:** OQI-007 - LLM Strategy Agent
**Versión:** 1.0
**Fecha:** 2025-12-05
**Estado:** Planificado
**Prioridad:** P0 - Crítico
---
## Resumen
Esta especificación define la arquitectura técnica del sistema de chat conversacional con el agente LLM, incluyendo backend, frontend, streaming y persistencia.
---
## Arquitectura General
```
┌─────────────────────────────────────────────────────────────────────────┐
│ FRONTEND │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ ChatWindow │ │ MessageList │ │ MessageInput │ │
│ │ Component │ │ Component │ │ Component │ │
│ └────────┬────────┘ └────────┬────────┘ └───────────┬─────────────┘ │
│ └────────────────────┴───────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴─────────────────────────────────────┐ │
│ │ WebSocket Client │ │
│ │ (Socket.IO / native WS) │ │
│ └─────────────────────────────┬─────────────────────────────────────┘ │
└────────────────────────────────┼────────────────────────────────────────┘
│ WSS
┌────────────────────────────────┼────────────────────────────────────────┐
│ BACKEND │
│ ┌─────────────────────────────┴─────────────────────────────────────┐ │
│ │ WebSocket Gateway │ │
│ │ (NestJS Gateway / Express WS) │ │
│ └────────┬────────────────────┬────────────────────┬────────────────┘ │
│ │ │ │ │
│ ┌────────▼────────┐ ┌────────▼────────┐ ┌───────▼─────────┐ │
│ │ ChatService │ │ AgentService │ │ ToolsService │ │
│ │ │ │ │ │ │ │
│ │ - createConv() │ │ - processMsg() │ │ - get_price() │ │
│ │ - getHistory() │ │ - buildCtx() │ │ - create_order()│ │
│ │ - saveMessage() │ │ - streamResp() │ │ - get_ml() │ │
│ └────────┬────────┘ └────────┬────────┘ └───────┬─────────┘ │
│ │ │ │ │
│ ┌────────▼────────────────────▼────────────────────▼────────────────┐ │
│ │ LLM Provider Adapter │ │
│ │ (OpenAI / Claude / Local Model) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────┼────────────────────────────────────────┐
│ STORAGE │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ S3/Blob │ │
│ │ │ │ │ │ │ │
│ │ conversations│ │ session cache│ │ attachments │ │
│ │ messages │ │ rate limits │ │ exports │ │
│ │ user_memory │ │ active WS │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
```
---
## Componentes Backend
### 1. WebSocket Gateway
```typescript
// src/modules/copilot/gateways/chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
ConnectedSocket,
MessageBody,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
namespace: '/copilot',
cors: {
origin: process.env.FRONTEND_URL,
credentials: true,
},
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
constructor(
private readonly chatService: ChatService,
private readonly agentService: AgentService,
private readonly authService: AuthService,
) {}
async handleConnection(client: Socket) {
try {
const token = client.handshake.auth.token;
const user = await this.authService.validateToken(token);
client.data.user = user;
client.join(`user:${user.id}`);
// Cargar conversación activa si existe
const activeConv = await this.chatService.getActiveConversation(user.id);
if (activeConv) {
client.emit('conversation:restored', activeConv);
}
} catch (error) {
client.disconnect();
}
}
handleDisconnect(client: Socket) {
// Cleanup
}
@SubscribeMessage('message:send')
async handleMessage(
@ConnectedSocket() client: Socket,
@MessageBody() data: { conversationId: string; content: string },
) {
const user = client.data.user;
// Rate limiting check
const canSend = await this.chatService.checkRateLimit(user.id, user.plan);
if (!canSend) {
client.emit('error', { code: 'RATE_LIMIT', message: 'Daily limit reached' });
return;
}
// Save user message
const userMessage = await this.chatService.saveMessage({
conversationId: data.conversationId,
role: 'user',
content: data.content,
});
client.emit('message:saved', userMessage);
client.emit('agent:thinking', { status: 'processing' });
// Process with agent (streaming)
const stream = await this.agentService.processMessage({
userId: user.id,
conversationId: data.conversationId,
message: data.content,
userPlan: user.plan,
});
let fullResponse = '';
for await (const chunk of stream) {
fullResponse += chunk.content;
client.emit('agent:stream', {
chunk: chunk.content,
toolCalls: chunk.toolCalls,
});
}
// Save complete response
const agentMessage = await this.chatService.saveMessage({
conversationId: data.conversationId,
role: 'assistant',
content: fullResponse,
});
client.emit('agent:complete', agentMessage);
}
@SubscribeMessage('message:cancel')
async handleCancel(
@ConnectedSocket() client: Socket,
@MessageBody() data: { conversationId: string },
) {
await this.agentService.cancelGeneration(data.conversationId);
client.emit('agent:cancelled');
}
}
```
### 2. Chat Service
```typescript
// src/modules/copilot/services/chat.service.ts
@Injectable()
export class ChatService {
constructor(
@InjectRepository(Conversation)
private conversationRepo: Repository<Conversation>,
@InjectRepository(Message)
private messageRepo: Repository<Message>,
private readonly redis: RedisService,
) {}
async createConversation(userId: string, title?: string): Promise<Conversation> {
const conversation = this.conversationRepo.create({
userId,
title: title || `Conversación ${new Date().toLocaleDateString()}`,
status: 'active',
});
return this.conversationRepo.save(conversation);
}
async getConversations(userId: string, limit = 20): Promise<Conversation[]> {
return this.conversationRepo.find({
where: { userId, deletedAt: null },
order: { updatedAt: 'DESC' },
take: limit,
});
}
async getConversationMessages(
conversationId: string,
limit = 50,
offset = 0,
): Promise<Message[]> {
return this.messageRepo.find({
where: { conversationId },
order: { createdAt: 'ASC' },
take: limit,
skip: offset,
});
}
async saveMessage(data: CreateMessageDto): Promise<Message> {
const message = this.messageRepo.create(data);
const saved = await this.messageRepo.save(message);
// Update conversation timestamp
await this.conversationRepo.update(
{ id: data.conversationId },
{ updatedAt: new Date() },
);
return saved;
}
async checkRateLimit(userId: string, plan: string): Promise<boolean> {
const limits = {
free: 10,
pro: 100,
premium: -1, // unlimited
};
if (plan === 'premium') return true;
const key = `rate:chat:${userId}:${new Date().toISOString().split('T')[0]}`;
const count = await this.redis.incr(key);
if (count === 1) {
await this.redis.expire(key, 86400); // 24 hours
}
return count <= limits[plan];
}
async deleteConversation(conversationId: string, userId: string): Promise<void> {
await this.conversationRepo.update(
{ id: conversationId, userId },
{ deletedAt: new Date() },
);
}
async renameConversation(
conversationId: string,
userId: string,
title: string,
): Promise<Conversation> {
await this.conversationRepo.update(
{ id: conversationId, userId },
{ title },
);
return this.conversationRepo.findOne({ where: { id: conversationId } });
}
}
```
### 3. Agent Service
```typescript
// src/modules/copilot/services/agent.service.ts
@Injectable()
export class AgentService {
private activeGenerations: Map<string, AbortController> = new Map();
constructor(
private readonly llmProvider: LLMProviderService,
private readonly contextService: ContextService,
private readonly toolsService: ToolsService,
) {}
async *processMessage(params: ProcessMessageParams): AsyncGenerator<StreamChunk> {
const { userId, conversationId, message, userPlan } = params;
const abortController = new AbortController();
this.activeGenerations.set(conversationId, abortController);
try {
// Build context
const context = await this.contextService.buildContext({
userId,
conversationId,
currentMessage: message,
userPlan,
});
// Define available tools based on plan
const tools = this.toolsService.getAvailableTools(userPlan);
// Create LLM request
const stream = await this.llmProvider.createChatCompletion({
model: this.getModelForPlan(userPlan),
messages: [
{ role: 'system', content: context.systemPrompt },
...context.conversationHistory,
{ role: 'user', content: message },
],
tools,
stream: true,
signal: abortController.signal,
});
// Process stream
for await (const chunk of stream) {
if (abortController.signal.aborted) {
break;
}
// Handle tool calls
if (chunk.toolCalls) {
for (const toolCall of chunk.toolCalls) {
const result = await this.toolsService.execute(
toolCall.name,
toolCall.arguments,
userId,
);
yield {
type: 'tool_result',
content: '',
toolCalls: [{ ...toolCall, result }],
};
}
}
// Yield text content
if (chunk.content) {
yield {
type: 'text',
content: chunk.content,
toolCalls: null,
};
}
}
} finally {
this.activeGenerations.delete(conversationId);
}
}
async cancelGeneration(conversationId: string): Promise<void> {
const controller = this.activeGenerations.get(conversationId);
if (controller) {
controller.abort();
}
}
private getModelForPlan(plan: string): string {
const models = {
free: 'gpt-4o-mini',
pro: 'gpt-4o',
premium: 'claude-3-5-sonnet',
};
return models[plan] || models.free;
}
}
```
---
## Componentes Frontend
### 1. Chat Store (Zustand)
```typescript
// src/stores/chat.store.ts
import { create } from 'zustand';
import { io, Socket } from 'socket.io-client';
interface ChatState {
socket: Socket | null;
connected: boolean;
conversations: Conversation[];
activeConversation: Conversation | null;
messages: Message[];
isStreaming: boolean;
streamingContent: string;
// Actions
connect: (token: string) => void;
disconnect: () => void;
sendMessage: (content: string) => void;
cancelGeneration: () => void;
createConversation: () => void;
selectConversation: (id: string) => void;
deleteConversation: (id: string) => void;
}
export const useChatStore = create<ChatState>((set, get) => ({
socket: null,
connected: false,
conversations: [],
activeConversation: null,
messages: [],
isStreaming: false,
streamingContent: '',
connect: (token: string) => {
const socket = io(`${API_URL}/copilot`, {
auth: { token },
transports: ['websocket'],
});
socket.on('connect', () => {
set({ connected: true });
});
socket.on('disconnect', () => {
set({ connected: false });
});
socket.on('conversation:restored', (conversation) => {
set({ activeConversation: conversation });
});
socket.on('message:saved', (message) => {
set((state) => ({
messages: [...state.messages, message],
}));
});
socket.on('agent:thinking', () => {
set({ isStreaming: true, streamingContent: '' });
});
socket.on('agent:stream', ({ chunk }) => {
set((state) => ({
streamingContent: state.streamingContent + chunk,
}));
});
socket.on('agent:complete', (message) => {
set((state) => ({
messages: [...state.messages, message],
isStreaming: false,
streamingContent: '',
}));
});
socket.on('agent:cancelled', () => {
set({ isStreaming: false });
});
set({ socket });
},
disconnect: () => {
get().socket?.disconnect();
set({ socket: null, connected: false });
},
sendMessage: (content: string) => {
const { socket, activeConversation } = get();
if (!socket || !activeConversation) return;
socket.emit('message:send', {
conversationId: activeConversation.id,
content,
});
},
cancelGeneration: () => {
const { socket, activeConversation } = get();
if (!socket || !activeConversation) return;
socket.emit('message:cancel', {
conversationId: activeConversation.id,
});
},
// ... other actions
}));
```
### 2. Chat Component
```tsx
// src/modules/copilot/components/ChatWindow.tsx
import { useEffect, useRef } from 'react';
import { useChatStore } from '@/stores/chat.store';
import { MessageList } from './MessageList';
import { MessageInput } from './MessageInput';
import { ConversationSidebar } from './ConversationSidebar';
export function ChatWindow() {
const {
connected,
connect,
messages,
isStreaming,
streamingContent,
sendMessage,
cancelGeneration,
} = useChatStore();
const messagesEndRef = useRef<HTMLDivElement>(null);
const { token } = useAuth();
useEffect(() => {
if (token && !connected) {
connect(token);
}
return () => {
useChatStore.getState().disconnect();
};
}, [token]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingContent]);
return (
<div className="flex h-full">
<ConversationSidebar />
<div className="flex-1 flex flex-col">
<div className="flex-1 overflow-y-auto p-4">
<MessageList messages={messages} />
{isStreaming && (
<div className="animate-pulse">
<MessageBubble role="assistant" content={streamingContent} />
</div>
)}
<div ref={messagesEndRef} />
</div>
<MessageInput
onSend={sendMessage}
onCancel={cancelGeneration}
isStreaming={isStreaming}
disabled={!connected}
/>
</div>
</div>
);
}
```
---
## Modelos de Datos
### Conversation Entity
```typescript
// src/modules/copilot/entities/conversation.entity.ts
@Entity('conversations')
export class Conversation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'user_id' })
userId: string;
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
@Column({ length: 255 })
title: string;
@Column({
type: 'enum',
enum: ['active', 'archived'],
default: 'active',
})
status: string;
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;
@Column({ name: 'messages_count', default: 0 })
messagesCount: number;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt: Date;
@OneToMany(() => Message, (message) => message.conversation)
messages: Message[];
}
```
### Message Entity
```typescript
// src/modules/copilot/entities/message.entity.ts
@Entity('messages')
export class Message {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'conversation_id' })
conversationId: string;
@ManyToOne(() => Conversation, (conv) => conv.messages)
@JoinColumn({ name: 'conversation_id' })
conversation: Conversation;
@Column({
type: 'enum',
enum: ['user', 'assistant', 'system'],
})
role: string;
@Column({ type: 'text' })
content: string;
@Column({ type: 'jsonb', nullable: true, name: 'tool_calls' })
toolCalls: ToolCall[];
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;
@Column({ type: 'smallint', nullable: true, name: 'feedback_rating' })
feedbackRating: number;
@Column({ type: 'text', nullable: true, name: 'feedback_comment' })
feedbackComment: string;
@Column({ name: 'tokens_input', nullable: true })
tokensInput: number;
@Column({ name: 'tokens_output', nullable: true })
tokensOutput: number;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}
interface ToolCall {
id: string;
name: string;
arguments: Record<string, any>;
result?: any;
}
```
---
## API REST Endpoints
| Method | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/api/copilot/conversations` | Listar conversaciones |
| POST | `/api/copilot/conversations` | Crear conversación |
| GET | `/api/copilot/conversations/:id` | Obtener conversación |
| PATCH | `/api/copilot/conversations/:id` | Renombrar conversación |
| DELETE | `/api/copilot/conversations/:id` | Eliminar conversación |
| GET | `/api/copilot/conversations/:id/messages` | Obtener mensajes |
| POST | `/api/copilot/messages/:id/feedback` | Dar feedback a mensaje |
---
## WebSocket Events
### Client → Server
| Event | Payload | Descripción |
|-------|---------|-------------|
| `message:send` | `{ conversationId, content }` | Enviar mensaje |
| `message:cancel` | `{ conversationId }` | Cancelar generación |
| `conversation:create` | `{ title? }` | Crear conversación |
| `typing:start` | `{ conversationId }` | Usuario escribiendo |
### Server → Client
| Event | Payload | Descripción |
|-------|---------|-------------|
| `message:saved` | `Message` | Mensaje guardado |
| `agent:thinking` | `{ status }` | Agente procesando |
| `agent:stream` | `{ chunk, toolCalls? }` | Chunk de respuesta |
| `agent:complete` | `Message` | Respuesta completa |
| `agent:cancelled` | `{}` | Generación cancelada |
| `error` | `{ code, message }` | Error |
| `conversation:restored` | `Conversation` | Conversación restaurada |
---
## Dependencias
### NPM Packages Backend
- `@nestjs/websockets`
- `@nestjs/platform-socket.io`
- `socket.io`
- `openai` / `@anthropic-ai/sdk`
### NPM Packages Frontend
- `socket.io-client`
- `zustand`
- `react-markdown`
- `remark-gfm`
---
## Notas de Implementación
1. **Reconexión automática:** Socket.IO maneja reconexión, pero debemos restaurar estado
2. **Rate limiting:** Implementar tanto en WS como en Redis para precisión
3. **Streaming:** Usar Server-Sent Events como fallback si WS falla
4. **Tokens:** Trackear uso de tokens para billing y límites
5. **Seguridad:** Validar token JWT en cada conexión WS
---
## Referencias
- [RF-LLM-001: Interfaz de Chat](../requerimientos/RF-LLM-001-chat-interface.md)
- [ET-LLM-006: Gestión de Memoria](./ET-LLM-006-gestion-memoria.md)
---
*Especificación técnica - Sistema NEXUS*
*Trading Platform*