Add packaging & split code.
This commit is contained in:
parent
8346df8a57
commit
34123a7e5a
10 changed files with 445 additions and 93 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/history
|
7
build.sh
Executable file
7
build.sh
Executable file
|
@ -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
|
1
history.json
Normal file
1
history.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"messages":[]}
|
24
package/aur/PKGBUILD
Normal file
24
package/aur/PKGBUILD
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Maintainer: JOLIMAITRE Matthieu <matthieu@imagevo.fr>
|
||||
|
||||
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/"
|
||||
}
|
17
src/kub-tcp-service.ts
Executable file
17
src/kub-tcp-service.ts
Executable file
|
@ -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()
|
93
src/kub.ts
93
src/kub.ts
|
@ -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<Message>(500)
|
||||
listen(8080, (conn) => session(conn, names.pick_for(conn.remoteAddr.hostname), messages))
|
||||
}
|
||||
|
||||
class SpillingQueue<T> {
|
||||
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<unknown>) {
|
||||
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<Message>) {
|
||||
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()
|
35
src/lib/history.ts
Normal file
35
src/lib/history.ts
Normal file
|
@ -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 }))
|
||||
}
|
||||
}
|
6
src/lib/listen.ts
Normal file
6
src/lib/listen.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export async function listen(port: number, handler: (conn: Deno.TcpConn) => Promise<unknown>) {
|
||||
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))
|
||||
}
|
||||
}
|
284
src/lib/names.ts
Normal file
284
src/lib/names.ts
Normal file
|
@ -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",
|
||||
]
|
69
src/lib/session.ts
Normal file
69
src/lib/session.ts
Normal file
|
@ -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[]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue