Classifies decisions into Stad Gent wijken (city districts) by point-in-polygon, using the location annotations that the NER/NEL pipeline has already produced.
It is a standard mu.semte.ch task service: it listens for
scheduled tasks on /delta, and for the one operation it owns it writes one
oa:classifying annotation per (work, wijk) pair.
For every input resource of the task:
- Resolve the
eli:Workbehind it (input containers holdeli:ExpressionURIs; the NEL annotations hang off the work that the expression realizes). - Take the work's
oa:linkinglocation annotations aboveCONFIDENCE_THRESHOLD, and read the point geometry off the linked location. - Intersect that point with every wijk polygon.
- Write an annotation per match, plus the provenance the annotation-review frontend filters on.
Same contract as every other task service in lblod/app-decide:
POST /deltareceives a delta-notifierv0.0.1changeset. The service picks out inserts ofadms:status→http://redpencil.data.gift/id/concept/JobStatus/scheduled.- For each such subject it resolves
task:operation(http://redpencil.data.gift/vocabularies/tasks/operation). Anything that is notOPERATION_URIis ignored and left untouched — every task service on the stack sees every scheduled task, so this is the normal case, not an error. - The task is set
busy, itstask:inputContainer/task:hasResourceresources are read, the work is done, annfo:DataContainerresults container is attached viatask:resultsContainer, and the task is setsuccess. - On failure the task is set
failedand anoslc:Erroris attached viatask:error.
A results container is written even when nothing was classified: the job controller keys the next task off its existence, not off its contents.
| Endpoint | Purpose |
|---|---|
GET / |
Liveness. |
POST /delta |
Delta-notifier callback. |
POST /find-and-start-unfinished-tasks |
Re-scan for busy/scheduled tasks of this operation and run them. Also runs automatically at startup. |
Task processing is serialised behind a mutex, so the startup sweep and an incoming delta cannot pick up the same task concurrently.
Per (work, wijk) pair, into AI_GRAPH:
<http://data.lblod.info/id/annotations/{uuid}>
a oa:Annotation ;
mu:uuid "{uuid}" ;
oa:motivatedBy oa:classifying ;
oa:hasTarget <...work> ;
oa:hasBody <https://stad.gent/id/administrative-area/wijk-N> ;
dct:created "..."^^xsd:dateTime .Plus the provenance that config/annotation-review/config.ts filters on. As in
the other DECIDE AI services, the prov:Activity is the task itself:
<...task> a prov:Activity ;
prov:generated <http://data.lblod.info/id/annotations/{uuid}> ;
prov:wasAssociatedWith <...agent> .
# in AGENT_GRAPH, created once on first boot
<...agent> a foaf:Agent, tcs:InstancePipelineComponent ;
mu:uuid "{uuid}" ;
prov:specializationOf <http://lblod.data.gift/id/components/area-classification/v1.0.0> .A work that already carries an oa:classifying annotation from this component
is skipped. Idempotency is tracked per work, not per pair — which is safe
because all pairs of one work are written in a single INSERT DATA, so a work
is never left half-classified.
The query lives in config/geo-query.sparql and is mounted, not compiled
in. The CRS, the literal encoding and the axis order are Gent-specific; the next
city ships its own copy of this file rather than patching the service.
It must SELECT exactly ?work and ?wijk, and the service substitutes four
placeholders before executing:
| Placeholder | Replaced with |
|---|---|
{{RESOURCE_VALUES}} |
The task's input resources, as newline-separated <uri> terms for a VALUES block. |
{{ANNOTATION_GRAPH}} |
AI_GRAPH |
{{WIJK_GRAPH}} |
WIJK_GRAPH |
{{CONFIDENCE_THRESHOLD}} |
CONFIDENCE_THRESHOLD |
Substitution is textual, so do not mention the placeholder tokens in the
file's own comments — they would be substituted too, and a multi-line
VALUES block pasted into a # comment breaks the comment across lines and
corrupts the query.
A leading comment block at the top of the file is fine: the service strips it
before sending, because sparql-client-2 decides read-vs-update by matching the
query's first token and rejects anything starting with #.
All four are confirmed empirically against Virtuoso 07.20.3241 with the proj4
and geos plugins loaded, and are commented in the query file itself. Preserve
them:
rdf:predicateis BOTHprov:atLocationANDdct:spatial. Pinning onlyprov:atLocationsilently drops half the decisions.- Stored points are
POINT(lat lon), butbif:st_transformreturns(lon lat). Hence thebif:st_point(bif:st_y(?raw), bif:st_x(?raw))swap. - The wijk WKT literal is prefixed with the 46-character string
"<https://www.opengis.net/def/crs/EPSG/0/31370> "(45 chars + one space), which must be stripped withbif:subseq(str(?gwkt), 46). bif:functions are Virtuoso-only. The sparql-parser in front of mu-auth will not pass them, so this query goes toGEO_SPARQL_ENDPOINT(http://virtuoso:8890/sparql) directly, while every write goes through mu-auth with amu-auth-sudo: trueheader so that deltas still fire.
The query is scoped to the task's input resources, not run store-wide. An empty
input short-circuits before the query is built: an empty SPARQL VALUES block
matches everything, which would silently widen it to the whole store.
eli:is_realized_by is spread over several graphs (public/gent holds the
bulk, public/pdf the rest), so the resource→work join is deliberately not
graph-pinned.
| Variable | Default | Description |
|---|---|---|
OPERATION_URI |
http://lblod.data.gift/id/jobs/concept/TaskOperation/area-classifying |
The one task operation this service answers to. |
AI_GRAPH |
http://mu.semte.ch/graphs/public/pdf |
Where the NEL annotations are read from and where this service writes its annotations and provenance. |
WIJK_GRAPH |
http://mu.semte.ch/graphs/public |
Where the wijk polygons live. |
GEO_SPARQL_ENDPOINT |
http://virtuoso:8890/sparql |
Virtuoso, bypassing mu-auth, for the bif: query only. |
CONFIDENCE_THRESHOLD |
0.8 |
Minimum nif:confidence on the NEL location annotation. |
GEO_QUERY_PATH |
/config/geo-query.sparql |
The mounted query template. |
COMPONENT_URI |
http://lblod.data.gift/id/components/area-classification/v1.0.0 |
What the prov agent specializes. Changing it hides the output from the review UI. |
AGENT_GRAPH |
http://mu.semte.ch/graphs/harvesting |
Where the prov agent is registered, matching the other DECIDE AI services. |
area-classification:
image: redpencil/decide-area-classification-service:0.1.0
environment:
OPERATION_URI: 'http://lblod.data.gift/id/jobs/concept/TaskOperation/area-classifying'
AI_GRAPH: 'http://mu.semte.ch/graphs/public/pdf'
WIJK_GRAPH: 'http://mu.semte.ch/graphs/public'
GEO_SPARQL_ENDPOINT: 'http://virtuoso:8890/sparql'
CONFIDENCE_THRESHOLD: '0.8'
volumes:
# The CRS, the 46-char prefix and the axis swap are Gent-specific.
- ./config/area-classification/geo-query.sparql:/config/geo-query.sparql:ro
depends_on:
- virtuoso
- database
labels:
- "logging=true"
restart: always
logging: *default-loggingAdd the delta rule (config/delta/area-classification.js, then spread it into
config/delta/rules.js):
export default [
{
match: {
predicate: {
type: 'uri',
value: 'http://www.w3.org/ns/adms#status',
},
object: {
type: 'uri',
value: 'http://redpencil.data.gift/id/concept/JobStatus/scheduled',
},
},
callback: {
method: 'POST',
url: 'http://area-classification/delta',
},
options: {
resourceFormat: 'v0.0.1',
gracePeriod: 1000,
ignoreFromSelf: true,
sendMatchesOnly: true,
},
},
];And chain the operation in config/job-controller/config.json, after
named-entity-linking:
{
"currentOperation": "http://lblod.data.gift/id/jobs/concept/TaskOperation/named-entity-linking",
"nextOperation": "http://lblod.data.gift/id/jobs/concept/TaskOperation/area-classifying",
"nextIndex": "8"
}The default (http://mu.semte.ch/graphs/public/pdf) is where the existing NEL
annotations live, so reads and writes stay together and the review UI resolves
them as it does today.
Worth knowing, though: app-decide's authorization config already defines a
graph that dispatches exactly the two classes this service emits, and grants it
public read —
(define-graph public-areas ("http://mu.semte.ch/graphs/public/areas")
("oa:Annotation" -> _)
("prov:Activity" -> _))whereas the harvested-pdf spec for public/pdf lists neither oa:Annotation
nor prov:Activity. If that graph was added with this service in mind, pointing
AI_GRAPH at it is a one-line compose change — but note it is used for reads as
well, so the NEL annotations would have to live there too.