mirror of
https://github.com/sergeyyarkov/xmpp-llm-bot.git
synced 2026-10-11 10:29:18 +03:00
done
This commit is contained in:
parent
b60ccd6915
commit
a7be9b1f31
@ -1 +1,9 @@
|
|||||||
API_BASE_URL=http://localhost:1234/v1
|
API_BASE_URL=http://localhost:1234/v1
|
||||||
|
API_KEY=lm-studio
|
||||||
|
XMPP_SERVICE=xmpp://chat.xmpp.com:5222/xmpp-websocket
|
||||||
|
XMPP_DOMAIN=chat.xmpp.com
|
||||||
|
XMPP_USERNAME=username
|
||||||
|
XMPP_PASSWORD=password
|
||||||
|
SESSION_MANAGER=local
|
||||||
|
MODEL_ID=qwen/qwen3-14b
|
||||||
|
MAX_MESSAGES_LIMIT=50
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
sys_prompt.md
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": false,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
144
bot.js
Normal file
144
bot.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { client, xml } from "@xmpp/client";
|
||||||
|
import { OpenAI } from "openai";
|
||||||
|
import debug from "@xmpp/debug";
|
||||||
|
import { createSessionManager } from "./session.js";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export const xmpp = client({
|
||||||
|
service: process.env.XMPP_SERVICE,
|
||||||
|
domain: process.env.XMPP_DOMAIN,
|
||||||
|
username: process.env.XMPP_USERNAME,
|
||||||
|
password: process.env.XMPP_PASSWORD
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lmStudio = new OpenAI({
|
||||||
|
baseURL: process.env.API_BASE_URL,
|
||||||
|
apiKey: process.env.API_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessions = createSessionManager(process.env.SESSION_MANAGER);
|
||||||
|
|
||||||
|
debug(xmpp, false);
|
||||||
|
|
||||||
|
xmpp.on("error", (err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
xmpp.on("online", async (address) => {
|
||||||
|
console.log("online as", address.toString());
|
||||||
|
await xmpp.send(xml("presence"));
|
||||||
|
});
|
||||||
|
|
||||||
|
xmpp.on("stanza", async (stanza) => {
|
||||||
|
if (!stanza.is("message") || stanza.attrs.type !== "chat") return;
|
||||||
|
|
||||||
|
const body = stanza.getChildText("body");
|
||||||
|
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
if (body.includes("@clr")) {
|
||||||
|
sessions.flush();
|
||||||
|
console.log("@clr: All sessions cleared");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemPromptReply = await readPromptFile();
|
||||||
|
|
||||||
|
const from = stanza.attrs.from;
|
||||||
|
const sessionId = from.split("/")[0];
|
||||||
|
console.log(stanza.toString());
|
||||||
|
|
||||||
|
const history = sessions.getHistory(sessionId);
|
||||||
|
|
||||||
|
await xmppSendTypingSignal(from);
|
||||||
|
|
||||||
|
sessions.saveMessage(sessionId, { role: "user", content: body });
|
||||||
|
|
||||||
|
let reply = "";
|
||||||
|
let lastPlacedChunkIdx = 0;
|
||||||
|
|
||||||
|
const responseStream = await lmStudio.chat.completions.create({
|
||||||
|
model: process.env.MODEL_ID,
|
||||||
|
messages: [{ role: "system", content: systemPromptReply }, ...history.messages],
|
||||||
|
stream: true,
|
||||||
|
temperature: 0.85,
|
||||||
|
top_p: 0.9,
|
||||||
|
frequency_penalty: 0.2,
|
||||||
|
presence_penalty: 0.1,
|
||||||
|
reasoning_effort: "none",
|
||||||
|
stream_options: {
|
||||||
|
include_usage: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of responseStream) {
|
||||||
|
const content = chunk.choices[0]?.delta?.content || "";
|
||||||
|
reply += content;
|
||||||
|
|
||||||
|
const replyPart = reply.slice(lastPlacedChunkIdx);
|
||||||
|
|
||||||
|
if (replyPart.length > 200) {
|
||||||
|
lastPlacedChunkIdx = reply.length;
|
||||||
|
await xmppSendMessage(from, replyPart);
|
||||||
|
await xmppSendTypingSignal(from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await xmppSendMessage(from, reply.slice(lastPlacedChunkIdx));
|
||||||
|
|
||||||
|
if (reply) {
|
||||||
|
sessions.saveMessage(sessionId, { role: "assistant", content: reply });
|
||||||
|
}
|
||||||
|
|
||||||
|
await xmppSendActiveSignal(from);
|
||||||
|
|
||||||
|
console.log(history);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @param {string} to */
|
||||||
|
async function xmppSendTypingSignal(to) {
|
||||||
|
await xmpp.send(
|
||||||
|
xml(
|
||||||
|
"message",
|
||||||
|
{ to, type: "chat" },
|
||||||
|
xml("composing", {
|
||||||
|
xmlns: "http://jabber.org/protocol/chatstates"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} to
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
async function xmppSendMessage(to, message) {
|
||||||
|
await xmpp.send(xml("message", { type: "chat", to }, xml("body", {}, `${message}`)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} to
|
||||||
|
*/
|
||||||
|
async function xmppSendActiveSignal(to) {
|
||||||
|
await xmpp.send(
|
||||||
|
xml(
|
||||||
|
"message",
|
||||||
|
{ type: "chat", to },
|
||||||
|
xml("active", {
|
||||||
|
xmlns: "http://jabber.org/protocol/chatstates"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPromptFile() {
|
||||||
|
try {
|
||||||
|
const sysPromptFilename = process.argv[2] || "";
|
||||||
|
const text = await fs.readFile(path.join(process.cwd(), sysPromptFilename), { encoding: "utf-8" });
|
||||||
|
return text;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error while reading system prompt file:`, error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
3
constants.js
Normal file
3
constants.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const LLM_MODEL_ID = "qwen/qwen3-14b";
|
||||||
|
|
||||||
|
export const MAX_MESSAGES_LIMIT = 50;
|
||||||
17
env.d.ts
vendored
Normal file
17
env.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
API_BASE_URL: string;
|
||||||
|
API_KEY: string;
|
||||||
|
XMPP_SERVICE: string;
|
||||||
|
XMPP_DOMAIN: string;
|
||||||
|
XMPP_USERNAME: string;
|
||||||
|
XMPP_PASSWORD: string;
|
||||||
|
SESSION_MANAGER: "local";
|
||||||
|
MODEL_ID: string;
|
||||||
|
MAX_MESSAGES_LIMIT: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
7
index.js
7
index.js
@ -0,0 +1,7 @@
|
|||||||
|
import { xmpp, lmStudio } from "./bot.js";
|
||||||
|
|
||||||
|
const { data: modelsList } = await lmStudio.models.list();
|
||||||
|
|
||||||
|
console.log("Available models:", modelsList.map((m) => m.id).join(", "));
|
||||||
|
|
||||||
|
await xmpp.start();
|
||||||
@ -8,5 +8,6 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
}
|
},
|
||||||
|
"include": ["**/*.js", "env.d.ts", "types.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@xmpp/client": "^0.14.0",
|
"@xmpp/client": "^0.14.0",
|
||||||
|
"@xmpp/debug": "^0.14.0",
|
||||||
"eslint": "^10.9.1",
|
"eslint": "^10.9.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
@ -313,6 +314,19 @@
|
|||||||
"node": ">= 20.10"
|
"node": ">= 20.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xmpp/debug": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmpp/debug/-/debug-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-Smj0ECPLVMbPjh4gMPgzhCzXHgQonJ9drZP/3MKJygRzUk92H/dr9Pfrq3odBAiqbTbnKQxxeanE2LG8m3YSgA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmpp/xml": "^0.14.0",
|
||||||
|
"ltx": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xmpp/error": {
|
"node_modules/@xmpp/error": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xmpp/error/-/error-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xmpp/error/-/error-0.14.0.tgz",
|
||||||
|
|||||||
@ -3,19 +3,20 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --env-file=.env index.js",
|
"start": "node --env-file=.env index.js ./sys_prompt.md",
|
||||||
"dev": "node --env-file=.env --watch index.js",
|
"dev": "node --env-file=.env --watch index.js ./sys_prompt.md",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"fix": "eslint . --fix"
|
"fix": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@xmpp/client": "^0.14.0",
|
"@xmpp/client": "^0.14.0",
|
||||||
|
"@xmpp/debug": "^0.14.0",
|
||||||
"eslint": "^10.9.1",
|
"eslint": "^10.9.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
|
|||||||
47
session.js
Normal file
47
session.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/** @import { OpenAI } from "openai" */
|
||||||
|
/** @import { SessionDriverInterface, SessionContent, ChatMessage } from "./types.js" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {'local'} driver
|
||||||
|
*/
|
||||||
|
export function createSessionManager(driver) {
|
||||||
|
switch (driver) {
|
||||||
|
case "local":
|
||||||
|
return createLocalSessionManager();
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown session manager");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {SessionDriverInterface} */
|
||||||
|
function createLocalSessionManager() {
|
||||||
|
/** @type {Map<string, SessionContent>} */
|
||||||
|
const sessions = new Map();
|
||||||
|
|
||||||
|
return {
|
||||||
|
getHistory(from) {
|
||||||
|
let chatHistory = sessions.get(from);
|
||||||
|
|
||||||
|
if (!chatHistory) {
|
||||||
|
chatHistory = { messages: [] };
|
||||||
|
sessions.set(from, chatHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatHistory;
|
||||||
|
},
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
sessions.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
saveMessage(from, message) {
|
||||||
|
const chatHistory = this.getHistory(from);
|
||||||
|
chatHistory.messages.push(message);
|
||||||
|
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
if (chatHistory.messages.length > Number.parseInt(process.env.MAX_MESSAGES_LIMIT, 10))
|
||||||
|
chatHistory.messages.splice(0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
13
types.d.ts
vendored
Normal file
13
types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { OpenAI } from "openai";
|
||||||
|
|
||||||
|
export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
||||||
|
|
||||||
|
export interface SessionContent {
|
||||||
|
messages: ChatMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionDriverInterface {
|
||||||
|
getHistory(from: string): SessionContent;
|
||||||
|
saveMessage(from: string, message: ChatMessage): void;
|
||||||
|
flush(): void;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user