AI-powered geopolitical analysis platform — hybrid LLM, automatic knowledge graphs, browser-style desktop UI
This is a demo / learning project
Version: v0.4.0
GeoVision Lab is an AI-powered geopolitical intelligence analysis platform. It ingests documents (PDF, Markdown), chunks and embeds them with all-MiniLM-L6-v2 into MongoDB's vector store, and lets you query them through a multi-agent LangGraph pipeline. A second-stage QA critic reviews responses before they're returned, and an automatic ontology extractor builds an interactive knowledge graph (7 entity types, 30+ relationship types) persisted to Neo4j. Location entities are geocoded via a self-hosted Nominatim service (with public API fallback). Supports both local LLM inference (Ollama with qwen3.5:9b/4b — switchable at runtime) and cloud LLM (Groq with Llama 4 Scout 17B) — toggle between online/offline modes via the browser-style desktop UI.
- Multi-Agent AI — Worker + Critic + Ontology Extractor architecture with autonomous tool selection
- Hybrid Search — Vector search (archival) + Web search (live events)
- Automatic Knowledge Graph — Real-time entity extraction and relationship mapping with interactive visualization
- 7 Entity Types: Location, Person, Organization, Event, Asset, Document, Concept
- Relationship Extraction: LOCATED_IN, AFFILIATED_WITH, SUPPORTS, TARGETS, CONFLICT_WITH, LEADS, PART_OF, etc.
- Automatic Geocoding: Locations are geocoded via Nominatim API with coordinates displayed on map
- Interactive Graph: Curved edges, color-coded nodes by type, hover tooltips with entity properties
- Accumulative Graph: Relationships build up during conversation sessions for context awareness
- Neo4j Graph Database: Native graph storage with Cypher queries and cross-session persistence
- Ontology-Aware RAG: Graph context augments document retrieval for richer answers
- Browser-Style OS UI — Web desktop interface with resizable, draggable, overlapping windows that snap into place
- Reasoning Chain Window — Real-time workflow step visualization
- Chat Result Window — AI response display
- Knowledge Graph Window — Interactive entity/relationship visualization
- Free Positioning — Windows can be moved, resized, and arranged freely
- Conversational Memory — Context-aware follow-up questions via LangGraph MemorySaver
- Hybrid LLM Support — Switch between local Ollama (qwen3.5:9b/4b) and cloud Groq (Llama 4 Scout) at runtime
- Model Switching — Dynamic qwen3.5 selection (9b/4b) at runtime for local inference
- GPU Status Indicator — Real-time display of GPU acceleration status
- Configurable RAG Features — Toggle context grading and re-ranking on/off via UI or environment variables
- Context Grading: Evaluates retrieval quality before generation (Corrective RAG pattern)
- BGE Re-ranker: Improves precision with cross-encoder re-ranking (optional, +150ms latency)
The platform ships with sample fantasy lore about the DuckyDucks and FrogyFrogs of Quackswamp — a rich test dataset for validating vector search capabilities.
%%{init: {'theme': 'dark', 'themeVariables': { 'fontSize': '11px', 'lineColor': '#666666'}}}%%
flowchart TD
User["User Query"] --> RAGSubgraph["RAG SUBGRAPH<br/>Retrieval + Grading + Re-ranking"]
subgraph RAGSubgraph["RAG SUBGRAPH"]
direction TB
VectorSearch["Vector Search<br/>Archival Lookup<br/>k=20 candidates"]
ReRanker["Re-ranker<br/>BGE Cross-Encoder<br/>Top-K selection"]
Grader["Grader<br/>Context Relevance<br/>RELEVANT/IRRELEVANT"]
VectorSearch --> ReRanker
ReRanker --> Grader
end
RAGSubgraph --> Agent["AGENT_NODE<br/>Worker LLM<br/>Reasoning + Tool Selection"]
Agent --> ShouldContinue{"Has tool<br/>calls?"}
ShouldContinue -->|Yes| Tools["TOOL_NODE<br/>DuckDuckGo / Wikipedia / Time"]
ShouldContinue -->|No| Reviewer["REVIEWER_NODE<br/>QA Critic LLM"]
Tools --> Agent
Reviewer --> IsValid{"Response<br/>VALID?"}
IsValid -->|No, <3 attempts| Agent
IsValid -->|No, ≥3 attempts| Final["Final Output + Knowledge Graph"]
IsValid -->|Yes| OntologySubgraph
subgraph OntologySubgraph["ONTOLOGY_EXTRACTOR SUBGRAPH"]
direction TB
ExtractOntology["extract_ontology<br/>Extract entities & links<br/>Identify gap references"]
ExtractOntology --> ProcessEntities["Process Entities<br/>Generate UUIDs"]
ProcessEntities --> IsLocation{"Is<br/>Location?"}
IsLocation -->|Yes| Geocode["Geocode Location<br/>Nominatim API<br/>lat, lon, country"]
IsLocation -->|No| BuildMap["Build Name to UUID Map"]
Geocode --> BuildMap
BuildMap --> DetectGaps["detect_gaps<br/>Check for missing<br/>entity references"]
DetectGaps --> HasGaps{"Gap<br/>entities<br/>found?"}
HasGaps -->|Yes| ExtractGap["extract_gap_entities<br/>Targeted LLM extraction<br/>for missing entities only"]
HasGaps -->|No| MergeFinalize["merge_and_finalize<br/>Create entities with UUIDs<br/>Process all links"]
ExtractGap --> MergeFinalize
MergeFinalize --> LinksOK{"All links<br/>resolvable?"}
LinksOK -->|No - Skip| LogHallucinated["Log as hallucinated<br/>relationship"]
LinksOK -->|Yes| CreateLink["Create link<br/>with UUIDs"]
LogHallucinated --> Neo4j["Neo4j Graph DB<br/>Native Graph Storage"]
CreateLink --> Neo4j
end
style Geocode fill:#1e5f4a,stroke:#3a8a6a,stroke-width:2px
Neo4j --> Final
style User fill:#2d5016,stroke:#4a8a2a,stroke-width:2px
style Final fill:#2d5016,stroke:#4a8a2a,stroke-width:2px
style Agent fill:#1e3a5f,stroke:#3a5a8a,stroke-width:2px
style Reviewer fill:#5f1e3a,stroke:#8a3a5a,stroke-width:2px
style Tools fill:#5f4a1e,stroke:#8a6a3a,stroke-width:2px
style RAGSubgraph fill:#1e5f4a,stroke:#3a8a6a,stroke-width:2px
style SessionOntology fill:#2d5016,stroke:#4a8a2a,stroke-width:2px
style Neo4j fill:#2d5016,stroke:#4a8a2a,stroke-width:2px
style IsValid fill:#4a2d1e,stroke:#6a4a3a,stroke-width:2px
style HasGaps fill:#4a2d1e,stroke:#6a4a3a,stroke-width:2px
style ShouldContinue fill:#4a2d1e,stroke:#6a4a3a,stroke-width:2px
style LinksOK fill:#4a2d1e,stroke:#6a4a3a,stroke-width:2px
style OntologySubgraph fill:#1a1a2e,stroke:#5a3a8a,stroke-width:3px,stroke-dasharray: 5 5
Flow Description:
-
RAG Subgraph (mandatory first step) - Retrieves and grades archival context:
- Vector Search: Searches MongoDB vector store for relevant documents (retrieves 20 candidates)
- Re-ranker (optional): BGE cross-encoder re-ranks candidates for better precision, selects top-K (default: 3)
- Grader: Evaluates context relevance (RELEVANT, PARTIALLY_RELEVANT, IRRELEVANT)
- Context Injection: Relevant context is injected; irrelevant context triggers a hint to use web tools
-
Agent - Worker LLM receives context, performs reasoning, decides on tool usage
-
Tools - DuckDuckGo, Wikipedia, Time lookup (loop back to Agent for iterative reasoning)
-
Reviewer - QA Critic validates response against constraints
-
Ontology Extractor Subgraph (dashed border) - Two-pass extraction with gap resolution:
- extract_ontology: Extract entities (7 types), extract links, identify missing references
- Process Entities: Generate UUIDs for all entities
- Geocode Locations: Location entities are geocoded via Nominatim API (lat, lon, country)
- detect_gaps: Check if any link targets don't exist as entities
- extract_gap_entities (if gaps): Targeted LLM extraction for missing entities only
- merge_and_finalize: Create entities with UUIDs, process all links, skip unresolvable
-
Final Output - Response + accumulated knowledge graph persisted to Neo4j
Entity Types Extracted:
- Location, Person, Organization, Event, Asset, Document, Concept
Relationship Types:
- Spatial: LOCATED_IN, STATIONED_IN, OPERATES_IN, HEADQUARTERED_IN, DEPLOYS_TO
- Organizational: AFFILIATED_WITH, PART_OF, LEADS, COMMANDS, REPORTS_TO, FOUNDED
- Political/Military: SUPPORTS, TARGETS, CONFLICT_WITH, ATTACKED, DEFENDS, ALLIES_WITH, SANCTIONS, ARMS, TRAINS
- Territorial: OCCUPIES, CONTROLS, LIBERATES, CAPTURES, SEIZES, FORTIFIES, BLOCKADES
- Diplomatic: NEGOTIATES_WITH, MET_WITH, VISITED, SIGNATORY_TO, RATIFIES, MEDIATES
- Legal/Judicial: INVESTIGATES, INDICTS, PROSECUTES, ARRESTS, EXTRADITES, SANCTIONED_BY
- Economic: OWNS, ACQUIRES, MERGES_WITH, PARTNERS_WITH, FUNDS, BOYCOTTS, GRANTS_AID_TO
- Generic: USES, RELATED_TO, COLLABORATES_WITH, INFLUENCED_BY, DERIVED_FROM
- Note: The system can discover and extract additional relationship types beyond this predefined list based on the text content.
The ontology system automatically extracts entities and relationships from the AI's reasoning output and displays them in an interactive knowledge graph:
Visual Features:
- Color-coded nodes by entity type (blue=Location, orange=Person, purple=Organization, red=Event, green=Asset, etc.)
- Curved edges with clear relationship labels (e.g., CONFLICT_WITH, LOCATED_IN, TARGETS)
- Hover tooltips showing entity properties and metadata
- Dynamic layout using force-directed physics for optimal spacing
- Accumulative graph that grows as the conversation progresses
Example Output: When querying "What happened in Iran last week?", the knowledge graph automatically builds a network showing:
- Countries and cities (Iran, Israel, Qatar, Gulf states)
- Key figures (President Trump, military leaders)
- Military bases and infrastructure (Ras Laffan Industrial City, Meyssam Tammar Basij base)
- Relationships between entities (ATTACKED, LOCATED_IN, SENT_PLAN_TO, etc.)
The ontology system provides a full knowledge graph management interface with multiple visualization modes, LLM-driven relationship discovery, and a review workflow for extracted changes.
Switch between four views using the tab bar in the Ontology panel:
| View | Description |
|---|---|
| Graph | Interactive force-directed network with color-coded nodes (blue=Location, orange=Person, purple=Organization, red=Event, green=Asset, etc.), curved relationship edges with labels, hover tooltips, and multi-select for relationship discovery |
| Table | Sortable data tables for entities (name, type, mentions, created date) and links (source → target, relationship type, mentions) |
| JSON | Expandable/collapsible tree view of the full ontology with copy-to-clipboard and download options |
| Cards | Filterable entity cards grouped by type (Person, Location, Organization, Event, Asset, Document, Concept) |
Select two or more entities in the Graph view (Ctrl/Cmd+click or drag-to-select), then click "Discover Relationships". The system:
- Gathers existing ontology context, conversation history, and relevant source document chunks
- Sends a structured prompt to the LLM asking it to discover all relevant relationships between the selected entities
- The LLM may also discover new intermediary entities and relationships involving them
- Discovered entities and links are added to Pending Review for approval
- The full LLM prompt is recorded in the Intelligence Log for transparency — expand any "Discover relationships" entry to inspect exactly what was asked
All extracted or discovered entities and links land in the Pending Review panel before being committed to the graph database:
- Inspect — Browse pending entities and links with type badges and context summaries
- Select — Click checkboxes to select individual entities or links for targeted approval
- Approve Selected — Commit only the selected items to Neo4j
- Reject Selected — Discard only the selected items
- Approve All — Commit everything in the pending queue at once
- Reject All — Discard everything at once
Approved changes are persisted to Neo4j and immediately reflected in all ontology views. Rejected changes are removed from the pending queue.
- Export Project — Download the current session's full ontology (entities + links) as a JSON file via the File menu
- Import Project — Upload a previously exported JSON file to restore an ontology into the current session
Click "Build Ontology from Conversation" to re-run ontology extraction on the entire chat history. This is useful when:
- You want to re-extract after adding new documents
- Previous extraction missed entities due to model limitations
- You're continuing an older session and want to refresh the graph
%%{init: {'theme': 'dark'}}%%
graph TB
subgraph User["User Interface"]
UI["Web Interface<br/>(Browser OS UI + Knowledge Graph)"]
end
subgraph Backend["Backend Services"]
API["FastAPI<br/>(REST + Streaming)"]
AGENT["LangGraph Agent<br/>(Worker + Critic + Ontology)"]
end
subgraph Data["Data Layer"]
MDB[("MongoDB 8.2+<br/>(Vector Search)")]
N4J[("Neo4j 5.26<br/>(Ontology Graph)")]
OL["Ollama<br/>(qwen3.5 LLM)"]
NOM["Nominatim<br/>(Self-Hosted Geocoding)"]
end
subgraph Tools["External Tools"]
WEB["DuckDuckGo<br/>(Live Search)"]
WIKI["Wikipedia API"]
end
UI --> API
API --> AGENT
AGENT --> MDB
AGENT --> N4J
AGENT --> OL
AGENT --> NOM
AGENT --> WEB
AGENT --> WIKI
The platform purposefully duplicates session ontology data across two databases to optimize for different read/write patterns:
-
MongoDB (UI State & Snapshots): Acts as the primary document store. The entire session context (chat messages and the raw JSON ontology tree) is continually saved here. This allows the backend to restore a session's UI state instantly with an
$O(1)$ query, without needing to reconstruct the graph structure from scratch. - Neo4j (AI Querying & Traversal): The ontology is synchronously mirrored to Neo4j. This graph database allows the AI agent to execute complex, multi-hop Cypher queries (e.g., finding all indirectly affiliated organizations of a person) in milliseconds during the reasoning pipeline.
For detailed technology decisions, see Technology Choices.
For agent orchestration details, see Agent Workflow.
For ontology system details, see Ontology System.
- Docker and Docker Compose installed
- Optional: NVIDIA GPU + Container Toolkit for accelerated inference
- Optional: Groq API key for cloud LLM fallback (get free key at console.groq.com)
# Install NVIDIA drivers and Container Toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# Verify GPU visibility
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smiCopy .env.example to .env and configure:
# For cloud LLM fallback (optional)
GROQ_API_KEY=your-groq-api-key
USE_ONLINE_LLM=false
# For self-hosted Nominatim (optional - avoids rate limiting)
NOMINATIM_URL=http://nominatim:8080/searchPlace PDF files into ./documents/pdf/ for the RAG archival pipeline.
docker compose up --buildThis orchestrates:
- MongoDB with vector search index
- Neo4j graph database for ontology storage
- Ollama pulling qwen3.5:9b and qwen3.5:4b models
- Nominatim self-hosted geocoding service (Europe OSM data)
- Document ingestion and chunking
- FastAPI backend with streaming
- Grafana + Loki observability stack
| Service | URL | Credentials |
|---|---|---|
| Web Interface | localhost:8000 | — |
| MongoDB Browser | localhost:8081 | admin / geovision |
| Neo4j Browser | localhost:7474 | neo4j / geovision |
| Nominatim Geocoding | localhost:8083 | — |
| Container Logs | localhost:9999 | — |
| Grafana Dashboards | localhost:3000 | admin / geovision |
Optional: LangSmith Tracing - See docs/langsmith.md for setup.
GeoVision Lab includes a self-hosted Nominatim service for geocoding location names to coordinates. This avoids rate limiting issues with the public Nominatim API (which limits to 1 request/second) and provides faster, more reliable geocoding.
The self-hosted Nominatim service is enabled by default in Docker Compose. It pre-loads OpenStreetMap data for Europe during initial startup.
Environment Variables:
# Self-hosted Nominatim (default)
NOMINATIM_URL=http://nominatim:8080/search
NOMINATIM_TIMEOUT=10To use the public Nominatim API instead, leave NOMINATIM_URL empty in your .env file:
NOMINATIM_URL=The application will automatically fall back to the public API if the self-hosted service is unavailable.
| Component | Minimum | Recommended |
|---|---|---|
| RAM | 4 GB | 8 GB |
| Storage | 20 GB | 50 GB (SSD) |
| CPU | 2 cores | 4 cores |
| Initial Import | ~60 min | ~30 min |
On first startup, Nominatim downloads and imports Europe OSM data from Geofabrik. This takes 30-60 minutes depending on your hardware and network speed. The service health check will monitor import progress.
For development/testing, you can use smaller extracts by modifying docker-compose.yml:
environment:
- PBF_URL=https://download.geofabrik.de/germany-latest.osm.pbf
- REPLICATION_URL=https://download.geofabrik.de/germany-updates/- API Endpoint:
http://localhost:8083/search?q=Berlin&format=json - Web Interface: localhost:8083
- No rate limiting
- Faster response times (local network)
- Reliable availability
- Full control over data updates
- Better for development/testing
GeoVision Lab supports hybrid LLM deployment with both local and cloud models:
Warning
The included smaller models (qwen3.5:9b and qwen3.5:4b) are often too weak for proper, reliable ontology extraction and should primarily be used for testing and development. It is highly recommended to add and configure more capable local models when your hardware allows it to ensure high-quality knowledge graph generation.
| Model | Size | Speed | Quality | Best For |
|---|---|---|---|---|
| qwen3.5:9b | 9B | Slower | Highest | Complex analysis, detailed reports (Testing) |
| qwen3.5:4b | 4B | Balanced | High | Default — general purpose (Testing) |
To switch local models:
- Open the Web Interface at localhost:8000
- Use the model selector dropdown above the chat input
- Selection takes effect immediately
| Model | Provider | Speed | Quality | Best For |
|---|---|---|---|---|
| meta-llama/llama-4-scout-17b-16e-instruct | Groq | Fast | High | Live events, current affairs |
To enable cloud LLM:
- Get a Groq API key at console.groq.com
- Add
GROQ_API_KEY=your-api-keyto your.envfile - Set
USE_ONLINE_LLM=truein.env - Restart the application or toggle via Web Interface (if available)
Hybrid Strategy: Use local qwen3.5 models for privacy-sensitive analysis and cloud Groq models for live events requiring up-to-date information. Switch between modes as needed.
GeoVision Lab supports configurable RAG features that can be toggled on/off independently via environment variables or the web UI.
| Feature | Description | Recommended |
|---|---|---|
| Context Grading | Evaluates retrieval quality before generation (Corrective RAG pattern) | Enabled |
| BGE Re-ranker | Improves precision with cross-encoder re-ranking | Enabled |
Add to your .env file:
# RAG Features Configuration
RAG_GRADER_ENABLED=true # Enable/disable context grading
RAG_RERANKER_ENABLED=true # Enable/disable BGE re-ranker
RAG_RERANKER_MODEL=BAAI/bge-reranker-v2-m3
RAG_RERANKER_TOP_K=3 # Number of results after re-ranking
RAG_RERANKER_CANDIDATES_K=20 # Number of candidates to retrieve- Open the Services window in the web interface
- Find the RAG Configuration panel
- Toggle features on/off:
- Context Grading: Evaluate context relevance before generation
- Re-ranking (BGE): Improve precision with cross-encoder
Changes take effect immediately for new queries.
| Configuration | Flow | Use Case |
|---|---|---|
| All disabled | Vector Search → Agent | Fastest, baseline quality |
| Grader only | Vector Search → Grader → Agent | Prevents hallucinations from poor context |
| Re-ranker only | Vector Search (k=20) → Re-rank (k=3) → Agent | Better precision for technical queries |
| Both enabled (default) | Vector Search (k=20) → Re-rank (k=3) → Grader → Agent | Best quality for critical analysis |
Disable re-ranker for:
- Simple queries where vector search is sufficient
- Latency-sensitive applications
- Small document collections
Disable grader for:
- Maximum speed when you trust vector search quality
- Testing and debugging
The platform includes a sample document (documents/fantasy.md) about the DuckyDucks and FrogyFrogs of Quackswamp. Use these test queries:
| Test Query | Expected Behavior |
|---|---|
| "Where is the secret base of the DuckyDucks located?" | Should retrieve Antarctica reference |
| "Tell me about the War of Ripples" | Should return details about the 6-year war (1247-1253) |
| "What are the characteristics of FrogyFrogs?" | Should list emerald skin, leaping ability, water magic |
| "Who signed the Treaty of Ripples?" | Should mention the peace treaty on a lily pad |
| "What is the Prophecy of the Golden Tadpole?" | Should retrieve the unity prophecy |
- Ingestion — Check Dozzle logs for
geovision-ingestdocument loading - Vector Search — Ask about DuckyDucks; watch
vector_searchtool trigger - Live Search — Ask about breaking news; verify
duckduckgo_searchexecution - Time Awareness — Ask "What exact date and time is it right now?"
- Ontology Extraction — Ask about real geopolitical entities (e.g., "What happened in Iran last week?"); verify entities and relationships appear in Knowledge Graph panel with:
- Color-coded nodes (blue locations, orange people, purple organizations)
- Clear curved edge labels showing relationship types (ATTACKED, LOCATED_IN, etc.)
- Hover tooltips with entity details
- Proper node spacing without overlapping labels
- Location Geocoding — Ask about specific cities/countries; verify coordinates are extracted and displayed on the map panel
- Model Switching — Switch between qwen3.5:9b and qwen3.5:4b; observe quality/speed differences
- Browser OS UI — Verify windows can be dragged, resized, and snapped; check Reasoning Chain shows workflow steps, Chat Result shows response, Knowledge Graph shows entities and relationships
- GPU Status — Check the top panel shows correct GPU status (Active/Standby/CPU Only)
- Online LLM — If Groq API key configured, verify cloud model responses for current events
geo-vision-lab/
├── app/ # Core application package
│ ├── agents/ # LangGraph architecture & tools
│ ├── api/routes/ # FastAPI REST endpoints
│ ├── core/ # Global settings & config
│ ├── ingestion/ # RAG data processing pipeline
│ ├── models/ # Pydantic models & schemas
│ └── services/ # LLM & MongoDB connectors
├── static/ # Vanilla JS / CSS Web Interface
├── documents/
│ ├── pdf/ # Your source PDFs (includes Iran - Wikipedia.pdf)
│ ├── ignore/ # Documents excluded from ingestion
│ └── fantasy.md # Sample test data (DuckyDucks & FrogyFrogs)
├── monitoring/ # Grafana, Loki, Promtail config
├── docs/ # Additional documentation
├── learnings/ # Technical insights & deployment guides
├── migrations/ # Database migration scripts
├── tests/ # PyTest test suite
├── docker-compose.yml # Full stack orchestration
├── Dockerfile # Application container
└── requirements.txt # Python dependencies
| Document | Description |
|---|---|
| Technology Choices | Detailed rationale for each technology decision |
| Agent Workflow | Deep dive into multi-agent orchestration |
| Ontology System | Knowledge graph architecture and entity extraction |
| Agent Learnings | Technical insights on reasoning LLMs |
| Debugging Guide | Troubleshooting common issues |
| MongoDB Vector Search | Vector search implementation details |
| LangSmith Tracing | Setup guide for LLM tracing and debugging |
| Dependency Injection | DI pattern implementation details |
| Error Handling | Error handling improvements and patterns |
| Layer | Technology |
|---|---|
| LLM Inference (Local) | Ollama + qwen3.5 (9b/4b) - switchable models |
| LLM Inference (Cloud) | Groq + Llama 4 Scout (17B) - optional fallback |
| Ontology Extraction | LLM structured output + Nominatim geocoding |
| Graph Database | Neo4j 5.26 (ontology storage + traversal) |
| Embeddings | all-MiniLM-L6-v2 (384 dims) |
| Vector Database | MongoDB 8.2+ Vector Search |
| Agent Framework | LangGraph + MemorySaver (with ontology subgraph) |
| Backend API | FastAPI + uvicorn |
| Frontend UI | Vanilla JS + Browser OS-style window manager + Knowledge Graph visualization |
| Geocoding | Nominatim (self-hosted with public API fallback) |
| Testing | PyTest + Testcontainers |
| CI/CD | GitHub Actions |
| Observability | Grafana + Loki + Dozzle |
| Tracing & Debugging | LangSmith (cloud or self-hosted) |
| Containerization | Docker + Docker Compose |
