feat: Office-Extraktion (docx/xlsx/pptx) fuer files-, mail- und notes-MCP

officeutil.py (analog pdfutil): server-seitige Text-Extraktion, damit das
LLM Inhalte als Text bekommt statt Base64-Blob. Verkabelt in files.read_file,
mail.read_attachment (und notes.read_resource, bereits in e34711d).
Aeltere Binaerformate (.doc/.xls/.ppt) und .rtf bewusst nicht behandelt --
is_office() False -> Fallback auf Rohdaten. requirements-extra: python-pptx.

War bereits deployed und lief (FileTypes-Tests 14 passed), lag aber
uncommittet im Arbeitsverzeichnis; ohne officeutil.py waere ein frischer
Clone seit e34711d kaputt gewesen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhHNJKk3RsHgUsJ27WQSom
This commit is contained in:
Stefan Lohmaier
2026-07-21 09:32:26 +02:00
co-authored by Claude Opus 4.8
parent e34711d0ca
commit 4d198017df
4 changed files with 112 additions and 2 deletions
+4 -1
View File
@@ -15,6 +15,7 @@ from starlette.routing import Mount
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from common import get_current_user, OAUTH_ROUTES, BearerAuthMiddleware
from pdfutil import pdf_to_content
from officeutil import office_to_content, is_office
from common import load_config as _lc
_cfg = _lc()
@@ -104,7 +105,7 @@ def _guess_mime(path, ct):
def read_file(
path: Annotated[str, Field(description="Full file path, e.g. '/Documents/notes.txt', '/report.pdf', '/photo.jpg'")],
) -> list[TextContent | ImageContent | EmbeddedResource]:
"""Read a file. Text files return content directly. Images inline. PDFs as extracted text. Other documents (docx, xlsx, pptx) as binary. Max 25 MB."""
"""Read a file. Text files return content directly. Images inline. PDFs and Office documents (docx, xlsx, pptx) as extracted text. Max 25 MB."""
user = get_current_user()
if not user: return [TextContent(type="text", text="Error: not authenticated")]
r = httpx.get(_dav(user, path), auth=_auth(user), timeout=60)
@@ -119,6 +120,8 @@ def read_file(
return [TextContent(type="text", text=r.text[:100000])]
if ct == "application/pdf" or path.lower().endswith(".pdf"):
return pdf_to_content(r.content, path, ct)
if is_office(ct, path):
return office_to_content(r.content, path, ct)
try:
text = r.content.decode("utf-8")
return [TextContent(type="text", text=text[:100000])]