generated from ztimson/template
Home screen update
All checks were successful
Build and publish / Build Container (push) Successful in 1m28s
All checks were successful
Build and publish / Build Container (push) Successful in 1m28s
This commit is contained in:
180
src/server.js
180
src/server.js
@@ -4,24 +4,22 @@ import {Server} from 'socket.io';
|
||||
import cors from 'cors';
|
||||
import fs from 'fs';
|
||||
import {join, dirname} from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils';
|
||||
import {contrast, deepCopy, isEqual, shadeColor} from '@ztimson/utils';
|
||||
import {contrast, deepCopy, deepMerge, isEqual, shadeColor} from '@ztimson/utils';
|
||||
import * as os from 'node:os';
|
||||
import {environment} from './environment.js';
|
||||
import {resize} from './utils.js';
|
||||
import * as path from 'node:path';
|
||||
|
||||
// ============================================
|
||||
// Settings
|
||||
// ============================================
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = join(dirname(__filename), '..');
|
||||
const storage = join(__dirname, 'storage');
|
||||
const navi = join(storage, 'navi');
|
||||
const protocols = join(storage, 'protocols');
|
||||
const worlds = join(storage, 'worlds');
|
||||
const logoFile = join(navi, 'logo.png');
|
||||
const settingsFile = join(navi, 'settings.json');
|
||||
const memoriesFile = join(navi, 'memories.json');
|
||||
const logo = join(environment.paths.navi, 'logo.png');
|
||||
const avatar = join(environment.paths.navi, 'avatar.png');
|
||||
const sprite = join(environment.paths.navi, 'sprite.png');
|
||||
const settingsFile = join(environment.paths.navi, 'settings.json');
|
||||
const memoriesFile = join(environment.paths.navi, 'memories.json');
|
||||
|
||||
function calcColors(theme) {
|
||||
return {
|
||||
@@ -43,7 +41,17 @@ function calcColors(theme) {
|
||||
|
||||
let orgSettings, orgMemories, memories = [], settings = {
|
||||
name: 'Navi',
|
||||
personality: '- You are inquisitive about your user trying to best adjust your personally to fit them\n- Keep responses short',
|
||||
personality: '- Keep your responses the same length or shorter than the previous user message',
|
||||
animations: {
|
||||
emote: {
|
||||
dead: {"x": 52, "y": 6},
|
||||
grey: {},
|
||||
realization: {"x": 64, "y": -5},
|
||||
sigh: {"x": 55, "y": 20},
|
||||
sweat: {"x": 59, "y": 5},
|
||||
stress: {"x": 58, "y": 4}
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
background: '#fff',
|
||||
border: '#000',
|
||||
@@ -61,7 +69,7 @@ let orgSettings, orgMemories, memories = [], settings = {
|
||||
function load() {
|
||||
try {
|
||||
orgSettings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
||||
settings = {...settings, ...deepCopy(orgSettings)};
|
||||
settings = deepMerge(settings, deepCopy(orgSettings));
|
||||
} catch { }
|
||||
try {
|
||||
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
||||
@@ -73,12 +81,12 @@ function save() {
|
||||
const dir = dirname(settingsFile);
|
||||
if(!fs.existsSync(dir)) fs.mkdirSync(dir, {recursive: true});
|
||||
if(!isEqual(orgSettings, settings)) {
|
||||
fs.writeFileSync(settingsFile, JSON.stringify(settings));
|
||||
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
||||
orgSettings = deepCopy(orgSettings);
|
||||
}
|
||||
const m = memories.map(m => ({...m, embeddings: undefined}));
|
||||
if(!isEqual(orgMemories, m)) {
|
||||
fs.writeFileSync(memoriesFile, JSON.stringify(memories));
|
||||
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
|
||||
orgMemories = m;
|
||||
}
|
||||
}
|
||||
@@ -87,23 +95,28 @@ function save() {
|
||||
// AI
|
||||
// ============================================
|
||||
|
||||
load();
|
||||
const ai = new Ai({
|
||||
llm: {
|
||||
models: {
|
||||
'Ministral-3': {
|
||||
proto: 'openai',
|
||||
host: 'http://10.69.0.55:11728',
|
||||
token: 'ignore'
|
||||
},
|
||||
},
|
||||
models: {[environment.llm.model]: {proto: 'openai', host: environment.llm.host, token: environment.llm.token},},
|
||||
compress: {min: environment.llm.context * 0.5, max: environment.llm.context},
|
||||
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
|
||||
name: 'adapt',
|
||||
name: 'emote',
|
||||
description: 'Make your avatar emote',
|
||||
args: {
|
||||
emote: {type: 'string', description: 'Emote to the user', required: true, enum: ['none', ...Object.keys(settings.animations.emote)]}
|
||||
},
|
||||
fn: (args, stream) => {
|
||||
if(!['none', ...Object.keys(settings.animations.emote)].includes(args.emote))
|
||||
throw new Error(`Invalid emote, must be one of: ${['none', ...Object.keys(settings.animations.emote)].join(', ')}`)
|
||||
stream({emote: args.emote});
|
||||
return 'done!';
|
||||
}
|
||||
}, {
|
||||
name: 'personalize',
|
||||
description: 'Replace your current personality',
|
||||
args: {
|
||||
instructions: {
|
||||
type: 'string',
|
||||
description: 'Bullet point list of how to behave'
|
||||
}
|
||||
instructions: {type: 'string', description: 'Full bullet point list of how to behave', required: true}
|
||||
},
|
||||
fn: (args) => {
|
||||
settings.personality = args.instructions;
|
||||
@@ -114,13 +127,14 @@ const ai = new Ai({
|
||||
});
|
||||
|
||||
const systemPrompt = () => {
|
||||
return `Your name is ${settings.name}, a NetNavi, companion & personal assistant. Roleplay with the user.
|
||||
Use your remember tool liberally to store all facts.
|
||||
When the user asks you to behave differently or you feel a different personality would better fit the user; create a bullet point list of how to behave and submit it to the adapt tool
|
||||
Access your ${os.platform()} workspace using the exec tool; Use \`${os.tmpdir()}\` as your working directory
|
||||
Keep responses unstyled.
|
||||
return `Roleplay a NetNavi & companion named ${settings.name}. and follow these steps:
|
||||
1. Start EVERY reply by calling the [emote] tool with the perfect vibe (or none).
|
||||
2. Next, identify facts in the latest user message and immediately call the [remember] tool on each one. Avoid facts about the conversation or the AI.
|
||||
3. If instructed, or the user seems unsatisfied, rewrite your "Personality Rules" and submit it to the [personalize] tool.
|
||||
4. If asked, you can access your ${os.platform()} workspace using the [exec] tool. Always start from \`${os.tmpdir()}\`.
|
||||
5. Create an unstyled response according to your "Personality Rules"
|
||||
|
||||
Personality:
|
||||
Personality Rules:
|
||||
${settings.personality || ''}`;
|
||||
};
|
||||
|
||||
@@ -128,7 +142,6 @@ ${settings.personality || ''}`;
|
||||
// Setup
|
||||
// ============================================
|
||||
|
||||
load();
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
@@ -140,7 +153,6 @@ const io = new Server(httpServer, {
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// ============================================
|
||||
// Socket
|
||||
@@ -300,37 +312,58 @@ io.of('/world').on('connection', (socket) => {
|
||||
// API ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
app.get('/favicon.*', (req, res) => {
|
||||
res.sendFile(logoFile);
|
||||
app.get('/avatar', (req, res) => {
|
||||
res.sendFile(avatar);
|
||||
});
|
||||
|
||||
app.get('/favicon*', async (req, res) => {
|
||||
let w = 256, h = w;
|
||||
if(req.query && req.query['size']) [w, h] = req.query['size'].split('x');
|
||||
res.contentType('image/png').set('Content-Disposition', 'inline');
|
||||
res.send(await resize(logo, w, h));
|
||||
});
|
||||
|
||||
app.get('/manifest.json', (req, res) => {
|
||||
res.json({
|
||||
id: environment.publicUrl,
|
||||
short_name: 'NetNavi',
|
||||
name: 'NetNavi',
|
||||
description: 'Network Navigation Program',
|
||||
display: 'standalone',
|
||||
start_url: '/',
|
||||
background_color: settings.theme.accent,
|
||||
theme_color: settings.theme.primary,
|
||||
icons: [
|
||||
{
|
||||
"src": `${environment.publicUrl}/favicon?size=192x192`,
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": `${environment.publicUrl}/favicon?size=512x512`,
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/spritesheet', (req, res) => {
|
||||
res.sendFile(sprite);
|
||||
});
|
||||
|
||||
// Get Navi info
|
||||
app.get('/api/info', (req, res) => {
|
||||
res.json({
|
||||
name: settings.name,
|
||||
avatar: settings.avatar,
|
||||
theme: calcColors(settings.theme),
|
||||
});
|
||||
});
|
||||
|
||||
// Get sprite sheet
|
||||
app.get('/api/sprite', (req, res) => {
|
||||
const {petId} = req.params;
|
||||
// TODO: Return actual sprite sheet URL
|
||||
res.json({
|
||||
spriteUrl: '/sprites/default-pet.png',
|
||||
frameWidth: 32,
|
||||
frameHeight: 32,
|
||||
animations: {
|
||||
idle: {
|
||||
frames: [0, 1, 2, 3],
|
||||
speed: 200
|
||||
},
|
||||
walk: {
|
||||
frames: [4, 5, 6, 7],
|
||||
speed: 100
|
||||
}
|
||||
}
|
||||
});
|
||||
app.get('/api/animations', (req, res) => {
|
||||
res.json(settings.animations);
|
||||
});
|
||||
|
||||
// Send message to user (push notification / email / etc)
|
||||
@@ -366,14 +399,39 @@ app.post('/api/link', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Static files
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
let p = (!req.path || req.path.endsWith('/'))
|
||||
? req.path + (req.path.endsWith('/') ? '' : '/') + 'index.html'
|
||||
: req.path + (req.path.split('/').pop().includes('.') ? '' : '.html');
|
||||
p = path.join(environment.paths.public, p);
|
||||
const data = await fs.readFileSync(p);
|
||||
|
||||
// Only process HTML files
|
||||
if(p.endsWith('.html')) {
|
||||
let body = data.toString('utf-8')
|
||||
.replaceAll('{{PUBLIC_URL}}', environment.publicUrl)
|
||||
.replaceAll('{{NAME}}', settings.name)
|
||||
.replaceAll('{{THEME_BACKGROUND}}', settings.theme.background)
|
||||
.replaceAll('{{THEME_PRIMARY}}', settings.theme.primary)
|
||||
.replaceAll('{{THEME_ACCENT}}', settings.theme.accent);
|
||||
res.set('Content-Type', 'text/html');
|
||||
return res.send(body);
|
||||
}
|
||||
return res.sendFile(p);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') return next();
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// START SERVER
|
||||
// ============================================
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
setInterval(() => save(), 5 * 60_000);
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on: http://localhost:${PORT}`);
|
||||
httpServer.listen(environment.port, () => {
|
||||
console.log(`🚀 Server running on: http://localhost:${environment.port}`);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
|
||||
Reference in New Issue
Block a user