UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

273 lines (231 loc) 7.37 kB
import { Embeddings } from '../adapters/embeddings'; import { Entity, Property, Id, Index, Labels, Auth, ManyToMany, OneToMany } from '../core/decorators'; import { Message } from './Message'; import { PostgresMetadata } from '../adapters/metadata-layer'; import { Space } from './Space'; import { SpaceType } from '../test-types/space.types'; import { User } from './User'; @Entity() @Labels(['Space', 'Conversation']) @Auth({ permissions: ['user'] }) @PostgresMetadata({ fields: ['id', 'name', 'messageCount', 'lastMessageAt', 'model', 'conversationType', 'status', 'isArchived', 'createdAt'], tableName: 'conversation_metadata' }) @Embeddings({ fields: ['name', 'systemPrompt'], model: 'nomic-embed-text:latest', provider: 'ollama', dimensions: 768, // nomic-embed-text dimensions vectorStore: 'pgvector', embeddingField: 'semanticEmbedding', metadataFields: ['id', 'model', 'conversationType', 'createdAt'] }) export class Conversation extends Space { // Explicit ID decorator to ensure inheritance works @Id() id!: string; // AI Model Configuration @Property({ required: true }) @Index() model!: string; // e.g., "gemma3:12b-it-qat", "qwen3:1.7b" @Property({ required: true }) temperature!: number; // 0.0 - 2.0 @Property() maxTokens?: number; @Property() systemPrompt?: string; // Custom system prompt for this conversation // Conversation Metadata @Property({ required: true }) @Index() conversationType!: 'chat' | 'task' | 'brainstorm' | 'debug' | 'research'; @Property() messageCount!: number; // Denormalized for quick access @Property() lastMessageAt?: Date; // Last activity timestamp @Property() isArchived!: boolean; // Mark conversations as archived @Property() tags?: string[]; // User-defined tags for organization // Conversation Analytics @Property() analytics?: { totalTokensUsed?: number; averageResponseTime?: number; hasThinkingContent?: boolean; modelSwitches?: number; temperatureChanges?: number; }; // Conversation Settings @Property() settings?: { verboseMode?: boolean; autoSave?: boolean; exportFormat?: 'json' | 'markdown' | 'txt'; }; // Conversation Status (extends Space status) @Property() status!: 'active' | 'pending' | 'completed' | 'blocked' | 'failed'; // Participants in the conversation (required by Space) @ManyToMany({ target: () => User, inverse: 'conversations' }) participants!: User[]; // AI Models that can participate (array of model names) @Property() aiModels?: string[]; // e.g., ["gemma3:12b-it-qat", "qwen3:1.7b"] // Active speaker/model (who's currently responding) @Property() activeSpeaker?: { type: 'human' | 'ai'; id: string; // userId or model name }; // Messages in this conversation @OneToMany({ target: () => Message, inverse: 'conversation' }) messages?: Message[]; // Semantic embedding for similarity search @Property() semanticEmbedding?: number[]; // Constructor with defaults constructor() { super(); this.messageCount = 0; this.isArchived = false; this.status = 'active'; this.conversationType = 'chat'; this.temperature = 0.7; // Set space type for this space this.spaceType = SpaceType.CONVERSATION; } // Helper methods for conversation management incrementMessageCount(): void { this.messageCount++; this.lastMessageAt = new Date(); } updateAnalytics(tokenCount?: number, responseTime?: number, hasThinking?: boolean): void { if (!this.analytics) { this.analytics = {}; } if (tokenCount) { this.analytics.totalTokensUsed = (this.analytics.totalTokensUsed || 0) + tokenCount; } if (responseTime) { const currentAvg = this.analytics.averageResponseTime || 0; const count = this.messageCount; this.analytics.averageResponseTime = (currentAvg * (count - 1) + responseTime) / count; } if (hasThinking !== undefined) { this.analytics.hasThinkingContent = this.analytics.hasThinkingContent || hasThinking; } } addTag(tag: string): void { if (!this.tags) { this.tags = []; } if (!this.tags.includes(tag)) { this.tags.push(tag); } } removeTag(tag: string): void { if (this.tags) { this.tags = this.tags.filter(t => t !== tag); } } archive(): void { this.isArchived = true; this.status = 'completed'; } pause(): void { this.status = 'blocked'; } resume(): void { this.status = 'active'; } complete(): void { this.status = 'completed'; } // Multi-participant conversation management addParticipant(user: User): void { if (!this.participants) { this.participants = []; } if (!this.participants.find(p => p.id === user.id)) { this.participants.push(user); } } addAiModel(modelName: string): void { if (!this.aiModels) { this.aiModels = []; } if (!this.aiModels.includes(modelName)) { this.aiModels.push(modelName); } } removeAiModel(modelName: string): void { if (this.aiModels) { this.aiModels = this.aiModels.filter(m => m !== modelName); } } setActiveSpeaker(type: 'human' | 'ai', id: string): void { this.activeSpeaker = { type, id }; } getAllParticipants(): Array<{type: 'human' | 'ai', id: string, displayName?: string}> { const participants: Array<{type: 'human' | 'ai', id: string, displayName?: string}> = []; // Add human participants if (this.participants) { this.participants.forEach(user => { participants.push({ type: 'human', id: user.id, displayName: user.name }); }); } // Add AI models if (this.aiModels) { this.aiModels.forEach(model => { participants.push({ type: 'ai', id: model, displayName: model }); }); } return participants; } // Generate a smart title based on conversation content generateSmartTitle(firstUserMessage?: string): string { if (firstUserMessage) { // Extract key concepts from first message const cleanMessage = firstUserMessage.trim().toLowerCase(); if (cleanMessage.includes('help') || cleanMessage.includes('how')) { return `Help: ${firstUserMessage.substring(0, 30)}...`; } else if (cleanMessage.includes('debug') || cleanMessage.includes('error')) { return `Debug: ${firstUserMessage.substring(0, 30)}...`; } else if (cleanMessage.includes('create') || cleanMessage.includes('build')) { return `Build: ${firstUserMessage.substring(0, 30)}...`; } else { return `Chat: ${firstUserMessage.substring(0, 30)}...`; } } return `${this.conversationType.charAt(0).toUpperCase()}${this.conversationType.slice(1)} - ${new Date().toLocaleDateString()}`; } // Get conversation summary for listings getSummary(): { id: string; title: string; messageCount: number; model: string; lastActivity: Date | undefined; status: string; tags: string[]; } { return { id: this.id, title: this.name, messageCount: this.messageCount, model: this.model, lastActivity: this.lastMessageAt, status: this.status, tags: this.tags || [] }; } }