immersive2/server/services/sessionStore.js
Michael Mainguy 4ca98cf980 Add LangChain model wrappers and enhance diagram AI tools
- Migrate to LangChain for model abstraction (@langchain/anthropic, @langchain/ollama)
- Add custom ChatCloudflare class for Cloudflare Workers AI
- Simplify API routes using unified LangChain interface
- Add session preferences API for storing user settings
- Add connection label preference (ask user once, remember for session)
- Add shape modification support (change entity shapes via AI)
- Add template setter to DiagramObject for shape changes
- Improve entity inference with fuzzy matching
- Map colors to 16 toolbox palette colors
- Limit conversation history to last 6 messages
- Fix model switching to accept display names

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:17:15 -06:00

200 lines
4.4 KiB
JavaScript

/**
* In-memory session store for diagram chat sessions.
* Stores conversation history and entity snapshots.
*/
import { v4 as uuidv4 } from 'uuid';
// Session structure:
// {
// id: string,
// diagramId: string,
// conversationHistory: Array<{role, content, toolResults?, timestamp}>,
// entities: Array<{id, template, text, color, position}>,
// cameraPosition: { position: {x,y,z}, forward: {x,y,z}, groundForward: {x,y,z}, groundRight: {x,y,z} },
// preferences: { useDefaultConnectionLabels?: boolean },
// createdAt: Date,
// lastAccess: Date
// }
const sessions = new Map();
// Session timeout (1 hour of inactivity)
const SESSION_TIMEOUT_MS = 60 * 60 * 1000;
/**
* Create a new session for a diagram
*/
export function createSession(diagramId) {
const id = uuidv4();
const session = {
id,
diagramId,
conversationHistory: [],
entities: [],
cameraPosition: null,
preferences: {},
createdAt: new Date(),
lastAccess: new Date()
};
sessions.set(id, session);
return session;
}
/**
* Get a session by ID
*/
export function getSession(sessionId) {
const session = sessions.get(sessionId);
if (session) {
session.lastAccess = new Date();
}
return session || null;
}
/**
* Find existing session for a diagram
*/
export function findSessionByDiagram(diagramId) {
for (const [, session] of sessions) {
if (session.diagramId === diagramId) {
session.lastAccess = new Date();
return session;
}
}
return null;
}
/**
* Update entities snapshot for a session
*/
export function syncEntities(sessionId, entities) {
const session = sessions.get(sessionId);
if (!session) return null;
session.entities = entities;
session.lastAccess = new Date();
return session;
}
/**
* Update camera position for a session
*/
export function syncCameraPosition(sessionId, cameraPosition) {
const session = sessions.get(sessionId);
if (!session) return null;
session.cameraPosition = cameraPosition;
session.lastAccess = new Date();
return session;
}
/**
* Add a message to conversation history
*/
export function addMessage(sessionId, message) {
const session = sessions.get(sessionId);
if (!session) return null;
session.conversationHistory.push({
...message,
timestamp: new Date()
});
session.lastAccess = new Date();
return session;
}
/**
* Get conversation history for API calls (formatted for Claude)
*/
export function getConversationForAPI(sessionId) {
const session = sessions.get(sessionId);
if (!session) return [];
// Convert to Claude message format
return session.conversationHistory.map(msg => ({
role: msg.role,
content: msg.content
}));
}
/**
* Clear conversation history but keep session
*/
export function clearHistory(sessionId) {
const session = sessions.get(sessionId);
if (!session) return null;
session.conversationHistory = [];
session.lastAccess = new Date();
return session;
}
/**
* Delete a session
*/
export function deleteSession(sessionId) {
return sessions.delete(sessionId);
}
/**
* Clean up expired sessions
*/
export function cleanupExpiredSessions() {
const now = Date.now();
for (const [id, session] of sessions) {
if (now - session.lastAccess.getTime() > SESSION_TIMEOUT_MS) {
sessions.delete(id);
}
}
}
// Run cleanup every 15 minutes
setInterval(cleanupExpiredSessions, 15 * 60 * 1000);
/**
* Get session stats (for debugging)
*/
export function getStats(includeDetails = false) {
return {
activeSessions: sessions.size,
sessions: Array.from(sessions.values()).map(s => ({
id: s.id,
diagramId: s.diagramId,
messageCount: s.conversationHistory.length,
entityCount: s.entities.length,
lastAccess: s.lastAccess,
// Include full details if requested
...(includeDetails && {
entities: s.entities,
conversationHistory: s.conversationHistory
})
}))
};
}
/**
* Get session preferences
*/
export function getPreferences(sessionId) {
const session = sessions.get(sessionId);
if (!session) return null;
session.lastAccess = new Date();
return session.preferences || {};
}
/**
* Set a session preference
*/
export function setPreference(sessionId, key, value) {
const session = sessions.get(sessionId);
if (!session) return null;
if (!session.preferences) {
session.preferences = {};
}
session.preferences[key] = value;
session.lastAccess = new Date();
return session.preferences;
}