Skip to content

Commit c492d31

Browse files
Implement core AI Journal Analysis Engine with OpenRouter integration
Co-authored-by: gitcoder89431 <211172822+gitcoder89431@users.noreply.github.com>
1 parent 0c24b79 commit c492d31

File tree

3 files changed

+215
-8
lines changed

3 files changed

+215
-8
lines changed

src/lib/components/panels/GuardianPanel.svelte

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ThemeSelector from '../ui/ThemeSelector.svelte';
44
import { User, Key, Settings, CheckCircle, AlertCircle } from 'lucide-svelte';
55
import { guardianStore, guardianHelpers } from '$lib/stores/guardian.js';
6+
import { aiAnalysisHelpers } from '$lib/stores/ai-analysis.js';
67
78
let apiKeyInput = '';
89
let guardianName = '';
@@ -33,17 +34,25 @@
3334
apiKeyStatus = 'checking';
3435
3536
try {
36-
const response = await fetch('/api/ai/validate', {
37-
method: 'POST',
38-
headers: { 'Content-Type': 'application/json' },
39-
body: JSON.stringify({ apiKey: apiKeyInput })
40-
});
41-
42-
if (response.ok) {
37+
// First try the AI analysis helper for more direct validation
38+
const isValid = await aiAnalysisHelpers.testConnection();
39+
if (isValid) {
4340
apiKeyStatus = 'valid';
4441
guardianHelpers.updateApiKey(apiKeyInput);
4542
} else {
46-
apiKeyStatus = 'invalid';
43+
// Fallback to backend validation if direct test fails
44+
const response = await fetch('/api/ai/validate', {
45+
method: 'POST',
46+
headers: { 'Content-Type': 'application/json' },
47+
body: JSON.stringify({ apiKey: apiKeyInput })
48+
});
49+
50+
if (response.ok) {
51+
apiKeyStatus = 'valid';
52+
guardianHelpers.updateApiKey(apiKeyInput);
53+
} else {
54+
apiKeyStatus = 'invalid';
55+
}
4756
}
4857
} catch (error) {
4958
apiKeyStatus = 'invalid';

src/lib/stores/ai-analysis.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { writable } from 'svelte/store';
2+
import { AIAnalyzer, type AnalysisResult } from '$lib/utils/ai-analysis';
3+
import { guardianStore } from './guardian';
4+
import type { Pet } from '../types/Pet';
5+
import type { JournalEntry } from '../types/JournalEntry';
6+
7+
export const analysisStore = writable<Record<string, AnalysisResult>>({});
8+
export const isAnalyzing = writable(false);
9+
10+
let analyzer: AIAnalyzer | null = null;
11+
12+
// Initialize analyzer when API key is available
13+
guardianStore.subscribe(guardian => {
14+
if (guardian.apiKey && guardian.apiKeyValid) {
15+
analyzer = new AIAnalyzer(guardian.apiKey);
16+
} else {
17+
analyzer = null;
18+
}
19+
});
20+
21+
export const aiAnalysisHelpers = {
22+
async analyzeEntry(pet: Pet, entry: JournalEntry): Promise<AnalysisResult | null> {
23+
if (!analyzer) {
24+
throw new Error('API key not configured');
25+
}
26+
27+
isAnalyzing.set(true);
28+
29+
try {
30+
const result = await analyzer.analyzeJournalEntry(pet, entry);
31+
32+
// Cache the result
33+
analysisStore.update(cache => ({
34+
...cache,
35+
[entry.id]: result
36+
}));
37+
38+
return result;
39+
} catch (error) {
40+
console.error('Analysis failed:', error);
41+
return null;
42+
} finally {
43+
isAnalyzing.set(false);
44+
}
45+
},
46+
47+
async testConnection(): Promise<boolean> {
48+
if (!analyzer) return false;
49+
return analyzer.testConnection();
50+
},
51+
52+
getAnalysis(entryId: string): AnalysisResult | null {
53+
let result = null;
54+
analysisStore.subscribe(cache => {
55+
result = cache[entryId] || null;
56+
})();
57+
return result;
58+
}
59+
};

src/lib/utils/ai-analysis.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { Pet } from '../types/Pet.js';
2+
import type { JournalEntry } from '../types/JournalEntry.js';
3+
4+
export interface AnalysisResult {
5+
summary: string;
6+
moodTrend: 'improving' | 'stable' | 'concerning';
7+
activityLevel: 'low' | 'normal' | 'high';
8+
healthConcerns: string[];
9+
recommendations: string[];
10+
nextCheckupSuggestion?: string;
11+
}
12+
13+
export class AIAnalyzer {
14+
private apiKey: string;
15+
private baseUrl = 'https://openrouter.ai/api/v1/chat/completions';
16+
17+
constructor(apiKey: string) {
18+
this.apiKey = apiKey;
19+
}
20+
21+
async analyzeJournalEntry(pet: Pet, entry: JournalEntry): Promise<AnalysisResult> {
22+
const prompt = this.buildAnalysisPrompt(pet, entry);
23+
24+
try {
25+
const response = await fetch(this.baseUrl, {
26+
method: 'POST',
27+
headers: {
28+
'Authorization': `Bearer ${this.apiKey}`,
29+
'Content-Type': 'application/json',
30+
'HTTP-Referer': window.location.origin,
31+
'X-Title': 'Petalytics'
32+
},
33+
body: JSON.stringify({
34+
model: 'anthropic/claude-3.5-sonnet',
35+
messages: [
36+
{
37+
role: 'system',
38+
content: 'You are a veterinary AI assistant specialized in pet health analysis. Provide helpful, accurate insights while encouraging professional veterinary care for serious concerns.'
39+
},
40+
{
41+
role: 'user',
42+
content: prompt
43+
}
44+
],
45+
max_tokens: 500,
46+
temperature: 0.7
47+
})
48+
});
49+
50+
if (!response.ok) {
51+
throw new Error(`API Error: ${response.status}`);
52+
}
53+
54+
const data = await response.json();
55+
return this.parseAnalysisResponse(data.choices[0].message.content);
56+
} catch (error) {
57+
console.error('AI Analysis Error:', error);
58+
throw error;
59+
}
60+
}
61+
62+
private buildAnalysisPrompt(pet: Pet, entry: JournalEntry): string {
63+
const recentEntries = pet.journalEntries
64+
?.slice(-5)
65+
.map((e: JournalEntry) => `${e.date}: ${e.content}`)
66+
.join('\n') || 'No previous entries available';
67+
68+
return `
69+
Analyze this journal entry for ${pet.name}, a ${pet.age || 'unknown age'}-year-old ${pet.breed || pet.species}:
70+
71+
LATEST ENTRY: "${entry.content}"
72+
73+
RECENT HISTORY:
74+
${recentEntries}
75+
76+
PET INFO:
77+
- Breed: ${pet.breed || pet.species}
78+
- Age: ${pet.age || 'unknown'} years
79+
- Gender: ${pet.gender || 'unknown'}
80+
81+
Please provide analysis in this JSON format:
82+
{
83+
"summary": "Brief summary of the entry",
84+
"moodTrend": "improving|stable|concerning",
85+
"activityLevel": "low|normal|high",
86+
"healthConcerns": ["concern1", "concern2"],
87+
"recommendations": ["rec1", "rec2"],
88+
"nextCheckupSuggestion": "When to see vet (if needed)"
89+
}
90+
91+
Consider breed-specific traits, age-related needs, and behavioral patterns. Keep recommendations practical and encouraging.`;
92+
}
93+
94+
private parseAnalysisResponse(content: string): AnalysisResult {
95+
try {
96+
// Extract JSON from response (in case there's extra text)
97+
const jsonMatch = content.match(/\{[\s\S]*\}/);
98+
if (jsonMatch) {
99+
return JSON.parse(jsonMatch[0]);
100+
}
101+
102+
// Fallback parsing if JSON format isn't perfect
103+
return {
104+
summary: content.substring(0, 100) + '...',
105+
moodTrend: 'stable',
106+
activityLevel: 'normal',
107+
healthConcerns: [],
108+
recommendations: ['Continue monitoring your pet\'s behavior'],
109+
nextCheckupSuggestion: undefined
110+
};
111+
} catch (error) {
112+
console.error('Error parsing AI response:', error);
113+
throw new Error('Failed to parse AI analysis');
114+
}
115+
}
116+
117+
async testConnection(): Promise<boolean> {
118+
try {
119+
const response = await fetch(this.baseUrl, {
120+
method: 'POST',
121+
headers: {
122+
'Authorization': `Bearer ${this.apiKey}`,
123+
'Content-Type': 'application/json',
124+
'HTTP-Referer': window.location.origin,
125+
'X-Title': 'Petalytics'
126+
},
127+
body: JSON.stringify({
128+
model: 'openai/gpt-3.5-turbo',
129+
messages: [{ role: 'user', content: 'test' }],
130+
max_tokens: 1
131+
})
132+
});
133+
134+
return response.ok;
135+
} catch (error) {
136+
return false;
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)