mirror of
https://github.com/sergeyyarkov/xmpp-llm-bot.git
synced 2026-10-11 02:19:06 +03:00
done
This commit is contained in:
parent
b60ccd6915
commit
a7be9b1f31
10
.env.example
10
.env.example
@ -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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
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,
|
||||
"module": "nodenext",
|
||||
"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": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@xmpp/client": "^0.14.0",
|
||||
"@xmpp/debug": "^0.14.0",
|
||||
"eslint": "^10.9.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
@ -313,6 +314,19 @@
|
||||
"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": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@xmpp/error/-/error-0.14.0.tgz",
|
||||
|
||||
@ -3,19 +3,20 @@
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --env-file=.env index.js",
|
||||
"dev": "node --env-file=.env --watch index.js",
|
||||
"start": "node --env-file=.env index.js ./sys_prompt.md",
|
||||
"dev": "node --env-file=.env --watch index.js ./sys_prompt.md",
|
||||
"lint": "eslint .",
|
||||
"fix": "eslint . --fix"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@xmpp/client": "^0.14.0",
|
||||
"@xmpp/debug": "^0.14.0",
|
||||
"eslint": "^10.9.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"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