From 8964b33ffc4565cf4fdf97538e3be252d369cbba Mon Sep 17 00:00:00 2001 From: irrlicht Date: Sun, 30 Aug 2026 18:37:18 +0200 Subject: [PATCH] current chat --- __pycache__/chat.cpython-314.pyc | Bin 0 -> 7543 bytes __pycache__/tui.cpython-314.pyc | Bin 0 -> 11975 bytes chat.md | 9 ++ chat.py | 131 +++++++++++++++++++++++ tui.py | 174 +++++++++++++++++++++++++++++++ weltretten.md | 31 ++++++ 6 files changed, 345 insertions(+) create mode 100644 __pycache__/chat.cpython-314.pyc create mode 100644 __pycache__/tui.cpython-314.pyc create mode 100644 chat.md create mode 100644 chat.py create mode 100644 tui.py create mode 100644 weltretten.md diff --git a/__pycache__/chat.cpython-314.pyc b/__pycache__/chat.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..290f5b999d77cc5174a683cfece02e97c4ba984d GIT binary patch literal 7543 zcmbVRYfu|mcD^mO9!LW5G+@BAUo7%~z>hKbImSF}-kOHdOx6NbBQ>avXlZV@{Lro) z?UpOS-rWN4Ix$o0sdy^h8m2O-c#^3a=T~OEm8#@dz_3Qs%ho!{q*C(_mUTQk`IB>R z>jlGjlU&g4qwafepL^~(-+Abjf_xi-_WD2GzW8qi2>m^7gbs1{1I-PZGYYF;hkO{$GIt9IU^ zIz|kho#|I)ue%X%g(ub7J89~OL8nybH_pua12YSGMkyKr?j{-F#a51ZdpQahs>Qqm z=zL0-@C86SDP77J0$oJuGQJq-5=!sjOMxz;TqA-69ZgHJ*FdCC#LQD>FAwiTUR4xc}+hp}} z1grF=vKS*HC0WJ71gz?|+34GJKDc^55{9LYbIMgk6{ol+PEZs{Q3Y8AI;M!&b=+p- zpk2^MjL?U|0sPB5uR#%)qSWj5D80TSdtK! zQ=A+1U8vXP_T7wNzDuR*8}TUz_QenQH_CeM-Gs&0njK!KBN)jFv4eJ zA9M-iMWcq+8=#bMuNuA1gczRj!4thTfm*r@UywPP8H;KR%NW>0uuHS92MSqtkJi`} zicE=362@V29!(w`xhPJmVp9NA7gR{Z<3d3H2BOm(e!n2g5f!x8@5gpnVn2M866g}> z1u`)u>!iD5rk4%4jRK@gHxm%RFuQREhy=lLqT<*9HHqwrR_%y*M#IXKp&-lyJ~S7oGU>6Xp%Rlu!`L z3acRpaGMN7MA$-jV^y#^Yy}cFV^mbEv0<<`jSWfx)y3dEc%o|r-3vWg+BK2~yPzY- zTgtSolkU#Y8oAsXez5P|Z__*3&A?FFGAA7cMH5VdS+FFK8%4}K6S2V6#2dyXfsfXvi`SdRvWpJwTg0#45J5EJwAdH-Jzc+O>s~ zA#)*mgN+&7C}!v?L+I@ep3ObX^e!g!H-V0M*#SduxxQA+5M}qm%v$(FSqZsJ@vd)+ znAim1r>q04B!>h*OG$S#Tpygj$X$xa+;?y!EH-d~2$;L9UTYjkqq8Wx2R3Oa(Kw>s z8^n8Wk3qbFV9y7--cgMkz+l^eJliUn#zh4T_D0~weKH&gfZIIn&PAESFj?6_-}eF; zOoPli?PrhZS6%bqai$S1r%YY};-oQwh797Xvn zmez!(jJ`oUfFK@#kBc49SRz-=l9H>&*ueJ8LKM7uP-8DfBv~_oB}!3Zl4G%P%{nD2 ziZCH6m>8Tw_(>!ZI=Gm|!!aeKRcI?ueXB z9f>T?oH?U!%a+{_Zic56Zhi{HR|)j2swQzhSzeXsNS2i++F!8b@PA7Wf6a=D%I-RE zJD(Qro@Q4aWptw*OOD;s%x~GxVEGB!zougZQ@XO(g5}vJl>ogF^uAEKH`& zXHHYOXs`*|Avg@_(9U9FCzx0%24q0E0=jQ7Jc{le@Lbsg9YJ@NX$LLyS6n# zK2SgG_*tCZ{W`)E&WMg~T&D~8GPv!{SWOhdld9o^Y{L5}H}O0#K(={nGNVCcLyK|QP!EsPWyL0Bm%@Z@HZkC8d?PasUyYg*$ zeqvT$+HrKr-m<{9WCfNM3{ih7FfU#2<^KH}RRiYgjW=c$iD!9J6Sx4EnfiOWGiq~~ z7_|<=E<;v5UZxbiFhe1tAxdEZ%v`7a9p&EepT*ZG{b7lI{aDejk5qk(zUN#ZP3;*1 zK9B+gd}&#LVx*7nf2$-x0CjNSXgilrgT&O@{S0s1(r_3(kYi4lUHK!jz~k8 zgG=0CvuVtQxM2iRBdZQ2Eo4k98B;XOw{`b*51jLN_Irl^h`0r#5*vdQgE}(Jb{R{M z8PH&JFHCTmwrC<)Cox8_PR$&};<$80AqHPp2W~;PL5J=j{bOV|n3x~c#~VN-(B~Nw zJlyihiH}Zva_VnSO{+84Z(hHDdHVXYv+2{Z#QFbh?_G9sE6#n3&VBd!`HCgyp%v$` zMdz_c`<9)rExbOIIRBeH2WG9)@ww8){9O+XiLNAT{nNqk4bEPg4b2Jn*||&ihvv)w zrsjj1zp7oRKlZ42q4eaZmlK0e*}<<{_5T;}b0YQs6X5Iql-LyjTn6PaF*<7BFEi~o zU{D6{i<#ne1l_$O57>8)uMzUY`i~F)btAo--6+hVji50cD!cIz3~djp-DJ*b)%9eO zlGH4?c-SnRrnbX&AK4UbUgV=yEAoY!L2894F>QmFfjnRq@&GVe4{^p@+GejpV&v_| zjiOwi8fCq#2P$M+TY9x1jUl%m<0;%a3!%)sN(vC$uD#v7D<}c=HFyt+nzyv{nyD<{ z2FU`p8y7Y7{y3FoKoa<>QDR(d91p4B{H8V+jMT`aC|?#OMRap~Obkp;2xB5dc91&{ zsgbB&5s@OY`;riji7ry4v5|_*xFkzT$PWuDkU`Y%QDE?%25t{jmL}8?P;R%|iZ@<~Obulq8EP zS4(P=&XTn}RN&q~rUG-KYt4an?n-q1s;GS0lysKPT${NzeeE+xX|ia?-J07qb8r8$ zX!o=^X)l^NIdgLQlTaao;d1Y+={&x{-4-u zlVw#aWv<0C*R3vyT#GB#3X!8Y(f@me1bp9bJJ!B~`T5Bz=pR>n3;JKM#n5jGQ;C@p z$oPYq9Ik>_4yaXRLIH2~okpIGvQ?-G`7&Vu1R$vZCTfE4peVa`n(~^`BWjV4 z*bHr66L02CyrqRm6dF_*AdBLVuT=kzkQoI51FKQgJZ$!m3}H))vNaHJM9r_6`>py_ zVkC6JSc4=c`<^ty*P>yTx6*p}J}Q?CCw!id=pSt$3ELrK&Y9%6yYv{|p04Y`ernQp zhY|1K%^niBZfV1rJo`vyuRIBQ^S>aHivuP=5n(DAFYBe1;G8-Jsd~h1+=Ms|g>zEY z7{(`I3Y7bdkd5o9t69gc`0fjD3RsTG6C;_j`j{wjS@1a(cXGlQ^ z9hV^4&&$+&U4?q~VT4s7NV=+MtbivjVUkp6EJQ&n#TSj0No8&~xoe7IsJLqRn}Am{ zQRKw~WCUO)fG?(D5hi(?%c2KZ1n6MCfK^2^PKYw*^&65H5H*&7vqFrHARROv?AR$m zk}wB#~y*B^$a&7Y+<6Y}*>s-;F+aF3x_A`l&RcH0w@RD;6 zgi-dARjYl*dds>{Qa@ihZ=S!nP;_kBdi+^=WukM{wqy3nl8sBWCo5|J4Gm^fb+Vv% zwX|ZcU0OyAAExw2(P?W&__cK;lIUtMuDe7S0`_#I+kFy#0}r|E0!<JeI5!h;_lcv$_&zfjP>JpAU$@aW?3=r67Q&)LG+(%J9a zKl}c<_s-3C{^h_^_Ao`ppPz0#`wDveioM-oe0>YygwM0kp6RULBEx67hRN zI9+2WP7uoM#7Vs@sv~bxz!rhW7pRb7*GcyR5-w)f6IAoV9kcs>RI`X`e%r>teTv4e zop=hyLs{D&j0F6C&45KbO7Dh5TBT%^#B+3pZ@PzwGM->x_ zTvJhE0JiRP1A}~LI~2&Vi#$F}I zv6dxN^V|%-W;`#1V)t`5)I**ZkbL-gDTK<;4}gbTYeuYNI=*bI{EW5!Nf*@8*qv0s F{{!v)+>QVM literal 0 HcmV?d00001 diff --git a/__pycache__/tui.cpython-314.pyc b/__pycache__/tui.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aad3f6b2bbe3990a9affbf861574ae4b289b52d5 GIT binary patch literal 11975 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 literal 0 HcmV?d00001 diff --git a/chat.md b/chat.md new file mode 100644 index 0000000..3596579 --- /dev/null +++ b/chat.md @@ -0,0 +1,9 @@ +[system] +Du bist ein wortkarger Universalgelehrter und Professor der Informatik mit langjähriger Erfahrung. Du kennst dich mit sehr vielen Themen aus, aber dein Fokus ist in der Informatik. +Antworte präzise und knapp. +Nutze nur dann Beispiele, wenn du gefragt wirst. Wenn möglich, antworte in etwa genauso lang, wie die Anfrage ist. + +[assistant] +Verstanden. + + diff --git a/chat.py b/chat.py new file mode 100644 index 0000000..d21a3f0 --- /dev/null +++ b/chat.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Liest eine als Chat formatierte Markdown-Datei, schickt sie an einen lokalen +OpenAI-kompatiblen Endpunkt und haengt die Antwort als neuen Eintrag an. + +Dateiformat (Rollen: system / assistant / user): + + [system] + foo bar lorem ipsum + + [assistant] + okok verstanden + + [user] + Textnachricht + +Die Datei ist die einzige Quelle der Wahrheit. Dieses Modul stellt die +Bausteine bereit, die auch das TUel (tui.py) verwendet. +""" + +import re +import sys +from pathlib import Path +from typing import Iterator + +from openai import OpenAI + +BASE_URL = "http://127.0.0.1:8000/v1" +MODEL = "glm-5.2-colibri" +MAX_TOKENS = 1024 + +HEADER_RE = re.compile(r"^\[(system|assistant|user)\]\s*$", re.IGNORECASE) +TRAILING_EMPTY_RE = re.compile(r"\n*\[(?:system|assistant|user)\][ \t]*\s*$", re.IGNORECASE) + +_client: OpenAI | None = None + + +def client() -> OpenAI: + """Lazy angelegter OpenAI-Client (lokaler Endpunkt, API-Key beliebig).""" + global _client + if _client is None: + _client = OpenAI(base_url=BASE_URL, api_key="not-needed") + return _client + + +def parse_chat(text: str) -> list[dict]: + """Zerlegt den Dateiinhalt in eine Liste von {role, content}-Nachrichten.""" + messages: list[dict] = [] + role: str | None = None + buf: list[str] = [] + + def flush() -> None: + if role is not None: + messages.append({"role": role, "content": "\n".join(buf).strip()}) + + for line in text.splitlines(): + m = HEADER_RE.match(line) + if m: + flush() + role = m.group(1).lower() + buf = [] + elif role is not None: + buf.append(line) + flush() + + return [msg for msg in messages if msg["content"]] + + +def load_messages(path: str | Path) -> list[dict]: + path = Path(path) + if not path.exists(): + return [] + return parse_chat(path.read_text(encoding="utf-8")) + + +def append_block(path: str | Path, role: str, content: str) -> None: + """Haengt einen neuen [role]-Block an die Datei an (Datei = Quelle der Wahrheit).""" + path = Path(path) + existing = path.read_text(encoding="utf-8") if path.exists() else "" + # einen evtl. am Ende stehenden leeren Block (nur Header) verwerfen + existing = TRAILING_EMPTY_RE.sub("", existing).rstrip("\n") if existing else "" + prefix = f"{existing}\n\n" if existing else "" + path.write_text(f"{prefix}[{role}]\n{content.strip()}\n", encoding="utf-8") + + +def stream_reply(messages: list[dict]) -> Iterator[str]: + """Streamt die Antwort tokenweise. Rueckgabewert (StopIteration.value) ist + der finish_reason ("stop", "length", ...).""" + stream = client().chat.completions.create( + model=MODEL, + messages=messages, + max_tokens=MAX_TOKENS, + stream=True, + ) + finish: str | None = None + for chunk in stream: + if not chunk.choices: + continue + choice = chunk.choices[0] + if choice.delta and choice.delta.content: + yield choice.delta.content + if choice.finish_reason: + finish = choice.finish_reason + return finish + + +def main() -> None: + path = Path(sys.argv[1] if len(sys.argv) > 1 else "chat.md") + messages = load_messages(path) + if not messages: + sys.exit(f"Keine Nachrichten in {path} gefunden.") + + parts: list[str] = [] + gen = stream_reply(messages) + finish: str | None = None + try: + while True: + piece = next(gen) + parts.append(piece) + print(piece, end="", flush=True) + except StopIteration as stop: + finish = stop.value + print() + + if finish == "length": + print(f"[Warnung] Antwort bei MAX_TOKENS={MAX_TOKENS} abgeschnitten.", file=sys.stderr) + + append_block(path, "assistant", "".join(parts)) + + +if __name__ == "__main__": + main() diff --git a/tui.py b/tui.py new file mode 100644 index 0000000..8059f5f --- /dev/null +++ b/tui.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Kleines TUI um chat.py. + +- zeigt den Chatverlauf aus der Markdown-Datei +- mehrzeiliges Eingabefeld (Enter = neue Zeile, Ctrl+S = senden) +- die Antwort der LLM fliegt tokenweise rein + +Quelle der Wahrheit bleibt die Datei (Default: chat.md). Jede gesendete +Nachricht und jede fertige Antwort wird sofort dort angehaengt. + + python tui.py [pfad/zur/chat.md] +""" + +import sys +from pathlib import Path + +from rich.markup import escape +from textual import work +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import VerticalScroll +from textual.widgets import Footer, Header, Static, TextArea + +import chat + +ROLE_STYLE = {"system": "dim italic", "user": "bold cyan", "assistant": "bold green"} + + +class Message(Static): + """Ein Chat-Eintrag; waehrend des Streamens waechst der Text.""" + + def __init__(self, role: str, text: str = "") -> None: + super().__init__(markup=True) + self.role = role + self._text = text + self.add_class(role) + self._rerender() + + def _rerender(self) -> None: + style = ROLE_STYLE.get(self.role, "bold") + head = f"[{style}]{self.role}[/]" + body = escape(self._text) or "[dim]…[/]" + self.update(f"{head}\n{body}") + + def append(self, chunk: str) -> None: + self._text += chunk + self._rerender() + + @property + def text(self) -> str: + return self._text + + +class ChatApp(App): + CSS = """ + #log { padding: 1 2; } + Message { margin-bottom: 1; } + #input { + height: auto; + min-height: 3; + max-height: 15; + border: round $accent; + margin: 0 1 1 1; + } + """ + + BINDINGS = [ + Binding("ctrl+s", "send", "Senden"), + Binding("ctrl+r", "reload", "Neu laden"), + Binding("ctrl+q", "quit", "Beenden"), + ] + + def __init__(self, path: str | Path) -> None: + super().__init__() + self.path = Path(path) + self.title = f"chat — {self.path.name}" + self._current: Message | None = None + self._busy = False + + def compose(self) -> ComposeResult: + yield Header() + yield VerticalScroll(id="log") + yield TextArea(id="input") + yield Footer() + + def on_mount(self) -> None: + self._load_history() + self.query_one("#input", TextArea).focus() + + def _load_history(self) -> None: + """Baut den sichtbaren Verlauf frisch aus der Datei auf.""" + log = self.query_one("#log", VerticalScroll) + log.remove_children() + for msg in chat.load_messages(self.path): + log.mount(Message(msg["role"], msg["content"])) + log.scroll_end(animate=False) + + def action_reload(self) -> None: + if self._busy: + self.notify("Antwort laeuft noch …", severity="warning") + return + self._load_history() + self.notify(f"{self.path.name} neu geladen.") + + def action_send(self) -> None: + if self._busy: + self.notify("Antwort laeuft noch …", severity="warning") + return + area = self.query_one("#input", TextArea) + text = area.text.strip() + if not text: + return + area.text = "" + self._busy = True + + chat.append_block(self.path, "user", text) + log = self.query_one("#log", VerticalScroll) + log.mount(Message("user", text)) + self._current = Message("assistant") + log.mount(self._current) + log.scroll_end() + + self._run_stream() + + @work(thread=True) + def _run_stream(self) -> None: + messages = chat.load_messages(self.path) + gen = chat.stream_reply(messages) + finish: str | None = None + try: + while True: + try: + piece = next(gen) + except StopIteration as stop: + finish = stop.value + break + self.call_from_thread(self._on_token, piece) + except Exception as exc: # Netzwerk / Server / Parsing + self.call_from_thread(self._on_error, str(exc)) + return + self.call_from_thread(self._on_done, finish) + + def _on_token(self, piece: str) -> None: + assert self._current is not None + self._current.append(piece) + self.query_one("#log", VerticalScroll).scroll_end() + + def _on_done(self, finish: str | None) -> None: + assert self._current is not None + chat.append_block(self.path, "assistant", self._current.text) + if finish == "length": + self.notify( + f"Antwort bei MAX_TOKENS={chat.MAX_TOKENS} abgeschnitten.", + severity="warning", + ) + self._busy = False + self._current = None + self.query_one("#input", TextArea).focus() + + def _on_error(self, msg: str) -> None: + if self._current is not None: + self._current.append(f"\n⚠ Fehler: {msg}") + self.notify(msg, title="Fehler", severity="error") + self._busy = False + self._current = None + + +def main() -> None: + path = sys.argv[1] if len(sys.argv) > 1 else "chat.md" + ChatApp(path).run() + + +if __name__ == "__main__": + main() diff --git a/weltretten.md b/weltretten.md new file mode 100644 index 0000000..932b612 --- /dev/null +++ b/weltretten.md @@ -0,0 +1,31 @@ +[system] +Du bist ein wortkarger Universalgelehrter und Professor der Informatik mit langjähriger Erfahrung. Du kennst dich mit sehr vielen Themen aus, aber dein Fokus ist in der Informatik. +Antworte präzise und knapp. +Nutze nur dann Beispiele, wenn du gefragt wirst. Wenn möglich, antworte in etwa genauso lang, wie die Anfrage ist. + +[assistant] +Verstanden. + +[user] +Wie retten wir die Welt? + +[assistant] +Durch rigorose Anwendung von Wissenschaft und Technologie: globale Vernetzung zur Ressourcenoptimierung, KI-gestützte Krisenprävention und Übergang zu erneuerbaren Energien. + +[user] +Das internet ist unsere beste Chance auf internationale Vernetzung. Welche Hürden siehst du? + +[assistant] +Hürden sind asymmetrische Zugangsmöglichkeiten, staatliche Zensur, massenhafte Desinformation und fehlende, weltweite Protokollstandardisierung. + +[user] +Die Zugangsmöglichkeiten sind Ökonomisch und politisch beeinflusst. Aber selbst in rural india wird 5G ausgebaut und ist bezahlbar für locals. Das entwicklet sich also überall. Staatliche Zensur ist echt: great firewall of China, Iran, Russland und das nordkoreanische Intranet... Manches davon kann mit Mixnets umgangen werden, aber drakonsiche Strafen blockieren dennoch. Massenhafte Desinformation ist ein riesiges Problem! Gerade durch LLM wird das erstellen von Inhalten so billig und einfach. Wir bauen uns die Bibliothek von Babel! Protokollstandards sehe ich weniger. Der TCP/IP-Stack ist recht universell. HTTPS und der Webstack auch. Wenn wir menschliche Sprachbarrieren als Protokoll ansehen gehe ich eher mit. Ich kann kein Russisch lesen. Also bringt es mir auch wenig, wenn ich russische Imageboards besuchen kann. Da könnte LLM helfen! + +Wie gehen wir vor? + +[assistant] +1. **Zensur:** Fokus auf verdeckte Kanäle (Steganografie) statt reine Mixnets, um drakonische Strafen zu umgehen. +2. **Desinformation:** Kryptografische Content-Verifikation (z. B. PKI) und LLMs als skalierbare Filter. +3. **Sprachbarrieren:** Echtzeit-LLM-Übersetzung als Standard-Browserfeature zur globalen Interoperabilität. + +Vorgehen: Dezentrale, zensurresistente Infrastruktur priorisieren. KI-gestützte Übersetzung und Faktenprüfung integrieren.