feat: Add Claude LLM support via Anthropic API and Infisical
This commit is contained in:
parent
294f10698e
commit
5892a7b734
|
|
@ -62,6 +62,11 @@ spec:
|
||||||
value: "+14085505485"
|
value: "+14085505485"
|
||||||
- name: GROUP_URL
|
- name: GROUP_URL
|
||||||
value: "https://chat.whatsapp.com/0Txfg7iZBbvIuvTbTqqgAX"
|
value: "https://chat.whatsapp.com/0Txfg7iZBbvIuvTbTqqgAX"
|
||||||
|
- name: ANTHROPIC_API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: trip-service-anthropic
|
||||||
|
key: api_key
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: app-volume
|
- name: app-volume
|
||||||
mountPath: /app
|
mountPath: /app
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: trip-service-anthropic
|
||||||
|
namespace: ai-agents
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "-1"
|
||||||
|
spec:
|
||||||
|
refreshInterval: 1h
|
||||||
|
secretStoreRef:
|
||||||
|
name: infisical
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
target:
|
||||||
|
name: trip-service-anthropic
|
||||||
|
creationPolicy: Owner
|
||||||
|
data:
|
||||||
|
- secretKey: api_key
|
||||||
|
remoteRef:
|
||||||
|
key: ANTHROPIC_API_KEY
|
||||||
|
|
@ -88,24 +88,61 @@ async function getTdotTraffic() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAiInsight(activityDetails, location, modelName = 'qwen2.5:0.5b') {
|
async function getAiInsight(activityDetails, location, trip) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const prompt = `Provide one short, clever tip or insight for someone visiting ${location} today, doing: ${activityDetails}. Keep it under 2 sentences.`;
|
const prompt = `Provide one short, clever tip or insight for someone visiting ${location} today, doing: ${activityDetails}. Keep it under 2 sentences.`;
|
||||||
const body = JSON.stringify({ model: modelName, prompt, stream: false });
|
|
||||||
const url = new URL(OLLAMA_URL);
|
if (trip && trip.llmProvider === 'anthropic') {
|
||||||
const req = http.request({
|
const modelName = trip.llmModel || 'claude-3-5-sonnet-20240620';
|
||||||
hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
|
const body = JSON.stringify({
|
||||||
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
model: modelName,
|
||||||
}, (res) => {
|
max_tokens: 150,
|
||||||
let d = '';
|
messages: [{ role: 'user', content: prompt }]
|
||||||
res.on('data', c => d += c);
|
|
||||||
res.on('end', () => {
|
|
||||||
try { resolve(JSON.parse(d).response.trim()); } catch { resolve('Have a great trip!'); }
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
req.on('error', () => resolve('Have a great trip!'));
|
const req = https.request({
|
||||||
req.write(body);
|
hostname: 'api.anthropic.com',
|
||||||
req.end();
|
path: '/v1/messages',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
}
|
||||||
|
}, (res) => {
|
||||||
|
let d = '';
|
||||||
|
res.on('data', c => d += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(d);
|
||||||
|
resolve(data.content[0].text.trim());
|
||||||
|
} catch { resolve('Have a great trip!'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', () => resolve('Have a great trip!'));
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Default to Ollama
|
||||||
|
const modelName = trip ? (trip.llmModel || 'qwen2.5:0.5b') : 'qwen2.5:0.5b';
|
||||||
|
const body = JSON.stringify({ model: modelName, prompt, stream: false });
|
||||||
|
const url = new URL(OLLAMA_URL);
|
||||||
|
const req = http.request({
|
||||||
|
hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
||||||
|
}, (res) => {
|
||||||
|
let d = '';
|
||||||
|
res.on('data', c => d += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve(JSON.parse(d).response.trim()); } catch { resolve('Have a great trip!'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', () => resolve('Have a great trip!'));
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,37 +155,73 @@ Date: ${date}
|
||||||
|
|
||||||
Return ONLY a JSON array of objects. Each object must have "time" (in 24-hour HH:mm format) and "message" (a short, fun 1-sentence reminder). Estimate reasonable times if none are given (e.g. breakfast 8:00, lunch 12:30, dinner 18:30). DO NOT output any markdown or conversational text.`;
|
Return ONLY a JSON array of objects. Each object must have "time" (in 24-hour HH:mm format) and "message" (a short, fun 1-sentence reminder). Estimate reasonable times if none are given (e.g. breakfast 8:00, lunch 12:30, dinner 18:30). DO NOT output any markdown or conversational text.`;
|
||||||
|
|
||||||
const modelName = trip.llmModel || 'qwen2.5:0.5b';
|
const handleResponse = (d) => {
|
||||||
// Support future providers via trip.llmProvider
|
try {
|
||||||
// Defaulting to Ollama for now
|
const raw = d;
|
||||||
const body = JSON.stringify({ model: modelName, prompt, stream: false });
|
const match = raw.match(/\[[\s\S]*\]/);
|
||||||
const url = new URL(OLLAMA_URL);
|
if (match) {
|
||||||
|
resolve(JSON.parse(match[0]));
|
||||||
const req = http.request({
|
} else {
|
||||||
hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
|
||||||
}, (res) => {
|
|
||||||
let d = '';
|
|
||||||
res.on('data', c => d += c);
|
|
||||||
res.on('end', () => {
|
|
||||||
try {
|
|
||||||
const raw = JSON.parse(d).response;
|
|
||||||
// Extract JSON array using regex in case of conversational wrapper
|
|
||||||
const match = raw.match(/\[[\s\S]*\]/);
|
|
||||||
if (match) {
|
|
||||||
resolve(JSON.parse(match[0]));
|
|
||||||
} else {
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[TripService] AI Schedule parsing failed:', e.message);
|
|
||||||
resolve([]);
|
resolve([]);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TripService] AI Schedule parsing failed:', e.message);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (trip && trip.llmProvider === 'anthropic') {
|
||||||
|
const modelName = trip.llmModel || 'claude-3-5-sonnet-20240620';
|
||||||
|
const body = JSON.stringify({
|
||||||
|
model: modelName,
|
||||||
|
max_tokens: 300,
|
||||||
|
messages: [{ role: 'user', content: prompt }]
|
||||||
});
|
});
|
||||||
});
|
const req = https.request({
|
||||||
req.on('error', () => resolve([]));
|
hostname: 'api.anthropic.com',
|
||||||
req.write(body);
|
path: '/v1/messages',
|
||||||
req.end();
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
}
|
||||||
|
}, (res) => {
|
||||||
|
let d = '';
|
||||||
|
res.on('data', c => d += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(d);
|
||||||
|
handleResponse(data.content[0].text);
|
||||||
|
} catch { resolve([]); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', () => resolve([]));
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Default to Ollama
|
||||||
|
const modelName = trip.llmModel || 'qwen2.5:0.5b';
|
||||||
|
const body = JSON.stringify({ model: modelName, prompt, stream: false });
|
||||||
|
const url = new URL(OLLAMA_URL);
|
||||||
|
const req = http.request({
|
||||||
|
hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
||||||
|
}, (res) => {
|
||||||
|
let d = '';
|
||||||
|
res.on('data', c => d += c);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
handleResponse(JSON.parse(d).response);
|
||||||
|
} catch { resolve([]); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', () => resolve([]));
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,7 +291,7 @@ async function generateAndSendBriefing(trip, dayPlan) {
|
||||||
const weather = await getWeather(dayPlan.location || 'Pigeon Forge, TN');
|
const weather = await getWeather(dayPlan.location || 'Pigeon Forge, TN');
|
||||||
const nps = await getNpsAlerts(trip.npsParkCode);
|
const nps = await getNpsAlerts(trip.npsParkCode);
|
||||||
const traffic = await getTdotTraffic();
|
const traffic = await getTdotTraffic();
|
||||||
const insight = await getAiInsight(dayPlan.activities, dayPlan.location || 'Smoky Mountains', trip.llmModel);
|
const insight = await getAiInsight(dayPlan.activities, dayPlan.location || 'Smoky Mountains', trip);
|
||||||
|
|
||||||
const message = `*🏕️ ${trip.name} - ${dayPlan.date} Briefing*\n\n` +
|
const message = `*🏕️ ${trip.name} - ${dayPlan.date} Briefing*\n\n` +
|
||||||
`*🌤️ Weather (${dayPlan.location || 'Pigeon Forge'}):* ${weather}\n\n` +
|
`*🌤️ Weather (${dayPlan.location || 'Pigeon Forge'}):* ${weather}\n\n` +
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue