-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathagent.js
More file actions
189 lines (159 loc) · 6.1 KB
/
Copy pathagent.js
File metadata and controls
189 lines (159 loc) · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
'use strict';
require('dotenv').config();
const express = require('express');
const fs = require('fs');
const GC_API = 'https://api.ghostchat.dev';
// Parse ghostchat.md — extracts config block and full text as system prompt
function parseGhostchatMd() {
const raw = fs.readFileSync('ghostchat.md', 'utf8');
const config = {};
const siteIdMatch = raw.match(/^site_id:\s*(.+)$/m);
const portMatch = raw.match(/^webhook_port:\s*(.+)$/m);
if (siteIdMatch) config.siteId = siteIdMatch[1].trim();
config.port = portMatch ? parseInt(portMatch[1].trim(), 10) : 3000;
// Full file becomes system prompt (LLM reads it as-is)
config.systemPrompt = raw;
return config;
}
// Call LLM based on provider
async function callLLM(systemPrompt, conversationHistory, newMessage) {
const provider = process.env.LLM_PROVIDER || 'ollama';
const model = process.env.LLM_MODEL || 'llama3';
const messages = [
{ role: 'system', content: systemPrompt },
...conversationHistory,
{ role: 'user', content: newMessage },
];
if (provider === 'ollama') {
const baseUrl = process.env.LLM_BASE_URL || 'http://localhost:11434';
const res = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, messages, stream: false }),
});
if (!res.ok) throw new Error(`Ollama error: ${res.status}`);
const data = await res.json();
return data.message?.content || '';
}
if (provider === 'openai') {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LLM_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model, messages, max_tokens: 300 }),
});
if (!res.ok) throw new Error(`OpenAI error: ${res.status}`);
const data = await res.json();
return data.choices?.[0]?.message?.content || '';
}
if (provider === 'anthropic') {
const system = messages.find(m => m.role === 'system')?.content || '';
const userMessages = messages.filter(m => m.role !== 'system');
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.LLM_API_KEY,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({ model, system, messages: userMessages, max_tokens: 300 }),
});
if (!res.ok) throw new Error(`Anthropic error: ${res.status}`);
const data = await res.json();
return data.content?.[0]?.text || '';
}
throw new Error(`Unknown LLM provider: ${provider}`);
}
// Send reply back to visitor via GhostChat API
async function sendReply(sessionId, content) {
const apiKey = process.env.GHOSTCHAT_API_KEY;
const res = await fetch(`${GC_API}/messages/owner`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ sessionId, content }),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`GhostChat API error ${res.status}: ${body}`);
}
}
// Fetch recent conversation history for context
async function getHistory(sessionId) {
const apiKey = process.env.GHOSTCHAT_API_KEY;
try {
const res = await fetch(`${GC_API}/messages/${sessionId}`, {
headers: { 'Authorization': `Bearer ${apiKey}` },
});
if (!res.ok) return [];
const messages = await res.json();
// Convert to LLM message format, last 10 messages for context
return messages.slice(-10).map(m => ({
role: m.sender === 'VISITOR' ? 'user' : 'assistant',
content: m.content,
}));
} catch {
return [];
}
}
module.exports = async function startAgent() {
if (!fs.existsSync('ghostchat.md')) {
console.error('❌ ghostchat.md not found. Run with --setup first.');
process.exit(1);
}
const apiKey = process.env.GHOSTCHAT_API_KEY;
if (!apiKey || !apiKey.startsWith('gc_bot_')) {
console.error('❌ GHOSTCHAT_API_KEY not set or invalid. Check your .env file.');
process.exit(1);
}
const config = parseGhostchatMd();
const provider = process.env.LLM_PROVIDER || 'ollama';
const model = process.env.LLM_MODEL || 'llama3';
const app = express();
app.use(express.json());
app.post('/webhook', async (req, res) => {
// Acknowledge immediately — GhostChat doesn't wait for a response
res.sendStatus(200);
const { sessionId, content, siteName } = req.body;
if (!sessionId || !content) return;
const timestamp = new Date().toLocaleTimeString();
console.log(`\n[${timestamp}] New message on ${siteName || 'your site'}`);
console.log(` Visitor: "${content}"`);
try {
// Get conversation history for context
const history = await getHistory(sessionId);
// Call LLM
const reply = await callLLM(config.systemPrompt, history, content);
if (!reply) {
console.log(' ⚠ LLM returned empty response — skipping reply');
return;
}
// Check if bot is flagging for human
const flagged = reply.toLowerCase().includes("i'll have someone follow up") ||
reply.toLowerCase().includes("someone will follow up");
// Send reply to visitor
await sendReply(sessionId, reply);
console.log(` Bot: "${reply.substring(0, 80)}${reply.length > 80 ? '...' : ''}"`);
if (flagged) {
console.log(' ⚑ Flagged for human review — check your dashboard');
}
} catch (err) {
console.error(` ❌ Error: ${err.message}`);
}
});
// Health check
app.get('/health', (req, res) => res.json({ status: 'ok', provider, model }));
app.listen(config.port, () => {
console.log(`\nGhostChat Bot Agent`);
console.log('───────────────────');
console.log(`✓ Running on port ${config.port}`);
console.log(`✓ LLM: ${provider} / ${model}`);
console.log(`✓ Waiting for visitor messages...`);
console.log(`\nAll conversations: https://app.ghostchat.dev`);
console.log('Press Ctrl+C to stop.\n');
});
};