feat(rag): PDF/docx ingest + chat citations

- Upload endpoint (base64 JSON, no multipart dep): extracts text from
  pdf/docx/txt/md via pypdf + python-docx, then runs the existing
  chunk/embed pipeline. Documents page uploads files straight through.
- Citations: the chat stream emits an SSE `sources` event listing the
  documents that fed the answer; the UI shows them as chips under the reply.
- Deps: pypdf, python-docx (both pure-Python, Windows-safe).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-23 13:48:32 -05:00
co-authored by Claude Opus 4.8
parent f509034fdd
commit ac0eb1e5f6
6 changed files with 123 additions and 9 deletions
+16
View File
@@ -55,3 +55,19 @@ def test_add_list_search_delete_roundtrip():
def test_search_empty_query_returns_nothing():
s = _store()
assert asyncio.run(s.search_documents("", _fake_embed)) == []
def test_extract_text_by_type():
from synapse.main import _extract_text
# plain text / markdown -> UTF-8 decode
assert _extract_text("notes.md", b"# Title\n\nbody") == "# Title\n\nbody"
assert _extract_text("x.txt", "café".encode("utf-8")) == "café"
# a real (tiny) PDF built with pypdf -> text extracted back out
from pypdf import PdfWriter, PdfReader
import io
w = PdfWriter()
w.add_blank_page(width=200, height=200)
buf = io.BytesIO(); w.write(buf)
out = _extract_text("blank.pdf", buf.getvalue())
assert isinstance(out, str) # blank page -> "" or whitespace, never raises
assert PdfReader(io.BytesIO(buf.getvalue())).pages # sanity: it was a valid PDF