Merge pull request #363 from ggozad/feat/chat-tui-markdown-streaming
Support streaming for markdown
This commit is contained in:
commit
45dfd4b382
5 changed files with 46 additions and 12 deletions
|
|
@ -9,6 +9,10 @@
|
|||
- **`[s3]` optional extra** (`obstore>=0.9`). Required for `s3://` sources and the S3 watcher. Uses obstore — the Python binding to the same Rust `object_store` crate that LanceDB uses internally — so `monitor.s3[*].storage_options` accepts the same dict shape as `lancedb.storage_options`. Empty/missing options fall back to the AWS default credential chain.
|
||||
- **`scripts/run-integration-tests.sh`** — wraps `docker compose up --wait`, `pytest -m integration`, and tear-down so the SeaweedFS-backed integration suite is a one-liner.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Chat TUI streams markdown incrementally.** Assistant messages now use Textual's `MarkdownStream` (`Markdown.get_stream`) and write per-token deltas instead of re-parsing the entire accumulated message on every token. Removes the O(n²) re-parse that visibly stuttered long responses. Bumps `textual` floor to `>=8.2.4` so `Markdown.get_stream` is reachable via the public API.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Conversion options now apply to non-PDF formats.** `DoclingLocalConverter` previously wired its `PdfPipelineOptions` only to `InputFormat.PDF`, so user settings (OCR knobs, `picture_description.enabled`, `images_scale`, etc.) silently no-op'd for HTML, Markdown, DOCX, PPTX, and IMAGE inputs. The converter now shares a single `PdfPipelineOptions` instance across PDF, IMAGE, HTML, MD, DOCX, and PPTX `FormatOption`s. SimplePipeline-backed formats ignore the PDF-specific fields; `ConvertPipelineOptions`-level enrichments (picture description / classification / chart extraction) now run uniformly. HTML and Markdown additionally receive `HTMLBackendOptions` / `MarkdownBackendOptions` gated on `fetch_remote_images`.
|
||||
|
|
|
|||
|
|
@ -233,9 +233,11 @@ class ChatApp(App):
|
|||
assert isinstance(event, TextMessageContentEvent)
|
||||
accumulated_text += event.delta
|
||||
if message:
|
||||
message.update_content(accumulated_text)
|
||||
await message.append_delta(event.delta)
|
||||
chat_history.scroll_end(animate=False)
|
||||
elif event.type == EventType.TEXT_MESSAGE_END:
|
||||
if message:
|
||||
await message.finish_stream()
|
||||
self._messages.append(
|
||||
AssistantMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
|
|
@ -308,9 +310,13 @@ class ChatApp(App):
|
|||
|
||||
except asyncio.CancelledError:
|
||||
chat_history.hide_thinking()
|
||||
if message:
|
||||
await message.finish_stream()
|
||||
await chat_history.add_message("assistant", "*Cancelled*")
|
||||
except Exception as e:
|
||||
chat_history.hide_thinking()
|
||||
if message:
|
||||
await message.finish_stream()
|
||||
await chat_history.add_message("assistant", f"Error: {e}")
|
||||
finally:
|
||||
self._is_processing = False
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from textual.containers import Horizontal, VerticalScroll
|
||||
from textual.css.query import NoMatches
|
||||
from textual.message import Message
|
||||
from textual.widgets import Collapsible, LoadingIndicator, Markdown, Static
|
||||
from textual.widgets.markdown import MarkdownStream
|
||||
|
||||
from haiku.rag.agents.research.models import Citation
|
||||
|
||||
|
|
@ -17,18 +19,36 @@ class ChatMessage(Static):
|
|||
def __init__(self, role: str, content: str = "", **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.role = role
|
||||
self.content = content
|
||||
self.body_text: str = content
|
||||
self._stream: MarkdownStream | None = None
|
||||
|
||||
def compose(self) -> "ComposeResult":
|
||||
prefix = "**You:**" if self.role == "user" else "**Assistant:**"
|
||||
yield Markdown(f"{prefix}\n\n{self.content}", id="message-content")
|
||||
prefix = "You:" if self.role == "user" else "Assistant:"
|
||||
yield Static(prefix, classes="message-prefix")
|
||||
yield Markdown(self.body_text, classes="message-body")
|
||||
|
||||
def update_content(self, content: str) -> None:
|
||||
"""Update the message content (for streaming)."""
|
||||
self.content = content
|
||||
prefix = "**You:**" if self.role == "user" else "**Assistant:**"
|
||||
markdown = self.query_one("#message-content", Markdown)
|
||||
markdown.update(f"{prefix}\n\n{content}")
|
||||
async def append_delta(self, delta: str) -> None:
|
||||
if not delta:
|
||||
return
|
||||
if self._stream is None:
|
||||
try:
|
||||
body = self.query_one(".message-body", Markdown)
|
||||
except NoMatches:
|
||||
return
|
||||
self._stream = Markdown.get_stream(body)
|
||||
self.body_text += delta
|
||||
await self._stream.write(delta)
|
||||
|
||||
async def finish_stream(self) -> None:
|
||||
if self._stream is None:
|
||||
return
|
||||
await self._stream.stop()
|
||||
self._stream = None
|
||||
try:
|
||||
body = self.query_one(".message-body", Markdown)
|
||||
except NoMatches:
|
||||
return
|
||||
await body.update(self.body_text)
|
||||
|
||||
|
||||
class ToolCallWidget(Static):
|
||||
|
|
@ -216,6 +236,10 @@ class ChatHistory(VerticalScroll):
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
ChatMessage .message-prefix {
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
/* Tool calls */
|
||||
ToolCallWidget {
|
||||
margin: 0 0 0 4;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ cohere = ["cohere>=5.21.1"]
|
|||
zeroentropy = ["zeroentropy>=0.1.0a11"]
|
||||
jina = ["transformers>=4.40.0", "torch>=2.0.0"]
|
||||
# TUI (chat and inspect commands)
|
||||
tui = ["textual>=8.2.1", "textual-image>=0.8.5"]
|
||||
tui = ["textual>=8.2.4", "textual-image>=0.8.5"]
|
||||
# Model providers (delegated to pydantic-ai-slim)
|
||||
anthropic = ["pydantic-ai-slim[anthropic]"]
|
||||
groq = ["pydantic-ai-slim[groq]"]
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ dependencies = [
|
|||
haiku-rag = "haiku.rag.cli:cli"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tui = ["textual>=1.0.0"]
|
||||
tui = ["textual>=8.2.4"]
|
||||
s3 = ["haiku.rag-slim[s3]==0.45.0"]
|
||||
|
||||
[build-system]
|
||||
|
|
|
|||
Loading…
Reference in a new issue