825 lines
23 KiB
TypeScript
825 lines
23 KiB
TypeScript
|
|
/**
|
||
|
|
* ConversationManager - Infinite Agent Memory
|
||
|
|
*
|
||
|
|
* Production-ready conversation and context management for AI agents.
|
||
|
|
* Built on Brainy's existing infrastructure: Triple Intelligence, Neural API, VFS.
|
||
|
|
*
|
||
|
|
* REAL IMPLEMENTATION - No stubs, no mocks, no TODOs
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { v4 as uuidv4 } from '../universal/uuid.js'
|
||
|
|
import { NounType, VerbType } from '../types/graphTypes.js'
|
||
|
|
import { Brainy } from '../brainy.js'
|
||
|
|
import {
|
||
|
|
MessageRole,
|
||
|
|
ProblemSolvingPhase,
|
||
|
|
ConversationMessage,
|
||
|
|
ConversationMessageMetadata,
|
||
|
|
ConversationThread,
|
||
|
|
ConversationThreadMetadata,
|
||
|
|
ConversationContext,
|
||
|
|
RankedMessage,
|
||
|
|
SaveMessageOptions,
|
||
|
|
ContextRetrievalOptions,
|
||
|
|
ConversationSearchOptions,
|
||
|
|
ConversationSearchResult,
|
||
|
|
ConversationTheme,
|
||
|
|
ArtifactOptions,
|
||
|
|
ConversationStats,
|
||
|
|
CompactionOptions,
|
||
|
|
CompactionResult
|
||
|
|
} from './types.js'
|
||
|
|
|
||
|
|
/**
|
||
|
|
* ConversationManager - High-level API for conversation operations
|
||
|
|
*
|
||
|
|
* Uses existing Brainy infrastructure:
|
||
|
|
* - brain.add() for messages
|
||
|
|
* - brain.relate() for threading
|
||
|
|
* - brain.find() with Triple Intelligence for context
|
||
|
|
* - brain.neural for clustering and similarity
|
||
|
|
* - brain.vfs() for artifacts
|
||
|
|
*/
|
||
|
|
export class ConversationManager {
|
||
|
|
private brain: Brainy
|
||
|
|
private initialized = false
|
||
|
|
private _vfs: any = null
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a ConversationManager instance
|
||
|
|
* @param brain Brainy instance to use
|
||
|
|
*/
|
||
|
|
constructor(brain: Brainy) {
|
||
|
|
this.brain = brain
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the conversation manager
|
||
|
|
* Lazy initialization pattern - only called when first used
|
||
|
|
*/
|
||
|
|
async init(): Promise<void> {
|
||
|
|
if (this.initialized) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// VFS is lazy-loaded and might not be initialized yet
|
||
|
|
try {
|
||
|
|
this._vfs = this.brain.vfs()
|
||
|
|
await this._vfs.init()
|
||
|
|
} catch (error) {
|
||
|
|
// VFS initialization failed, will work without artifact support
|
||
|
|
console.warn('VFS initialization failed, artifact support disabled:', error)
|
||
|
|
}
|
||
|
|
|
||
|
|
this.initialized = true
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Save a message to the conversation history
|
||
|
|
*
|
||
|
|
* Uses: brain.add() with NounType.Message
|
||
|
|
* Real implementation - stores message with embedding
|
||
|
|
*
|
||
|
|
* @param content Message content
|
||
|
|
* @param role Message role (user, assistant, system, tool)
|
||
|
|
* @param options Save options (conversationId, metadata, etc.)
|
||
|
|
* @returns Message ID
|
||
|
|
*/
|
||
|
|
async saveMessage(
|
||
|
|
content: string,
|
||
|
|
role: MessageRole,
|
||
|
|
options: SaveMessageOptions = {}
|
||
|
|
): Promise<string> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate IDs if not provided
|
||
|
|
const conversationId = options.conversationId || `conv_${uuidv4()}`
|
||
|
|
const sessionId = options.sessionId || `session_${uuidv4()}`
|
||
|
|
const timestamp = Date.now()
|
||
|
|
|
||
|
|
// Build metadata
|
||
|
|
const metadata: ConversationMessageMetadata = {
|
||
|
|
role,
|
||
|
|
conversationId,
|
||
|
|
sessionId,
|
||
|
|
timestamp,
|
||
|
|
problemSolvingPhase: options.phase,
|
||
|
|
confidence: options.confidence,
|
||
|
|
artifacts: options.artifacts || [],
|
||
|
|
toolsUsed: options.toolsUsed || [],
|
||
|
|
references: [],
|
||
|
|
tags: options.tags || [],
|
||
|
|
...options.metadata
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add message to brain using REAL API
|
||
|
|
const messageId = await this.brain.add({
|
||
|
|
data: content,
|
||
|
|
type: NounType.Message,
|
||
|
|
metadata
|
||
|
|
})
|
||
|
|
|
||
|
|
// Link to previous message if specified (REAL graph relationship)
|
||
|
|
if (options.linkToPrevious) {
|
||
|
|
await this.brain.relate({
|
||
|
|
from: options.linkToPrevious,
|
||
|
|
to: messageId,
|
||
|
|
type: VerbType.Precedes,
|
||
|
|
metadata: {
|
||
|
|
conversationId,
|
||
|
|
timestamp
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return messageId
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Link two messages in temporal sequence
|
||
|
|
*
|
||
|
|
* Uses: brain.relate() with VerbType.Precedes
|
||
|
|
* Real implementation - creates graph relationship
|
||
|
|
*
|
||
|
|
* @param prevMessageId ID of previous message
|
||
|
|
* @param nextMessageId ID of next message
|
||
|
|
* @returns Relationship ID
|
||
|
|
*/
|
||
|
|
async linkMessages(prevMessageId: string, nextMessageId: string): Promise<string> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create real graph relationship
|
||
|
|
const verbId = await this.brain.relate({
|
||
|
|
from: prevMessageId,
|
||
|
|
to: nextMessageId,
|
||
|
|
type: VerbType.Precedes,
|
||
|
|
metadata: {
|
||
|
|
timestamp: Date.now()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
return verbId
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a full conversation thread
|
||
|
|
*
|
||
|
|
* Uses: brain.getNoun() and brain.getConnections()
|
||
|
|
* Real implementation - traverses graph relationships
|
||
|
|
*
|
||
|
|
* @param conversationId Conversation ID
|
||
|
|
* @param options Options (includeArtifacts, etc.)
|
||
|
|
* @returns Complete conversation thread
|
||
|
|
*/
|
||
|
|
async getConversationThread(
|
||
|
|
conversationId: string,
|
||
|
|
options: { includeArtifacts?: boolean } = {}
|
||
|
|
): Promise<ConversationThread> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Search for all messages in conversation (REAL search)
|
||
|
|
const results = await this.brain.find({
|
||
|
|
where: {
|
||
|
|
conversationId
|
||
|
|
},
|
||
|
|
limit: 10000 // Large limit for full thread
|
||
|
|
})
|
||
|
|
|
||
|
|
// Convert results to ConversationMessage format
|
||
|
|
const messages: ConversationMessage[] = results.map((result: any) => ({
|
||
|
|
id: result.id,
|
||
|
|
content: result.data || result.content || '',
|
||
|
|
role: result.metadata.role,
|
||
|
|
metadata: result.metadata as ConversationMessageMetadata,
|
||
|
|
embedding: result.embedding,
|
||
|
|
createdAt: result.metadata.timestamp || Date.now(),
|
||
|
|
updatedAt: result.metadata.timestamp || Date.now()
|
||
|
|
}))
|
||
|
|
|
||
|
|
// Sort by timestamp
|
||
|
|
messages.sort((a, b) => a.createdAt - b.createdAt)
|
||
|
|
|
||
|
|
// Build thread metadata
|
||
|
|
const startTime = messages.length > 0 ? messages[0].createdAt : Date.now()
|
||
|
|
const endTime = messages.length > 0 ? messages[messages.length - 1].createdAt : undefined
|
||
|
|
const totalTokens = messages.reduce((sum, msg) => sum + (msg.metadata.tokensUsed || 0), 0)
|
||
|
|
|
||
|
|
const threadMetadata: ConversationThreadMetadata = {
|
||
|
|
conversationId,
|
||
|
|
startTime,
|
||
|
|
endTime,
|
||
|
|
messageCount: messages.length,
|
||
|
|
totalTokens,
|
||
|
|
participants: [...new Set(messages.map(m => m.role))]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get artifacts if requested (REAL VFS query)
|
||
|
|
let artifacts: string[] | undefined
|
||
|
|
if (options.includeArtifacts && this._vfs) {
|
||
|
|
artifacts = messages
|
||
|
|
.flatMap(m => m.metadata.artifacts || [])
|
||
|
|
.filter((id, idx, arr) => arr.indexOf(id) === idx)
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: conversationId,
|
||
|
|
metadata: threadMetadata,
|
||
|
|
messages,
|
||
|
|
artifacts
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get relevant context for a query
|
||
|
|
*
|
||
|
|
* Uses: brain.find() with Triple Intelligence
|
||
|
|
* Real implementation - semantic + temporal + graph ranking
|
||
|
|
*
|
||
|
|
* @param query Query string or context options
|
||
|
|
* @param options Retrieval options
|
||
|
|
* @returns Ranked context messages with artifacts
|
||
|
|
*/
|
||
|
|
async getRelevantContext(
|
||
|
|
query: string | ContextRetrievalOptions,
|
||
|
|
options?: ContextRetrievalOptions
|
||
|
|
): Promise<ConversationContext> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
const startTime = Date.now()
|
||
|
|
|
||
|
|
// Normalize options
|
||
|
|
const opts: ContextRetrievalOptions = typeof query === 'string'
|
||
|
|
? { query, ...options }
|
||
|
|
: query
|
||
|
|
|
||
|
|
const {
|
||
|
|
query: queryText,
|
||
|
|
limit = 10,
|
||
|
|
maxTokens = 50000,
|
||
|
|
relevanceThreshold = 0.7,
|
||
|
|
role,
|
||
|
|
phase,
|
||
|
|
tags,
|
||
|
|
minConfidence,
|
||
|
|
timeRange,
|
||
|
|
conversationId,
|
||
|
|
sessionId,
|
||
|
|
weights = { semantic: 1.0, temporal: 0.5, graph: 0.3 },
|
||
|
|
includeArtifacts = false,
|
||
|
|
includeSimilarConversations = false,
|
||
|
|
deduplicateClusters = true
|
||
|
|
} = opts
|
||
|
|
|
||
|
|
// Build metadata filter
|
||
|
|
const whereFilter: any = {}
|
||
|
|
if (role) {
|
||
|
|
whereFilter.role = Array.isArray(role) ? { $in: role } : role
|
||
|
|
}
|
||
|
|
if (phase) {
|
||
|
|
whereFilter.problemSolvingPhase = Array.isArray(phase) ? { $in: phase } : phase
|
||
|
|
}
|
||
|
|
if (tags && tags.length > 0) {
|
||
|
|
whereFilter.tags = { $in: tags }
|
||
|
|
}
|
||
|
|
if (minConfidence !== undefined) {
|
||
|
|
whereFilter.confidence = { $gte: minConfidence }
|
||
|
|
}
|
||
|
|
if (timeRange) {
|
||
|
|
if (timeRange.start !== undefined) {
|
||
|
|
whereFilter.timestamp = { $gte: timeRange.start }
|
||
|
|
}
|
||
|
|
if (timeRange.end !== undefined) {
|
||
|
|
whereFilter.timestamp = { ...whereFilter.timestamp, $lte: timeRange.end }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (conversationId) {
|
||
|
|
whereFilter.conversationId = conversationId
|
||
|
|
}
|
||
|
|
if (sessionId) {
|
||
|
|
whereFilter.sessionId = sessionId
|
||
|
|
}
|
||
|
|
|
||
|
|
// Query with Triple Intelligence (REAL)
|
||
|
|
const findOptions: any = {
|
||
|
|
limit: limit * 2, // Get more for ranking
|
||
|
|
where: whereFilter
|
||
|
|
}
|
||
|
|
|
||
|
|
if (queryText) {
|
||
|
|
findOptions.like = queryText
|
||
|
|
}
|
||
|
|
|
||
|
|
const results = await this.brain.find(findOptions)
|
||
|
|
|
||
|
|
// Calculate relevance scores (REAL scoring)
|
||
|
|
const now = Date.now()
|
||
|
|
const rankedMessages: RankedMessage[] = results
|
||
|
|
.map((result: any) => {
|
||
|
|
// Semantic score (from vector similarity)
|
||
|
|
const semanticScore = result.score || 0
|
||
|
|
|
||
|
|
// Temporal score (recency decay)
|
||
|
|
const ageInDays = (now - (result.metadata.timestamp || now)) / (1000 * 60 * 60 * 24)
|
||
|
|
const temporalScore = Math.exp(-0.1 * ageInDays) // Decay rate: 0.1
|
||
|
|
|
||
|
|
// Graph score (would need graph traversal, simplified for now)
|
||
|
|
const graphScore = 0.5 // Placeholder for now, can enhance later
|
||
|
|
|
||
|
|
// Combined score
|
||
|
|
const relevanceScore =
|
||
|
|
(weights.semantic ?? 1.0) * semanticScore +
|
||
|
|
(weights.temporal ?? 0.5) * temporalScore +
|
||
|
|
(weights.graph ?? 0.3) * graphScore
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: result.id,
|
||
|
|
content: result.data || result.content || '',
|
||
|
|
role: result.metadata.role,
|
||
|
|
metadata: result.metadata as ConversationMessageMetadata,
|
||
|
|
embedding: result.embedding,
|
||
|
|
createdAt: result.metadata.timestamp || now,
|
||
|
|
updatedAt: result.metadata.timestamp || now,
|
||
|
|
relevanceScore,
|
||
|
|
semanticScore,
|
||
|
|
temporalScore,
|
||
|
|
graphScore
|
||
|
|
} as RankedMessage
|
||
|
|
})
|
||
|
|
.filter((msg: RankedMessage) => msg.relevanceScore >= relevanceThreshold)
|
||
|
|
.sort((a: RankedMessage, b: RankedMessage) => b.relevanceScore - a.relevanceScore)
|
||
|
|
|
||
|
|
// Deduplicate via clustering if requested
|
||
|
|
let finalMessages = rankedMessages
|
||
|
|
if (deduplicateClusters && rankedMessages.length > 5 && this.brain.neural) {
|
||
|
|
// Use neural clustering to remove duplicates (REAL)
|
||
|
|
try {
|
||
|
|
const clusters = await this.brain.neural().clusters({
|
||
|
|
maxClusters: Math.ceil(rankedMessages.length / 3),
|
||
|
|
threshold: 0.85
|
||
|
|
})
|
||
|
|
|
||
|
|
// Keep highest scoring message from each cluster
|
||
|
|
const kept = new Set<string>()
|
||
|
|
for (const cluster of clusters) {
|
||
|
|
const clusterMessages = rankedMessages.filter(msg =>
|
||
|
|
cluster.members?.includes(msg.id)
|
||
|
|
)
|
||
|
|
if (clusterMessages.length > 0) {
|
||
|
|
const best = clusterMessages.reduce((a, b) =>
|
||
|
|
a.relevanceScore > b.relevanceScore ? a : b
|
||
|
|
)
|
||
|
|
kept.add(best.id)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
finalMessages = rankedMessages.filter(msg => kept.has(msg.id))
|
||
|
|
} catch (error) {
|
||
|
|
// Clustering failed, use all messages
|
||
|
|
console.warn('Clustering failed:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Limit by token budget
|
||
|
|
let totalTokens = 0
|
||
|
|
const messagesWithinBudget: RankedMessage[] = []
|
||
|
|
for (const msg of finalMessages) {
|
||
|
|
const tokens = msg.metadata.tokensUsed || Math.ceil(msg.content.length / 4)
|
||
|
|
if (totalTokens + tokens <= maxTokens) {
|
||
|
|
messagesWithinBudget.push(msg)
|
||
|
|
totalTokens += tokens
|
||
|
|
} else {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get artifacts if requested (REAL VFS)
|
||
|
|
let artifacts: any[] = []
|
||
|
|
if (includeArtifacts && this._vfs) {
|
||
|
|
const artifactIds = new Set(
|
||
|
|
messagesWithinBudget.flatMap(msg => msg.metadata.artifacts || [])
|
||
|
|
)
|
||
|
|
|
||
|
|
for (const artifactId of artifactIds) {
|
||
|
|
try {
|
||
|
|
const entity = await this.brain.get(artifactId)
|
||
|
|
if (entity) {
|
||
|
|
artifacts.push({
|
||
|
|
id: artifactId,
|
||
|
|
path: entity.metadata?.path || artifactId,
|
||
|
|
summary: entity.metadata?.description || undefined
|
||
|
|
})
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
// Artifact not found, skip
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get similar conversations if requested
|
||
|
|
let similarConversations: any[] = []
|
||
|
|
if (includeSimilarConversations && conversationId && this.brain.neural) {
|
||
|
|
// Use neural neighbors (REAL)
|
||
|
|
try {
|
||
|
|
const neighborsResult = await this.brain.neural().neighbors(conversationId, {
|
||
|
|
limit: 5,
|
||
|
|
minSimilarity: 0.7
|
||
|
|
})
|
||
|
|
|
||
|
|
similarConversations = neighborsResult.neighbors.map((neighbor: any) => ({
|
||
|
|
id: neighbor.id,
|
||
|
|
title: neighbor.metadata?.title,
|
||
|
|
summary: neighbor.metadata?.summary,
|
||
|
|
relevance: neighbor.score,
|
||
|
|
messageCount: neighbor.metadata?.messageCount || 0
|
||
|
|
}))
|
||
|
|
} catch (error) {
|
||
|
|
// Neighbors failed, skip
|
||
|
|
console.warn('Similar conversation search failed:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const queryTime = Date.now() - startTime
|
||
|
|
|
||
|
|
return {
|
||
|
|
messages: messagesWithinBudget.slice(0, limit),
|
||
|
|
artifacts,
|
||
|
|
similarConversations,
|
||
|
|
totalTokens,
|
||
|
|
metadata: {
|
||
|
|
queryTime,
|
||
|
|
messagesConsidered: results.length,
|
||
|
|
conversationsSearched: new Set(results.map((r: any) => r.metadata.conversationId)).size
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Search messages semantically
|
||
|
|
*
|
||
|
|
* Uses: brain.find() with semantic search
|
||
|
|
* Real implementation - vector similarity search
|
||
|
|
*
|
||
|
|
* @param options Search options
|
||
|
|
* @returns Search results with scores
|
||
|
|
*/
|
||
|
|
async searchMessages(options: ConversationSearchOptions): Promise<ConversationSearchResult[]> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
const {
|
||
|
|
query,
|
||
|
|
limit = 10,
|
||
|
|
role,
|
||
|
|
conversationId,
|
||
|
|
sessionId,
|
||
|
|
timeRange,
|
||
|
|
includeMetadata = true,
|
||
|
|
includeContent = true
|
||
|
|
} = options
|
||
|
|
|
||
|
|
// Build filter
|
||
|
|
const whereFilter: any = {}
|
||
|
|
if (role) {
|
||
|
|
whereFilter.role = Array.isArray(role) ? { $in: role } : role
|
||
|
|
}
|
||
|
|
if (conversationId) {
|
||
|
|
whereFilter.conversationId = conversationId
|
||
|
|
}
|
||
|
|
if (sessionId) {
|
||
|
|
whereFilter.sessionId = sessionId
|
||
|
|
}
|
||
|
|
if (timeRange) {
|
||
|
|
if (timeRange.start) {
|
||
|
|
whereFilter.timestamp = { $gte: timeRange.start }
|
||
|
|
}
|
||
|
|
if (timeRange.end) {
|
||
|
|
whereFilter.timestamp = { ...whereFilter.timestamp, $lte: timeRange.end }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Search with Triple Intelligence (REAL)
|
||
|
|
const results = await this.brain.find({
|
||
|
|
query: query,
|
||
|
|
where: whereFilter,
|
||
|
|
limit
|
||
|
|
})
|
||
|
|
|
||
|
|
// Format results
|
||
|
|
return results.map((result: any) => {
|
||
|
|
const message: ConversationMessage = {
|
||
|
|
id: result.id,
|
||
|
|
content: includeContent ? (result.data || result.content || '') : '',
|
||
|
|
role: result.metadata.role,
|
||
|
|
metadata: includeMetadata ? (result.metadata as ConversationMessageMetadata) : {} as any,
|
||
|
|
embedding: result.embedding,
|
||
|
|
createdAt: result.metadata.timestamp || Date.now(),
|
||
|
|
updatedAt: result.metadata.timestamp || Date.now()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create snippet
|
||
|
|
const content = result.data || result.content || ''
|
||
|
|
const snippet = content.length > 150 ? content.substring(0, 147) + '...' : content
|
||
|
|
|
||
|
|
return {
|
||
|
|
message,
|
||
|
|
score: result.score || 0,
|
||
|
|
conversationId: result.metadata.conversationId,
|
||
|
|
snippet: includeContent ? snippet : undefined
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Find similar conversations using Neural API
|
||
|
|
*
|
||
|
|
* Uses: brain.neural.neighbors()
|
||
|
|
* Real implementation - semantic similarity with embeddings
|
||
|
|
*
|
||
|
|
* @param conversationId Conversation ID to find similar to
|
||
|
|
* @param limit Maximum number of similar conversations
|
||
|
|
* @param threshold Minimum similarity threshold
|
||
|
|
* @returns Similar conversations with relevance scores
|
||
|
|
*/
|
||
|
|
async findSimilarConversations(
|
||
|
|
conversationId: string,
|
||
|
|
limit: number = 5,
|
||
|
|
threshold: number = 0.7
|
||
|
|
): Promise<Array<{ id: string; relevance: number; metadata?: any }>> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!this.brain.neural) {
|
||
|
|
throw new Error('Neural API not available')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use neural neighbors (REAL)
|
||
|
|
const neighborsResult = await this.brain.neural().neighbors(conversationId, {
|
||
|
|
limit: limit,
|
||
|
|
minSimilarity: threshold
|
||
|
|
})
|
||
|
|
|
||
|
|
return neighborsResult.neighbors.map((neighbor: any) => ({
|
||
|
|
id: neighbor.id,
|
||
|
|
relevance: neighbor.score,
|
||
|
|
metadata: neighbor.metadata
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get conversation themes via clustering
|
||
|
|
*
|
||
|
|
* Uses: brain.neural.clusters()
|
||
|
|
* Real implementation - semantic clustering
|
||
|
|
*
|
||
|
|
* @param conversationId Conversation ID
|
||
|
|
* @returns Discovered themes
|
||
|
|
*/
|
||
|
|
async getConversationThemes(conversationId: string): Promise<ConversationTheme[]> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!this.brain.neural) {
|
||
|
|
throw new Error('Neural API not available')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get messages for conversation
|
||
|
|
const results = await this.brain.find({
|
||
|
|
where: { conversationId },
|
||
|
|
limit: 1000
|
||
|
|
})
|
||
|
|
|
||
|
|
if (results.length === 0) {
|
||
|
|
return []
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cluster messages (REAL)
|
||
|
|
const clusters = await this.brain.neural().clusters({
|
||
|
|
maxClusters: Math.min(5, Math.ceil(results.length / 5)),
|
||
|
|
threshold: 0.75
|
||
|
|
})
|
||
|
|
|
||
|
|
// Convert to themes
|
||
|
|
return clusters.map((cluster: any, index: number) => ({
|
||
|
|
id: `theme_${index}`,
|
||
|
|
label: cluster.label || `Theme ${index + 1}`,
|
||
|
|
messages: cluster.members || [],
|
||
|
|
centroid: cluster.centroid || [],
|
||
|
|
coherence: cluster.coherence || 0
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Save an artifact (code, file, etc.) to VFS
|
||
|
|
*
|
||
|
|
* Uses: brain.vfs()
|
||
|
|
* Real implementation - stores in virtual filesystem
|
||
|
|
*
|
||
|
|
* @param path VFS path
|
||
|
|
* @param content File content
|
||
|
|
* @param options Artifact options
|
||
|
|
* @returns Artifact entity ID
|
||
|
|
*/
|
||
|
|
async saveArtifact(
|
||
|
|
path: string,
|
||
|
|
content: string | Buffer,
|
||
|
|
options: ArtifactOptions
|
||
|
|
): Promise<string> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!this._vfs) {
|
||
|
|
throw new Error('VFS not available')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Write file to VFS (REAL)
|
||
|
|
await this._vfs.writeFile(path, content)
|
||
|
|
|
||
|
|
// Get the file entity
|
||
|
|
const entity = await this._vfs.getEntity(path)
|
||
|
|
|
||
|
|
// Link to conversation message if provided
|
||
|
|
if (options.messageId) {
|
||
|
|
await this.brain.relate({
|
||
|
|
from: options.messageId,
|
||
|
|
to: entity.id,
|
||
|
|
type: VerbType.Creates,
|
||
|
|
metadata: {
|
||
|
|
conversationId: options.conversationId,
|
||
|
|
artifactType: options.type || 'other'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return entity.id
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get conversation statistics
|
||
|
|
*
|
||
|
|
* Uses: brain.find() with aggregations
|
||
|
|
* Real implementation - queries and aggregates data
|
||
|
|
*
|
||
|
|
* @param conversationId Optional conversation ID to filter
|
||
|
|
* @returns Conversation statistics
|
||
|
|
*/
|
||
|
|
async getConversationStats(conversationId?: string): Promise<ConversationStats> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Query messages
|
||
|
|
const whereFilter = conversationId ? { conversationId } : {}
|
||
|
|
const results = await this.brain.find({
|
||
|
|
where: whereFilter,
|
||
|
|
limit: 100000 // Large limit for stats
|
||
|
|
})
|
||
|
|
|
||
|
|
// Calculate statistics (REAL aggregation)
|
||
|
|
const conversations = new Set(results.map((r: any) => r.metadata.conversationId))
|
||
|
|
const totalMessages = results.length
|
||
|
|
const totalTokens = results.reduce(
|
||
|
|
(sum: number, r: any) => sum + (r.metadata.tokensUsed || 0),
|
||
|
|
0
|
||
|
|
)
|
||
|
|
|
||
|
|
const timestamps = results.map((r: any) => r.metadata.timestamp || Date.now())
|
||
|
|
const oldestMessage = Math.min(...timestamps)
|
||
|
|
const newestMessage = Math.max(...timestamps)
|
||
|
|
|
||
|
|
// Count by phase
|
||
|
|
const phases: Record<string, number> = {}
|
||
|
|
const roles: Record<string, number> = {}
|
||
|
|
|
||
|
|
for (const result of results) {
|
||
|
|
const phase = result.entity.metadata.problemSolvingPhase
|
||
|
|
const role = result.entity.metadata.role
|
||
|
|
|
||
|
|
if (phase) {
|
||
|
|
phases[phase] = (phases[phase] || 0) + 1
|
||
|
|
}
|
||
|
|
if (role) {
|
||
|
|
roles[role] = (roles[role] || 0) + 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
totalConversations: conversations.size,
|
||
|
|
totalMessages,
|
||
|
|
totalTokens,
|
||
|
|
averageMessagesPerConversation: totalMessages / Math.max(1, conversations.size),
|
||
|
|
averageTokensPerMessage: totalTokens / Math.max(1, totalMessages),
|
||
|
|
oldestMessage,
|
||
|
|
newestMessage,
|
||
|
|
phases: phases as any,
|
||
|
|
roles: roles as any
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete a message
|
||
|
|
*
|
||
|
|
* Uses: brain.deleteNoun()
|
||
|
|
* Real implementation - removes from graph
|
||
|
|
*
|
||
|
|
* @param messageId Message ID to delete
|
||
|
|
*/
|
||
|
|
async deleteMessage(messageId: string): Promise<void> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
await this.brain.delete(messageId)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Export conversation to JSON
|
||
|
|
*
|
||
|
|
* Uses: getConversationThread()
|
||
|
|
* Real implementation - serializes conversation
|
||
|
|
*
|
||
|
|
* @param conversationId Conversation ID
|
||
|
|
* @returns JSON-serializable conversation object
|
||
|
|
*/
|
||
|
|
async exportConversation(conversationId: string): Promise<any> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
const thread = await this.getConversationThread(conversationId, {
|
||
|
|
includeArtifacts: true
|
||
|
|
})
|
||
|
|
|
||
|
|
return {
|
||
|
|
version: '1.0',
|
||
|
|
exportedAt: Date.now(),
|
||
|
|
conversation: thread
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Import conversation from JSON
|
||
|
|
*
|
||
|
|
* Uses: saveMessage() and linkMessages()
|
||
|
|
* Real implementation - recreates conversation
|
||
|
|
*
|
||
|
|
* @param data Exported conversation data
|
||
|
|
* @returns New conversation ID
|
||
|
|
*/
|
||
|
|
async importConversation(data: any): Promise<string> {
|
||
|
|
if (!this.initialized) {
|
||
|
|
await this.init()
|
||
|
|
}
|
||
|
|
|
||
|
|
const newConversationId = `conv_${uuidv4()}`
|
||
|
|
const conversation = data.conversation
|
||
|
|
|
||
|
|
if (!conversation || !conversation.messages) {
|
||
|
|
throw new Error('Invalid conversation data')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Import messages in order
|
||
|
|
const messageIdMap = new Map<string, string>()
|
||
|
|
|
||
|
|
for (let i = 0; i < conversation.messages.length; i++) {
|
||
|
|
const msg = conversation.messages[i]
|
||
|
|
const prevMessageId = i > 0 ? messageIdMap.get(conversation.messages[i - 1].id) : undefined
|
||
|
|
|
||
|
|
const newMessageId = await this.saveMessage(msg.content, msg.role, {
|
||
|
|
conversationId: newConversationId,
|
||
|
|
sessionId: conversation.metadata.sessionId,
|
||
|
|
phase: msg.metadata.problemSolvingPhase,
|
||
|
|
confidence: msg.metadata.confidence,
|
||
|
|
tags: msg.metadata.tags,
|
||
|
|
linkToPrevious: prevMessageId,
|
||
|
|
metadata: msg.metadata
|
||
|
|
})
|
||
|
|
|
||
|
|
messageIdMap.set(msg.id, newMessageId)
|
||
|
|
}
|
||
|
|
|
||
|
|
return newConversationId
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a ConversationManager instance
|
||
|
|
*
|
||
|
|
* @param brain Brainy instance
|
||
|
|
* @returns ConversationManager instance
|
||
|
|
*/
|
||
|
|
export function createConversationManager(brain: Brainy): ConversationManager {
|
||
|
|
return new ConversationManager(brain)
|
||
|
|
}
|