A minimal, explainable financial intelligence system that enhances user queries using vector similarity search (Qdrant) and graph traversal (Neo4j). It now also supports text-based ingestion from the UI to upsert both stores.
User Query
│
▼
embeddings.py — embed query with SentenceTransformers (all-MiniLM-L6-v2)
│
▼
vector_store.py — Qdrant cosine similarity search → top-k similar events
│
▼ extract entity names from vector result payloads
│
▼
graph_store.py — Neo4j multi-hop traversal starting from seed entities
│
▼
main.py — merge vector + graph results into structured output
│
▼
app.py — Streamlit dashboard for analysts
| Step | File | What happens |
|---|---|---|
| 1 | embeddings.py |
Query text → 384-dim dense vector via SentenceTransformers |
| 2 | vector_store.py |
Qdrant finds the k most semantically similar historical events |
| 3 | main.py |
Entity lists are extracted from the Qdrant payload metadata |
| 4 | graph_store.py |
Neo4j follows supply-chain edges up to N hops from seed entities |
| 5 | main.py |
Results are merged into a single structured dict |
| 6 | app.py |
Streamlit renders the result as an interactive dashboard |
| File | Purpose |
|---|---|
config.py |
All constants — DB connection details, entity/relationship lists, sample data |
embeddings.py |
SentenceTransformers model loading and get_embedding() helper |
vector_store.py |
Qdrant collection management and similarity search |
graph_store.py |
Neo4j driver, graph seeding, and multi-hop traversal query |
ingestion_agent.py |
Agent prompt + parsing logic to convert plain text into ingestion payload JSON |
main.py |
Orchestration pipeline and CLI entry point |
app.py |
Streamlit frontend |
requirements.txt |
Python dependencies |
| Service | Default address |
|---|---|
| Qdrant | localhost:6333 |
| Neo4j | bolt://localhost:7687 (user: neo4j, password: password) |
pip install -r requirements.txtstreamlit run app.pyThe Ingest tab now supports two modes:
- Structured JSON mode for direct payload entry.
- Agent from article text mode that parses plain text into payload JSON.
For AI Analyst Brief generation and Agent ingestion mode, set your API key before starting Streamlit.
Option 1 (recommended): create a .env file from .env.example and set GOOGLE_API_KEY.
Option 2: export it in shell:
export GOOGLE_API_KEY="your_key_here"Structured JSON example:
Example payload:
{
"event": {
"text": "Lithium refinery shutdown in Chile raises EV battery risk",
"entities": ["Lithium", "Chile", "Tesla"],
"entity": "Lithium",
"region": "Chile",
"sentiment": "negative",
"industry": "battery"
},
"graph_edges": [
{
"from_label": "Country",
"from_name": "Chile",
"rel": "SUPPLIES",
"to_label": "Commodity",
"to_name": "Lithium"
},
{
"from_label": "Company",
"from_name": "Tesla",
"rel": "USES_RESOURCE",
"to_label": "Commodity",
"to_name": "Lithium"
}
]
}You can also send events (array) instead of event (single object).
Neo4j constraints are verified before every graph ingestion write.
python main.py "lithium supply disruption"{
"query": "lithium supply disruption",
"similar_events": [
{
"score": 0.92,
"text": "Chile lithium strike disrupts battery supply chain",
"entity": "Lithium",
"region": "Chile",
"sentiment": "negative",
"industry": "battery"
}
],
"related_entities": ["Battery Manufacturing", "Chile", "EV Companies", "Lithium", "Tesla"],
"relationships": [
"Chile --[SUPPLIES]--> Lithium",
"Lithium --[SUPPLIES]--> Battery Manufacturing",
"Battery Manufacturing --[SUPPLIES]--> Electric Vehicles",
"Tesla --[USES_RESOURCE]--> Lithium"
],
"supporting_news": [
"Chile lithium strike disrupts battery supply chain",
"Battery makers seek alternative suppliers amid lithium shortage"
]
}Node labels: Company · Commodity · Country · Industry
Relationship types:
| Relationship | Meaning |
|---|---|
SUPPLIES |
Entity A provides goods/services to Entity B |
USES_RESOURCE |
Company consumes a commodity |
COMPETES_WITH |
Two companies compete in the same market |
SUBSTITUTE_FOR |
Commodity A can replace Commodity B |
SUBSIDIARY_OF |
Company A is owned by Company B |
All tuneable parameters live in config.py:
QDRANT_HOST = "localhost"
QDRANT_PORT = 6333
NEO4J_URI = "bolt://localhost:7687"
GRAPH_TRAVERSAL_DEPTH = 2 # increase for deeper supply-chain discovery
EMBEDDING_MODEL = "all-MiniLM-L6-v2"
LLM_MODEL = "gemini-2.5-flash" # single Gemini model used across the project- Small — fewer than 7 source files, each under 120 lines
- Explainable — every function has a docstring describing its role in the pipeline
- Readable — functions over classes, plain dicts over ORM objects
- Self-contained — sample data is loaded automatically so the system works without a live data feed