AX_Ploration is an AI-assisted application for querying ALKIS building data (AX_Gebaeude features) using natural language. This project is part of the "Spatial Information Search" course at University of Münster (WiSe 25/26).
- Overview
- List of Questions the Application Can Answer
- Quick Start
- Project Structure
- Backend Scripts
- REST API
- Frontend Integration
- Docker Deployment
- Additional Tools
The system uses a LangGraph-based agent that processes natural language queries in German and converts them into Cypher queries to retrieve building data from a Neo4j database containing Berlin ALKIS data (96,572 buildings, 234 building functions, 12 districts).
Key Technologies:
- LangGraph for agent workflow orchestration
- OpenAI GPT-4o for natural language understanding
- Neo4j for graph database storage
- FastAPI for REST API with Server-Sent Events streaming
- React Frontend built with Vite
- Leaflet for map display
- Docker for containerized deployment
You can find the planned and realized questions for our application here.
- Python 3.11+
- Neo4j database (Aura or local instance)
- OpenAI API key
- Clone the repository:
git clone <repository-url>
cd AX_Ploration- Install dependencies:
pip install -r requirements.txt- Configure environment variables by creating a
.envfile:
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_MODEL=gpt-4o
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your-password-here
LANGSMITH_API_KEY=
LANGSMITH_PROJECT=ax_ploration
API_PORT=8000# Build and start the API server
docker-compose up --build
# The API will be available at http://localhost:8000# Start the API server
python -m backend.api.server
# Start the frontend
cd alkis-frontend
npm run dev
# Or use the CLI directly
python -m backend.scripts.main "Wie viele Gebäude gibt es in Mitte?"AX_Ploration/
├── frontend/
│ └── sources/ # React app
├── backend/
│ ├── scripts/ # Core agent implementation
│ └── api/ # REST API with streaming support
├── sample-scripts/ # Jupyter notebook examples
├── docker-compose.yml # Docker deployment configuration
├── Dockerfile # Container image definition
└── requirements.txt # Python dependencies
The backend/scripts/ directory contains the core LangGraph agent implementation for direct programmatic use.
Run queries directly from the command line:
# Basic query
python -m backend.scripts.main "Zeige mir alle Wohngebäude in Mitte"
# Verbose mode (shows processing steps)
python -m backend.scripts.main "Wie viele Schulen gibt es?" --verbosefrom backend.scripts.graph import graph
from backend.scripts.main import create_initial_state
# Execute a query
initial_state = create_initial_state("Zeige mir alle Krankenhäuser")
result = graph.invoke(initial_state)
print(result["final_answer"])
print(f"Query Type: {result['query_type']}")
print(f"Results: {len(result['results'])}")The agent processes queries through these stages:
- Attribute Identification - Extracts requested building attributes
- Embedding Search - Finds matching building functions (if needed)
- Query Interpretation - Classifies query type (district/nearby/statistics)
- Cypher Generation - Creates appropriate database query
- Data Retrieval - Executes query against Neo4j
- Spatial Comparison - Performs spatial analysis (for location-based queries)
- Answer Generation - Formats natural language response
For detailed documentation, see README.md.
The backend/api/ directory provides a FastAPI-based REST API with streaming support, enabling real-time query processing updates.
Option 1: Docker (Recommended)
docker-compose up --buildOption 2: Local
python -m backend.api.serverOption 3: Custom Configuration
# Set port in .env
echo "API_PORT=5000" >> .env
python -m backend.api.serverProcess natural language queries with optional streaming and optional spatial filter.
Request:
{
"query": "Wie viele Gebäude gibt es in Mitte?",
"stream": true
}Response (Streaming mode):
Server-Sent Events stream with incremental updates:
data: {"type": "message", "content": "Identified attributes: ['Gebäudeanzahl']"}
data: {"type": "message", "content": "Query type: district"}
data: {"type": "message", "content": "Generated Cypher query for cypher_district"}
data: {"type": "final", "state": {
"query": "Wie viele Gebäude gibt es in Mitte?",
"attributes": ["Gebäudeanzahl"],
"query_type": "district",
"cypher_query": "MATCH (b:Building)-[:IN_DISTRICT]->(d:District)...",
"results": [{"Gebäudeanzahl": 20746}],
"final_answer": "In Mitte gibt es insgesamt 20.746 Gebäude.",
"messages": [...]
}}
Response (Non-streaming mode):
Complete AgentState as JSON:
{
"query": "Wie viele Gebäude gibt es in Mitte?",
"attributes": ["Gebäudeanzahl"],
"needs_building_function": false,
"building_functions": [],
"query_type": "district",
"cypher_query": "MATCH (b:Building)-[:IN_DISTRICT]->(d:District)\nWHERE d.Gemeinde_name = 'Mitte'\nRETURN count(b) AS Gebäudeanzahl",
"results": [{"Gebäudeanzahl": 20746}],
"final_answer": "In Mitte gibt es insgesamt 20.746 Gebäude.",
"messages": [...]
}Check API and database connectivity:
curl http://localhost:8000/healthResponse:
{
"status": "healthy",
"database": "connected"
}List all available building functions:
curl http://localhost:8000/functionsasync function queryBuildings(query) {
const response = await fetch('http://localhost:8000/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
stream: false
})
});
const data = await response.json();
console.log('Answer:', data.final_answer);
console.log('Results:', data.results);
console.log('Results:', data.cypher_query);
return data;
}
// Usage
queryBuildings('Wie viele Schulen gibt es in Berlin?');function streamQuery(query) {
const messages = [];
let finalState = null;
const eventSource = new EventSource(
`http://localhost:8000/query?` +
new URLSearchParams({
query: query,
stream: 'true'
})
);
// Alternative: Use fetch with POST
fetch('http://localhost:8000/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
stream: true
})
}).then(async response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {done, value} = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.type === 'message') {
console.log('Progress:', data.content);
messages.push(data.content);
// Update UI with progress message
updateProgressUI(data.content);
}
else if (data.type === 'final') {
console.log('Final answer:', data.state.final_answer);
finalState = data.state;
// Update UI with final results
displayResults(data.state);
}
else if (data.type === 'error') {
console.error('Error:', data.error);
displayError(data.error);
}
}
}
}
});
}
// Usage
streamQuery('Zeige mir alle Wohngebäude in Mitte');# Build and start in detached mode
docker-compose up -d --build
# View logs
docker-compose logs -f
# Stop the service
docker-compose downThe docker-compose.yml uses environment variables from .env: docker-compose.yml
The container includes automatic health monitoring:
# Check container health
docker ps
# Manual health check
curl http://localhost:8000/healthNote: The following directories contain supplementary tools for data preparation and database management. They are not required for running the main application.
Utility scripts for:
- Loading detailed building data into Neo4j
- Extracting building function catalogs
- Database schema inspection and maintenance
These are primarily for data preprocessing and are not needed for regular application use.