From b5a3e68a4c20c7ced0c4e0b524939f5efbfeb71b Mon Sep 17 00:00:00 2001 From: rixabhh <143704619+rixabhh@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:45:17 +0000 Subject: [PATCH] feat: improve AI pipeline robustness, add OpenRouter and Grok support - Rewrite analysis prompts for deeper emotional insight - Add robust JSON schema validation and retry logic for LLM responses - Add OpenRouter, Grok, and Anthropic provider integrations - Add client-side validation for specific AI provider API key formats - Render new coaching advice and growth area insights on the dashboard - Handle malformed LLM JSON responses and timeouts gracefully --- .github/issues/feature_request.md | 13 ++ .github/issues/provider_integration.md | 13 ++ .jules/scribe.md | 5 + dashboard.html | 14 +- functions/api/analyze.js | 208 ++++++++++++++++++++++--- static/js/app.js | 66 ++++++-- static/js/dashboard.js | 4 + 7 files changed, 287 insertions(+), 36 deletions(-) create mode 100644 .github/issues/feature_request.md create mode 100644 .github/issues/provider_integration.md create mode 100644 .jules/scribe.md diff --git a/.github/issues/feature_request.md b/.github/issues/feature_request.md new file mode 100644 index 0000000..e0a8756 --- /dev/null +++ b/.github/issues/feature_request.md @@ -0,0 +1,13 @@ +Title: Add Follow-Up Conversational Interface to Insights + +## Why this feature matters +Currently, the AI analysis is a static, one-time read. Users often have questions about specific red flags, want elaboration on their coaching advice, or want to ask specific context about their chat data. Making the report interactive will significantly increase user retention and session time. + +## Rough implementation approach +- Add a new "Ask The Algorithm" chat input box below the Deep Insights section. +- Create a new backend endpoint `/api/followup` that accepts the original `stats`, the initial `report`, and the user's `question`. +- Pass these as context to the LLM (using the same provider abstraction logic) and stream the response back to the UI. +- Ensure the prompt maintains the persona chosen by the user (Playful, Balanced, or Direct). + +## User benefit +Allows users to treat their chat analysis as a personalized relationship coach rather than just a one-off report. It deepens emotional engagement and makes the product significantly more shareable and valuable. diff --git a/.github/issues/provider_integration.md b/.github/issues/provider_integration.md new file mode 100644 index 0000000..bbeab15 --- /dev/null +++ b/.github/issues/provider_integration.md @@ -0,0 +1,13 @@ +Title: Add Mistral AI Support for Privacy-Focused Processing + +## Why this provider matters +The Algorithm's entire branding and architecture centers around being "paranoid-level privacy-first". Mistral is a European AI alternative known for strong open-weight models and better privacy alignment than OpenAI or Google. Supporting it naturally fits the product's ethos and gives users another option for BYOK. + +## Rough implementation approach +- Add `mistral` to the `providers` list in the UI dropdown (`index.html` or settings modal). +- Implement basic client-side API key validation (Mistral keys typically start with a specific format or are alphanumeric). +- Add a new block in `functions/api/analyze.js`'s `callLLM` function to make a POST request to `https://api.mistral.ai/v1/chat/completions`. +- Ensure JSON parsing is handled gracefully since Mistral might have slightly different output tendencies. + +## User benefit +Enhances trust among privacy-conscious users and developers. Provides access to fast, cost-effective models like `mistral-small` or `mistral-large` for analysis without data entering US-based corporate LLM pipelines. diff --git a/.jules/scribe.md b/.jules/scribe.md new file mode 100644 index 0000000..4e7e590 --- /dev/null +++ b/.jules/scribe.md @@ -0,0 +1,5 @@ +## 2024-05-24 — Initial Setup +**Discovery:** Need to set up scribe journal and improve prompt structure for different providers. +**Provider:** All +**Impact:** Will allow better error handling, consistent JSON responses, and more engaging outputs. +**Pattern:** Provide explicit JSON schemas and format instructions per provider. \ No newline at end of file diff --git a/dashboard.html b/dashboard.html index 2f0f61e..eebcd4e 100644 --- a/dashboard.html +++ b/dashboard.html @@ -136,16 +136,26 @@

-
+

🚩 RED FLAGS

    -

    🚩 GREEN FLAGS

    +

    ✅ GREEN FLAGS

      +
      +
      +

      🌱 GROWTH AREAS

      +
        +
        +
        +

        🗣️ COACHING ADVICE

        +

        +
        +

        ⚖️ THE FINAL WORD

        "..."
        diff --git a/functions/api/analyze.js b/functions/api/analyze.js index c3a53a3..2c6d678 100644 --- a/functions/api/analyze.js +++ b/functions/api/analyze.js @@ -16,35 +16,192 @@ export async function onRequestPost(context) { await env.KV_RATELIMIT.put(limitKey, (count + 1).toString(), { expirationTtl: 3600 }); } - // 2. AI GENERATION - let report = null; - const systemPrompt = "You are 'The Algorithm', a brutally honest relationship analyst. Return ONLY a JSON object: { relationship_persona, compatibility_score, ai_insight: { dynamic_title, reality_check, recent_shift, red_flags: [], green_flags: [], brutal_verdict } }."; + // 2. AI GENERATION SETTINGS + const ANALYSIS_SCHEMA = { + "relationship_persona": "string (creative title)", + "compatibility_score": "integer 1-100", + "ai_insight": { + "dynamic_title": "string (short headline)", + "reality_check": "string (1-2 sentences)", + "recent_shift": "string (1-2 sentences)", + "red_flags": ["string"], + "green_flags": ["string"], + "brutal_verdict": "string (short impactful summary)", + "coaching_advice": "string (2-3 actionable sentences)", + "growth_areas": ["string"] + } + }; + + const PROVIDER_SYSTEM_PROMPTS = { + "openai": `You are an expert relationship analyst and communication coach. You provide brutally honest but helpful insights.`, + "anthropic": `You are an expert relationship analyst and communication coach. You provide brutally honest but helpful insights.`, + "gemini": `You are an expert relationship analyst and communication coach. You provide brutally honest but helpful insights. 1. Be direct. 2. Follow schema exactly.`, + "openrouter": `You are an expert relationship analyst and communication coach. You provide brutally honest but helpful insights.`, + "cloudflare": `You are an expert relationship analyst and communication coach. You provide brutally honest but helpful insights.`, + "grok": `You are an expert relationship analyst and communication coach. You provide brutally honest but helpful insights.`, + "groq": `You are an expert relationship analyst and communication coach. You provide brutally honest but helpful insights.` + }; + + const ANALYSIS_PROMPT_TEMPLATE = ` +Analyze these anonymous conversation statistics and provide deep behavioral insights. Tone: ${tone}. + +## Statistics +{stats_json} + +## Relationship Context +- Person A: ${my_name} +- Person B: ${partner_name} + +## Required Output +Respond ONLY with valid JSON matching this exact schema. No preamble, no explanation, no markdown blocks. + +${JSON.stringify(ANALYSIS_SCHEMA, null, 2)} +`; - let userPrompt = `Analyze chat: ${my_name} & ${partner_name}. Tone: ${tone}. Stats: ${JSON.stringify(stats)}.`; + let statsJson = JSON.stringify(stats); if (compare_data) { - userPrompt = `COMPARE two chats for ${my_name}. Chat A: ${compare_data.nameA} vs Chat B: ${compare_data.nameB}. Stats A: ${JSON.stringify(compare_data.a)}. Stats B: ${JSON.stringify(compare_data.b)}. Be direct.`; + statsJson = `COMPARE two chats for ${my_name}. Chat A: ${compare_data.nameA} vs Chat B: ${compare_data.nameB}. Stats A: ${JSON.stringify(compare_data.a)}. Stats B: ${JSON.stringify(compare_data.b)}.`; } - if (provider === 'cloudflare' && env.AI) { - const aiResult = await env.AI.run('@cf/meta/llama-3-8b-instruct', { - messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }] - }); - const match = aiResult.response.match(/\{[\s\S]*\}/); - if (match) report = JSON.parse(match[0]); - } else if (api_key && (provider === 'openai' || provider === 'groq')) { - const url = provider === 'openai' ? 'https://api.openai.com/v1/chat/completions' : 'https://api.groq.com/openai/v1/chat/completions'; - const model = provider === 'openai' ? 'gpt-4o-mini' : 'llama-3.1-70b-versatile'; - const resp = await fetch(url, { - method: 'POST', - headers: { 'Authorization': `Bearer ${api_key}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }], response_format: { type: "json_object" } }) - }); - const resData = await resp.json(); - if (resData.choices) report = JSON.parse(resData.choices[0].message.content); + const systemPrompt = PROVIDER_SYSTEM_PROMPTS[provider] || PROVIDER_SYSTEM_PROMPTS["openai"]; + const userPrompt = ANALYSIS_PROMPT_TEMPLATE.replace('{stats_json}', statsJson); + + let report = null; + + // Validation Function + const validateAnalysisResponse = (response) => { + if (!response) return false; + const requiredKeys = ['relationship_persona', 'compatibility_score', 'ai_insight']; + const hasOuter = requiredKeys.every(key => key in response); + if (!hasOuter) return false; + const requiredInsightKeys = ['dynamic_title', 'reality_check', 'recent_shift', 'red_flags', 'green_flags', 'brutal_verdict', 'coaching_advice', 'growth_areas']; + const hasInner = requiredInsightKeys.every(key => key in response.ai_insight); + return hasInner; + }; + + // LLM Caller + const callLLM = async (currentProvider, apiKey, sysPrompt, usrPrompt, strict = false) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); + + let finalUserPrompt = usrPrompt; + if (strict) { + finalUserPrompt += "\n\nCRITICAL: You MUST return ONLY valid JSON. No markdown backticks, no text before or after."; + } + + try { + let parsed = null; + + if (currentProvider === 'cloudflare' && env.AI) { + const aiResult = await env.AI.run('@cf/meta/llama-3-8b-instruct', { + messages: [{ role: 'system', content: sysPrompt }, { role: 'user', content: finalUserPrompt }] + }); + const match = aiResult.response.match(/\{[\s\S]*\}/); + if (match) parsed = JSON.parse(match[0]); + } else if (currentProvider === 'anthropic' && apiKey) { + const resp = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json' + }, + body: JSON.stringify({ + model: 'claude-3-haiku-20240307', + max_tokens: 1000, + system: sysPrompt, + messages: [{ role: 'user', content: finalUserPrompt }] + }), + signal: controller.signal + }); + const resData = await resp.json(); + if (resData.content && resData.content.length > 0) { + const text = resData.content[0].text; + const match = text.match(/\{[\s\S]*\}/); + if (match) parsed = JSON.parse(match[0]); + } + } else if (currentProvider === 'gemini' && apiKey) { + const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`; + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + systemInstruction: { parts: [{ text: sysPrompt }] }, + contents: [{ parts: [{ text: finalUserPrompt }] }], + generationConfig: { responseMimeType: "application/json" } + }), + signal: controller.signal + }); + const resData = await resp.json(); + if (resData.candidates && resData.candidates.length > 0) { + const text = resData.candidates[0].content.parts[0].text; + const match = text.match(/\{[\s\S]*\}/); + if (match) parsed = JSON.parse(match[0]); + } + } else if (apiKey && (currentProvider === 'openai' || currentProvider === 'grok' || currentProvider === 'openrouter' || currentProvider === 'groq')) { + let url, model; + if (currentProvider === 'openai') { + url = 'https://api.openai.com/v1/chat/completions'; + model = 'gpt-4o-mini'; + } else if (currentProvider === 'openrouter') { + url = 'https://openrouter.ai/api/v1/chat/completions'; + model = 'openai/gpt-4o-mini'; + } else if (currentProvider === 'grok') { + url = 'https://api.x.ai/v1/chat/completions'; + model = 'grok-beta'; + } else { + url = 'https://api.groq.com/openai/v1/chat/completions'; + model = 'llama-3.1-70b-versatile'; + } + + const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }; + if (currentProvider === 'openrouter') { + headers['HTTP-Referer'] = 'https://thealgorithm.reports'; + headers['X-Title'] = 'The Algorithm'; + } + + const body = { + model, + messages: [{ role: 'system', content: sysPrompt }, { role: 'user', content: finalUserPrompt }] + }; + + if (currentProvider !== 'openrouter') { + body.response_format = { type: "json_object" }; + } + + const resp = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(body), + signal: controller.signal + }); + const resData = await resp.json(); + if (resData.choices && resData.choices.length > 0) { + const text = resData.choices[0].message.content; + const match = text.match(/\{[\s\S]*\}/); + if (match) parsed = JSON.parse(match[0]); + } + } + clearTimeout(timeoutId); + return parsed; + } catch (err) { + clearTimeout(timeoutId); + let safeKey = apiKey ? apiKey.substring(0, 4) + '...' : 'none'; + console.error(`LLM call failed for ${currentProvider} with key ${safeKey}: ${err.message}`); + return null; + } + }; + + // 3. EXECUTE & VALIDATE + report = await callLLM(provider, api_key, systemPrompt, userPrompt, false); + + if (!validateAnalysisResponse(report)) { + // Retry once with stricter prompt + report = await callLLM(provider, api_key, systemPrompt, userPrompt, true); } - // 3. FALLBACK - if (!report) { + // 4. FALLBACK + if (!validateAnalysisResponse(report)) { report = { relationship_persona: "Vibe Explorer", compatibility_score: 80, @@ -54,7 +211,9 @@ export async function onRequestPost(context) { recent_shift: "The energy is stable.", red_flags: ["Limited data for deep read"], green_flags: ["Active check-ins"], - brutal_verdict: "It's a vibe." + brutal_verdict: "It's a vibe.", + coaching_advice: "Keep communicating openly and building trust.", + growth_areas: ["More frequent deep conversations"] } }; } @@ -65,4 +224,3 @@ export async function onRequestPost(context) { return new Response(JSON.stringify({ error: e.message }), { status: 500 }); } } - diff --git a/static/js/app.js b/static/js/app.js index 14aa667..9621a31 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -147,14 +147,6 @@ document.addEventListener('DOMContentLoaded', () => { } llmProviderEl?.addEventListener('change', (e) => updateProviderHint(e.target.value)); - saveSettingsBtn?.addEventListener('click', () => { - const key = apiKeyEl ? apiKeyEl.value.trim() : ''; - sessionStorage.setItem('_llm_token', btoa(key)); - localStorage.setItem('hf_url', hfUrlEl ? hfUrlEl.value.trim() : ''); - localStorage.setItem('llm_provider', llmProviderEl ? llmProviderEl.value : 'cloudflare'); - updateApiKeyUI(); - hideModal(settingsModal); - }); } // --- UI Utilities --- @@ -182,11 +174,67 @@ document.addEventListener('DOMContentLoaded', () => { const hintEl = document.getElementById('providerHint'); const hfContainer = document.getElementById('hfUrlContainer'); if (!hintEl) return; - const hints = { 'cloudflare': 'Free Tier (2 reports/hr)', 'openai': 'sk-proj-...', 'anthropic': 'sk-ant-...', 'gemini': 'Google API Key' }; + const hints = { + 'cloudflare': 'Free Tier (2 reports/hr)', + 'openai': 'sk-proj-...', + 'anthropic': 'sk-ant-...', + 'gemini': '39-char Google API Key', + 'openrouter': 'sk-or-v1-...', + 'groq': 'gsk_...', + 'grok': 'xai-...' + }; hintEl.textContent = hints[provider] || ''; if (hfContainer) hfContainer.classList.toggle('hidden', provider !== 'huggingface'); } + const validateApiKeyFormat = (provider, key) => { + if (!key || key.trim() === '') return provider === 'cloudflare'; + if (provider === 'openai' && !key.startsWith('sk-')) return false; + if (provider === 'anthropic' && !key.startsWith('sk-ant-')) return false; + if (provider === 'openrouter' && !key.startsWith('sk-or-')) return false; + if (provider === 'groq' && !key.startsWith('gsk_')) return false; + if (provider === 'grok' && !key.startsWith('xai-')) return false; + if (provider === 'gemini' && key.length !== 39) return false; + return true; + }; + + if (settingsBtn && settingsModal) { + settingsBtn.addEventListener('click', () => { + showModal(settingsModal); + document.getElementById('llmProvider')?.focus(); + }); + closeSettings?.addEventListener('click', () => hideModal(settingsModal)); + + const apiKeyEl = document.getElementById('apiKey'); + const hfUrlEl = document.getElementById('hfUrl'); + const llmProviderEl = document.getElementById('llmProvider'); + + if (apiKeyEl) apiKeyEl.value = sessionStorage.getItem('_llm_token') ? atob(sessionStorage.getItem('_llm_token')) : ''; + if (hfUrlEl) hfUrlEl.value = localStorage.getItem('hf_url') || ''; + const savedProvider = localStorage.getItem('llm_provider') || 'cloudflare'; + if (llmProviderEl) { + llmProviderEl.value = savedProvider; + updateProviderHint(savedProvider); + } + llmProviderEl?.addEventListener('change', (e) => updateProviderHint(e.target.value)); + + saveSettingsBtn?.addEventListener('click', () => { + const key = apiKeyEl ? apiKeyEl.value.trim() : ''; + const provider = llmProviderEl ? llmProviderEl.value : 'cloudflare'; + + if (provider !== 'cloudflare' && !validateApiKeyFormat(provider, key)) { + alert(`Invalid API key format for ${provider}. Please check and try again.`); + return; + } + + sessionStorage.setItem('_llm_token', btoa(key)); + localStorage.setItem('hf_url', hfUrlEl ? hfUrlEl.value.trim() : ''); + localStorage.setItem('llm_provider', provider); + updateApiKeyUI(); + hideModal(settingsModal); + }); + } + const showError = (message) => { const container = document.getElementById('errorContainer'); if (container) { diff --git a/static/js/dashboard.js b/static/js/dashboard.js index d554955..8c45729 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -262,8 +262,12 @@ document.addEventListener('DOMContentLoaded', () => { const red = document.getElementById('ai-insight-red-flags'); const green = document.getElementById('ai-insight-green-flags'); + const growth = document.getElementById('ai-insight-growth'); + const coaching = document.getElementById('ai-insight-coaching'); if (red) red.innerHTML = (report.ai_insight?.red_flags || []).map(f => `
      • ${f}
      • `).join(''); if (green) green.innerHTML = (report.ai_insight?.green_flags || []).map(f => `
      • ${f}
      • `).join(''); + if (growth) growth.innerHTML = (report.ai_insight?.growth_areas || []).map(f => `
      • ${f}
      • `).join(''); + if (coaching) coaching.textContent = report.ai_insight?.coaching_advice || ""; loading.classList.add('hidden'); results.classList.remove('hidden');