High-Level Workflow (The Big Picture)
Before we dive into the technical details, let’s outline the end-to-end process for both ingestion and querying. A. Ingestion & Graph Building (Asynchronous Process):- Document Upload: A new document is uploaded to your Laravel app and stored in Minio. A record is created in MongoDB.
- Trigger Processing: A job is dispatched to a queue to process this new document.
- Document Processing (Docling): The job sends the document (or a pre-signed URL to it) to the
doclingservice.doclingextracts text, identifies entities (e.g., people, projects, dates, invoice numbers), and understands the relationships between them. It returns this structured data as a JSON object. - Graph Construction (Neo4j): The job receives the structured data from
docling. It then translates this data into Cypher queries and populates the Neo4j graph. For example, it creates(:Person {name: 'John Doe'}),(:Project {name: 'Q4 Report'}), and a relationship[:WORKED_ON]. - Indexing: Key text chunks from the document are also stored in the graph (or a vector database, but Neo4j can handle this with vector indexes) and linked to their source nodes.
- User Question: A user asks a question in your Laravel application’s UI, e.g., “What projects did Jane Doe work on in Q2?”
- Entity Extraction: Laravel (using Gemini or a simpler local model) extracts key entities from the user’s question (e.g., “Jane Doe”, “Q2”).
- Graph Retrieval: Laravel queries the Neo4j graph to find relevant subgraphs based on the extracted entities. For example, it finds the “Jane Doe” node and traverses its relationships to find connected “Project” nodes within the Q2 timeframe.
- Context Augmentation: The data retrieved from the graph (nodes, relationships, and associated text chunks) is compiled into a context string.
- LLM Prompting (Gemini): Laravel, using the
Prism AIlibrary, constructs a detailed prompt for Gemini. The prompt includes the original user question and the context retrieved from Neo4j.
- Example Prompt: `“Based on the following context, answer the user’s question. Context: [Data from Neo4j…]. Question: What projects did Jane Doe work on in Q2?”*
- Response Generation: Gemini processes the prompt and generates a natural language answer based only on the provided context.
- Display Answer: The Laravel app displays the answer to the user, often with links to the source documents.
1. Server / VM Network Topology
Here is a logical network topology. For production, you’d ideally have these on separate VMs or containers (e.g., using Docker Compose or Kubernetes). For development, they could run on a single powerful machine.- Laravel App Server: Handles user-facing HTTP requests. This is where the user submits questions. It validates input and either handles the request directly (for RAG) or dispatches a job (for ingestion).
- Laravel Queue Worker: A separate process (or set of processes) that runs in the background. It pulls jobs from the queue (like Redis) and executes them. This is critical for performance. Document processing is slow and must not block web requests.
- Docling Service: A self-contained service (likely a Docker container) that exposes an HTTP API. It receives a document, performs NLP/entity extraction, and returns structured JSON.
- Neo4j Database: The graph database. Needs to be accessible by the Laravel Queue Worker (for writing) and the Laravel App Server (for reading/RAG).
- n8n (Optional): If you use n8n, it acts as an orchestrator. Laravel would send a webhook to n8n, and n8n would manage the multi-step process of calling
docling, formatting data, and writing toNeo4j. This is great for visualizing and modifying the workflow without changing your Laravel code. - Existing Stack (Mongo, Minio): They remain central to your DMS. The new services will interact with them as needed.
2. Required APIs
This details the “contracts” between your services.- Laravel -> Docling API (Internal REST API)
- Endpoint:
POST /process - Request Body (Example):
- Response Body (Example):
- Laravel -> Neo4j API (using a Driver)
- This isn’t a REST API but a connection via the Bolt protocol. You’ll use a PHP library like
laudis/neo4j-php-client. - Write Operation (from Queue Worker):
- Read Operation (from App Server for RAG):
- Laravel -> Gemini API (via Prism AI)
Prism AIwill handle the complexity of the REST API call to Google. Your Laravel code will be simple.- Example Laravel Code:
3. Required Processes (Queues, Artisan Commands)
This is how you’ll implement and manage the workflow within Laravel.Queues and Jobs
Using Laravel’s Queue system is non-negotiable for this architecture.App\Jobs\ProcessDocumentForGraph.php
- Triggered: When a document is marked for archival or a new one is uploaded.
- Constructor:
__construct(Document $document) handle()method logic:
- Generate a pre-signed temporary URL for the document in Minio.
-
Make an HTTP call to your
doclingservice API with this URL. -
Wait for the response from
docling. -
On success, dispatch the next job:
BuildGraphFromProcessedData::dispatch($document, $doclingResponseData). -
Handle errors (e.g.,
doclingfails, requeue the job with a delay). -
App\Jobs\BuildGraphFromProcessedData.php
- Triggered: By the
ProcessDocumentForGraphjob. - Constructor:
__construct(Document $document, array $processedData) handle()method logic:
- Parse the
$processedDataarray (entities, relationships). - Connect to the Neo4j service.
- Build a series of Cypher
MERGEqueries to create nodes and relationships without duplication. Wrap this in a transaction. - Execute the queries against Neo4j.
- On success, update the document’s status in MongoDB (e.g.,
graph_status: 'processed'). - Handle database errors.
Artisan Commands
These are essential for backfilling your existing data and for maintenance.php artisan docs:process-archive
- Purpose: To process all your existing archived documents.
- Logic:
- Find all documents in MongoDB that need processing (e.g., where
graph_statusis not'processed'). - You can add options:
--allfor everything,--limit=100for testing,--since=YYYY-MM-DDfor recent ones. - Loop through the documents and dispatch
ProcessDocumentForGraph::dispatch($document)for each one. - Use chunking (
->chunkById(100, ...) to avoid loading millions of documents into memory at once.
php artisan docs:reprocess <document-id>
- Purpose: Manually re-trigger the processing for a single document, which is invaluable for debugging.
- Logic:
- Find the document by its ID.
- Clear any existing graph data related to this document from Neo4j (this is an important step!).
- Dispatch
ProcessDocumentForGraph::dispatch($document).
php artisan graph:stats
- Purpose: A simple health-check command.
- Logic:
- Connect to Neo4j.
- Run simple queries like
MATCH (n) RETURN count(n)andMATCH ()-[r]->() RETURN count(r). - Output the number of nodes and relationships to the console. This confirms your connection is working.
