personal-dash-cl/claudash/src/services/claudeService.js
2026-02-14 02:53:26 -05:00

339 lines
No EOL
11 KiB
JavaScript

import {
ANTHROPIC_API_URL,
DEFAULT_CLAUDE_MODEL,
MAX_TOKENS
} from '../utils/constants';
import { condenseSourcesForClaude } from './sourceParser';
/**
* Make a request to Claude API
* @param {string} prompt - The prompt to send
* @param {Array} messages - Optional message history
* @returns {Promise<string>} Claude's response
*/
async function callClaude(prompt, messages = []) {
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
console.log('=== callClaude function ===');
console.log('API Key configured:', !!apiKey);
console.log('API Key starts with:', apiKey?.substring(0, 15) + '...');
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
console.error('Anthropic API key not configured');
return 'Please configure your Anthropic API key in .env.local';
}
const model = import.meta.env.REACT_APP_CLAUDE_MODEL || DEFAULT_CLAUDE_MODEL;
console.log('Using model:', model);
console.log('Max tokens:', MAX_TOKENS);
console.log('Prompt length:', prompt.length);
console.log('Previous messages:', messages.length);
try {
const requestBody = {
apiKey: apiKey,
model: model,
maxTokens: MAX_TOKENS,
messages: [
...messages,
{ role: 'user', content: prompt }
]
};
console.log('Sending request to proxy...');
console.log('Request messages:', requestBody.messages.map(m => ({ role: m.role, contentLength: m.content.length })));
// Use local proxy server instead of direct API call
const response = await fetch('http://localhost:3001/api/claude', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
console.log('Proxy response status:', response.status);
if (!response.ok) {
const error = await response.text();
console.error('Proxy error response:', error);
throw new Error(`Claude API error: ${response.status} - ${error}`);
}
const data = await response.json();
console.log('Response data structure:', Object.keys(data));
console.log('Response content:', data.content?.[0]?.text?.substring(0, 200) + '...');
console.log('Usage data:', data.usage);
return {
text: data.content[0].text,
usage: data.usage // { input_tokens, output_tokens }
};
} catch (error) {
console.error('Error calling Claude:', error);
throw error;
}
}
/**
* Build briefing prompt with memory
* @param {Object} currentData - Current source data
* @param {Object} previousBriefing - Previous briefing object (condensed summary)
* @returns {string} The prompt
*/
function buildBriefingPrompt(currentData, previousBriefing) {
const customInstructions = import.meta.env.REACT_APP_CLAUDE_INSTRUCTIONS;
let prompt = '';
// Prepend custom instructions if provided
if (customInstructions && customInstructions.trim()) {
prompt += `CUSTOM INSTRUCTIONS: ${customInstructions}\n\n`;
}
prompt += `You are creating a personalized daily briefing. Be conversational and highlight what's interesting.
CURRENT DATA:
`;
// CONDENSE DATA BEFORE SENDING TO CLAUDE
const condensedData = condenseSourcesForClaude(currentData.sources || currentData);
console.log('Original data size:', JSON.stringify(currentData).length);
console.log('Condensed data size:', JSON.stringify(condensedData).length);
console.log('Size reduction:', Math.round((1 - JSON.stringify(condensedData).length / JSON.stringify(currentData).length) * 100) + '%');
// Add all condensed source data
Object.entries(condensedData).forEach(([source, data]) => {
if (data && !data.error) {
prompt += `\n${source.toUpperCase()}:\n${JSON.stringify(data, null, 2)}\n`;
}
});
// Add historical context if available (now using condensed summary)
if (previousBriefing && previousBriefing.condensedSummary) {
prompt += `\n\nPREVIOUS BRIEFING SUMMARY:\n${previousBriefing.condensedSummary}\n`;
prompt += `\nHighlight what's changed, any follow-up stories, or interesting trends.`;
}
prompt += `\n\nCreate a friendly briefing (300-400 words) that:
- Starts with a warm greeting appropriate for the time of day
- Highlights the most interesting items from the data
- Notes anything that's changed since yesterday (if applicable)
- Has personality and isn't just a dry list
- Includes relevant weather information if available
- Ends with something engaging or thought-provoking
Focus on the most newsworthy and interesting items. Don't try to mention everything.
IMPORTANT: Provide your response as valid JSON with exactly two fields:
{
"fullBriefing": "Your complete 300-400 word friendly briefing text here",
"condensedSummary": "A 50-75 word ultra-condensed summary of the key topics and main stories for historical context"
}
The JSON must be properly formatted with escaped quotes where necessary.`;
return prompt;
}
/**
* Generate a condensed summary of a briefing for historical storage
* @param {string} fullBriefing - The full briefing text
* @returns {Promise<string>} Condensed summary (50-75 words)
*/
async function generateCondensedSummary(fullBriefing) {
const prompt = `Create an ultra-condensed summary of this briefing in 50-75 words.
Focus only on the key topics and main stories mentioned.
This will be used as historical context for tomorrow's briefing generation.
FULL BRIEFING:
${fullBriefing}
Provide ONLY the condensed summary, no preamble or explanation.`;
try {
const result = await callClaude(prompt);
return result.text;
} catch (error) {
console.error('Error generating condensed summary:', error);
// Fallback to first 300 chars of original
return fullBriefing.substring(0, 300) + '...';
}
}
/**
* Generate daily briefing with condensed summary
* @param {Object} currentData - Current data from all sources
* @param {Object} previousBriefing - Previous briefing for comparison
* @returns {Promise<Object>} Object containing full briefing and condensed summary
*/
export async function generateBriefing(currentData, previousBriefing) {
if (!currentData || Object.keys(currentData.sources || currentData).length === 0) {
return {
text: 'No data available to generate briefing. Please check your data sources configuration.',
condensedSummary: 'No data available.'
};
}
try {
const prompt = buildBriefingPrompt(currentData, previousBriefing);
const result = await callClaude(prompt);
// Parse JSON response that now includes both fullBriefing and condensedSummary
try {
// Strip markdown code blocks if present
let jsonText = result.text;
if (jsonText.includes('```json')) {
jsonText = jsonText.replace(/```json\s*/g, '').replace(/```\s*/g, '');
} else if (jsonText.includes('```')) {
jsonText = jsonText.replace(/```\s*/g, '');
}
jsonText = jsonText.trim();
const parsed = JSON.parse(jsonText);
// Validate the JSON structure
if (!parsed.fullBriefing || !parsed.condensedSummary) {
throw new Error('Invalid response structure from Claude');
}
console.log('Successfully parsed briefing with condensed summary');
console.log('Condensed summary:', parsed.condensedSummary.substring(0, 100) + '...');
return {
text: parsed.fullBriefing,
condensedSummary: parsed.condensedSummary,
usage: result.usage
};
} catch (parseError) {
console.error('Failed to parse JSON response, attempting fallback:', parseError);
// Fallback: if JSON parsing fails, use the raw text and generate summary separately
// This maintains backward compatibility but should rarely happen
console.warn('Using fallback: generating condensed summary separately');
const condensedSummary = await generateCondensedSummary(result.text);
return {
text: result.text,
condensedSummary: condensedSummary,
usage: result.usage
};
}
} catch (error) {
console.error('Error generating briefing:', error);
// Return a fallback briefing
const fallbackText = `Good morning! I encountered an issue generating your personalized briefing today.
Error: ${error.message}
Please check your API key configuration and try again.`;
return {
text: fallbackText,
condensedSummary: `Error: ${error.message}`
};
}
}
/**
* Send a chat message to Claude
* @param {string} message - User's message
* @param {Array} conversationHistory - Previous messages
* @returns {Promise<string>} Claude's response
*/
export async function sendChatMessage(message, conversationHistory = []) {
if (!message || !message.trim()) {
throw new Error('Message cannot be empty');
}
// Build context from conversation history
const messages = conversationHistory.map(msg => ({
role: msg.role === 'user' ? 'user' : 'assistant',
content: msg.content
}));
// Add context about the dashboard
const contextPrompt = `You are a helpful assistant embedded in a personal dashboard application.
You have access to the user's daily briefing data and can help answer questions about news, weather,
and other information sources configured in their dashboard. Be conversational and helpful.
User's message: ${message}`;
try {
const result = await callClaude(contextPrompt, messages);
return result.text; // For chat, just return text for now
} catch (error) {
console.error('Error in chat:', error);
return `I apologize, but I encountered an error: ${error.message}. Please try again.`;
}
}
/**
* Extract meaningful content from HTML using Claude
* @param {string} html - The HTML content
* @param {string} url - The source URL
* @returns {Promise<Object>} Extracted content
*/
export async function extractContentFromHTML(html, url) {
const prompt = `Extract the main content from this HTML page.
Focus on the primary article, news content, or main information.
Ignore navigation, ads, and other peripheral content.
URL: ${url}
HTML (truncated to first 10000 chars):
${html.substring(0, 10000)}
Please provide a JSON response with:
- title: The main title
- summary: A brief summary (2-3 sentences)
- mainContent: The main text content (up to 500 words)
- metadata: Any relevant metadata (author, date, etc.)
Respond with valid JSON only.`;
try {
const response = await callClaude(prompt);
// Try to parse as JSON
try {
return JSON.parse(response);
} catch {
// If not valid JSON, return structured response
return {
title: 'Content from ' + url,
summary: response.substring(0, 200),
mainContent: response,
metadata: { url, extractedAt: Date.now() }
};
}
} catch (error) {
console.error('Error extracting HTML content:', error);
return {
error: true,
message: error.message,
url: url
};
}
}
/**
* Generate a summary of specific data
* @param {Object} data - Data to summarize
* @param {string} dataType - Type of data (reddit, news, etc.)
* @returns {Promise<string>} Summary
*/
export async function summarizeData(data, dataType) {
const prompt = `Summarize this ${dataType} data in 2-3 sentences, highlighting the most interesting or important items:
${JSON.stringify(data, null, 2)}
Be concise and focus on what would be most relevant to someone checking their daily news.`;
try {
return await callClaude(prompt);
} catch (error) {
console.error(`Error summarizing ${dataType}:`, error);
return `Unable to summarize ${dataType} data.`;
}
}