feat: add implementation plan and monorepo guide for DreamChat project
This commit is contained in:
572
doc/api-spec.md
Normal file
572
doc/api-spec.md
Normal file
@@ -0,0 +1,572 @@
|
||||
# DreamChat API Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the REST API and WebSocket specifications for DreamChat.
|
||||
|
||||
- **REST API**: For CRUD operations, file uploads, and synchronous requests
|
||||
- **WebSocket**: For real-time chat streaming
|
||||
- **OpenAPI**: Auto-generated from NestJS decorators for frontend client generation
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
Development: http://localhost:3000/api
|
||||
Production: https://api.dreamchat.example/api
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### JWT Token Flow
|
||||
|
||||
```http
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user@example.com",
|
||||
"password": "securepassword"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expiresIn": 3600
|
||||
}
|
||||
```
|
||||
|
||||
### Keycloak Integration
|
||||
|
||||
```http
|
||||
GET /api/auth/keycloak
|
||||
Redirect to Keycloak login
|
||||
|
||||
Callback:
|
||||
GET /api/auth/keycloak/callback?code=...
|
||||
Response: { accessToken, refreshToken, expiresIn }
|
||||
```
|
||||
|
||||
### WebSocket Auth
|
||||
|
||||
WebSocket connections authenticate via query parameter:
|
||||
```
|
||||
ws://localhost:3000/chat?token=eyJhbGciOiJIUzI1NiIs...
|
||||
```
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### Authentication Module
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| POST | `/auth/login` | Local login | Public |
|
||||
| POST | `/auth/refresh` | Refresh token | Public |
|
||||
| POST | `/auth/logout` | Logout | Bearer |
|
||||
| GET | `/auth/me` | Get current user | Bearer |
|
||||
| GET | `/auth/keycloak` | Keycloak login URL | Public |
|
||||
| GET | `/auth/keycloak/callback` | Keycloak callback | Public |
|
||||
|
||||
### Users Module
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| POST | `/users` | Register new user | Public |
|
||||
| GET | `/users/:id` | Get user profile | Bearer |
|
||||
| PATCH | `/users/:id` | Update user | Bearer (own only) |
|
||||
| DELETE | `/users/:id` | Delete user | Bearer (own/admin) |
|
||||
|
||||
### Characters Module
|
||||
|
||||
```typescript
|
||||
// DTOs
|
||||
class CreateCharacterDto {
|
||||
name: string;
|
||||
personalityPrompt: string;
|
||||
backstory?: string;
|
||||
attributes?: Record<string, any>;
|
||||
avatarUrl?: string;
|
||||
config?: Record<string, any>;
|
||||
}
|
||||
|
||||
class CharacterResponseDto {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
personalityPrompt: string;
|
||||
backstory: string;
|
||||
attributes: Record<string, any>;
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| GET | `/characters` | List user's characters | Bearer |
|
||||
| POST | `/characters` | Create character | Bearer |
|
||||
| GET | `/characters/:id` | Get character details | Bearer |
|
||||
| PATCH | `/characters/:id` | Update character | Bearer (owner) |
|
||||
| DELETE | `/characters/:id` | Delete character | Bearer (owner) |
|
||||
|
||||
**Example Requests:**
|
||||
|
||||
```http
|
||||
POST /api/characters
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Alice",
|
||||
"personalityPrompt": "You are Alice, a curious and adventurous explorer...",
|
||||
"backstory": "Alice grew up in a small village...",
|
||||
"attributes": {
|
||||
"traits": ["curious", "brave", "witty"],
|
||||
"age": 25,
|
||||
"species": "human",
|
||||
"skills": ["navigation", "survival"]
|
||||
}
|
||||
}
|
||||
|
||||
Response: 201 Created
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Alice",
|
||||
"personalityPrompt": "...",
|
||||
"attributes": { ... },
|
||||
"createdAt": "2026-02-23T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Conversations Module
|
||||
|
||||
```typescript
|
||||
class CreateConversationDto {
|
||||
characterId: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
class ConversationResponseDto {
|
||||
id: string;
|
||||
title: string;
|
||||
character: CharacterSummaryDto;
|
||||
messageCount: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| GET | `/conversations` | List conversations | Bearer |
|
||||
| POST | `/conversations` | Create conversation | Bearer |
|
||||
| GET | `/conversations/:id` | Get conversation | Bearer |
|
||||
| PATCH | `/conversations/:id` | Update conversation | Bearer |
|
||||
| DELETE | `/conversations/:id` | Delete conversation | Bearer |
|
||||
|
||||
### Messages Module
|
||||
|
||||
```typescript
|
||||
class CreateMessageDto {
|
||||
content: string;
|
||||
}
|
||||
|
||||
class MessageResponseDto {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
tokensUsed?: number;
|
||||
model?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| GET | `/conversations/:id/messages` | Get messages (paginated) | Bearer |
|
||||
| POST | `/conversations/:id/messages` | Send message (non-streaming) | Bearer |
|
||||
| DELETE | `/messages/:id` | Delete message | Bearer |
|
||||
|
||||
### Import Module
|
||||
|
||||
```typescript
|
||||
class FileImportDto {
|
||||
// Multipart form data
|
||||
file: File;
|
||||
}
|
||||
|
||||
class UrlImportDto {
|
||||
url: string;
|
||||
}
|
||||
|
||||
class ImportResponseDto {
|
||||
id: string;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
sourceName: string;
|
||||
fileSize?: number;
|
||||
content?: string;
|
||||
errorMessage?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| POST | `/import/file` | Import file (txt, pdf, md) | Bearer |
|
||||
| POST | `/import/url` | Import from URL | Bearer |
|
||||
| GET | `/import/:id` | Get import status | Bearer |
|
||||
| GET | `/import` | List imports | Bearer |
|
||||
| DELETE | `/import/:id` | Delete import | Bearer |
|
||||
|
||||
**File Upload Request:**
|
||||
|
||||
```http
|
||||
POST /api/import/file
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
------Boundary
|
||||
Content-Disposition: form-data; name="file"; filename="story.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
(file content here)
|
||||
------Boundary--
|
||||
|
||||
Response: 202 Accepted
|
||||
{
|
||||
"id": "import-uuid",
|
||||
"status": "processing",
|
||||
"sourceName": "story.txt",
|
||||
"createdAt": "2026-02-23T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**URL Import Request:**
|
||||
|
||||
```http
|
||||
POST /api/import/url
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"url": "https://archiveofourown.org/works/12345678"
|
||||
}
|
||||
|
||||
Response: 202 Accepted
|
||||
{
|
||||
"id": "import-uuid",
|
||||
"status": "processing",
|
||||
"sourceName": "https://archiveofourown.org/works/12345678"
|
||||
}
|
||||
|
||||
// If no scraper available:
|
||||
Response: 400 Bad Request
|
||||
{
|
||||
"error": "UNSUPPORTED_URL",
|
||||
"message": "No scraper available for this URL"
|
||||
}
|
||||
```
|
||||
|
||||
### Story Module (Phase 2)
|
||||
|
||||
```typescript
|
||||
class CreateStoryBranchDto {
|
||||
parentId?: string; // null for root
|
||||
userDirection: string;
|
||||
}
|
||||
|
||||
class StoryBranchResponseDto {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
parentId?: string;
|
||||
title: string;
|
||||
content: string;
|
||||
userDirection: string;
|
||||
depth: number;
|
||||
children: StoryBranchResponseDto[];
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| GET | `/conversations/:id/story` | Get story tree | Bearer |
|
||||
| POST | `/conversations/:id/story/branches` | Create new branch | Bearer |
|
||||
| GET | `/story-branches/:id` | Get branch details | Bearer |
|
||||
| PATCH | `/story-branches/:id` | Update branch | Bearer |
|
||||
| DELETE | `/story-branches/:id` | Delete branch | Bearer |
|
||||
|
||||
**Get Story Tree:**
|
||||
|
||||
```http
|
||||
GET /api/conversations/conv-id/story
|
||||
Authorization: Bearer {token}
|
||||
|
||||
Response:
|
||||
{
|
||||
"root": {
|
||||
"id": "root-uuid",
|
||||
"title": "The Beginning",
|
||||
"content": "Once upon a time...",
|
||||
"depth": 0,
|
||||
"children": [
|
||||
{
|
||||
"id": "branch-1",
|
||||
"title": "The Left Path",
|
||||
"content": "You chose the left path...",
|
||||
"userDirection": "Go left into the dark forest",
|
||||
"depth": 1,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "branch-2",
|
||||
"title": "The Right Path",
|
||||
"content": "You chose the right path...",
|
||||
"userDirection": "Go right towards the castle",
|
||||
"depth": 1,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Character Module (Phase 3)
|
||||
|
||||
```typescript
|
||||
class AddParticipantDto {
|
||||
characterId: string;
|
||||
autoRespond: boolean;
|
||||
}
|
||||
|
||||
class ParticipantResponseDto {
|
||||
id: string;
|
||||
character: CharacterSummaryDto;
|
||||
isActive: boolean;
|
||||
autoRespond: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| GET | `/conversations/:id/participants` | List participants | Bearer |
|
||||
| POST | `/conversations/:id/participants` | Add participant | Bearer |
|
||||
| DELETE | `/conversations/:id/participants/:charId` | Remove participant | Bearer |
|
||||
|
||||
## WebSocket Specification
|
||||
|
||||
### Connection
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('ws://localhost:3000/chat?token=JWT_TOKEN');
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('Connected');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
handleMessage(message);
|
||||
};
|
||||
```
|
||||
|
||||
### Message Protocol
|
||||
|
||||
All WebSocket messages use JSON format:
|
||||
|
||||
```typescript
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
payload: any;
|
||||
timestamp: string;
|
||||
requestId?: string; // For correlating responses
|
||||
}
|
||||
```
|
||||
|
||||
### Client → Server Events
|
||||
|
||||
#### 1. Join Conversation
|
||||
|
||||
```typescript
|
||||
// Join a conversation room
|
||||
{
|
||||
"type": "JOIN_CONVERSATION",
|
||||
"payload": {
|
||||
"conversationId": "conv-uuid"
|
||||
},
|
||||
"requestId": "req-123"
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
"type": "CONVERSATION_JOINED",
|
||||
"payload": {
|
||||
"conversationId": "conv-uuid",
|
||||
"history": [ /* last N messages */ ]
|
||||
},
|
||||
"requestId": "req-123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Send Message (Streaming)
|
||||
|
||||
```typescript
|
||||
{
|
||||
"type": "SEND_MESSAGE",
|
||||
"payload": {
|
||||
"conversationId": "conv-uuid",
|
||||
"content": "Hello, how are you?",
|
||||
"streaming": true // Enable streaming response
|
||||
},
|
||||
"requestId": "msg-456"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Stop Generation
|
||||
|
||||
```typescript
|
||||
{
|
||||
"type": "STOP_GENERATION",
|
||||
"payload": {
|
||||
"conversationId": "conv-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Leave Conversation
|
||||
|
||||
```typescript
|
||||
{
|
||||
"type": "LEAVE_CONVERSATION",
|
||||
"payload": {
|
||||
"conversationId": "conv-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Server → Client Events
|
||||
|
||||
#### 1. Message Acknowledged
|
||||
|
||||
```typescript
|
||||
{
|
||||
"type": "MESSAGE_ACK",
|
||||
"payload": {
|
||||
"messageId": "msg-uuid",
|
||||
"status": "received"
|
||||
},
|
||||
"requestId": "msg-456"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Stream Chunk
|
||||
|
||||
```typescript
|
||||
{
|
||||
"type": "STREAM_CHUNK",
|
||||
"payload": {
|
||||
"conversationId": "conv-uuid",
|
||||
"chunk": " part of response",
|
||||
"isComplete": false
|
||||
},
|
||||
"requestId": "msg-456"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Stream Complete
|
||||
|
||||
```typescript
|
||||
{
|
||||
"type": "STREAM_COMPLETE",
|
||||
"payload": {
|
||||
"conversationId": "conv-uuid",
|
||||
"messageId": "assistant-msg-uuid",
|
||||
"content": "Full response text",
|
||||
"tokensUsed": 150,
|
||||
"model": "openai/gpt-4o"
|
||||
},
|
||||
"requestId": "msg-456"
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Error
|
||||
|
||||
```typescript
|
||||
{
|
||||
"type": "ERROR",
|
||||
"payload": {
|
||||
"code": "LLM_ERROR",
|
||||
"message": "Failed to generate response",
|
||||
"details": { ... }
|
||||
},
|
||||
"requestId": "msg-456"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | Description | HTTP Status |
|
||||
|------|-------------|-------------|
|
||||
| `UNAUTHORIZED` | Invalid or expired token | 401 |
|
||||
| `FORBIDDEN` | Insufficient permissions | 403 |
|
||||
| `NOT_FOUND` | Resource not found | 404 |
|
||||
| `VALIDATION_ERROR` | Invalid input data | 400 |
|
||||
| `LLM_ERROR` | LLM provider error | 502 |
|
||||
| `RATE_LIMITED` | Too many requests | 429 |
|
||||
| `FILE_TOO_LARGE` | File exceeds 50MB | 413 |
|
||||
| `UNSUPPORTED_URL` | No scraper for URL | 400 |
|
||||
|
||||
## OpenAPI Configuration
|
||||
|
||||
### NestJS Setup
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('DreamChat API')
|
||||
.setDescription('Character simulation and chat API')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.addTag('auth', 'Authentication endpoints')
|
||||
.addTag('users', 'User management')
|
||||
.addTag('characters', 'Character CRUD')
|
||||
.addTag('conversations', 'Chat sessions')
|
||||
.addTag('messages', 'Chat messages')
|
||||
.addTag('import', 'Data import')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
|
||||
// Also serve raw JSON for client generation
|
||||
writeFileSync('./openapi-spec.json', JSON.stringify(document, null, 2));
|
||||
```
|
||||
|
||||
### Frontend Client Generation
|
||||
|
||||
```bash
|
||||
# Generate TypeScript client
|
||||
npx openapi-generator-cli generate \
|
||||
-i ./openapi-spec.json \
|
||||
-g typescript-fetch \
|
||||
-o ./apps/frontend/src/api/generated \
|
||||
--additional-properties=supportsES6=true,npmName=dreamchat-api
|
||||
|
||||
# Or use Orval for React Query hooks
|
||||
npx orval --config ./orval.config.js
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
| Endpoint | Limit |
|
||||
|----------|-------|
|
||||
| Auth endpoints | 10 req/min |
|
||||
| General API | 100 req/min |
|
||||
| File upload | 5 req/min |
|
||||
| WebSocket messages | 60 msg/min |
|
||||
| LLM streaming | 20 requests/min |
|
||||
|
||||
```typescript
|
||||
// Rate limit headers
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1645603200
|
||||
```
|
||||
Reference in New Issue
Block a user