573 lines
12 KiB
Markdown
573 lines
12 KiB
Markdown
# 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
|
|
```
|