Files
nc-talk-ai/docs/pseudo_streaming.md
Pascal Kienast 0739d3da6a Initial open-source release of Talk AI
Talk AI is a multi-bot AI assistant manager for Nextcloud Talk:
per-bot prompts and models, agentic tool calling (MCP + built-in
tools), RAG over Nextcloud files, room-document search, vision and
speech-to-text attachments, persistent bot wikis, approval workflows,
rate limiting, and multi-provider LLM support (any OpenAI-compatible
endpoint).

Developed within EDUC - the European Digital UniverCity
(https://educalliance.eu), where it runs as the 'EDUC AI' assistant on
the alliance-wide Nextcloud portal. This public repository is the
upstream point of truth; deployment-specific tools plug in via the
tool-provider extension point (docs/TOOL_PROVIDERS.md).

License: AGPL-3.0-or-later.
2026-07-08 21:13:13 +02:00

67 lines
3.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pseudo Streaming (Chunked Replies) in Talk AI
This document explains how Talk AI simulates “streaming” replies in Nextcloud Talk by emitting partial chunks of the assistant response while the LLM stream is still in progress.
## High-level idea
- The LLM API is called with `stream=true` (SSE / chunked HTTP response).
- Incoming token deltas are buffered on the server.
- The buffer is flushed periodically (paragraph / sentence boundaries) as **separate Talk messages**.
- Only the **first** chunk is sent as a reply to the original user message (`replyTo=<messageId>`). All subsequent chunks are sent without `replyTo` so they dont all appear as nested replies.
## Where this is implemented
- `lib/Service/LLMClient.php::streamChatCompletion()`
- Opens the streaming connection to the OpenAI-compatible endpoint and calls a callback for each `delta` chunk (`delta.content`, and optionally `delta.tool_calls`).
- `lib/Service/BotService.php::processMessage()`
- When **no tools** are used, it converts streaming `delta.content` into readable chunks via an `onProgress(string $partial)` callback.
- `lib/Service/AgentExecutor.php::run()`
- When **tools are enabled**, it also streams via `LLMClient::streamChatCompletion()` and uses the same buffering/flush strategy.
- Additionally, it can emit small progress messages (e.g. “🔧 _Using tool(s): …_”) via the streaming callback.
- `lib/Webhook/TalkHandler.php::processNormalMessage()`
- Wires `onProgress(...)` to Talk by calling `sendReplyToTalk(...)` for every emitted chunk.
- Manages `replyTo` behavior (only first chunk replies to the user message).
- Filters model “thinking tokens” and optionally sends a placeholder while the model is thinking.
## Chunking / flush strategy (server-side)
Both `BotService` (no-tools path) and `AgentExecutor` (tools path) implement the same strategy:
1. Append each incoming `delta['content']` to a `$buffer`.
2. Flush when:
- a paragraph boundary is detected (`\n\n`), **or**
- enough time passed (3 seconds) and enough text accumulated (>100 chars) → then flush at the next sentence boundary (best-effort).
3. After the stream ends, flush any remaining buffer.
This avoids token-by-token “spam” and produces readable chunks.
## Delivery to Talk + `replyTo` behavior
In `TalkHandler::processNormalMessage()`, the streaming callback:
- filters out “thinking tokens” (for models that emit them),
- sends each non-empty chunk to Talk via `sendReplyToTalk($roomToken, $chunk, $replyTo)`,
- uses `replyTo=<original message id>` **only for the first streamed chunk**, then switches to `replyTo=0` for all later chunks.
This prevents Talk from rendering every chunk as a reply to the original user message.
Also note:
- If **no** chunks were streamed (`$alreadySent === false`), the handler sends a single final message (normal non-streamed behavior).
- If chunks **were** streamed, the handler skips sending the final full response to avoid duplicate content.
## Why this is “pseudo” streaming
- Nextcloud Talk bot messages are sent as discrete messages (no server-side “edit this message” API).
- The app therefore simulates streaming by sending multiple messages as the response arrives.
- Chunking happens at the PHP application level; Talk clients are not consuming the LLM stream directly.
## Troubleshooting
- **No streaming visible**:
- Verify your LLM provider supports `stream=true`.
- Check `nextcloud.log` for `EducAI:` entries (from `LLMClient`, `AgentExecutor`, `TalkHandler`).
- **Too many tiny chunks**:
- Adjust the flush strategy in `BotService` / `AgentExecutor` (paragraph vs time-based flush thresholds).