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>
766 lines
23 KiB
Markdown
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*
|