current chat
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user