current chat

This commit is contained in:
irrlicht
2026-08-30 18:46:48 +02:00
committed by irrlicht
commit 8964b33ffc
6 changed files with 345 additions and 0 deletions
Binary file not shown.
Binary file not shown.
+9
View File
@@ -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.
+131
View File
@@ -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()
+174
View File
@@ -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()
+31
View File
@@ -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.