Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

decide-area-classification-service

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.

How it works

For every input resource of the task:

  1. Resolve the eli:Work behind it (input containers hold eli:Expression URIs; the NEL annotations hang off the work that the expression realizes).
  2. Take the work's oa:linking location annotations above CONFIDENCE_THRESHOLD, and read the point geometry off the linked location.
  3. Intersect that point with every wijk polygon.
  4. Write an annotation per match, plus the provenance the annotation-review frontend filters on.

The task contract

Same contract as every other task service in lblod/app-decide:

  • POST /delta receives a delta-notifier v0.0.1 changeset. The service picks out inserts of adms: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 not OPERATION_URI is 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, its task:inputContainer / task:hasResource resources are read, the work is done, an nfo:DataContainer results container is attached via task:resultsContainer, and the task is set success.
  • On failure the task is set failed and an oslc:Error is attached via task: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.

Other endpoints

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.

Output

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> .

Idempotency

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 geo query

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 #.

Four non-obvious constraints

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:

  1. rdf:predicate is BOTH prov:atLocation AND dct:spatial. Pinning only prov:atLocation silently drops half the decisions.
  2. Stored points are POINT(lat lon), but bif:st_transform returns (lon lat). Hence the bif:st_point(bif:st_y(?raw), bif:st_x(?raw)) swap.
  3. 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 with bif:subseq(str(?gwkt), 46).
  4. bif: functions are Virtuoso-only. The sparql-parser in front of mu-auth will not pass them, so this query goes to GEO_SPARQL_ENDPOINT (http://virtuoso:8890/sparql) directly, while every write goes through mu-auth with a mu-auth-sudo: true header 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.

Configuration

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.

docker-compose

  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-logging

Add 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"
}

A note on AI_GRAPH

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages