-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
210 lines (185 loc) · 7.71 KB
/
server.js
File metadata and controls
210 lines (185 loc) · 7.71 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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
const http = require('http');
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
// Minimal .env parser for api_keys.env
function loadEnv(filePath) {
const abs = path.resolve(filePath);
if (!fs.existsSync(abs)) return {};
const content = fs.readFileSync(abs, 'utf8');
const env = {};
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
const val = trimmed.slice(eq + 1).trim().replace(/^"|"$/g, '');
env[key] = val;
}
return env;
}
const ENV = loadEnv('api_keys.env');
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ENV.OPENAI_API_KEY || ENV.OPENAI_APIKEY || ENV.OPENAI_KEY;
const OPENAI_MODEL = process.env.OPENAI_MODEL || ENV.OPENAI_MODEL || 'gpt-4o-mini';
if (!OPENAI_API_KEY) {
console.warn('Warning: OPENAI_API_KEY not found in environment or api_keys.env. The /chat endpoint will fail until it is set.');
}
// Extract text from DOCX by unzipping and reading word/document.xml
function xmlDecode(s) {
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'");
}
function getProfileTextSync() {
// 1) Prefer a plain text profile if provided
const txtPath = path.resolve('Profilo di Simone.txt');
try {
if (fs.existsSync(txtPath)) {
let t = fs.readFileSync(txtPath, 'utf8');
if (t && t.length) {
if (t.charCodeAt(0) === 0xFEFF) t = t.slice(1); // strip BOM
const trimmed = t.replace(/[\s\u00A0]+/g, ' ').replace(/\s*\n\s*/g, '\n').trim();
if (trimmed.length > 20) return trimmed;
}
}
} catch {}
// 2) Otherwise, extract from DOCX with caching
const cacheTxt = path.resolve('profile.txt');
const docxPath = path.resolve('Simone Profile.docx');
// Use cache only if non-empty and up-to-date
if (fs.existsSync(cacheTxt)) {
try {
const cached = fs.readFileSync(cacheTxt, 'utf8');
const okLen = (cached || '').trim().length > 20;
if (!fs.existsSync(docxPath)) return cached;
const cacheMtime = fs.statSync(cacheTxt).mtimeMs;
const docxMtime = fs.statSync(docxPath).mtimeMs;
if (okLen && cacheMtime >= docxMtime) return cached;
} catch {}
}
if (!fs.existsSync(docxPath)) {
console.warn('Simone Profile.docx not found. Place it in the project root.');
return '';
}
const unzipDir = path.resolve('_profile_unzip');
try {
execFileSync('powershell', [
'-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-Command',
`Expand-Archive -LiteralPath \"${docxPath}\" -DestinationPath \"${unzipDir}\" -Force`
], { stdio: 'ignore' });
} catch (err) {
console.warn('Failed to unzip DOCX via PowerShell Expand-Archive.\n', String(err));
return '';
}
const docXml = path.join(unzipDir, 'word', 'document.xml');
if (!fs.existsSync(docXml)) {
console.warn('word/document.xml not found inside DOCX.');
return '';
}
const xml = fs.readFileSync(docXml, 'utf8');
// Robust extraction: collect all w:t text runs per paragraph
const paragraphs = xml.split(/<\/w:p>/gi).map(block => {
const runs = [...block.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/gi)].map(m => xmlDecode(m[1]));
const para = runs.join(' ').replace(/[\s\u00A0]+/g, ' ').trim();
return para;
}).filter(Boolean);
const text = paragraphs.join('\n');
try { fs.writeFileSync(cacheTxt, text, 'utf8'); } catch {}
return text;
}
// No global cache: reload profile each request so edits are picked up.
// Simple static server and JSON API
const publicDir = path.resolve('public');
function serveStatic(req, res) {
let urlPath = req.url.split('?')[0];
if (urlPath === '/' || urlPath === '') urlPath = '/index.html';
const filePath = path.join(publicDir, path.normalize(urlPath));
if (!filePath.startsWith(publicDir)) {
res.writeHead(403); res.end('Forbidden'); return;
}
fs.readFile(filePath, (err, data) => {
if (err) { res.writeHead(404); res.end('Not Found'); return; }
const ext = path.extname(filePath).toLowerCase();
const types = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript', '.css': 'text/css', '.png': 'image/png', '.svg': 'image/svg+xml' };
res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream' });
res.end(data);
});
}
async function handleChat(req, res) {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', async () => {
try {
const data = JSON.parse(body || '{}');
const userMessage = String(data.message || '').trim();
if (!userMessage) {
res.writeHead(400, cors({ 'Content-Type': 'application/json; charset=utf-8' }));
return res.end(JSON.stringify({ error: 'Messaggio mancante.' }));
}
if (!OPENAI_API_KEY) {
res.writeHead(500, cors({ 'Content-Type': 'application/json; charset=utf-8' }));
return res.end(JSON.stringify({ error: 'OPENAI_API_KEY non configurata.' }));
}
const systemPrompt = 'Sei l\'assistente del profilo di Simone (uomo). Usa SOLO il profilo fornito qui sotto come fonte. Se l\'informazione è presente (anche sintetica/implicita), ricavala e rispondi chiaramente. Se davvero non trovi nulla nel profilo, dillo esplicitamente. Usa pronomi maschili. Stile: italiano, conciso, cordiale e utile.';
const profileText = getProfileTextSync();
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: 'Contesto del profilo (usa solo questo):\n---\n' + (profileText || 'Nessun testo del profilo disponibile.') },
{ role: 'user', content: userMessage }
];
try { console.log(`[Chat] Model=${OPENAI_MODEL} profileChars=${(profileText||'').length} msg="${userMessage.replace(/\s+/g,' ').slice(0,120)}"`); } catch {}
// Use global fetch (Node 18+). If unavailable, error with guidance.
if (typeof fetch !== 'function') {
throw new Error('Global fetch not available. Use Node 18+ or install node-fetch.');
}
const resp = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENAI_API_KEY}`
},
body: JSON.stringify({
model: OPENAI_MODEL,
temperature: 0.3,
messages,
max_tokens: 400
})
});
if (!resp.ok) {
const errText = await resp.text();
throw new Error(`OpenAI API error ${resp.status}: ${errText}`);
}
const json = await resp.json();
const reply = json.choices?.[0]?.message?.content?.trim() || 'Mi dispiace, ho avuto difficoltà a generare una risposta.';
res.writeHead(200, cors({ 'Content-Type': 'application/json; charset=utf-8' }));
res.end(JSON.stringify({ reply }));
} catch (e) {
res.writeHead(500, cors({ 'Content-Type': 'application/json; charset=utf-8' }));
res.end(JSON.stringify({ error: e.message || String(e) }));
}
});
}
function cors(headers = {}) {
return Object.assign({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}, headers);
}
const server = http.createServer((req, res) => {
if (req.method === 'OPTIONS') {
res.writeHead(204, cors()); res.end(); return;
}
if (req.url.startsWith('/chat') && req.method === 'POST') {
return handleChat(req, res);
}
return serveStatic(req, res);
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});