immersive2/server/api/claude.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

125 lines
5.0 KiB
JavaScript

import { Router } from "express";
import { getSession, addMessage } from "../services/sessionStore.js";
import { trackUsage, getUsageSummary, formatCost, getSessionUsage } from "../services/usageTracker.js";
import {
getClaudeModel,
buildLangChainMessages,
aiMessageToClaudeResponse
} from "../services/langchainModels.js";
const router = Router();
router.post("/*path", async (req, res) => {
const requestStart = Date.now();
console.log(`[Claude API] ========== REQUEST START ==========`);
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error(`[Claude API] ERROR: API key not configured`);
return res.status(500).json({ error: "API key not configured" });
}
const { sessionId, model: modelId, system: systemPrompt, ...requestBody } = req.body;
console.log(`[Claude API] Session ID: ${sessionId || 'none'}`);
console.log(`[Claude API] Model: ${modelId}`);
console.log(`[Claude API] Messages count: ${requestBody.messages?.length || 0}`);
try {
// Get LangChain model with tools bound
const model = getClaudeModel(modelId);
// Build messages with entity context and history
const messages = buildLangChainMessages(
sessionId,
requestBody.messages,
systemPrompt
);
console.log(`[Claude API] Sending request via LangChain...`);
const fetchStart = Date.now();
// Invoke model
const response = await model.invoke(messages);
const fetchDuration = Date.now() - fetchStart;
console.log(`[Claude API] Response received in ${fetchDuration}ms`);
// Convert to Claude API format for client compatibility
const data = aiMessageToClaudeResponse(response, modelId);
console.log(`[Claude API] Response converted. Stop reason: ${data.stop_reason}, content blocks: ${data.content?.length || 0}`);
// Track and log token usage
if (data.usage) {
const userMessage = requestBody.messages?.[requestBody.messages.length - 1];
const inputText = typeof userMessage?.content === 'string' ? userMessage.content : null;
const outputText = data.content
?.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n') || null;
const toolCalls = data.content
?.filter(c => c.type === 'tool_use')
.map(c => ({ name: c.name, input: c.input })) || [];
const usageRecord = trackUsage(sessionId, modelId, data.usage, {
inputText,
outputText,
toolCalls
});
console.log(`[Claude API] REQUEST USAGE: ${getUsageSummary(usageRecord)}`);
if (sessionId) {
const sessionStats = getSessionUsage(sessionId);
if (sessionStats) {
console.log(`[Claude API] SESSION TOTALS (${sessionStats.requestCount} requests):`);
console.log(`[Claude API] Total input: ${sessionStats.totalInputTokens} tokens`);
console.log(`[Claude API] Total output: ${sessionStats.totalOutputTokens} tokens`);
console.log(`[Claude API] Total cost: ${formatCost(sessionStats.totalCost)}`);
}
}
}
// Store messages to session
if (sessionId && data.content) {
const session = getSession(sessionId);
if (session) {
const userMessage = requestBody.messages?.[requestBody.messages.length - 1];
if (userMessage && userMessage.role === 'user' && typeof userMessage.content === 'string') {
addMessage(sessionId, {
role: 'user',
content: userMessage.content
});
console.log(`[Claude API] Stored user message to session`);
}
const assistantContent = data.content
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
if (assistantContent) {
addMessage(sessionId, {
role: 'assistant',
content: assistantContent
});
console.log(`[Claude API] Stored assistant response to session (${assistantContent.length} chars)`);
}
}
}
const totalDuration = Date.now() - requestStart;
console.log(`[Claude API] ========== REQUEST COMPLETE (${totalDuration}ms) ==========`);
res.json(data);
} catch (error) {
const totalDuration = Date.now() - requestStart;
console.error(`[Claude API] ========== REQUEST FAILED (${totalDuration}ms) ==========`);
console.error(`[Claude API] Error:`, error);
console.error(`[Claude API] Error message:`, error.message);
res.status(500).json({ error: "Failed to call Claude API", details: error.message });
}
});
export default router;