- Add bot management system with creation, suspension, and reinstatement functionality - Implement autonomous bot posting with scheduling, rate limiting, and content generation - Add content fetching system supporting RSS feeds and multiple content sources - Implement LLM-based content generation with customizable bot personalities - Add mention handling and automated response system for bot interactions - Implement API key management with encryption using AUTH_SECRET for simplified deployment - Add comprehensive bot logging system for activity tracking and error monitoring - Create bot administration pages and settings UI for managing bot configurations - Add database migrations for bot system schema including users, sources, and content items - Implement cron job system for automated bot operations and scheduled tasks - Add extensive test coverage with unit and property-based tests for core bot modules - Simplify encryption by deriving keys from AUTH_SECRET instead of separate environment variable - Implement automatic content fetching on post trigger with retry logic - Add Reddit-specific link preview handling using oEmbed API for reliable metadata extraction - Create utility scripts for bot inspection and cleanup operations - Add comprehensive bot system documentation and improvement tracking
25 KiB
Design Document: Bot System
Overview
This document describes the technical design for implementing an autonomous bot system in Synapsis. The system enables users to create bots that monitor external content sources (RSS, Reddit, news APIs), generate posts using LLM APIs, and respond to mentions. Bots operate as special user entities with clear identification and configurable personalities.
The design integrates with existing Synapsis infrastructure:
- Drizzle ORM for database operations
- Next.js API routes for endpoints
- ActivityPub federation for cross-instance communication
- Existing authentication and user systems
Architecture
graph TB
subgraph "User Interface"
UI[Bot Management UI]
end
subgraph "API Layer"
BA[Bot API Routes]
CA[Content Source API]
LA[Activity Log API]
end
subgraph "Core Services"
BM[Bot Manager]
CS[Content Source Monitor]
PS[Post Scheduler]
MH[Mention Handler]
CG[Content Generator]
RL[Rate Limiter]
AL[Activity Logger]
end
subgraph "External Services"
LLM[LLM Providers]
RSS[RSS Feeds]
RD[Reddit API]
NEWS[News APIs]
end
subgraph "Data Layer"
DB[(PostgreSQL)]
CACHE[Redis Cache]
end
subgraph "Federation"
AP[ActivityPub]
end
UI --> BA
BA --> BM
BA --> CS
BA --> LA
BM --> DB
BM --> RL
BM --> AL
BM --> CG
CS --> RSS
CS --> RD
CS --> NEWS
CS --> DB
CG --> LLM
PS --> BM
MH --> BM
MH --> CG
BM --> AP
Component Responsibilities
- Bot Manager: Core orchestrator for bot lifecycle, configuration, and operations
- Content Source Monitor: Fetches and parses external content from RSS, Reddit, and news APIs
- Post Scheduler: Manages scheduled and autonomous posting based on configuration
- Mention Handler: Detects mentions and triggers response generation
- Content Generator: Interfaces with LLM providers to generate posts and replies
- Rate Limiter: Enforces posting limits and prevents abuse
- Activity Logger: Records all bot actions for auditing and debugging
Components and Interfaces
Bot Manager Service
interface BotManager {
// Bot CRUD
createBot(userId: string, config: BotCreateInput): Promise<Bot>;
updateBot(botId: string, config: BotUpdateInput): Promise<Bot>;
deleteBot(botId: string): Promise<void>;
getBotsByUser(userId: string): Promise<Bot[]>;
getBotById(botId: string): Promise<Bot | null>;
// Bot Operations
triggerPost(botId: string, sourceContentId?: string): Promise<Post>;
processScheduledPosts(): Promise<void>;
suspendBot(botId: string, reason: string): Promise<void>;
reinstateBot(botId: string): Promise<void>;
}
interface BotCreateInput {
name: string;
handle: string;
bio?: string;
avatarUrl?: string;
personality: PersonalityConfig;
llmConfig: LLMConfig;
contentSources?: ContentSourceConfig[];
schedule?: ScheduleConfig;
autonomousMode?: boolean;
}
interface PersonalityConfig {
systemPrompt: string;
temperature: number;
maxTokens: number;
responseStyle?: string;
}
interface LLMConfig {
provider: 'openrouter' | 'openai' | 'anthropic';
apiKey: string; // Encrypted before storage
model: string;
}
Content Source Monitor
interface ContentSourceMonitor {
addSource(botId: string, config: ContentSourceConfig): Promise<ContentSource>;
removeSource(sourceId: string): Promise<void>;
fetchContent(sourceId: string): Promise<ContentItem[]>;
processAllSources(): Promise<void>;
}
interface ContentSourceConfig {
type: 'rss' | 'reddit' | 'news_api';
url: string;
subreddit?: string; // For Reddit
apiKey?: string; // For news APIs
fetchInterval: number; // Minutes
keywords?: string[]; // Optional filtering
}
interface ContentItem {
id: string;
sourceId: string;
title: string;
content: string;
url: string;
publishedAt: Date;
processed: boolean;
}
Content Generator
interface ContentGenerator {
generatePost(
bot: Bot,
sourceContent?: ContentItem,
context?: string
): Promise<GeneratedContent>;
generateReply(
bot: Bot,
mentionPost: Post,
conversationContext: Post[]
): Promise<GeneratedContent>;
evaluateContentInterest(
bot: Bot,
content: ContentItem
): Promise<{ interesting: boolean; reason: string }>;
}
interface GeneratedContent {
text: string;
tokensUsed: number;
model: string;
}
Mention Handler
interface MentionHandler {
detectMentions(botId: string): Promise<Mention[]>;
processMention(mentionId: string): Promise<void>;
getUnprocessedMentions(botId: string): Promise<Mention[]>;
}
interface Mention {
id: string;
botId: string;
postId: string;
authorId: string;
content: string;
createdAt: Date;
processedAt?: Date;
responsePostId?: string;
}
Rate Limiter
interface RateLimiter {
canPost(botId: string): Promise<{ allowed: boolean; reason?: string }>;
recordPost(botId: string): Promise<void>;
getPostCount(botId: string, windowHours: number): Promise<number>;
getRemainingQuota(botId: string): Promise<{ daily: number; hourly: number }>;
}
// Rate limit constants
const RATE_LIMITS = {
MAX_POSTS_PER_DAY: 50,
MIN_POST_INTERVAL_MINUTES: 5,
MAX_REPLIES_PER_HOUR: 20,
};
Activity Logger
interface ActivityLogger {
log(entry: ActivityLogEntry): Promise<void>;
getLogsForBot(botId: string, options: LogQueryOptions): Promise<ActivityLog[]>;
getErrorLogs(botId: string): Promise<ActivityLog[]>;
}
interface ActivityLogEntry {
botId: string;
action: 'post_created' | 'mention_response' | 'content_fetched' |
'llm_call' | 'error' | 'config_changed' | 'rate_limited';
details: Record<string, unknown>;
success: boolean;
errorMessage?: string;
}
interface LogQueryOptions {
actionTypes?: string[];
startDate?: Date;
endDate?: Date;
limit?: number;
offset?: number;
}
Data Models
Database Schema Additions
// New tables for bot system
export const bots = pgTable('bots', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
handle: text('handle').notNull().unique(),
bio: text('bio'),
avatarUrl: text('avatar_url'),
// Personality configuration (JSON)
personalityConfig: text('personality_config').notNull(), // JSON
// LLM configuration
llmProvider: text('llm_provider').notNull(), // openrouter, openai, anthropic
llmModel: text('llm_model').notNull(),
llmApiKeyEncrypted: text('llm_api_key_encrypted').notNull(),
// Scheduling
scheduleConfig: text('schedule_config'), // JSON
autonomousMode: boolean('autonomous_mode').default(false).notNull(),
// Status
isActive: boolean('is_active').default(true).notNull(),
isSuspended: boolean('is_suspended').default(false).notNull(),
suspensionReason: text('suspension_reason'),
suspendedAt: timestamp('suspended_at'),
// ActivityPub keys
publicKey: text('public_key').notNull(),
privateKeyEncrypted: text('private_key_encrypted').notNull(),
// Timestamps
lastPostAt: timestamp('last_post_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => [
index('bots_user_id_idx').on(table.userId),
index('bots_handle_idx').on(table.handle),
index('bots_active_idx').on(table.isActive),
]);
export const botContentSources = pgTable('bot_content_sources', {
id: uuid('id').primaryKey().defaultRandom(),
botId: uuid('bot_id').notNull().references(() => bots.id, { onDelete: 'cascade' }),
type: text('type').notNull(), // rss, reddit, news_api
url: text('url').notNull(),
subreddit: text('subreddit'), // For Reddit sources
apiKeyEncrypted: text('api_key_encrypted'), // For news APIs
fetchIntervalMinutes: integer('fetch_interval_minutes').default(30).notNull(),
keywords: text('keywords'), // JSON array for filtering
isActive: boolean('is_active').default(true).notNull(),
lastFetchAt: timestamp('last_fetch_at'),
lastError: text('last_error'),
consecutiveErrors: integer('consecutive_errors').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => [
index('bot_content_sources_bot_idx').on(table.botId),
index('bot_content_sources_type_idx').on(table.type),
]);
export const botContentItems = pgTable('bot_content_items', {
id: uuid('id').primaryKey().defaultRandom(),
sourceId: uuid('source_id').notNull().references(() => botContentSources.id, { onDelete: 'cascade' }),
externalId: text('external_id').notNull(), // Unique ID from source
title: text('title').notNull(),
content: text('content'),
url: text('url').notNull(),
publishedAt: timestamp('published_at').notNull(),
fetchedAt: timestamp('fetched_at').defaultNow().notNull(),
isProcessed: boolean('is_processed').default(false).notNull(),
processedAt: timestamp('processed_at'),
postId: uuid('post_id').references(() => posts.id), // If a post was created
interestScore: integer('interest_score'), // LLM evaluation score
interestReason: text('interest_reason'),
}, (table) => [
index('bot_content_items_source_idx').on(table.sourceId),
index('bot_content_items_processed_idx').on(table.isProcessed),
index('bot_content_items_external_idx').on(table.externalId),
]);
export const botMentions = pgTable('bot_mentions', {
id: uuid('id').primaryKey().defaultRandom(),
botId: uuid('bot_id').notNull().references(() => bots.id, { onDelete: 'cascade' }),
postId: uuid('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
authorId: uuid('author_id').notNull().references(() => users.id),
content: text('content').notNull(),
isProcessed: boolean('is_processed').default(false).notNull(),
processedAt: timestamp('processed_at'),
responsePostId: uuid('response_post_id').references(() => posts.id),
// For federated mentions
isRemote: boolean('is_remote').default(false).notNull(),
remoteActorUrl: text('remote_actor_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => [
index('bot_mentions_bot_idx').on(table.botId),
index('bot_mentions_processed_idx').on(table.isProcessed),
index('bot_mentions_created_idx').on(table.createdAt),
]);
export const botActivityLogs = pgTable('bot_activity_logs', {
id: uuid('id').primaryKey().defaultRandom(),
botId: uuid('bot_id').notNull().references(() => bots.id, { onDelete: 'cascade' }),
action: text('action').notNull(), // post_created, mention_response, etc.
details: text('details').notNull(), // JSON
success: boolean('success').notNull(),
errorMessage: text('error_message'),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => [
index('bot_activity_logs_bot_idx').on(table.botId),
index('bot_activity_logs_action_idx').on(table.action),
index('bot_activity_logs_created_idx').on(table.createdAt),
]);
export const botRateLimits = pgTable('bot_rate_limits', {
id: uuid('id').primaryKey().defaultRandom(),
botId: uuid('bot_id').notNull().references(() => bots.id, { onDelete: 'cascade' }),
windowStart: timestamp('window_start').notNull(),
windowType: text('window_type').notNull(), // daily, hourly
postCount: integer('post_count').default(0).notNull(),
replyCount: integer('reply_count').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => [
index('bot_rate_limits_bot_window_idx').on(table.botId, table.windowStart),
]);
TypeScript Types
// Bot types
export interface Bot {
id: string;
userId: string;
name: string;
handle: string;
bio?: string;
avatarUrl?: string;
personalityConfig: PersonalityConfig;
llmProvider: 'openrouter' | 'openai' | 'anthropic';
llmModel: string;
scheduleConfig?: ScheduleConfig;
autonomousMode: boolean;
isActive: boolean;
isSuspended: boolean;
suspensionReason?: string;
lastPostAt?: Date;
createdAt: Date;
updatedAt: Date;
}
export interface PersonalityConfig {
systemPrompt: string;
temperature: number;
maxTokens: number;
responseStyle?: string;
}
export interface ScheduleConfig {
type: 'interval' | 'times' | 'cron';
intervalMinutes?: number;
times?: string[]; // HH:MM format
cronExpression?: string;
timezone?: string;
}
export interface ContentSource {
id: string;
botId: string;
type: 'rss' | 'reddit' | 'news_api';
url: string;
subreddit?: string;
fetchIntervalMinutes: number;
keywords?: string[];
isActive: boolean;
lastFetchAt?: Date;
lastError?: string;
}
API Endpoints
Bot Management
POST /api/bots - Create a new bot
GET /api/bots - List user's bots
GET /api/bots/:id - Get bot details
PUT /api/bots/:id - Update bot configuration
DELETE /api/bots/:id - Delete a bot
POST /api/bots/:id/suspend - Suspend a bot (admin)
POST /api/bots/:id/reinstate - Reinstate a bot (admin)
API Key Management
POST /api/bots/:id/api-key - Set/update LLM API key
DELETE /api/bots/:id/api-key - Remove API key
GET /api/bots/:id/api-key/status - Check if API key is configured
Content Sources
POST /api/bots/:id/sources - Add content source
GET /api/bots/:id/sources - List content sources
PUT /api/bots/:id/sources/:sid - Update content source
DELETE /api/bots/:id/sources/:sid - Remove content source
POST /api/bots/:id/sources/:sid/fetch - Manually trigger fetch
Bot Operations
POST /api/bots/:id/post - Manually trigger a post
GET /api/bots/:id/mentions - Get pending mentions
POST /api/bots/:id/mentions/:mid/respond - Manually respond to mention
Activity Logs
GET /api/bots/:id/logs - Get activity logs
GET /api/bots/:id/logs/errors - Get error logs only
Public Bot Endpoints (for federation)
GET /api/users/:handle - Returns bot actor (with bot flag)
GET /api/users/:handle/outbox - Bot's outbox
POST /api/users/:handle/inbox - Bot's inbox (for mentions)
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: Bot Creation Links to User
For any valid bot configuration and user, when a bot is created, the resulting bot entity SHALL be linked to the creating user's account and have a unique identifier.
Validates: Requirements 1.1, 1.2
Property 2: Bot Listing Completeness
For any user with N bots, querying that user's bots SHALL return exactly N bots, all belonging to that user.
Validates: Requirements 1.3
Property 3: Bot Deletion Cascade
For any bot with associated content sources, content items, mentions, and activity logs, deleting the bot SHALL remove all associated data from the database.
Validates: Requirements 1.4
Property 4: Bot Identification Immutability
For any bot entity, the bot identification flag SHALL always be true and cannot be modified to false.
Validates: Requirements 1.5, 12.1, 12.2, 12.4
Property 5: Bot Limit Enforcement
For any user at the maximum bot limit, attempting to create an additional bot SHALL fail with an appropriate error.
Validates: Requirements 1.6
Property 6: API Key Encryption Round-Trip
For any valid API key, encrypting then decrypting the key SHALL produce the original key value, and the encrypted value SHALL differ from the original.
Validates: Requirements 2.2, 2.3
Property 7: API Key Format Validation
For any string that does not match valid API key formats for supported providers, the validation SHALL reject the key.
Validates: Requirements 2.1
Property 8: LLM Provider Support
For any of the supported LLM providers (OpenRouter, OpenAI, Anthropic), creating a bot with that provider SHALL succeed.
Validates: Requirements 2.6
Property 9: Personality Configuration Persistence
For any valid personality configuration, storing then retrieving the configuration SHALL produce an equivalent configuration object.
Validates: Requirements 3.1, 3.3, 3.4
Property 10: Personality in LLM Prompts
For any bot with a configured personality, all LLM calls (posts and replies) SHALL include the personality system prompt in the request.
Validates: Requirements 3.2, 3.5
Property 11: Content Source URL Validation
For any invalid URL or unsupported source type, adding a content source SHALL fail with a validation error.
Validates: Requirements 4.1
Property 12: RSS Parsing Correctness
For any valid RSS feed XML, parsing SHALL extract all items with their titles, content, URLs, and publication dates.
Validates: Requirements 4.2
Property 13: Content Item Storage
For any fetched content item, the item SHALL be stored with its source reference and marked as unprocessed.
Validates: Requirements 4.5
Property 14: Multiple Source Types Per Bot
For any bot, adding content sources of different types (RSS, Reddit, news API) SHALL all succeed and be retrievable.
Validates: Requirements 4.6
Property 15: Fetch Error Retry with Backoff
For any content source that fails to fetch, consecutive failures SHALL increase the retry delay exponentially up to a maximum.
Validates: Requirements 4.7
Property 16: Schedule Configuration Persistence
For any valid schedule configuration (interval, times, or cron), storing then retrieving SHALL produce an equivalent configuration.
Validates: Requirements 5.1, 5.3
Property 17: Scheduled Post Triggering
For any bot with a due schedule and available content, the scheduler SHALL trigger post generation.
Validates: Requirements 5.2, 5.4
Property 18: Skip When No Content
For any bot with a due schedule but no unprocessed content, the scheduler SHALL skip the posting cycle without creating a post.
Validates: Requirements 5.5
Property 19: Rate Limit Enforcement
For any bot, posting more than 50 times per day OR posting within 5 minutes of the last post SHALL be rejected.
Validates: Requirements 5.6, 10.1, 10.2, 10.4
Property 20: Autonomous Mode Content Evaluation
For any bot in autonomous mode with new content, the system SHALL evaluate content interest before deciding to post.
Validates: Requirements 6.1, 6.2, 6.3
Property 21: Autonomous Mode Toggle
For any bot with autonomous mode disabled, the bot SHALL only create posts on schedule, not autonomously.
Validates: Requirements 6.5
Property 22: Mention Detection
For any post that mentions a bot's handle, the mention SHALL be detected and stored for processing.
Validates: Requirements 7.1, 7.2
Property 23: Mention Response Context
For any mention being processed, the LLM prompt SHALL include the original post content and conversation context.
Validates: Requirements 7.3, 7.4
Property 24: Mention Chronological Processing
For any bot with multiple unprocessed mentions, processing SHALL occur in chronological order (oldest first).
Validates: Requirements 7.5
Property 25: Reply Rate Limiting
For any bot, replying more than 20 times per hour SHALL be rejected.
Validates: Requirements 7.6
Property 26: Activity Log Completeness
For any bot action (post, reply, error, config change), an activity log entry SHALL be created with action type, timestamp, and result.
Validates: Requirements 8.1, 8.3, 8.4
Property 27: Activity Log Ordering
For any bot's activity logs, querying logs SHALL return them in reverse chronological order.
Validates: Requirements 8.2
Property 28: Activity Log Filtering
For any log query with action type or date range filters, only matching logs SHALL be returned.
Validates: Requirements 8.6
Property 29: Federation Post Distribution
For any post created by a bot, an ActivityPub Create activity SHALL be generated and queued for delivery to followers.
Validates: Requirements 9.1
Property 30: Federated Mention Handling
For any mention received via ActivityPub from a remote instance, the mention SHALL be detected and processed like local mentions.
Validates: Requirements 9.2
Property 31: ActivityPub Bot Flag
For any bot's ActivityPub actor representation, the actor object SHALL include "type": "Service" or a bot flag indicating automated status.
Validates: Requirements 9.3
Property 32: Bot Follow Handling
For any follow request to or from a bot, the federation protocol SHALL handle the relationship correctly (Accept/Reject activities).
Validates: Requirements 9.5
Property 33: Input Sanitization
For any user input containing potential injection attacks (SQL, XSS, command injection), the input SHALL be sanitized or rejected.
Validates: Requirements 10.5
Property 34: Bot Suspension Enforcement
For any suspended bot, all actions (posting, replying, fetching) SHALL be blocked until reinstated.
Validates: Requirements 10.6
Property 35: LLM Prompt Construction
For any post generation request, the LLM prompt SHALL combine source content with personality context and configured parameters.
Validates: Requirements 11.1, 11.2
Property 36: Content Truncation
For any source content exceeding the maximum length, the content SHALL be truncated or summarized before being sent to the LLM.
Validates: Requirements 11.3
Property 37: LLM Retry Logic
For any LLM call that fails, the system SHALL retry up to 3 times before logging an error.
Validates: Requirements 11.4
Property 38: Post Content Validation
For any generated post, the content SHALL be validated against platform requirements (length, format) before publishing.
Validates: Requirements 11.5
Property 39: Bot Creator Attribution
For any bot, the bot's data SHALL include a reference to the creating user that is retrievable.
Validates: Requirements 12.3
Error Handling
API Errors
| Error Code | Condition | Response |
|---|---|---|
| 400 | Invalid input (validation failure) | { error: "Invalid input", details: [...] } |
| 401 | Not authenticated | { error: "Authentication required" } |
| 403 | Not authorized (not bot owner) | { error: "Not authorized" } |
| 403 | Bot suspended | { error: "Bot is suspended", reason: "..." } |
| 404 | Bot/resource not found | { error: "Not found" } |
| 429 | Rate limit exceeded | { error: "Rate limit exceeded", retryAfter: seconds } |
| 500 | Internal error | { error: "Internal server error" } |
Content Source Errors
- Network failures: Log error, increment consecutive error count, schedule retry with exponential backoff
- Parse failures: Log error with content sample, mark source as errored
- API rate limits: Respect Retry-After header, adjust fetch interval
LLM Provider Errors
- Authentication failures: Log error, notify user to check API key
- Rate limits: Implement exponential backoff, queue requests
- Content policy violations: Log violation, skip content item
- Timeout: Retry up to 3 times with increasing timeout
Federation Errors
- Delivery failures: Queue for retry with exponential backoff
- Invalid signatures: Log and reject activity
- Unknown actors: Attempt to fetch actor, reject if unavailable
Testing Strategy
Unit Tests
Unit tests focus on specific examples and edge cases:
- Bot CRUD operations with valid/invalid inputs
- API key encryption/decryption edge cases
- RSS parsing with malformed feeds
- Schedule parsing for various formats
- Rate limit boundary conditions
- Input sanitization for injection attempts
Property-Based Tests
Property-based tests validate universal properties using a library like fast-check:
- Minimum 100 iterations per property test
- Each test references its design document property
- Tag format:
Feature: bot-system, Property N: [property title]
Example test structure:
// Feature: bot-system, Property 6: API Key Encryption Round-Trip
test.prop([fc.string().filter(isValidApiKey)], (apiKey) => {
const encrypted = encryptApiKey(apiKey);
const decrypted = decryptApiKey(encrypted);
expect(decrypted).toBe(apiKey);
expect(encrypted).not.toBe(apiKey);
});
Integration Tests
- Bot creation flow with database
- Content source fetching with mocked external APIs
- Mention detection and response flow
- Federation activity generation and delivery
- Rate limiting across multiple requests
Test Coverage Requirements
- All 39 correctness properties must have corresponding property tests
- Critical paths (bot creation, posting, mentions) require integration tests
- Error handling paths require unit tests with specific error conditions