# Implementation Plan: Conversaciones múltiples del asistente con contexto

**Branch**: `004-chat-conversations` | **Date**: 2026-05-25 | **Spec**: [spec.md](./spec.md)

**Input**: Feature specification from `/specs/004-chat-conversations/spec.md`

## Summary

Evolucionar el chat de `/assistant` de **un hilo implícito por usuario** (`session_id = api-user-{id}`) a **conversaciones explícitas** (`chat_conversations` + `conversation_id` en mensajes): listar, crear, cambiar y eliminar hilos por operador. Introducir `ConversationHistoryService` para que **todos** los flujos del agente (clasificador, chat general, consultas BD) reciban los últimos N turnos de la conversación activa. Migración brownfield: historial existente → una conversación por defecto por usuario. API REST bajo `/api/v1/chat/conversations/{id}/…`; streaming y adjuntos conservados con `conversation_id` en la ruta. UI Angular: sidebar/lista + `sessionStorage` para conversación activa.

## Technical Context

**Language/Version**: PHP 8.2+, Laravel 12; TypeScript / Angular 19  
**Primary Dependencies**: `ChatConversationService`, `ChatConversationStreamService`, `AgentOrchestratorService`, `OpenAiChatService`, `IntentClassifierService`, `AgentDatabaseQueryCapabilityHandler`, JWT auth  
**Storage**: Nueva tabla `chat_conversations`; migración `chat_messages.conversation_id` (FK); deprecación de `session_id`  
**Testing**: PHPUnit Feature (`ChatConversationsTest`, `ConversationHistoryServiceTest`, migración); tests existentes `ChatConversationStreamTest` adaptados; Karma en `ChatApiService` + sidebar  
**Target Platform**: API Laravel + SPA `ChatFront/features/conversation`  
**Project Type**: Extensión full-stack brownfield (backend + frontend + migración datos)  
**Performance Goals**: Listado conversaciones < 500 ms p95; carga mensajes conversación activa sin regresión perceptible vs. baseline  
**Constraints**: Autorización estricta por `user_id`; ventana contexto 24 mensajes (`AGENT_MAX_HISTORY_MESSAGES`); hilos vacíos no persistidos; compatibilidad streaming 003  
**Scale/Scope**: 1 migración + 1 modelo nuevo + 1 servicio historial + 6 endpoints REST + refactor 4 servicios agente + UI sidebar  

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

| Principle | Status | Notes |
|-----------|--------|-------|
| **I. Spec-First** | ✅ | Trazado a FR-001–FR-013, US1–US5 |
| **II. Skinny Controllers** | ✅ | `ChatController` delega en `ChatConversationService` / `ConversationQueryService` |
| **III. Contratos IA** | ✅ | Historial inyectado vía servicio; sin acoplar prompts a proveedor |
| **IV. API v1** | ✅ | Rutas `/api/v1/chat/conversations/*`; JWT; 403/404 sin filtrar recursos ajenos |
| **V. Contrato API** | ✅ | `contracts/chat-conversations-api.md`; SSE actualizado; tipos `api.types.ts` |
| **VI. Frontend desacoplado** | ✅ | `ChatApiService`; componente sidebar; estado en signals + `sessionStorage` |
| **VII. Fidelidad PDF** | ✅ N/A | Adjuntos sin cambio de extracción; solo scope `conversation_id` |
| **VIII. Simplicidad** | ✅ | Una tabla nueva; un servicio historial compartido; rutas legacy deprecadas una release |

**Post-design re-check**: ✅ Todos los gates pasan. Sin entradas en Complexity Tracking.

## Project Structure

### Documentation (this feature)

```text
specs/004-chat-conversations/
├── spec.md
├── plan.md                          # Este archivo
├── research.md                      # Phase 0
├── data-model.md                    # Phase 1
├── quickstart.md                    # Phase 1
├── contracts/
│   ├── chat-conversations-api.md    # REST CRUD + mensajes
│   └── chat-stream-sse.md           # SSE con conversation_id (extiende 003)
├── checklists/
│   └── requirements.md
└── tasks.md                         # (/speckit-tasks — generado)
```

### Source Code (cambios previstos)

```text
app/
├── Models/
│   ├── ChatConversation.php              # NUEVO
│   └── ChatMessage.php                   # + conversation_id, scope forConversation
├── Services/
│   ├── Chat/
│   │   ├── ConversationHistoryService.php  # NUEVO — ventana LLM + título auto
│   │   └── ConversationQueryService.php    # NUEVO — list/create/delete/authorize
│   ├── ChatConversationService.php         # refactor conversation_id
│   ├── ChatConversationStreamService.php   # refactor + orden persistencia user
│   ├── OpenAiChatService.php               # historial por conversation_id
│   └── Agent/
│       ├── IntentClassifierService.php     # + historial
│       ├── AgentOrchestratorService.php      # conversationId param
│       └── Handlers/
│           ├── GeneralChatCapabilityHandler.php
│           └── AgentDatabaseQueryCapabilityHandler.php  # historial turno 1 y 2 + meta
├── Http/
│   ├── Controllers/Api/V1/ChatController.php
│   └── Requests/Api/V1/
│       └── ChatConversationMessageRequest.php  # validación conversation ownership
database/migrations/
├── YYYY_MM_DD_create_chat_conversations_table.php
└── YYYY_MM_DD_add_conversation_id_to_chat_messages.php  # incluye backfill + drop session_id
config/
└── agent.php                               # + max_history_messages

routes/api.php                              # rutas conversations/*

ChatFront/src/app/
├── core/
│   ├── models/api.types.ts                 # ConversationSummary, etc.
│   └── services/chat-api.service.ts        # CRUD conversaciones + message/stream con id
└── features/conversation/
    ├── conversation.component.ts/html/scss # sidebar + activeConversationId
    └── conversation-sidebar.component.ts   # NUEVO (opcional inline en parent v1)

tests/
├── Feature/Chat/
│   ├── ChatConversationsTest.php
│   ├── ChatConversationStreamTest.php      # actualizar conversation_id
│   └── ConversationMigrationTest.php
└── Unit/Services/Chat/
    └── ConversationHistoryServiceTest.php
```

**Structure Decision**: Monorepo brownfield. Rutas legacy `GET/DELETE /chat/conversation` deprecadas (proxy a conversación más reciente) durante transición; eliminar en tarea posterior post-UAT.

## Architecture

### Modelo de datos (resumen)

```mermaid
erDiagram
    User ||--o{ ChatConversation : owns
    ChatConversation ||--o{ ChatMessage : contains
    ChatConversation {
        int id PK
        int user_id FK
        string title
        timestamp updated_at
    }
    ChatMessage {
        int id PK
        int conversation_id FK
        string role
        text content
        json meta
    }
```

### Flujo — enviar mensaje con contexto

```mermaid
sequenceDiagram
    participant UI as ConversationComponent
    participant API as ChatApiService
    participant C as ChatController
    participant Q as ConversationQueryService
    participant S as ChatConversationStreamService
    participant H as ConversationHistoryService
    participant O as AgentOrchestrator

    UI->>API: POST /conversations/{id}/message/stream
    API->>C: JWT + conversation_id
    C->>Q: assertOwned(user, id)
    C->>S: streamResponse(user, conversation, text)
    S->>H: recentTurns(conversationId, limit=24)
    S->>O: handleStreaming(conversationId, text, history)
    O->>O: classify + handler con historial
    S->>S: persist user + assistant
    S-->>UI: SSE done
```

### Contexto LLM (FR-006)

| Componente | Antes | Después |
|------------|-------|---------|
| `OpenAiChatService` | `forSession(session_id)` | `ConversationHistoryService::toLlmMessages()` |
| `IntentClassifierService` | solo mensaje actual | system + últimos N turnos + actual |
| `AgentDatabaseQueryCapabilityHandler` turno 1 | solo user actual | historial + user actual |
| `AgentDatabaseQueryCapabilityHandler` turno 2 | prompt + JSON resultados | historial reciente + prompt enriquecido |
| Meta assistant (FR-013) | null | `{ capability, query: { query_name, parameters, row_count } }` |

### Migración brownfield (FR-007)

1. Crear `chat_conversations`.
2. Por cada `session_id` distinto `api-user-{id}`: INSERT conversación `{ user_id: id, title: 'Conversación anterior' }`.
3. UPDATE `chat_messages` SET `conversation_id` según mapeo session_id → conversation.id.
4. Hacer `conversation_id` NOT NULL; eliminar columna `session_id` e índice.

Comando artisan: `php artisan chat:migrate-legacy-sessions` (idempotente).

### Rutas API

Ver [contracts/chat-conversations-api.md](./contracts/chat-conversations-api.md) y [contracts/chat-stream-sse.md](./contracts/chat-stream-sse.md).

| Método | Ruta | FR |
|--------|------|-----|
| GET | `/chat/conversations` | FR-004 |
| POST | `/chat/conversations` | FR-003 |
| GET | `/chat/conversations/{id}/messages` | FR-002, FR-010 |
| DELETE | `/chat/conversations/{id}` | FR-008 |
| POST | `/chat/conversations/{id}/message` | FR-011 sync |
| POST | `/chat/conversations/{id}/message/stream` | FR-011 stream |

Legacy (deprecado, 1 release):

| Método | Ruta | Comportamiento |
|--------|------|----------------|
| GET | `/chat/conversation` | → mensajes de conversación más reciente |
| DELETE | `/chat/conversation` | → DELETE conversación más reciente (documentar breaking) |

### Frontend (`/assistant`)

1. **Al init**: `GET /chat/conversations`; restaurar `activeConversationId` de `sessionStorage` o usar más reciente.
2. **Sidebar**: lista títulos + fecha; click → cargar mensajes.
3. **Nueva conversación**: crear id en cliente (`POST`) o estado local hasta primer mensaje; al enviar primer mensaje persistir.
4. **Eliminar**: confirmación PrimeNG → `DELETE /conversations/{id}`.
5. **Stream activo**: deshabilitar cambio de conversación (US5).

`sessionStorage` key: `assistant_active_conversation_id`.

### Orden persistencia streaming (fix 003)

Unificar con sync: **persistir mensaje user después de `handleStreaming` exitoso** (o excluir último user del historial si se persiste antes). Evita duplicado en payload LLM.

### Configuración

```env
AGENT_MAX_HISTORY_MESSAGES=24
AGENT_CONVERSATION_TITLE_MAX=60
```

## FR Traceability

| FR | Implementación |
|----|----------------|
| FR-001 | `chat_conversations.user_id` + policies |
| FR-002 | `chat_messages.conversation_id` |
| FR-003 | POST `/chat/conversations` |
| FR-004 | GET `/chat/conversations` + UI sidebar |
| FR-005 | Historial scoped por `conversation_id` |
| FR-006 | `ConversationHistoryService` en agente |
| FR-007 | Migración + comando artisan |
| FR-008 | DELETE `/chat/conversations/{id}` |
| FR-009 | `ConversationQueryService::assertOwned()` → 404 |
| FR-010 | sessionStorage + GET messages |
| FR-011 | Rutas message/stream con `{id}` |
| FR-012 | Título auto en primer mensaje user |
| FR-013 | `meta` en mensaje assistant post-query |

## Testing Strategy

- **Feature**: CRUD conversaciones, autorización cruzada, migración backfill.
- **Feature**: Seguimiento en mismo hilo vs. hilos distintos (mock LLM o integración).
- **Unit**: `ConversationHistoryService` — límite 24, exclusión mensaje duplicado, formato LLM.
- **Regression**: `ChatConversationStreamTest`, adjuntos sync, streaming 003.
- **Frontend**: `ng build`; tests parser + listado conversaciones.

## Out of Scope (v1)

- Renombrar conversación manualmente
- Búsqueda en listado
- Compartir conversaciones entre usuarios
- Archivar vs. borrar (solo hard delete)

## Next Steps

1. `/speckit-tasks` — generar `tasks.md`
2. `/speckit-implement` — migración → backend → frontend → UAT quickstart
