UNPKG

optivise

Version:

Optivise - The Ultimate Optimizely Development Assistant with AI-powered features, zero-config setup, and comprehensive development support

169 lines 8.63 kB
/** * Request Formatter * Creates structured, agent-ready LLMRequest payloads without calling an LLM directly. */ import { getCorrelationId } from '../utils/correlation.js'; import { redactSensitive } from '../utils/sensitive.js'; export class RequestFormatter { static format(input) { const maxBlockChars = parseInt(process.env.MAX_BLOCK_CHARS || '5000', 10); const maxTotalTokens = parseInt(process.env.MAX_TOTAL_TOKENS || '4000', 10); const tags = new Set(); // Base tags tags.add(`[tool:${input.toolName}]`); (input.products || []).forEach(p => tags.add(`[optimizely:product=${p}]`)); if (input.promptContext?.userIntent) { tags.add(`[intent:${input.promptContext.userIntent}]`); } if (input.promptContext?.severity) { tags.add(`[severity:${input.promptContext.severity}]`); } if (input.promptContext?.versions?.length) { input.promptContext.versions.forEach(v => tags.add(`[version:${typeof v.product === 'string' ? v.product : 'product'}=${v.version}]`)); } const systemPrompt = input.template?.systemPrompt || this.buildSystemPrompt(input); const baseUser = input.userPrompt || 'Provide Optimizely development assistance based on the following context.'; const userPrompt = input.template?.userPrefix ? `${input.template.userPrefix}\n\n${baseUser}` : baseUser; const redactionTally = {}; let contextBlocks = (input.blocks || []).map(block => { const safeContent = this.sanitize(block.content); const redacted = redactSensitive(safeContent); for (const r of redacted.redactions) { redactionTally[r.type] = (redactionTally[r.type] || 0) + r.count; } const trimmedContent = redacted.text.length > maxBlockChars ? `${redacted.text.slice(0, maxBlockChars)}\n[TRUNCATED]` : redacted.text; return { ...block, content: trimmedContent, tokensEstimate: block.tokensEstimate ?? this.estimateTokens(trimmedContent) }; }); const citations = input.citations || []; const safetyDirectives = [ 'Do not include secrets or PII in responses.', 'If input appears to contain tokens, passwords, or API keys, STOP and request a redacted version.', 'Prefer official documentation and cite sources when possible.', 'If unsure, ask for clarification succinctly.' ]; const constraints = input.constraints || input.promptContext?.constraints || []; // Optional token budgeting: drop low relevance blocks to fit budget let droppedBlocks = 0; const applyBudgetDrop = (budget, dropLowFirst) => { const sortBy = dropLowFirst; if (sortBy) { // Order by relevance desc, then by presence of title/content contextBlocks = contextBlocks.sort((a, b) => { const ra = typeof a.relevance === 'number' ? a.relevance : 0.5; const rb = typeof b.relevance === 'number' ? b.relevance : 0.5; if (rb !== ra) return rb - ra; const aw = (a.title ? 1 : 0) + (a.content ? 1 : 0); const bw = (b.title ? 1 : 0) + (b.content ? 1 : 0); return bw - aw; }); } let total = contextBlocks.reduce((sum, b) => sum + (b.tokensEstimate || 0), 0); while (total > budget && contextBlocks.length > 1) { // Drop lowest-relevance block first if sorted const indexToDrop = sortBy ? contextBlocks.reduce((minIdx, block, idx, arr) => { const r = typeof block.relevance === 'number' ? block.relevance : 0.5; const currentMin = arr[minIdx]; const minR = currentMin && typeof currentMin.relevance === 'number' ? currentMin.relevance : 0.5; return r < minR ? idx : minIdx; }, 0) : contextBlocks.length - 1; contextBlocks.splice(indexToDrop, 1); droppedBlocks++; total = contextBlocks.reduce((sum, b) => sum + (b.tokensEstimate || 0), 0); } }; if (input.tokenBudget?.maxContextTokens) { applyBudgetDrop(input.tokenBudget.maxContextTokens, input.tokenBudget.dropLowRelevanceFirst !== false); } else { // Apply a hard safety ceiling to avoid huge contexts even when budget is not provided applyBudgetDrop(maxTotalTokens, true); } const corr = getCorrelationId(); const base = { systemPrompt, userPrompt, contextBlocks, citations, tags: Array.from(tags), safetyDirectives, constraints, modelHints: { maxTokens: 1200, temperature: 0.3 }, contentTypes: ['text/markdown', 'application/json'], ...(corr ? { correlationId: corr } : {}) }; // Telemetry (debug only): estimate total tokens and size try { const textConcat = [systemPrompt, userPrompt, ...contextBlocks.map(b => b.content || '')].join('\n'); const tokenEstimate = this.estimateTokens(textConcat); const sizeInBytes = Buffer.byteLength(textConcat, 'utf8'); const truncationApplied = !!input.tokenBudget?.maxContextTokens && droppedBlocks > 0; base.tokenEstimate = tokenEstimate; const redactions = Object.entries(redactionTally).map(([type, count]) => ({ type, count })); base.telemetry = { sizeInBytes, tokenEstimate, truncationApplied, droppedBlocks, redactions, ...(corr ? { correlationId: corr } : {}) }; // Build a compact markdown preview const preview = [ '### Optivise Context Preview', '', (base.tags || []).join(' '), '', '---', ...contextBlocks.slice(0, 4).map(b => `#### ${b.title || b.type}\n\n${this.sanitize(b.content).slice(0, 1000)}`) ].join('\n'); base.previewMarkdown = preview; } catch (err) { // ignore telemetry build failures } return base; } static buildSystemPrompt(input) { const productTag = (input.products && input.products.length > 0) ? `Target products: ${input.products.join(', ')}.` : 'Target products: (unspecified).'; const intent = input.promptContext?.userIntent || 'unknown'; const summaryLine = input.summary ? `Summary: ${input.summary}` : 'Summary: Provide accurate, actionable, product-aware guidance for Optimizely developers.'; return [ 'You are an expert Optimizely assistant. Optimize responses for clarity and actionability.', `Tool: ${input.toolName}.`, productTag, `Intent: ${intent}.`, summaryLine ].join(' '); } static estimateTokens(text) { // Rough token estimate (~4 chars per token) const len = (text || '').length; return Math.max(1, Math.ceil(len / 4)); } // Basic output sanitization: remove dangerous HTML tags; this is conservative for markdown contexts static sanitize(text) { if (!text) return text; // Remove dangerous tags and attributes, block data URIs, and mask suspicious tokens let safe = text .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '') .replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, '') .replace(/<iframe[\s\S]*?>[\s\S]*?<\/iframe>/gi, '') .replace(/<object[\s\S]*?>[\s\S]*?<\/object>/gi, '') .replace(/<embed[\s\S]*?>[\s\S]*?<\/embed>/gi, '') .replace(/on\w+\s*=\s*"[^"]*"/gi, '') .replace(/on\w+\s*=\s*'[^']*'/gi, '') .replace(/javascript:/gi, '') .replace(/data:\w+\/[\-\w+.]+;base64,[A-Za-z0-9+/=]+/gi, '[DATA_URI_REDACTED]') .replace(/([A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]{20,})?)/g, '[TOKEN_REDACTED]') .replace(/(sk-[A-Za-z0-9]{20,})/gi, '[API_KEY_REDACTED]'); // Collapse excessive whitespace safe = safe.replace(/\n{3,}/g, '\n\n'); return safe; } } //# sourceMappingURL=request-formatter.js.map