mirror of
https://github.com/Jeuners/astra-vision.git
synced 2026-09-09 15:02:35 +02:00
feat: read German news from curated RSS feeds
Neues Werkzeug read_news, gleiche Trennung wie ComfyUI: - astra/feeds.py: kuratierte deutsche Feed-Liste nach Thema (tech, nachrichten, wirtschaft) — 11 Feeds, alle live gegen die echten Endpunkte verifiziert (heise, Golem, t3n, netzpolitik.org, tagesschau.de, Zeit Online, Spiegel, SZ, Handelsblatt, WiWo, manager magazin). - astra/rss.py: async Feed-Client (feedparser), holt Feeds eines Themas parallel ab, überspringt einzelne nicht erreichbare Feeds statt komplett zu scheitern. - astra/tools.py: read_news als natives Ollama-Tool, gleiches tool_start/tool_result/tool_error-UI-Muster wie generate_image. Mit zwei Tools gleichzeitig verfügbar sank die Zuverlässigkeit des lokalen 9.7B-Modells spürbar (0/8 Tool-Aufrufe mit dem ursprünglichen, langen System-Prompt) — reproduzierbar isoliert über direkte Ollama-Requests. Ursache: Instruction-Dilution bei mehreren Tool-Regeln in einem langen Prompt. Behoben durch gestrafften, Tool-Regeln-zuerst-Prompt und Temperatur 0.6→0.4: Bildgenerierung bleibt ~100% zuverlässig, Nachrichten-Tool bei ~60–75% je nach Formulierung. Per echtem Browser-Test mit synthetisierter Sprache verifiziert: echte aktuelle Schlagzeilen erscheinen im Transkript. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA
This commit is contained in:
parent
bf1e72bc55
commit
e7760688f0
10 changed files with 360 additions and 6 deletions
18
README.md
18
README.md
|
|
@ -49,6 +49,24 @@ Gesprächs über den Button „Bild oder PDF hinzufügen“ hochladen
|
|||
eingefügt, ein Bild als Base64 mit Qwens nativer Vision-Fähigkeit — beides
|
||||
nur für die Dauer der Session, nichts wird auf Disk geschrieben.
|
||||
|
||||
## Nachrichten aus deutschen RSS-Feeds
|
||||
|
||||
- **`astra/feeds.py`** — kuratierte Feed-Liste, nach Thema gruppiert
|
||||
(`tech`, `nachrichten`, `wirtschaft`). Reine Daten, editierbar.
|
||||
- **`astra/rss.py`** — async Feed-Client (`feedparser`), holt konfigurierte
|
||||
Feeds eines Themas parallel ab, überspringt nicht erreichbare Feeds statt
|
||||
komplett zu scheitern.
|
||||
- Tool `read_news` in `astra/tools.py`: Astra ruft es auf, wenn nach
|
||||
aktuellen Nachrichten gefragt wird, und fasst die Schlagzeilen mündlich
|
||||
zusammen statt sie roh vorzulesen.
|
||||
|
||||
Mit zwei gleichzeitig verfügbaren Tools (Bild + Nachrichten) sinkt beim
|
||||
lokalen 9,7B-Modell die Zuverlässigkeit, tatsächlich das Werkzeug
|
||||
aufzurufen statt Inhalte zu erfinden — gemessen ca. 60–75 % je nach
|
||||
Formulierung, trotz gestrafftem System-Prompt und reduzierter Temperatur
|
||||
(`0.4`). Bildgenerierung allein bleibt zuverlässig (~100 %). Bekannte
|
||||
Grenze eines kleinen lokalen Modells, kein Bug.
|
||||
|
||||
## Starten
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -64,13 +64,16 @@ VOICE_NAMES = frozenset(voice["name"] for voice in VOICES)
|
|||
|
||||
SYSTEM_PROMPT = (
|
||||
"Du bist Astra, ein freundlicher deutschsprachiger Gesprächsassistent. "
|
||||
"Du hast zwei Werkzeuge: generate_image für Bilder, Grafiken oder Illustrationen, und "
|
||||
"read_news für aktuelle Nachrichten. Fragt der Nutzer danach, verwende SOFORT das passende "
|
||||
"Werkzeug, statt nur anzukündigen, dass du das tun wirst, und statt Inhalte selbst zu "
|
||||
"erfinden. Fasse Ergebnisse von read_news danach mündlich in eigenen Worten zusammen, "
|
||||
"lies sie nicht roh vor. "
|
||||
"Antworte natürlich und knapp, normalerweise in ein bis drei kurzen Sätzen. "
|
||||
"Deine Antwort wird vorgelesen: kein Markdown, keine Sternchen, keine Listen. "
|
||||
"Sprich Zahlen und Abkürzungen verständlich aus. Stelle bei Bedarf eine kurze Rückfrage. "
|
||||
"Du hast keinen Internetzugang und keinen Zugriff auf Dateien oder Apps, außer den dir "
|
||||
"explizit gegebenen Werkzeugen. Behaupte nicht, andere Aktionen ausgeführt zu haben. "
|
||||
"Wenn der Nutzer ein Bild, eine Grafik oder eine Illustration möchte, rufe sofort das "
|
||||
"Werkzeug generate_image auf, statt das Bild nur in Worten zu beschreiben. "
|
||||
"Du hast sonst keinen Internetzugang und keinen Zugriff auf Dateien oder Apps. "
|
||||
"Behaupte nicht, andere Aktionen ausgeführt zu haben. "
|
||||
"Wenn der Nutzer ein Bild, ein Dokument oder ein PDF hochlädt, geht dessen Inhalt oder "
|
||||
"eine Textzusammenfassung als Nachricht in dieses Gespräch ein."
|
||||
)
|
||||
|
|
@ -143,7 +146,7 @@ def build_request(settings: Settings, messages: list[dict]) -> dict:
|
|||
"options": {
|
||||
"num_ctx": settings.context_tokens,
|
||||
"num_predict": 256,
|
||||
"temperature": 0.6,
|
||||
"temperature": 0.4,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
31
astra/feeds.py
Normal file
31
astra/feeds.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Curated German RSS/Atom feed catalog, grouped by topic. Pure data."""
|
||||
|
||||
FEEDS: dict[str, tuple[dict[str, str], ...]] = {
|
||||
"tech": (
|
||||
{"name": "heise online", "url": "https://www.heise.de/rss/heise-atom.xml"},
|
||||
{"name": "Golem.de", "url": "https://rss.golem.de/rss.php?feed=RSS2.0"},
|
||||
{"name": "t3n", "url": "https://t3n.de/rss.xml"},
|
||||
{"name": "netzpolitik.org", "url": "https://netzpolitik.org/feed/"},
|
||||
),
|
||||
"nachrichten": (
|
||||
{
|
||||
"name": "tagesschau.de",
|
||||
"url": "https://www.tagesschau.de/infoservices/alle-meldungen-100~rss2.xml",
|
||||
},
|
||||
{"name": "Zeit Online", "url": "https://newsfeed.zeit.de/index"},
|
||||
{"name": "Spiegel", "url": "https://www.spiegel.de/schlagzeilen/index.rss"},
|
||||
{"name": "Süddeutsche Zeitung", "url": "https://rss.sueddeutsche.de/rss/Topthemen"},
|
||||
),
|
||||
"wirtschaft": (
|
||||
{
|
||||
"name": "Handelsblatt",
|
||||
"url": "https://www.handelsblatt.com/contentexport/feed/schlagzeilen",
|
||||
},
|
||||
{
|
||||
"name": "WirtschaftsWoche",
|
||||
"url": "https://www.wiwo.de/contentexport/feed/rss/schlagzeilen",
|
||||
},
|
||||
{"name": "manager magazin", "url": "https://www.manager-magazin.de/unternehmen/index.rss"},
|
||||
),
|
||||
}
|
||||
TOPICS = tuple(FEEDS.keys())
|
||||
62
astra/rss.py
Normal file
62
astra/rss.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""Async reader for the curated RSS/Atom feeds. Knows nothing about the voice pipeline."""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
import feedparser
|
||||
import httpx
|
||||
|
||||
from astra.feeds import FEEDS
|
||||
|
||||
USER_AGENT = "Mozilla/5.0 (compatible; AstraVoice/1.0; +https://github.com/Jeuners/astra-vision)"
|
||||
|
||||
|
||||
class RSSError(Exception):
|
||||
"""Raised when no feed for a topic could be read."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Entry:
|
||||
source: str
|
||||
title: str
|
||||
summary: str
|
||||
link: str
|
||||
|
||||
|
||||
def _clean_summary(html: str, max_chars: int = 220) -> str:
|
||||
text = re.sub(r"<[^>]+>", "", html or "").strip()
|
||||
return text[:max_chars]
|
||||
|
||||
|
||||
async def _fetch_one(client: httpx.AsyncClient, feed: dict, per_feed: int) -> list[Entry]:
|
||||
try:
|
||||
response = await client.get(feed["url"])
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError:
|
||||
return []
|
||||
parsed = feedparser.parse(response.content)
|
||||
return [
|
||||
Entry(
|
||||
source=feed["name"],
|
||||
title=item.get("title", "").strip(),
|
||||
summary=_clean_summary(item.get("summary", "")),
|
||||
link=item.get("link", ""),
|
||||
)
|
||||
for item in parsed.entries[:per_feed]
|
||||
]
|
||||
|
||||
|
||||
async def read_topic(topic: str, *, per_feed: int = 4, total: int = 10) -> list[Entry]:
|
||||
"""Fetch the latest entries for one configured topic, feeds in parallel."""
|
||||
feeds = FEEDS.get(topic)
|
||||
if not feeds:
|
||||
raise RSSError(f"Unbekanntes Thema: {topic}")
|
||||
async with httpx.AsyncClient(
|
||||
headers={"User-Agent": USER_AGENT}, timeout=10, follow_redirects=True
|
||||
) as client:
|
||||
results = await asyncio.gather(*(_fetch_one(client, feed, per_feed) for feed in feeds))
|
||||
entries = [entry for group in results for entry in group]
|
||||
if not entries:
|
||||
raise RSSError(f'Keine Feeds für "{topic}" waren erreichbar.')
|
||||
return entries[:total]
|
||||
|
|
@ -10,6 +10,8 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|||
|
||||
from astra.comfyui import ComfyUIError, generate_image
|
||||
from astra.core import Settings
|
||||
from astra.feeds import TOPICS
|
||||
from astra.rss import RSSError, read_topic
|
||||
|
||||
|
||||
def build_tools(config: Settings, notify: Callable[[dict], None], media_store: dict[str, bytes]) -> ToolsSchema:
|
||||
|
|
@ -33,6 +35,50 @@ def build_tools(config: Settings, notify: Callable[[dict], None], media_store: d
|
|||
{"status": "ok", "message": "Bild wurde erzeugt und dem Nutzer angezeigt."}
|
||||
)
|
||||
|
||||
async def handle_read_news(params):
|
||||
topic = params.arguments.get("topic", "")
|
||||
notify({"type": "activity", "text": "Astra liest Nachrichten …"})
|
||||
notify({"type": "tool_start", "text": f'Lese RSS-Feeds: "{topic}"'})
|
||||
try:
|
||||
entries = await read_topic(topic)
|
||||
except RSSError as exc:
|
||||
notify({"type": "activity", "text": "Nachrichten konnten nicht geladen werden."})
|
||||
notify({"type": "tool_error", "text": str(exc)})
|
||||
await params.result_callback({"error": str(exc)})
|
||||
return
|
||||
headlines = "\n".join(f"- ({entry.source}) {entry.title}" for entry in entries)
|
||||
notify({"type": "tool_result", "text": headlines})
|
||||
await params.result_callback(
|
||||
{
|
||||
"status": "ok",
|
||||
"headlines": [
|
||||
{"source": e.source, "title": e.title, "summary": e.summary, "link": e.link}
|
||||
for e in entries
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
read_news_schema = FunctionSchema(
|
||||
name="read_news",
|
||||
description=(
|
||||
"Lies aktuelle Schlagzeilen aus kuratierten deutschen RSS-Feeds zu einem "
|
||||
"Themenbereich vor. Nutze dieses Werkzeug, wenn der Nutzer nach aktuellen "
|
||||
"Nachrichten, News oder was gerade in einem Themenbereich passiert, fragt."
|
||||
),
|
||||
properties={
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"enum": list(TOPICS),
|
||||
"description": (
|
||||
"'tech' für Technologie & KI, 'nachrichten' für allgemeine "
|
||||
"Tagesnachrichten, 'wirtschaft' für Wirtschaftsnews."
|
||||
),
|
||||
},
|
||||
},
|
||||
required=["topic"],
|
||||
handler=handle_read_news,
|
||||
)
|
||||
|
||||
generate_image_schema = FunctionSchema(
|
||||
name="generate_image",
|
||||
description=(
|
||||
|
|
@ -53,4 +99,4 @@ def build_tools(config: Settings, notify: Callable[[dict], None], media_store: d
|
|||
required=["prompt"],
|
||||
handler=handle_generate_image,
|
||||
)
|
||||
return ToolsSchema(standard_tools=[generate_image_schema])
|
||||
return ToolsSchema(standard_tools=[generate_image_schema, read_news_schema])
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ dependencies = [
|
|||
"httpx",
|
||||
"pypdf",
|
||||
"python-multipart",
|
||||
"feedparser",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
|
|
|||
91
tests/test_rss.py
Normal file
91
tests/test_rss.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import httpx
|
||||
import pytest
|
||||
|
||||
import astra.rss as rss_module
|
||||
from astra.rss import RSSError, read_topic
|
||||
|
||||
SAMPLE_RSS = b"""<?xml version="1.0"?>
|
||||
<rss version="2.0"><channel>
|
||||
<title>Test Feed</title>
|
||||
<item><title>Erste Meldung</title><description><p>Kurzer Text</p></description><link>https://example.com/1</link></item>
|
||||
<item><title>Zweite Meldung</title><description>Noch mehr Text</description><link>https://example.com/2</link></item>
|
||||
</channel></rss>"""
|
||||
|
||||
|
||||
def _patched_client(monkeypatch, handler):
|
||||
real_async_client = httpx.AsyncClient
|
||||
|
||||
def factory(*args, **kwargs):
|
||||
kwargs["transport"] = httpx.MockTransport(handler)
|
||||
return real_async_client(**kwargs)
|
||||
|
||||
monkeypatch.setattr(httpx, "AsyncClient", factory)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_topic_combines_entries_from_all_feeds(monkeypatch):
|
||||
monkeypatch.setitem(
|
||||
rss_module.FEEDS,
|
||||
"testtopic",
|
||||
(
|
||||
{"name": "Feed A", "url": "https://feed-a.example/rss"},
|
||||
{"name": "Feed B", "url": "https://feed-b.example/rss"},
|
||||
),
|
||||
)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, content=SAMPLE_RSS)
|
||||
|
||||
_patched_client(monkeypatch, handler)
|
||||
entries = await read_topic("testtopic")
|
||||
|
||||
assert len(entries) == 4
|
||||
assert entries[0].title == "Erste Meldung"
|
||||
assert entries[0].summary == "Kurzer Text"
|
||||
assert entries[0].link == "https://example.com/1"
|
||||
assert entries[0].source in ("Feed A", "Feed B")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_topic_skips_unreachable_feeds(monkeypatch):
|
||||
monkeypatch.setitem(
|
||||
rss_module.FEEDS,
|
||||
"testtopic",
|
||||
(
|
||||
{"name": "Down", "url": "https://down.example/rss"},
|
||||
{"name": "Up", "url": "https://up.example/rss"},
|
||||
),
|
||||
)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if "down" in str(request.url):
|
||||
raise httpx.ConnectError("refused", request=request)
|
||||
return httpx.Response(200, content=SAMPLE_RSS)
|
||||
|
||||
_patched_client(monkeypatch, handler)
|
||||
entries = await read_topic("testtopic")
|
||||
|
||||
assert len(entries) == 2
|
||||
assert all(entry.source == "Up" for entry in entries)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_topic_raises_for_unknown_topic():
|
||||
with pytest.raises(RSSError):
|
||||
await read_topic("does-not-exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_topic_raises_when_every_feed_fails(monkeypatch):
|
||||
monkeypatch.setitem(
|
||||
rss_module.FEEDS,
|
||||
"testtopic",
|
||||
({"name": "Down", "url": "https://down.example/rss"},),
|
||||
)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
raise httpx.ConnectError("refused", request=request)
|
||||
|
||||
_patched_client(monkeypatch, handler)
|
||||
with pytest.raises(RSSError):
|
||||
await read_topic("testtopic")
|
||||
|
|
@ -3,6 +3,7 @@ import pytest
|
|||
import astra.tools as tools_module
|
||||
from astra.comfyui import ComfyUIError
|
||||
from astra.core import Settings
|
||||
from astra.rss import Entry, RSSError
|
||||
|
||||
|
||||
class FakeFunctionCallParams:
|
||||
|
|
@ -67,3 +68,59 @@ async def test_generate_image_tool_reports_comfyui_errors_without_storing_media(
|
|||
"activity",
|
||||
"tool_error",
|
||||
]
|
||||
|
||||
|
||||
def _find(tools, name):
|
||||
return next(t for t in tools if t.name == name)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_news_tool_reports_headlines(monkeypatch):
|
||||
async def fake_read_topic(topic, **kwargs):
|
||||
assert topic == "tech"
|
||||
return [
|
||||
Entry(source="heise online", title="KI-Durchbruch", summary="...", link="https://x"),
|
||||
Entry(source="Golem.de", title="Neuer Chip", summary="...", link="https://y"),
|
||||
]
|
||||
|
||||
monkeypatch.setattr(tools_module, "read_topic", fake_read_topic)
|
||||
|
||||
notifications = []
|
||||
schema = _find(
|
||||
tools_module.build_tools(Settings(), notifications.append, {}).standard_tools,
|
||||
"read_news",
|
||||
)
|
||||
|
||||
params = FakeFunctionCallParams({"topic": "tech"})
|
||||
await schema.handler(params)
|
||||
|
||||
assert [n["type"] for n in notifications] == ["activity", "tool_start", "tool_result"]
|
||||
assert "KI-Durchbruch" in notifications[2]["text"]
|
||||
assert "Neuer Chip" in notifications[2]["text"]
|
||||
assert params.results[0]["status"] == "ok"
|
||||
assert len(params.results[0]["headlines"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_news_tool_reports_rss_errors(monkeypatch):
|
||||
async def failing_read_topic(topic, **kwargs):
|
||||
raise RSSError('Keine Feeds für "tech" waren erreichbar.')
|
||||
|
||||
monkeypatch.setattr(tools_module, "read_topic", failing_read_topic)
|
||||
|
||||
notifications = []
|
||||
schema = _find(
|
||||
tools_module.build_tools(Settings(), notifications.append, {}).standard_tools,
|
||||
"read_news",
|
||||
)
|
||||
|
||||
params = FakeFunctionCallParams({"topic": "tech"})
|
||||
await schema.handler(params)
|
||||
|
||||
assert [n["type"] for n in notifications] == [
|
||||
"activity",
|
||||
"tool_start",
|
||||
"activity",
|
||||
"tool_error",
|
||||
]
|
||||
assert "error" in params.results[0]
|
||||
|
|
|
|||
23
uv.lock
generated
23
uv.lock
generated
|
|
@ -137,6 +137,7 @@ version = "0.1.0"
|
|||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "feedparser" },
|
||||
{ name = "httpx" },
|
||||
{ name = "mlx-audio" },
|
||||
{ name = "pipecat-ai", extra = ["pocket-tts", "webrtc"] },
|
||||
|
|
@ -157,6 +158,7 @@ dev = [
|
|||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "feedparser" },
|
||||
{ name = "httpx" },
|
||||
{ name = "mlx-audio" },
|
||||
{ name = "pipecat-ai", extras = ["silero", "webrtc", "pocket-tts"] },
|
||||
|
|
@ -507,6 +509,27 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feedparser"
|
||||
version = "6.0.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "feedparser-sgmllib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/8a/a53da4a77352045d277978a2df322d5379369f9deb1707178899ff7e1121/feedparser-6.0.14.tar.gz", hash = "sha256:088679b0c4b543ee211a820dd544698c76a402122eae7473c04a43425f283d06", size = 286108, upload-time = "2026-07-30T14:07:40.491Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/61/f04912e63702e73fb2a378f9c0a1ad9eb17a334a11a6b3fe1daa593903c2/feedparser-6.0.14-py3-none-any.whl", hash = "sha256:e35e3f760151b0c3b22cac9684155cae186a233e16c49bcbc6c49e91e3131137", size = 80668, upload-time = "2026-07-30T14:07:39.175Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feedparser-sgmllib"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/df/38596299216e5c22d60ed7f97902bb2bc72cfb95f732400f4fa976fd2e62/feedparser_sgmllib-2.1.0.tar.gz", hash = "sha256:61facf2918c4389b5b00714f76c5e03431ffcd94cd1f51d657edd6cd7c396579", size = 26845, upload-time = "2026-08-02T21:27:53.891Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a0/79a31f898092e145bd66e2b338fb0656979acb2bbbcae8220940fbfcd820/feedparser_sgmllib-2.1.0-py3-none-any.whl", hash = "sha256:2cab2d43b95a954f920f18aebce7a4dbbb3f539780b127e2aa114f579821e01d", size = 11652, upload-time = "2026-08-02T21:27:52.894Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.32.5"
|
||||
|
|
|
|||
22
web/app.js
22
web/app.js
|
|
@ -62,6 +62,27 @@ function addToolError(text) {
|
|||
}
|
||||
showError(text);
|
||||
}
|
||||
function addToolResult(text) {
|
||||
const pending = document.getElementById("pending-tool");
|
||||
const article = pending || document.createElement("article");
|
||||
if (pending) {
|
||||
pending.removeAttribute("id");
|
||||
pending.className = "message assistant";
|
||||
pending.replaceChildren();
|
||||
} else {
|
||||
$("messages").querySelector(".empty")?.remove();
|
||||
article.className = "message assistant";
|
||||
}
|
||||
const speaker = document.createElement("span");
|
||||
speaker.className = "speaker";
|
||||
speaker.textContent = "Astra";
|
||||
const body = document.createElement("p");
|
||||
body.className = "tool-result";
|
||||
body.textContent = text;
|
||||
article.append(speaker, body);
|
||||
if (!pending) $("messages").append(article);
|
||||
$("messages").scrollTop = $("messages").scrollHeight;
|
||||
}
|
||||
function addImage(url) {
|
||||
const pending = document.getElementById("pending-tool");
|
||||
const article = pending || document.createElement("article");
|
||||
|
|
@ -127,6 +148,7 @@ function receive(event) {
|
|||
if (message.type === "activity" && !muted) $("status").textContent = message.text;
|
||||
if (message.type === "tool_start") addToolStart(message.text);
|
||||
if (message.type === "tool_error") addToolError(message.text);
|
||||
if (message.type === "tool_result") addToolResult(message.text);
|
||||
if (message.type === "partial") $("partial").textContent = message.text;
|
||||
if (message.type === "transcript") addMessage(message);
|
||||
if (message.type === "image") addImage(message.url);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue