From 34123a7e5a26a36f15bd2c81d47c71e61234445d Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Wed, 28 May 2025 18:42:43 +0200 Subject: [PATCH] Add packaging & split code. --- .gitignore | 2 + build.sh | 7 + history.json | 1 + package/aur/PKGBUILD | 24 ++++ src/kub-tcp-service.ts | 17 +++ src/kub.ts | 93 -------------- src/lib/history.ts | 35 +++++ src/lib/listen.ts | 6 + src/lib/names.ts | 284 +++++++++++++++++++++++++++++++++++++++++ src/lib/session.ts | 69 ++++++++++ 10 files changed, 445 insertions(+), 93 deletions(-) create mode 100644 .gitignore create mode 100755 build.sh create mode 100644 history.json create mode 100644 package/aur/PKGBUILD create mode 100755 src/kub-tcp-service.ts delete mode 100755 src/kub.ts create mode 100644 src/lib/history.ts create mode 100644 src/lib/listen.ts create mode 100644 src/lib/names.ts create mode 100644 src/lib/session.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df0918f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/history diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..275e22e --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/bash +set -e +cd "$(dirname "$(realpath "$0")")" + + +mkdir -p target +deno compile --allow-all --output=target/kub-tcp-service src/kub-tcp-service.ts diff --git a/history.json b/history.json new file mode 100644 index 0000000..eb26cf5 --- /dev/null +++ b/history.json @@ -0,0 +1 @@ +{"messages":[]} \ No newline at end of file diff --git a/package/aur/PKGBUILD b/package/aur/PKGBUILD new file mode 100644 index 0000000..809799e --- /dev/null +++ b/package/aur/PKGBUILD @@ -0,0 +1,24 @@ +# Maintainer: JOLIMAITRE Matthieu + +pkgname=kub-tcp +pkgver=1.0.0 +pkgrel=1 +pkgdesc="TODO list management service." +url="https://git.barnulf.net/mb/kub-tcp" +license=("GPL-3.0+") +arch=("x86_64") +makedepends=("deno") +depends=("ollama") +provides=("kub-tcp-service" "todo") +conflicts=("kub-tcp-service" "todo") +source=("git+https://git.barnulf.net/mb/kub-tcp.git#branch=master") +sha256sums=("SKIP") + +OPTIONS=(!strip !docs libtool emptydirs) + +package() { + feseur/build.sh + mkdir -p "$pkgdir/usr/bin/" "$pkgdir/usr/lib/systemd/user/" + install -Dm755 feseur/target/kub-tcp-service "$pkgdir/usr/bin/" + install -Dm644 feseur/kub-tcp.service "$pkgdir/usr/lib/systemd/user/" +} diff --git a/src/kub-tcp-service.ts b/src/kub-tcp-service.ts new file mode 100755 index 0000000..1b02940 --- /dev/null +++ b/src/kub-tcp-service.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env -S deno run -A + +import { History } from "./lib/history.ts" +import { pick_name_for } from "./lib/names.ts" +import { listen } from "./lib/listen.ts" +import { session } from "./lib/session.ts" + +const history_limit = 500 +const history_path = "./history.json" +const port = 4800 + +async function main() { + const history = await History.load(history_path, history_limit) ?? await History.create(history_path, history_limit) + listen(port, (conn) => session(conn, pick_name_for(conn.remoteAddr.hostname), history)) +} + +if (import.meta.main) await main() diff --git a/src/kub.ts b/src/kub.ts deleted file mode 100755 index 1763ae1..0000000 --- a/src/kub.ts +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env -S deno run -A - -import { TextLineStream } from "https://deno.land/std@0.224.0/streams/mod.ts" -import ollama, { Message } from "npm:ollama@0.5.15" - -function main() { - const names = new Names() - const messages = new SpillingQueue(500) - listen(8080, (conn) => session(conn, names.pick_for(conn.remoteAddr.hostname), messages)) -} - -class SpillingQueue { - public items: T[] = [] - - public constructor( - public limit: number, - ) {} - - public push(value: T) { - this.items.push(value) - if (this.items.length > this.limit) return this.items.splice(0, 1)[0] - } -} - -async function listen(port: number, handler: (conn: Deno.TcpConn) => Promise) { - console.log("Listening on port", port) - for await (const connection of Deno.listen({ transport: "tcp", port })) { - handler(connection).catch((except) => console.log("Session closed", except)) - } -} - -const header = (name: string) => ` - KUB - -Kub is a LLM running on TCP which includes all messages in the same context. -As Kub is having several discussions at once, he will identify you as : ${name} -─────────────────────────────────────────────────────────────────────────────── -` - -async function session(conn: Deno.TcpConn, name: string, messages: SpillingQueue) { - console.log("Opening session for", name, "at", conn.remoteAddr.hostname, conn.remoteAddr.port) - conn.setNoDelay(true) - await conn.write(new TextEncoder().encode(header(name))) - async function prompt() { - await conn.write(new TextEncoder().encode("> ")) - } - const lines = conn.readable - .pipeThrough(new TextDecoderStream()) - .pipeThrough(new TextLineStream()) - await prompt() - for await (const line of lines) { - await conn.write(new TextEncoder().encode("\n< ")) - const user_content = `(message from:${name}) ${line}` - const user_message = { role: "user", content: user_content } - const response = await ollama.chat({ - model: "llama3.2", - messages: request_messages(messages.items, user_message), - stream: true, - options: {}, - }) - let content = "", role = "assistant" - for await (const part of response) { - await conn.write(new TextEncoder().encode(part.message.content)) - content += part.message.content - role = part.message.role - } - await conn.write(new TextEncoder().encode("\n\n")) - await prompt() - messages.push(user_message) - messages.push({ role, content }) - } -} - -function request_messages(history_messages: Message[], user_message: Message) { - return [ - { role: "system", content: "You are 'Kub', a bot which talks to people on the internet. Be concise and helpful." }, - ...history_messages, - user_message, - ] as Message[] -} - -class Names { - options = ["Adam", "Bastien", "Corentin", "Dominique", "Ethan"] - - public pick_for(key: string) { - let sum = 0 - for (const letter of key) sum += letter.charCodeAt(0) - const index = Math.floor((sum + Math.random() * 100) % this.options.length) - return this.options[index] - } -} - -if (import.meta.main) main() diff --git a/src/lib/history.ts b/src/lib/history.ts new file mode 100644 index 0000000..b3c503b --- /dev/null +++ b/src/lib/history.ts @@ -0,0 +1,35 @@ +import { Message } from "npm:ollama@0.5.15" + +export class History { + private constructor( + public messages: Message[], + private path: string, + private limit: number, + ) {} + + public push(value: Message) { + this.messages.push(value) + while (this.messages.length > this.limit) this.messages.splice(0, 1) + } + + public static async load(path: string, limit: number) { + try { + const content = await Deno.readTextFile(path) + const { messages } = JSON.parse(content) + return new History(messages, path, limit) + } catch (except) { + console.error("Failed to load spilling queue :", except) + return undefined + } + } + + public static async create(path: string, limit: number) { + const result = new History([], path, limit) + await result.save() + return result + } + + private async save() { + await Deno.writeTextFile(this.path, JSON.stringify({ messages: this.messages })) + } +} diff --git a/src/lib/listen.ts b/src/lib/listen.ts new file mode 100644 index 0000000..294e1f3 --- /dev/null +++ b/src/lib/listen.ts @@ -0,0 +1,6 @@ +export async function listen(port: number, handler: (conn: Deno.TcpConn) => Promise) { + console.log("Listening on port", port) + for await (const connection of Deno.listen({ transport: "tcp", port })) { + handler(connection).catch((except) => console.log("Session closed", except)) + } +} diff --git a/src/lib/names.ts b/src/lib/names.ts new file mode 100644 index 0000000..d513285 --- /dev/null +++ b/src/lib/names.ts @@ -0,0 +1,284 @@ +export function pick_name_for(key: string) { + let sum = 0 + for (const letter of key) sum += letter.charCodeAt(0) + const index = Math.floor((sum + Math.random() * 100) % options.length) + return options[index] +} + +const options = [ + "Abel", + "Absolon", + "Achille", + "Adam", + "Adolphe", + "Adrien", + "Aimé", + "Alain", + "Albert", + "Alexandre", + "Alexis", + "Alfred", + "Alison", + "Alphonse", + "Amaury", + "Ambroise", + "Amédée", + "Anatole", + "André", + "Anselme", + "Antoine", + "Apollinaire", + "Aristide", + "Armand", + "Armel", + "Arnaud", + "Auguste", + "Augustin", + "Aurèle", + "Aurelien", + "Baptiste", + "Barnabé", + "Barthélémy", + "Basile", + "Bastien", + "Baudouin", + "Benjamin", + "Benoit", + "Bernard", + "Bertrand", + "Blaise", + "Boniface", + "Brice", + "Bruno", + "Camille", + "Célestin", + "Cesaire", + "César", + "Charles", + "Charlot", + "Christian", + "Christophe", + "Claude", + "Clément", + "Colombain", + "Colombe", + "Constant", + "Constantin", + "Corentin", + "Corin", + "Cyrille", + "Damien", + "Daniel", + "David", + "Denis", + "Dennis", + "Désiré", + "Didier", + "Dieudonné", + "Dimitri", + "Diodore", + "Dion", + "Dominique", + "Donat", + "Donatien", + "Edgar", + "Edgard", + "Edmond", + "édouard", + "Eloi", + "émile", + "émilien", + "Emmanuel", + "Eric", + "Ermenegilde", + "Esmé", + "étienne", + "Eugène", + "Eustache", + "évariste", + "Evrard", + "Fabien", + "Fabrice", + "Felicien", + "Félix", + "Ferdinand", + "Fernand", + "Fiacre", + "Firmin", + "Florence", + "Florentin", + "Florian", + "Franck", + "François", + "Frédéric", + "Gabin", + "Gabriel", + "Gaétan", + "Gaspard", + "Gaston", + "Gautier", + "Geoffroi", + "Georges", + "Gerald", + "Gérard", + "Géraud", + "Germain", + "Gervais", + "Gervaise", + "Ghislain", + "Gilbert", + "Gilles", + "Godelieve", + "Gratien", + "Grégoire", + "Guillaume", + "Gustave", + "Guy", + "Hector", + "Henri", + "Herbert", + "Hercule", + "Hervé", + "Hilaire", + "Hippolyte", + "Honoré", + "Horace", + "Hubert", + "Hugues", + "Humbert", + "Ignace", + "Iréné", + "Isidore", + "Jacques", + "Jean", + "Jeannot", + "Jérémie", + "Jérôme", + "Joachim", + "Joël", + "Joseph", + "Josue", + "Jourdain", + "Jules", + "Julien", + "Juste", + "Justin", + "Lambert", + "Laurence", + "Laurent", + "Lazare", + "Léandre", + "Léon", + "Léonard", + "Léonce", + "Léopold", + "Lionel", + "Loic", + "Lothaire", + "Louis", + "Loup", + "Luc", + "Lucas", + "Lucien", + "Marc", + "Marcel", + "Marcellin", + "Marin", + "Marius", + "Martin", + "Mathieu", + "Mathis", + "Matthieu", + "Maurice", + "Maxime", + "Maximilien", + "Michel", + "Modeste", + "Modestine", + "Narcisse", + "Nazaire", + "Nicholas", + "Nicodème", + "Nicolas", + "Noah", + "Noé", + "Noel", + "Odilon", + "Olivier", + "Onesime", + "Osanne", + "Ozanne", + "Papillion", + "Pascal", + "Paschal", + "Patrice", + "Patrick", + "Paul", + "Perceval", + "Philbert", + "Philibert", + "Philippe", + "Pierre", + "Pierrick", + "Pons", + "Prosper", + "Quentin", + "Rainier", + "Raoul", + "Raphaël", + "Raphael", + "Raymond", + "Régis", + "Rémi", + "Rémy", + "Renard", + "Renaud", + "René", + "Reynaud", + "Richard", + "Robert", + "Roch", + "Rodolph", + "Rodolphe", + "Rodrigue", + "Roger", + "Roland", + "Romain", + "Sacha", + "Samuel", + "Sébastien", + "Serge", + "Séverin", + "Simon", + "Simone", + "Stéphane", + "Sylvain", + "Sylvestre", + "Telesphore", + "Theirn", + "Théo", + "Théodore", + "Théophile", + "Thibault", + "Thierry", + "Thomas", + "Timothée", + "Toussaint", + "Tristan", + "Ulrich", + "Urbain", + "Valentin", + "Valère", + "Valéry", + "Vespasien", + "Victor", + "Vincent", + "Vivien", + "Xavier", + "Yanick", + "Yann", + "Yannic", + "Yannick", + "Yves", + "Zacharie", +] diff --git a/src/lib/session.ts b/src/lib/session.ts new file mode 100644 index 0000000..6ac4073 --- /dev/null +++ b/src/lib/session.ts @@ -0,0 +1,69 @@ +import { TextLineStream } from "https://deno.land/std@0.224.0/streams/mod.ts" +import { writeAll } from "https://deno.land/std@0.224.0/io/write_all.ts" +import ollama, { Message } from "npm:ollama@0.5.15" + +import { History } from "./history.ts" + +const header = (name: string) => ` + KUB + +Kub is a LLM running on TCP which includes all messages in the same context. +As Kub is having several discussions at once, he will know you as : ${name} +─────────────────────────────────────────────────────────────────────────────── + +` + +export async function session(conn: Deno.TcpConn, name: string, history: History) { + console.log("Opening session for", name, "at", conn.remoteAddr.hostname, conn.remoteAddr.port) + conn.setNoDelay(true) + await write_encoded(conn, header(name)) + await write_encoded(conn, "> ") + const lines = conn.readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()) + for await (const line of lines) { + if (line === "") continue + if (line.length > 10_000) return + console.log("prompted '", line, "' by ", conn.remoteAddr.hostname) + await write_encoded(conn, "\n< ") + const user_message = make_message(name, line) + const response_parts = await get_response_parts(history, user_message) + let content = "", role = "assistant" + for await (const part of response_parts) { + await write_encoded(conn, part.message.content) + content += part.message.content + role = part.message.role + } + await write_encoded(conn, "\n\n>") + history.push(user_message) + history.push({ role, content }) + } +} + +async function write_encoded(connection: Deno.TcpConn, text: string) { + const encoded = new TextEncoder().encode(text) + await writeAll(connection, encoded) +} + +async function get_response_parts(messages: History, user_message: { role: string; content: string }) { + return await ollama.chat({ + model: "llama3.2", + messages: request_messages(messages.messages, user_message), + stream: true, + options: {}, + }) +} + +function make_message(name: string, line: string) { + const user_content = `(message from:${name}) ${line}` + const user_message = { role: "user", content: user_content } + return user_message +} + +function request_messages(history_messages: Message[], user_message: Message) { + return [ + { role: "system", content: "You are 'Kub', a bot which talks to people on the internet. Be concise and helpful." }, + ...history_messages, + user_message, + ] as Message[] +}