feat(summary): summarize attached files, and fix document attachments on Mistral - #419
feat(summary): summarize attached files, and fix document attachments on Mistral#419cbcoutinho wants to merge 2 commits into
Conversation
buildDocumentContent() only ever emitted OpenAI's content part, so every document attachment sent to Mistral came back as an HTTP 422 with a confusing "Input should be a valid string". Mistral takes a flat document_url string rather than a nested file object; its schema has no variant matching ours, so the discriminated union falls through to parsing the content as a plain string and reports that instead. Pick the envelope from the configured service URL. The payload itself is unchanged: every backend that supports documents accepts them inline as a base64 data URI, so no separate upload API is involved either way. Signed-off-by: Chris Coutinho <chris@coutinho.io>
The summary provider could only ever work on a string, so callers had to extract the text of a document before asking for a summary. That loses whatever the extractor cannot read, which for PDFs is often everything. Advertise an optional input_attachments slot and, when files are given, inline them in a single chat completion instead. Chunking is skipped entirely on that path: the point is that we never read the file, so there is no text to split. The slot is only advertised when document support is enabled and the chat completion endpoint is usable, since attachments cannot be carried over the legacy completions endpoint. Advertising it unconditionally would tell callers attachments work and then fail every request. An empty prompt with no usable attachment is now rejected rather than summarized, so a dropped attachment surfaces as an error instead of a summary of nothing. Signed-off-by: Chris Coutinho <chris@coutinho.io>
Verified against live endpoints ✅Tested with a real Mistral API key and a real OpenRouter key. To avoid testing a hand-written payload that merely resembles the code, I extracted the exact content part The fixture is a 659-byte PDF containing one invented fact — "The annual rainfall in Zenda is 812 millimetres" — so a correct
The negative control reproduces the exact error quoted in the description: Also confirmed Unrelated finding worth knowing about: OpenRouter silently drops PDFs on some upstreamsWhile testing I hit non-deterministic answers on OpenRouter. It turns out to depend on which upstream OpenRouter routes to, and it is reproducible when pinned:
Unpinned, the same request alternated between the two: So OpenRouter's There is an existing workaround needing no code change — admins can force pre-parsing through the {"plugins": [{"id": "file-parser", "pdf": {"engine": "mistral-ocr"}}]}Happy to add explicit handling (e.g. defaulting that plugin when the service URL is OpenRouter) if maintainers want it — I left it out here because This comment was generated with the help of AI, and reviewed by a Human |
Summary
Two related changes that let a document be summarized by the model itself rather than by whatever text our caller managed to extract first.
1.
fix(multimodal): documents are broken on MistralOpenAiFileService::buildDocumentContent()only ever emitted OpenAI's content part:{"type": "file", "file": {"filename": "…", "file_data": "data:application/pdf;base64,…"}}Mistral rejects that with an HTTP 422 and a misleading message:
Mistral's
DocumentURLChunktakes a flat string, not a nested object. Because no variant of itsContentChunkunion matches ours, the validator falls back to parsingcontentas a plain string and reports that failure — which is why the error says nothing about documents. Mistral therefore needs:{"type": "document_url", "document_url": "data:application/pdf;base64,…", "document_name": "…"}The envelope is now chosen from the configured service URL. The payload is unchanged — every backend that supports documents accepts them inline as a base64 data URI (OpenAI, OpenRouter and Mistral all do), so no separate upload/Files API is involved on any path.
This also fixes document attachments for nextcloud/assistant#602 when running against Mistral.
2.
feat(summary):input_attachmentsonSummaryProviderSummaryProvider::process()could only work on a string, so a caller had to extract a document's text before asking for a summary. For PDFs that extraction is frequently lossy or empty.The provider now advertises an optional
input_attachments(ListOfFiles) slot. When files are present it skipsChunkServiceentirely — the whole point is that we never read the file, so there is no text to split — and inlines them in one chat completion.Deliberate choices:
multimodal_document_enabledis on and the chat endpoint is usable. Attachments cannot be carried over the legacy/completionsendpoint, so advertising unconditionally would tell callers attachments work and then fail every request.MultimodalChatWithToolsProvider.Consumer
nextcloud/assistant PR (Files → Summarize sends the file itself when this slot is advertised): cbcoutinho/assistant@feat/summarize-file-attachment. It feature-detects
optionalInputShape['input_attachments']and falls back to today's text extraction when absent, so ordering between the two is not critical.Testing
Three tests added to
OpenAiProviderTest, asserting the exact request body:testSummaryProviderWithPdfAttachment— OpenAItype: fileenvelope, no chunkingtestSummaryProviderWithPdfAttachmentOnMistral— flatdocument_urlenvelopetestSummaryProviderAdvertisesAttachmentsOnlyWhenDocumentsAreEnabledpsalm,cs:checkandphp -lare clean locally.phpunitwas not run locally — the suite is@group DBand extendsTest\TestCase, so it needs a full Nextcloud install; the three new tests will first execute in CI. They are modelled ontestMultimodalChatWithToolsProviderandtestSummaryProvider, but exact-body assertions are brittle, so a first-run adjustment is possible.Verified against live Mistral and OpenRouter endpoints — see the comment below for the full matrix, including a negative control reproducing the 422. The payloads tested were extracted from
buildFileContentFromFile()itself rather than hand-written.This PR was generated with the help of AI, and reviewed by a Human