A multi-agent ontology search system for the NFDI4Cat ontology portfolio. Resolves terms, identifiers, synonyms, and fuzzy matches across 19 biomedical and chemical ontologies via the EBI OLS4 API, with a dark-theme web interface.
- Hybrid matching — identifier lookup (
CHEBI:35255), exact match, synonym match, fuzzy match - Classes and properties — searches OWL classes and object/data/annotation properties (e.g.
IAO:0000136"is about"); a scope selector narrows results to Classes, Properties, or Both (default). Named individuals are excluded. - Parallel search — all selected ontologies queried simultaneously via
asyncio - LangGraph orchestration — stateful pipeline: query parsing → parallel specialist search → aggregation
- Batch search — upload a TXT/CSV file with multiple terms, download results as CSV
- Custom ontologies — add any OLS4 ontology to the sidebar; persisted across server restarts
- Web GUI — dark-theme single-page app with ontology filter sidebar and sortable results
- REST API — JSON endpoints for integration into other tools
Requirements: Python ≥ 3.11 — tested with Anaconda 3.12 on Windows
PowerShell
# 1. Install dependencies
C:\Users\<you>\anaconda3\python.exe -m pip install -r requirements.txt
# 2. Start the server
$env:PYTHONUTF8="1"; C:\Users\<you>\anaconda3\python.exe server.py
# 3. Open in browser
# http://localhost:8000Git Bash
# 1. Install dependencies
/c/Users/<you>/anaconda3/python.exe -m pip install -r requirements.txt
# 2. Start the server
PYTHONUTF8=1 /c/Users/<you>/anaconda3/python.exe server.py
# 3. Open in browser
# http://localhost:8000Port already in use? If port 8000 is occupied by a previous instance, free it first:
# PowerShell Get-NetTCPConnection -LocalPort 8000 -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }# Git Bash netstat -ano | grep ':8000 ' | awk '{print $NF}' | sort -u | xargs -I{} taskkill //PID {} //F
All settings live in onto_agent/config.py and are overridable via environment variables:
| Variable | Default | Purpose |
|---|---|---|
ONTOAGENT_HOST |
127.0.0.1 |
Bind address. The server is loopback-only by default; set 0.0.0.0 to expose it on the network (note: there is no authentication). |
ONTOAGENT_PORT |
8000 |
Listen port |
ONTOAGENT_OLS4_BASE |
EBI OLS4 | Upstream API base URL |
ONTOAGENT_MAX_UPLOAD_BYTES |
1000000 |
Max batch-upload size (DoS guard) |
ONTOAGENT_MAX_TERMS |
500 |
Max terms per batch (DoS guard) |
ONTOAGENT_OLS4_CONCURRENCY |
8 |
Process-wide cap on OLS4 search requests |
ONTOAGENT_ANCESTOR_CONCURRENCY |
4 |
Separate cap on superclass/ancestor fetches (so they can't starve searches) |
ONTOAGENT_BATCH_CONCURRENCY |
5 |
Concurrent terms per batch run |
ONTOAGENT_MAX_CUSTOM |
50 |
Max number of user-added custom ontologies |
ONTOAGENT_SEARCH_TIMEOUT |
60 |
Wall-clock deadline (seconds) for a single search |
ONTOAGENT_LOG_LEVEL |
INFO |
Log level |
Integer settings are clamped to >= 1, so a stray 0 or negative value falls back to a safe minimum instead of hanging or crashing the server.
⚠️ Security: there is no authentication. Every response carries hardening headers (X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer) and request inputs are bounded (query ≤ 200 chars, batch upload/term caps), but these do not replace auth. Do not bind to0.0.0.0on an untrusted network without putting an authenticating reverse proxy in front.
python -m pip install -r requirements-dev.txt
PYTHONUTF8=1 python -m pytestAll tests are network-free (pure-logic unit tests, TestClient endpoint guards, and respx-mocked OLS4 paths), so they run offline and in CI. GitHub Actions (.github/workflows/ci.yml) runs the suite on Python 3.12/3.13 plus pip-audit.
Dependencies are locked with pip-tools. Edit the ranges in requirements.in / requirements-dev.in, then regenerate the pinned, hashed locks:
pip-compile --generate-hashes requirements.in
pip-compile --generate-hashes --allow-unsafe requirements-dev.inrequirements.txt / requirements-dev.txt are generated — don't hand-edit them. The pip-audit CI job fails on known vulnerabilities, so keep the locks current (langgraph is held at >=1.0.10 because the 0.x line carries known CVEs). The lock is generated on Python 3.13; regenerate it on the lowest target before adding older interpreters to CI.
Open http://localhost:8000 in any browser.
- Sidebar — toggle individual ontologies on/off; green dot = OLS4 available,
*= not available - Search bar — enter any term, OBO identifier, or synonym and press Enter or click Search
- Sort — switch between Relevance, Match type, Alphabetical, or grouped By ontology
Example queries:
| Input | What happens |
|---|---|
catalyst |
Exact and fuzzy matches across all active ontologies |
CHEBI:35255 |
Identifier lookup — finds the exact OBO class |
IAO:0000136 |
Identifier lookup for a property (is about) |
hydroxyl |
Synonym and fuzzy search |
Use the scope selector next to the search bar to restrict results to Classes only, Properties only, or both (default).
Upload a TXT or CSV file with multiple terms to search in one go:
- TXT — one term per line
- CSV — terms in the first column (optional header row is auto-detected)
Click Search & Download CSV — the server searches all selected ontologies for every term in parallel and returns a CSV file with one row per ontology per term (best match only).
CSV columns: input_term, ontology_id, ontology_name, match_type, entity_type, matched_label, obo_id, uri, definition
At the bottom of the sidebar, click ▶ Add ontology to expand the form:
- Enter the OLS4 ID (e.g.
efo) — the system validates it against the live OLS4 API - Optionally set a Display name and Prefix
- Click Add ontology — it appears immediately in the sidebar and is persisted to
custom_ontologies.json
To remove a custom ontology, click the × button on its sidebar chip. Base NFDI4Cat ontologies cannot be removed.
Each result shows:
- Match type badge — ID (blue) / Exact (green) / Synonym (amber) / Fuzzy (gray)
- Entity type badge — Class (gray) / Property (purple)
- Term label and ontology name
- OBO ID and clickable URI
- Definition (when available)
- Superclasses (up to 5 ancestors from OLS4 hierarchy)
- TTL excerpt — collapsible Turtle/RDF snippet
List ontologies
GET /api/ontologies
Returns all ontologies (NFDI4Cat base set + any custom additions) with OLS4 support status.
Search
POST /api/search
Content-Type: application/json
{
"query": "catalyst",
"ontologies": ["chebi", "chmo", "afo"], // optional — omit to search all
"scope": "both" // optional — "class" | "property" | "both" (default)
}
query must be 1–200 characters, ontologies at most 128 ids, and scope one of class / property / both; oversized or invalid payloads return 422.
Response:
{
"query": "catalyst",
"matches": [
{
"id": "BAO:0002000",
"label": "catalyst",
"synonyms": [],
"ontology": "bao",
"ontology_name": "BioAssay Ontology",
"source": "NFDI4Cat",
"uri": "http://www.bioassayontology.org/bao#BAO_0002000",
"definition": "A substance that increases the rate of a reaction...",
"superclasses": ["role", "chemical entity"],
"match_type": "exact_match",
"entity_type": "class",
"ttl_excerpt": "@prefix rdfs: ..."
}
],
"errors": {}
}Match type priority: identifier_match > exact_match > synonym_match > fuzzy_match
Batch search
POST /api/batch-search
Content-Type: multipart/form-data
file=<TXT or CSV file>
ontologies=["chebi","chmo"] // optional JSON array — omit to use all
scope=both // optional — "class" | "property" | "both" (default)
Returns a CSV file attachment (onto_results.csv).
Add custom ontology
POST /api/ontologies
Content-Type: application/json
{
"ols4_id": "efo",
"name": "Experimental Factor Ontology",
"prefix": "EFO"
}
Validates against OLS4, returns 201 with the new entry. Persisted to custom_ontologies.json. Limited to ONTOAGENT_MAX_CUSTOM entries (default 50); exceeding the cap returns 409, as does a duplicate id.
Remove custom ontology
DELETE /api/ontologies/{ols4_id}
Returns 204. Base NFDI4Cat ontologies are protected (returns 403).
server.py FastAPI — serves GUI + REST endpoints
│ POST /api/batch-search (multipart upload → CSV)
│ POST /api/ontologies (add custom)
│ DELETE /api/ontologies/{id}
└── onto_agent/
├── orchestrator.py LangGraph StateGraph (3 nodes) + shared httpx client
│ parse_query → search_all (asyncio.gather) → aggregate
├── ols_client.py Async OLS4 API client (httpx)
├── batch.py Term parsing + CSV assembly for batch search
├── registry.py 27 NFDI4Cat ontologies + custom persistence
├── config.py Env-driven settings (ONTOAGENT_*)
└── models.py Pydantic models + LangGraph TypedDict
gui/
└── index.html Single-file dark-theme SPA (vanilla JS)
Search tab + Batch tab + Add ontology form
custom_ontologies.json Runtime state (git-ignored); stores custom ontology entries
- Base URL:
https://www.ebi.ac.uk/ols4/api - One pooled
httpx.AsyncClientis shared process-wide (created at app startup, closed at shutdown) instead of one per request - Concurrency: searches via
asyncio.Semaphore(8), ancestor fetches via a separateasyncio.Semaphore(4)so enrichment can't starve searches - Timeout: 15 s per upstream request; 60 s overall deadline per
/api/search - Entity scope:
class/propertymap to the OLS4typefilter;bothomits it (and drops individuals). The combinedtype=class,propertyform is avoided — OLS4 parses it loosely and leaks individuals. - Ancestor fetch: double-URL-encoded IRI — classes via
/terms/{iri}/hierarchicalAncestors, properties via/properties/{iri}/ancestors(the property/hierarchicalAncestorspath 404s)
| ID | Name |
|---|---|
afo |
Allotrope Foundation Ontology |
bao |
BioAssay Ontology |
bfo |
Basic Formal Ontology |
chebi |
ChEBI |
cheminf |
Chemical Information Ontology |
chmo |
Chemical Methods Ontology |
envo |
Environmental Ontology |
iao |
Information Artifact Ontology |
ms |
Mass Spectrometry Ontology |
obi |
Ontology for Biomedical Investigations |
pato |
Phenotype and Trait Ontology |
po |
Plant Ontology |
rex |
Physico-chemical process ontology |
ro |
Relations Ontology |
rxno |
Named Reaction Ontology |
sbo |
Systems Biology Ontology |
sio |
Semanticscience Integrated Ontology |
uo |
Units of Measurement Ontology |
xlmod |
HUPO-PSI XLMOD |
| ID | Name |
|---|---|
cao |
Chemical Analysis Ontology |
cif |
Crystallographic Information Framework |
enmo |
Engineering Methods Ontology |
m3 |
M3 |
metadata4ing |
metadata4ing |
osmo |
OSMO |
ontocape |
OntoCAPE |
vimmp |
VIMMP |
* Shown in the sidebar but not queried.
| Issue | Workaround |
|---|---|
Windows cp1252 encoding errors |
Set $env:PYTHONUTF8="1" before starting the server (handled automatically in server.py) |
| OLS4 rate limiting (HTTP 429) | Search semaphore caps concurrent requests to 8 (+4 for ancestor fetches); retries not yet implemented |
| Superclass fetch may be slow | Only fetched for the top 3 results per ontology, under a separate concurrency budget; a slow search is bounded by the 60 s overall deadline |