mirror of
https://github.com/crowetic/nc-talk-ai.git
synced 2026-09-07 07:32:31 +00:00
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.
3.6 KiB
3.6 KiB
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 withoutreplyToso they don’t 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
deltachunk (delta.content, and optionallydelta.tool_calls).
- Opens the streaming connection to the OpenAI-compatible endpoint and calls a callback for each
lib/Service/BotService.php::processMessage()- When no tools are used, it converts streaming
delta.contentinto readable chunks via anonProgress(string $partial)callback.
- When no tools are used, it converts streaming
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.
- When tools are enabled, it also streams via
lib/Webhook/TalkHandler.php::processNormalMessage()- Wires
onProgress(...)to Talk by callingsendReplyToTalk(...)for every emitted chunk. - Manages
replyTobehavior (only first chunk replies to the user message). - Filters model “thinking tokens” and optionally sends a placeholder while the model is thinking.
- Wires
Chunking / flush strategy (server-side)
Both BotService (no-tools path) and AgentExecutor (tools path) implement the same strategy:
- Append each incoming
delta['content']to a$buffer. - 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).
- a paragraph boundary is detected (
- 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 toreplyTo=0for 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.logforEducAI:entries (fromLLMClient,AgentExecutor,TalkHandler).
- Verify your LLM provider supports
- Too many tiny chunks:
- Adjust the flush strategy in
BotService/AgentExecutor(paragraph vs time-based flush thresholds).
- Adjust the flush strategy in