30. Oktober 2023 von Alexander Zielinski
Vom Python-Anfänger zur produktiven Anwendung: Meine Erfahrungen mit FastAPI und PyMuPDF
In diesem Blog-Beitrag beschreibe ich meine Reise vom Python-Anfänger zu einem REST-Service-(Junior-)Entwickler. Ich habe einen REST-Service mit FastAPI, PyMuPDF und weiteren Komponenten erstellt, um Texte in PDF-Dateien zu suchen und mit einer Highlight-Annotation zu versehen. Die Anwendung läuft als Docker-Container.
Der Blog-Beitrag beginnt mit einem Bericht meiner Lernreise. Anschließend gehe ich auf technische Details der Lösung, aufgetretene Herausforderungen und Anwendungsbeispiele ein. Zuletzt beschreibe ich Weiterentwicklungsmöglichkeiten und gebe ein Fazit.
Ich bedanke mich bei meinem geschätzten Kollegen Marc Fabian Metzger für die Inspiration zu diesem Blog-Beitrag und sein Feedback während der Entwicklung des Projekts.
Meine Lernreise
Mein Background: Ich bin IT Management Consultant, momentan jedoch als IT-Consultant beziehungsweise IT-Projektleiter tätig. Privat beschäftige ich mich mit einer Private Cloud auf Docker-Basis und entwickle gelegentlich Skripte für die Cloud und/oder meine Heimautomatisierung. Vor diesem Projekt habe ich schon ein paar kleinere Aufgaben mit Python erledigt.
Die Idee zu diesem Projekt entstand während einer Schulung zur Nutzung der Aleph Alpha Luminous API und deren Funktion, mit sogenannten Explanations Quellenangaben zu gefundenen Informationen zu generieren. Mein derzeitiger Kunde, eine gesetzliche Krankenversicherung, plant, für Aktenrecherchen – etwa im Widerspruchsprozess – künftig auf Unterstützung durch KI zu setzen. Um für diese Verfahren Akzeptanz sowohl in der Belegschaft als auch bei Gremien der Selbstverwaltung zu schaffen, ist die Nachvollziehbarkeit von KI-Ergebnissen sehr wichtig. Die zu recherchierenden Akten liegen als PDF-Dateien vor, insofern ist es naheliegend, gefundene Informationen in diesen Dateien zu markieren.
Da ich Python bisher als sehr gut autodidaktisch erlernbar erlebt habe, war für mich die Entscheidung für Python 3 alternativlos. Zunächst habe ich die Kernfunktion der Anwendung in einem Prototyp realisiert. Am zeitaufwändigsten war die Suche und Erprobung geeigneter Python-PDF-Bibliotheken, die zur Verwendung von PyMuPDF führte. Am Ende einer langen Zugfahrt von München nach Hamburg hatte ich ein Skript, das ein PDF nach einem bestimmten Text durchsuchte und Annotationen für den Text anbringen konnte.
Anschließend habe ich einen ersten REST-Service mit Flask erstellt. Ein Kollege erwähnte, dass er statt Flask das Framework FastAPI verwendet. FastAPI und Flask sind Web-Frameworks zur API-Erstellung mittels Python 3.x. Aufgrund der Möglichkeit, meinen Kolleginnen und Kollegen bei etwaigen Herausforderungen fragen zu können, habe ich die API auf FastAPI umgestellt. Nach der „Migration“ zu FastAPI stellte sich diese Entscheidung als sehr hilfreich heraus: Durch die Swagger-Integration in FastAPI konnten die erstellten Resultate direkt im Browser getestet werden, außerdem ist FastAPI nach Auffassung des Autors sehr gut dokumentiert. Während der Auseinandersetzung mit FastAPI habe ich außerdem rudimentäre Kenntnisse in Pydantic für die verwendeten Objekte sowie weitere Python-Libraries erworben.
Häufig werden in der Softwareentwicklung etwaige automatisierte Tests zuerst entwickelt und anschließend der eigentliche Code. In diesem Projekt wurden die automatisierten Tests am Ende erstellt. Hintergrund ist, dass ich den eigentlichen Ablauf in der Anwendung iterativ entwickelt habe und mir die Komplexität der Entwicklung auch ohne Tests ausreichend erschien.
Die technischen Details
Aufbau der Anwendung
Die Anwendung besteht aus der REST-Schnittstelle, einer Klassendatei sowie einem Modul, um die Annotationen zu erzeugen. So kann künftig mit geringen Anpassungen auch eine Alternative zu PyMuPDF eingesetzt werden.
Ablauf der Annotation
Folgende Endpunkte stellt die API bereit:
Um einen Annotationsjob zu erstellen, wird zunächst per POST /annotationsjobs aufgerufen und die zu suchenden Texte werden im Body als JSON übergeben:
{
"explanations": [
"string1", "string2"
]
}
In der Response ist die Job-ID des soeben generierten Jobs enthalten:
{
"id": "aed32b65-e142-4bdb-9a9d-fb91e09ffc83",
"explanations": [
"string1", "string2"
],
"documentdetails": null,
"status": 1
}
Anschließend werden mit einem POST auf /annotationsjobs/{job_id}/documents
eine oder mehrere PDF-Dateien übergeben.
Anschließend werden mit einem POST auf /annotationsjobs/{job_id}/documents
eine oder mehrere PDF-Dateien übergeben.
Zur Steuerung werden einige Metadaten pro Dokument in „documentdetails“ gespeichert. Der Aufruf der Annotation erfolgt asynchron als Background Task.
Mit den beiden GET-Methoden /annotationsjobs
und /annotationsjobs/{job_id}/documents
können der Status/die Metadaten sämtlicher Jobs beziehungsweise eines einzelnen Jobs abgerufen werden. Sobald ein Dokument im Status done_annotated ist, wurde einer oder mehrere der übergebenen Texte gefunden. Das annotierte Dokument kann nun mit /annotationsjobs/{job_id}/documents/{document_id}
heruntergeladen werden. Die Originaldatei wurde hierzu mit dem Suffix „_anno“ ergänzt.
Sobald alle Dateien heruntergeladen sind, können mit DELETE /annotationsjobs/{job_id}
der Job und die temporär abgelegten Dateien gelöscht werden.
Die Herausforderungen während der Entwicklung
An mehreren Stellen dachte ich vor meiner Recherche, dass die jeweilige Aufgabe eine Herausforderung sei. Ich habe jedoch gelernt, dass Python beziehungsweise die verwendeten Libraries fast immer elegante und einfach zu verwendende Lösungen bieten. Hier sind einige Beispiele:
1. Wie finde ich in einem Array ein Objekt mit bestimmten Eigenschaften?
Die Lösung erledigt dieses Problem in einer Zeile und setzt das Objekt auf None, falls die ID nicht gefunden wurde, so dass mit dem Ergebnis sehr gut weitergearbeitet werden kann und der Code übersichtlich bleibt:
currentjob = next((x for x in jobs if x.id == job_id), None)
2. Wie führe ich eine Aufgabe im Hintergrund aus, so dass die API zügig eine Response an den Aufrufer schicken kann?
Auch hierfür existiert eine einfache Lösung:
background_tasks.add_task(search_and_annotate_allpages, job, tmp_dir)
3. Bei der Suche nach geeigneten PDF-Bibliotheken habe ich gelernt, dass Annotationen in PDFs über Koordinaten erzeugt werden. In mehreren getesteten Libraries konnte zwar Text gesucht werden, jedoch schien die Umwandlung in Koordinaten eine größere Herausforderung zu sein. Bei PyMuPDF kann die Textsuche direkt Koordinaten ausgeben; mehrzeilige Ergebnisse haben jeweils zwei Koordinaten pro Zeile:
rl = page.search_for(explanation, quads=True)
for result in rl:
annot = page.add_highlight_annot(result)
Anwendungsbeispiel
Die Idee dieser Anwendung ist es, Informationen, die ein Large Language Model in einem Text gefunden hat, zu annotieren, um Akzeptanz und/oder Vertrauen bei den Benutzerinnen und Benutzern zu schaffen. Zur Realisierung dieser Idee wird die Anwendung als Docker-Container neben weiteren Containern betrieben. Dieses Setup ermöglicht einen Showcase, in dem PDF-Dokumente hochgeladen werden können und die Nutzerin oder der Nutzer Fragen an die Aleph Alpha API formulieren kann. Die Fragen werden anschließend durch das Luminous-Sprachmodell auf Basis der PDF-Dokumente beantwortet. Zusätzlich werden den Nutzenden die Explanations angezeigt und es wird eine Kopie des Original-PDFs zum Download angeboten, in dem die Explanations annotiert sind.
Ausblick
Eine mögliche Erweiterung der Anwendung ist, dass zusätzlich zu den Highlight-Annotationen auch ein Kommentar oder Ähnliches eingefügt wird, welche Frage mit der jeweiligen Textpassage beantwortet wurde. Diesbezüglich werde ich die Resonanz aus den Kundenterminen auswerten.
Die Jobs sind momentan zur Laufzeit gültig und werden nicht persistiert. Für einen produktiven Einsatz außerhalb von Showcases erscheint das Persistieren der Jobdaten sinnvoll.
Fazit
Für mich war das Entwickeln der Anwendung und meiner Python-Fähigkeiten eine spannende Herausforderung, deren Bewältigung mir viel Spaß gemacht hat. Python ist nach meiner Einschätzung sehr niederschwellig erlernbar und es lassen sich schnell gute Lernfortschritte und Resultate erreichen.
Weitere spannende Themen aus der adesso-Welt findet ihr in unseren bisher erschienen Blog-Beiträgen.