FastAPI named-entity style extraction service with a TypeScript HTTP client, alias + optional schema-driven matching, optional GLiNER (off by default), merge logic for overlapping spans, and pytest coverage. POST /extract is a pure pipeline over the request body and does not query TypeDB. Optional read-only TypeDB HTTP endpoints under /schema-pipeline/* (when TYPEDB_* is set on the server) help apps fetch raw define text, validate ER assumptions, then build a schema for /extract. The /extract response JSON shape is stable; new behavior is added only via optional request fields.
This document is written so you can reproduce the setup from an empty machine after you have this repository’s source tree (clone or copy files). It does not duplicate every source line; it describes layout, contracts, tooling, and commands end to end.
For integrators and AI agents (architecture, HTTP contract, schema, TypeDB env, tests, and troubleshooting in one place), use:
docs/USER_AND_AGENT_GUIDE.md · docs/ENTITY_SERVICE_WORKFLOWS.md (Mermaid: flows inside ES + integrators)
To onboard a new coding agent with a single copy-paste brief: docs/NEW_AGENT_ONBOARDING_PROMPT.md (paste from the file’s horizontal rule through section 10).
| Layer | Role |
|---|---|
app/ |
FastAPI app: routes (/extract, optional /schema-pipeline/*), Pydantic models, services (matcher, extractor, schema → aliases, optional GLiNER, optional TypeDB HTTP reads). |
app/config/aliases.py |
Default aliases as a nested dict (flattened to rows for matching). |
app/services/merge.py |
Merges alias + model candidates (RapidFuzz-assisted overlap handling). |
tests/ |
pytest for matcher, extractor, models, schema, merge, GLiNER wiring. |
src/ |
TypeScript: NerServiceClient, types, smoke CLI, TypeDB-oriented schema adapter. |
Install on your PATH:
| Tool | Version | Purpose |
|---|---|---|
| uv | current | Python 3.12+ env, lockfile, uv run. |
| Node.js + npm | LTS | TypeScript client, ts-node, smoke script. |
| Git | any | Clone / version control. |
Target: Python 3.12+ (requires-python in pyproject.toml).
entity-service/
docs/
USER_AND_AGENT_GUIDE.md # End-to-end guide for users and AI agents
ENTITY_SERVICE_WORKFLOWS.md # Mermaid diagrams: ES flows + TypeDB/TS paths
NEW_AGENT_ONBOARDING_PROMPT.md # Copy-paste handoff for new agents
AGENTS.md # Agent / roadmap notes (optional for humans)
README.md # This file
vitest.config.ts # Vitest (TS tests, including optional TypeDB integration)
pyproject.toml # Python project, deps, hatchling wheel, pytest config
uv.toml # uv defaults (e.g. link-mode=copy on Windows)
uv.lock # Locked Python deps (commit this)
package.json # npm scripts + TS devDependencies
package-lock.json # npm lock (commit if you use npm ci)
tsconfig.json # TypeScript compiler options
test.ps1 # PowerShell wrapper: sets NER_SERVICE_URL, runs npm smoke
scripts/
smoke_schema_pipeline.py # Health + /validate (+ optional /raw with --typedb)
repair_stale_pydantic_dist_info.py # Remove pydantic_core *.dist-info without RECORD
app/
__init__.py
main.py # FastAPI app + router includes
models.py # Request/response + schema + options Pydantic models
schema_pipeline_models.py # Pydantic models for /schema-pipeline/*
config/
__init__.py
aliases.py # Default ALIASES rows
routes/
__init__.py
health.py # GET /health
extract.py # POST /extract
schema_pipeline.py # POST /schema-pipeline/raw|validate|formatted (optional TypeDB)
services/
__init__.py
alias_matcher.py # AliasMatcher: substring scan + overlap dedupe
extractor.py # Orchestrates aliases, schema, model, merge, labels, options
schema_aliases.py # EntitySchemaPayload → extra alias rows
schema_pipeline.py # Raw → validate → formatted schema (TypeDB HTTP, read-only)
typedb_connection.py # Env → TypeDbHttpSettings (optional)
typedb_http_client.py # Sign-in + type-schema + one-shot query
typedb_define_parse.py # Parse define text (entity / owns / string attrs)
typeql_builders.py # Safe TypeQL fragments
merge.py # Overlap / near-duplicate merge (RapidFuzz)
gliner_extractor.py # extract_with_model (optional GLiNER; off by default)
spacy_extractor.py # Stub for future spaCy EntityRuler path
tests/
test_alias_matcher.py
test_extractor.py
test_models.py
test_schema_aliases.py
test_gliner_extractor.py
test_merge.py
test_spacy_extractor.py
src/
test-ner.ts # Smoke: health + several extract scenarios
ner-client/
client.ts # fetch() client
types.ts # Mirrors Python API types
typedb/
index.ts # Barrel exports (client, env, verify, fetch schema, …)
env.ts # TYPEDB_* + TYPEDB_CONNECTION_STRING → TypeDbEnv
connection-string.ts # typedb://… Cloud URI parser
verify-connection.ts # verifyTypeDbConnection (health + DB + type schema)
schema-adapter.ts # TypeDbSchemaSnapshot → EntitySchemaPayload
fetch-schema-for-extraction.ts
introspect-ontology.ts # Ontology checks from define type schema
… # typeql-builders, validate-data-concepts, tests, scripts
Wheel packaging: pyproject.toml uses Hatchling with packages = ["app"] only. There is no separate publishable entity_service Python package under src/ (that tree is TypeScript only).
Either clone this repo or copy the tree above into an empty folder and cd into it.
Windows / pydantic_core: If uv sync warns about pydantic_core-*.dist-info missing RECORD, stop any running fastapi dev / Python using .venv, then run uv run python scripts/repair_stale_pydantic_dist_info.py and uv sync --all-groups again. The repo uv.toml sets link-mode = "copy" to reduce hardlink-related install issues.
# Install runtime + dev (pytest) from uv.lock
uv sync --all-groupsIf uv.lock is missing (truly blank start), generate it once:
uv lock
uv sync --all-groupsEditable install: uv sync installs the local package entity-service (wheel contains the app package only).
From the repo root:
uv run fastapi dev app/main.pyOn Windows, backslashes are fine:
uv run fastapi dev app\main.pyDefault URL: http://127.0.0.1:8000. Open /docs for Swagger.
Production-style (no dev reload):
uv run fastapi run app/main.pynpm installuv run --group dev pytestQuiet:
uv run --group dev pytest -qOffline HTTP + schema-pipeline contract tests (mocked TypeDB, no Brave / no live DB):
uv run pytest -q -m contractnpm run typecheckTerminal A: start the server (step 3).
Terminal B:
npm run smokeOptional base URL:
# Unix
NER_SERVICE_URL=http://127.0.0.1:8000 npm run smoke# Windows PowerShell
$env:NER_SERVICE_URL = "http://127.0.0.1:8000"; npm run smokeOr use .\test.ps1 / .\test.ps1 http://127.0.0.1:9000 on Windows.
Copy-paste “schema pipeline OK” (no live TypeDB): with the API up, POST /schema-pipeline/validate always runs offline. A minimal check from the repo root:
curl -sS -X POST "http://127.0.0.1:8000/schema-pipeline/validate" \
-H "Content-Type: application/json" \
-d "{\"typeSchemaDefine\":\"define\\nattribute name, value string;\\nentity musician, owns name;\\n\",\"assumptions\":{\"entityTypes\":[\"musician\"],\"nameAttribute\":\"name\"}}" | jq .Expect "ready": true in the JSON (omit | jq . if you do not have jq). For /raw and /formatted, load TYPEDB_* into the same environment as uv run fastapi dev … (see docs/USER_AND_AGENT_GUIDE.md); a 200 from /schema-pipeline/raw is the strongest “TypeDB + database + pipeline” signal.
Debug logging: set ENTITY_SERVICE_DEBUG_TYPEDB_BODY=1 to allow truncated TypeQL / type-schema text in logs. Default logs record sizes and hashes only (see app/services/typedb_http_client.py).
One command (Python smoke, API must already be running): from repo root, with uv on your PATH:
# Health + offline validate (no TypeDB on server required)
uv run python scripts/smoke_schema_pipeline.pyWith TypeDB enabled on the FastAPI process, also verify POST /schema-pipeline/raw returns 200:
npm run smoke:schema-pipeline:typedb
# equivalent:
uv run python scripts/smoke_schema_pipeline.py --typedbOptional: SMOKE_ENTITY_TYPES (comma-separated) for the /raw assumptions when using --typedb.
Start the API with the same env file TypeDB uses, e.g. uv run --env-file .env fastapi dev app/main.py, then run the smoke from another terminal.
Response: { "ok": true } — identical contract; use either for automation.
Content-Type: application/json
When TYPEDB_USERNAME, TYPEDB_DATABASE, and either TYPEDB_CONNECTION_STRING or TYPEDB_ADDRESSES are set (same as the TS client), the server can help an app inspect then format graph-backed schema data:
| Method | Path | Purpose |
|---|---|---|
POST |
/schema-pipeline/raw |
Raw define type schema + bounded sample rows per ER assumptions (entityTypes, nameAttribute). |
POST |
/schema-pipeline/validate |
Check assumptions against a typeSchemaDefine string (no live TypeDB call). |
POST |
/schema-pipeline/formatted |
Produce { entityTypes, knownEntities } for POST /extract after ontology checks. |
See docs/USER_AND_AGENT_GUIDE.md §3. If TypeDB env is missing, /raw and /formatted return 503 with a hint; /validate always works.
Do not change field names or top-level structure:
{
"entities": [
{
"text": "string",
"label": "string",
"start": 0,
"end": 0,
"confidence": 0.0
}
]
}| Field | Type | Default / omit behavior |
|---|---|---|
text |
string | Required. Input text. |
labels |
string[] | Omit or []: no label filter. Non-empty: only entities whose label is in the list. |
options |
object | Omit: { "use_aliases": true, "use_model": false }. |
options.use_aliases |
boolean | If false, skips file + schema alias matching. |
options.use_model |
boolean | If true, merges GLiNER spans when GLINER_ENABLED and the ml extra are installed; otherwise the model path returns no entities. |
schema |
object | Omit: no extra runtime aliases. See below. |
schema (JSON key; Python attribute entity_schema):
{
"entityTypes": ["work", "person"],
"knownEntities": [
{
"label": "work",
"canonical": "Invisible Cities",
"aliases": ["Invis Cities"]
}
]
}entityTypes: each entry may be a string (type id) or an object{ "name": "...", "aliases": [...] }for richer hints; carried for future constraints. Matching today usesknownEntities.knownEntities: each row becomes alias catalog entries(label, canonical, canonical)plus one row per non-empty alias string.- Runtime rows are merged after
app/config/aliases.pyrows (same matcher pass).
Pydantic accepts camelCase (entityTypes, knownEntities) or snake_case (entity_types, known_entities) on nested objects where configured.
Minimal:
{ "text": "Girlfriend by Matt Sweet" }With labels and options:
{
"text": "Girlfriend by Matt Sweet",
"labels": ["artist"],
"options": { "use_aliases": true, "use_model": false }
}With schema (or build the same object in TS via toEntitySchemaPayload in src/typedb/schema-adapter.ts):
{
"text": "Reading Invis Cities tonight",
"schema": {
"entityTypes": ["work"],
"knownEntities": [
{
"label": "work",
"canonical": "Invisible Cities",
"aliases": ["Invis Cities"]
}
]
}
}| Module | Purpose |
|---|---|
src/ner-client/types.ts |
ExtractRequest, ExtractResponse, EntitySchemaPayload, ExtractOptions, etc. |
src/ner-client/client.ts |
NerServiceClient: health(), extract(req). |
src/typedb/ |
TypeDB HTTP helpers, env + connection string, verifyTypeDbConnection, schema fetch, toEntitySchemaPayload — see docs/USER_AND_AGENT_GUIDE.md §6–§7. |
src/test-ner.ts |
Manual smoke scenarios. |
fetch is used (requires DOM lib in tsconfig.json; Node 18+ has global fetch).
Tests: npm test (Vitest: parsers + optional live TypeDB if .env is set), npm run test:typedb, npm run typedb:dump-ontology for ontology introspection.
text
→ [optional] alias matcher (file ALIASES + schema-derived rows)
→ [optional] extract_with_model (GLiNER when enabled + ml extra)
→ merge_entity_candidates (RapidFuzz overlap policy)
→ label filter (if labels non-empty)
→ sort by (start, end, label, text)
→ JSON entities[]
Routes stay thin; logic lives under app/services/.
- Default aliases: edit
app/config/aliases.py— nested dictALIASES_NESTEDkeyed by entity label, then canonical string, then a list of surface substrings. Flattened rows power the matcher. - No TypeDB inside
POST /extract: supplyschemafrom TypeScript (src/typedb/) or fromPOST /schema-pipeline/formattedwhen server env is configured.
Music Ontology (MO)
The Music Ontology defines RDF/OWL concepts (artists, recordings, performances, etc.). This HTTP service does not parse MO TTL/RDF itself. To make extraction ontology-aware, supply a schema slice on each request (built in TS or another service):
- Label vocabulary — Map MO classes or roles you care about (e.g. artist-like vs release-like concepts) to the string
labelvalues you send inlabels/schema.knownEntities[].label/ default aliases inapp/config/aliases.py. - Known entities + synonyms — From your knowledge graph or curated data, emit
knownEntities:{ label, canonical | canonical_text, aliases[] }so substring + GLiNER outputs align with your MO instances. - Optional
entityTypes— Use objects{ "name": "...", "aliases": [...] }for future ruler / constraint layers.
If you share a concrete MO subset (labels you use, example individuals, synonym policy), that can drive the TS adapter and default alias config without adding database calls to Python.
Heavy wheels (torch, transformers, onnxruntime, spaCy, gliner, etc.) are optional so a default uv sync stays lean.
uv sync --all-groups --extra mlEnglish spaCy pipeline (required if you call spaCy code that loads en_core_web_sm; not bundled as a normal PyPI dep for all Python versions):
uv run python -m spacy download en_core_web_smGLiNER (example):
export GLINER_ENABLED=1
export GLINER_MODEL_ID=urchade/gliner_small-v2.1 # optional overrideIf GLINER_ENABLED is unset or the ml extra was not installed, extract_with_model returns no entities and alias extraction is unchanged. The spacy_extractor stub is present for a future EntityRuler path; it is not wired into the main extractor yet.
If you recreate the repo by hand, you need at least:
[project]:name,requires-python >= 3.12,dependenciesincludingfastapi[standard]>=0.115.0andrapidfuzz.[project.optional-dependencies].ml:glinerandspacy(install withuv sync --extra ml).[build-system]:hatchling+[tool.hatch.build.targets.wheel]packages = ["app"].[dependency-groups].dev:pytestand[tool.pytest.ini_options]withtestpaths = ["tests"],pythonpath = ["."]soimport appworks from the repo root.
Lock with uv lock and commit uv.lock.
| Script | Command |
|---|---|
npm run smoke |
ts-node src/test-ner.ts |
npm run typecheck |
tsc --noEmit |
Another process is locking the venv (editor, antivirus, old Python). Close it, remove .venv, then:
uv sync --all-groupsIf hardlinks are problematic:
$env:UV_LINK_MODE = "copy"
uv sync --all-groupstsconfig.json sets ignoreDeprecations": "6.0" and explicit moduleResolution compatible with TypeScript 6.
Run pytest via uv run --group dev pytest from the repository root so pythonpath in pyproject.toml applies.
Longer-lived goals, checklist, and constraints for contributors and automation live in AGENTS.md.
See package.json (license field) for the npm package stub; set a project-wide license as needed for your organization.