UNPKG

configure

Version:

Identity layer SDK for AI agents

396 lines (343 loc) 12.8 kB
import 'dotenv/config'; import express from 'express'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { createHash } from 'crypto'; import Anthropic from '@anthropic-ai/sdk'; import { Configure, classifyError, toOpenAIFunctions, } from 'configure'; const MAX_TOOL_ROUNDS = 5; const PORT = process.env.PORT || 4000; const DEFAULT_MEMORY_CRITERIA = 'User identity, preferences, durable goals, explicit corrections, recurring needs, and details that should improve future conversations.'; const ENABLED_CONNECTORS = ['gmail', 'calendar', 'drive', 'notion']; function getProviderConfig() { const provider = (process.env.MODEL_PROVIDER || 'anthropic').toLowerCase(); const configs = { anthropic: { keyName: 'ANTHROPIC_API_KEY', model: process.env.MODEL_NAME || 'claude-sonnet-4-20250514', }, openai: { keyName: 'OPENAI_API_KEY', model: process.env.MODEL_NAME || 'gpt-4o', endpoint: 'https://api.openai.com/v1/chat/completions', }, openrouter: { keyName: 'OPENROUTER_API_KEY', model: process.env.MODEL_NAME, endpoint: 'https://openrouter.ai/api/v1/chat/completions', }, }; const config = configs[provider]; if (!config) { throw new Error(`MODEL_PROVIDER must be one of: ${Object.keys(configs).join(', ')}`); } if (!config.model) { throw new Error('MODEL_NAME is required when MODEL_PROVIDER=openrouter'); } return { provider, apiKey: process.env[config.keyName], ...config }; } function profileRuntimeOptions(token, conversationId) { const stableId = conversationId && String(conversationId).trim() ? String(conversationId).trim() : createHash('sha256').update(token).digest('hex').slice(0, 32); return { token, sessionId: `conversation_${stableId}`.replace(/[^a-zA-Z0-9:_-]/g, '_').slice(0, 255), }; } let providerConfig; try { providerConfig = getProviderConfig(); } catch (err) { console.error(err.message); process.exit(1); } const required = [ 'CONFIGURE_API_KEY', 'CONFIGURE_PUBLISHABLE_KEY', 'CONFIGURE_AGENT', ]; const missing = required.filter(key => !process.env[key]); if (missing.length) { console.error(`Missing required environment variables: ${missing.join(', ')}`); console.error('Run `npx configure setup` for Configure keys, then restart the template.'); process.exit(1); } const runtimeReady = Boolean(providerConfig.apiKey); const runtimeMessage = runtimeReady ? `${providerConfig.provider} runtime ready` : `Add ${providerConfig.keyName} to enable chat and tool execution. Configure auth and profile surfaces still work.`; const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); app.use(express.json()); app.use(express.static(join(__dirname, 'public'))); const configure = new Configure({ apiKey: process.env.CONFIGURE_API_KEY, agent: process.env.CONFIGURE_AGENT, }); const CONFIGURE_BASE_URL = process.env.CONFIGURE_BASE_URL || 'https://api.configure.dev'; const anthropic = providerConfig.provider === 'anthropic' ? new Anthropic({ apiKey: providerConfig.apiKey }) : null; const SYSTEM_DESCRIPTION = `You are a concise, capable assistant powered by Configure. Use the user's approved profile and connectors when they help. Be specific when real context is available. Do not pretend to know private details that were not provided.`; app.get('/api/config', (_req, res) => { res.json({ publishableKey: process.env.CONFIGURE_PUBLISHABLE_KEY, agent: process.env.CONFIGURE_AGENT, displayName: process.env.CONFIGURE_AGENT_NAME || humanizeAgent(process.env.CONFIGURE_AGENT), runtimeReady, runtimeMessage, modelProvider: providerConfig.provider, modelName: providerConfig.model, providerKeyName: providerConfig.keyName, }); }); function humanizeAgent(agent = '') { return agent .replace(/[-_]+/g, ' ') .replace(/\b\w/g, char => char.toUpperCase()) .trim() || 'Your agent'; } app.post('/api/hello', async (req, res) => { const { token } = req.body; if (!token) return res.status(400).json({ error: 'token is required' }); try { const identity = await readSelfIdentity(token); const name = identity?.name || (identity?.email ? identity.email.split('@')[0] : ''); res.json({ name }); } catch { res.json({ name: '' }); } }); async function readSelfIdentity(token) { const response = await fetch(`${CONFIGURE_BASE_URL}/v1/me`, { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) return null; const profile = await response.json(); return profile.identity || null; } function sendEvent(res, event) { res.write(`data: ${JSON.stringify(event)}\n\n`); } function compactMemoryText(value, maxLength) { const text = String(value || '').replace(/\s+/g, ' ').trim(); return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text; } function buildReadBackedMemories(message, fullResponse) { const request = compactMemoryText(message, 280); const answer = compactMemoryText(fullResponse, 360); if (request && !answer) { return [`Recent ${process.env.CONFIGURE_AGENT || 'agent'} conversation: the user asked "${request}".`]; } if (!request || !answer) return []; return [ `Recent ${process.env.CONFIGURE_AGENT || 'agent'} conversation: the user asked "${request}" and the assistant replied about "${answer}".`, ]; } function getProviderErrorMessage(err) { const message = err instanceof Error ? err.message : String(err); const lower = message.toLowerCase(); if (!runtimeReady) { return runtimeMessage; } if (lower.includes('anthropic error 401') || lower.includes('openai error 401') || lower.includes('openrouter error 401')) { return `The ${providerConfig.provider} runtime rejected ${providerConfig.keyName}. Add a valid provider key to continue.`; } if (lower.includes('api key') && lower.includes('provider')) { return runtimeMessage; } return ''; } function normalizeToolCall(provider, block) { if (provider === 'anthropic') { return { id: block.id, name: block.name, input: block.input || {} }; } return { id: block.id, name: block.function?.name, input: JSON.parse(block.function?.arguments || '{}'), }; } async function streamAnthropicTurn(messages, systemPrompt, tools, res) { let text = ''; const stream = anthropic.messages.stream({ model: providerConfig.model, max_tokens: 4096, system: systemPrompt, tools, messages, }); stream.on('text', chunk => { text += chunk; sendEvent(res, { type: 'delta', text: chunk }); }); const response = await stream.finalMessage(); const toolCalls = response.content .filter(block => block.type === 'tool_use') .map(block => normalizeToolCall('anthropic', block)); return { text, toolCalls, assistantMessage: { role: 'assistant', content: response.content } }; } async function* readSse(body) { const reader = body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (data && data !== '[DONE]') yield JSON.parse(data); } } } async function streamOpenAiTurn(messages, systemPrompt, tools, res) { const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${providerConfig.apiKey}`, }; if (providerConfig.provider === 'openrouter') { headers['HTTP-Referer'] = 'https://configure.dev'; headers['X-Title'] = 'Configure Quickstart'; } const response = await fetch(providerConfig.endpoint, { method: 'POST', headers, body: JSON.stringify({ model: providerConfig.model, stream: true, messages: [{ role: 'system', content: systemPrompt }, ...messages], tools: toOpenAIFunctions(tools), tool_choice: 'auto', }), }); if (!response.ok) { throw new Error(`${providerConfig.provider} error ${response.status}: ${await response.text()}`); } let text = ''; const toolParts = new Map(); for await (const event of readSse(response.body)) { const delta = event.choices?.[0]?.delta || {}; if (delta.content) { text += delta.content; sendEvent(res, { type: 'delta', text: delta.content }); } for (const call of delta.tool_calls || []) { const index = call.index ?? 0; const current = toolParts.get(index) || { id: '', type: 'function', function: { name: '', arguments: '' } }; if (call.id) current.id = call.id; if (call.function?.name) current.function.name += call.function.name; if (call.function?.arguments) current.function.arguments += call.function.arguments; toolParts.set(index, current); } } const toolCalls = [...toolParts.values()].map(block => normalizeToolCall('openai', block)); return { text, toolCalls, assistantMessage: { role: 'assistant', content: text || null, tool_calls: [...toolParts.values()], }, }; } async function streamModel(messages, systemPrompt, tools, res) { if (providerConfig.provider === 'anthropic') { return streamAnthropicTurn(messages, systemPrompt, tools, res); } return streamOpenAiTurn(messages, systemPrompt, tools, res); } app.post('/api/chat', async (req, res) => { const { token, message, history = [], conversationId } = req.body; if (!token || !message) { return res.status(400).json({ error: 'token and message are required' }); } res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders(); if (!runtimeReady) { sendEvent(res, { type: 'error', message: runtimeMessage }); res.end(); return; } let profileForCommit = null; let profileReadSucceeded = false; let fullResponse = ''; try { const profile = configure.profile(profileRuntimeOptions(token, conversationId)); profileForCommit = profile; const tools = profile.tools({ connectors: ENABLED_CONNECTORS, }); const read = await profile.read({ sections: ['identity', 'summary', 'integrations'], }); profileReadSucceeded = true; const context = read.profile.format({ guidelines: true }); const systemPrompt = `${SYSTEM_DESCRIPTION}\n\nMemory criteria: ${DEFAULT_MEMORY_CRITERIA}\n\n${context}`; const messages = [ ...history.map(entry => ({ role: entry.role, content: entry.content })), { role: 'user', content: message }, ]; for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { const turn = await streamModel(messages, systemPrompt, tools, res); fullResponse += turn.text; if (!turn.toolCalls.length) break; messages.push(turn.assistantMessage); const toolResults = []; for (const call of turn.toolCalls) { sendEvent(res, { type: 'tool_status', status: 'running', tool: call.name }); let result; try { result = await profile.executeTool({ name: call.name, arguments: call.input }); sendEvent(res, { type: 'tool_status', status: 'complete', tool: call.name }); } catch (err) { result = { error: err.message }; sendEvent(res, { type: 'tool_status', status: 'error', tool: call.name }); } if (providerConfig.provider === 'anthropic') { toolResults.push({ type: 'tool_result', tool_use_id: call.id, content: JSON.stringify(result) }); } else { toolResults.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) }); } } if (providerConfig.provider === 'anthropic') { messages.push({ role: 'user', content: toolResults }); } else { messages.push(...toolResults); } } sendEvent(res, { type: 'done' }); res.end(); } catch (err) { console.error('Chat error:', err); const providerMessage = getProviderErrorMessage(err); sendEvent(res, { type: 'error', message: providerMessage || classifyError(err).message }); res.end(); } finally { if (profileReadSucceeded && profileForCommit) { profileForCommit.commit({ memories: buildReadBackedMemories(message, fullResponse), messages: [ { role: 'user', content: message }, ...(fullResponse ? [{ role: 'assistant', content: fullResponse }] : []), ], }).catch(() => {}); } } }); app.listen(PORT, () => { console.log(`Configure quickstart running at http://localhost:${PORT}`); });