Skip to content

Commit 659b683

Browse files
committed
Simplify: SERVICE pre-filter, shared row helpers, tidy walk
- _uses_service short-circuits when the query has no 'service' substring, skipping a ~1ms SPARQL parse on the common (no-federation) path — safe since SERVICE is the only federation spelling; string literals still fall through to the algebra walk. Walk rewritten as an early-return any(). - extract the JSON row shapes that cli and mcp_server both emit: propose. proposal_row() and Relation.as_row() — one wire contract for lokf propose --json / lokf vocab --json and the MCP propose_relations / get_vocabulary tools, so the two surfaces can't drift. Kept (per /simplify): FORMATS aliases (helpful spellings, listed in errors), inline server HTML (fine for one page), the query-form dispatch per surface (renders to different output types). 118 tests pass.
1 parent 8839e93 commit 659b683

5 files changed

Lines changed: 57 additions & 74 deletions

File tree

src/lokf/cli.py

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def propose(
142142
) -> None:
143143
"""Propose typed relations from markdown links in concept bodies."""
144144
from lokf.model import load_bundle
145-
from lokf.propose import apply
145+
from lokf.propose import apply, proposal_row
146146
from lokf.propose import propose as propose_relations
147147
from lokf.schema import vocabulary
148148

@@ -161,20 +161,8 @@ def propose(
161161
applied_ids = {id(p) for p in applied}
162162

163163
if json_:
164-
rows = []
165-
for p in proposals:
166-
row = {
167-
"source": p.source.concept_id,
168-
"link_text": p.link.text,
169-
"target": p.target_iri,
170-
"predicate": p.relation.name,
171-
"curie": p.relation.curie,
172-
"confidence": round(p.confidence, 2),
173-
"rationale": p.rationale,
174-
}
175-
if apply_:
176-
row["applied"] = id(p) in applied_ids
177-
rows.append(row)
164+
ids = applied_ids if apply_ else None
165+
rows = [proposal_row(p, ids) for p in proposals]
178166
typer.echo(json.dumps(rows, indent=2))
179167
return
180168

@@ -220,21 +208,7 @@ def vocab(
220208
v = vocabulary()
221209
relations = sorted(v.relation_types.values(), key=lambda r: r.name)
222210
if json_:
223-
typer.echo(
224-
json.dumps(
225-
[
226-
{
227-
"name": r.name,
228-
"curie": r.curie,
229-
"uri": r.uri,
230-
"frontmatter_key": r.is_slot,
231-
"description": r.description,
232-
}
233-
for r in relations
234-
],
235-
indent=2,
236-
)
237-
)
211+
typer.echo(json.dumps([r.as_row() for r in relations], indent=2))
238212
return
239213
name_w = max(len(r.name) for r in relations)
240214
curie_w = max(len(r.curie) for r in relations)

src/lokf/mcp_server.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def propose_relations(
129129
"""
130130
from lokf.model import load_bundle
131131
from lokf.propose import apply as apply_proposals
132-
from lokf.propose import propose
132+
from lokf.propose import propose, proposal_row
133133
from lokf.schema import vocabulary
134134

135135
b = load_bundle(bundle)
@@ -138,26 +138,12 @@ def propose_relations(
138138
for p in propose(b, vocabulary())
139139
if p.confidence >= min_confidence
140140
]
141-
applied_ids: set[int] = set()
141+
applied_ids: set[int] | None = None
142142
if apply:
143143
applied_ids = {
144144
id(p) for p in apply_proposals(proposals, min_confidence=min_confidence)
145145
}
146-
rows = []
147-
for p in proposals:
148-
row = {
149-
"source": p.source.concept_id,
150-
"link_text": p.link.text,
151-
"target": p.target_iri,
152-
"predicate": p.relation.name,
153-
"curie": p.relation.curie,
154-
"confidence": round(p.confidence, 2),
155-
"rationale": p.rationale,
156-
}
157-
if apply:
158-
row["applied"] = id(p) in applied_ids
159-
rows.append(row)
160-
return rows
146+
return [proposal_row(p, applied_ids) for p in proposals]
161147

162148

163149
@server.tool()
@@ -175,16 +161,7 @@ def get_vocabulary() -> dict:
175161
relations = sorted(v.relation_types.values(), key=lambda r: r.name)
176162
return {
177163
"classes": dict(v.classes),
178-
"relations": [
179-
{
180-
"name": r.name,
181-
"curie": r.curie,
182-
"uri": r.uri,
183-
"frontmatter_key": r.is_slot,
184-
"description": r.description,
185-
}
186-
for r in relations
187-
],
164+
"relations": [r.as_row() for r in relations],
188165
}
189166

190167

src/lokf/propose.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,27 @@ def propose(
255255
return proposals
256256

257257

258+
def proposal_row(proposal: Proposal, applied_ids: set[int] | None = None) -> dict:
259+
"""A JSON-serializable row for a proposal.
260+
261+
Shared wire shape for ``lokf propose --json`` and the MCP
262+
``propose_relations`` tool. When *applied_ids* is given (apply mode), the
263+
row gains an ``applied`` flag; in dry-run mode it is omitted.
264+
"""
265+
row = {
266+
"source": proposal.source.concept_id,
267+
"link_text": proposal.link.text,
268+
"target": proposal.target_iri,
269+
"predicate": proposal.relation.name,
270+
"curie": proposal.relation.curie,
271+
"confidence": round(proposal.confidence, 2),
272+
"rationale": proposal.rationale,
273+
}
274+
if applied_ids is not None:
275+
row["applied"] = id(proposal) in applied_ids
276+
return row
277+
278+
258279
def _rt_yaml():
259280
from ruamel.yaml import YAML
260281

src/lokf/schema.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ class Relation:
8888
is_slot: bool # True: usable as a frontmatter key; False: via `relations:`
8989
domains: frozenset[str] = frozenset() # classes declaring the slot; "Concept" = all
9090

91+
def as_row(self) -> dict:
92+
"""A JSON-serializable row (the ``lokf vocab --json`` / MCP shape)."""
93+
return {
94+
"name": self.name,
95+
"curie": self.curie,
96+
"uri": self.uri,
97+
"frontmatter_key": self.is_slot,
98+
"description": self.description,
99+
}
100+
91101

92102
class Vocabulary:
93103
"""The LOKF vocabulary derived from the LinkML schema."""

src/lokf/store.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,30 +55,31 @@ def _uses_service(sparql: str) -> bool:
5555
not trip it. If rdflib cannot parse the query, returns False and lets the
5656
engine reject it — this only gates the federation vector, not validity.
5757
"""
58+
# SERVICE is the only spelling of the federation keyword, so a query with
59+
# no "service" substring cannot contain one — skip the ~1ms parse. (A
60+
# "service" inside a string literal still falls through to the algebra
61+
# walk, which correctly ignores it.)
62+
if "service" not in sparql.lower():
63+
return False
64+
5865
from rdflib.plugins.sparql import prepareQuery
5966
from rdflib.plugins.sparql.parserutils import CompValue
6067

6168
try:
6269
algebra = prepareQuery(sparql).algebra
6370
except Exception: # noqa: BLE001 — parse failures fall through to the engine
6471
return False
65-
found = []
66-
67-
def walk(node):
68-
if isinstance(node, CompValue):
69-
if node.name == "ServiceGraphPattern":
70-
found.append(True)
71-
for v in node.values():
72-
walk(v)
73-
elif isinstance(node, (list, tuple, set)):
74-
for v in node:
75-
walk(v)
76-
elif isinstance(node, dict):
77-
for v in node.values():
78-
walk(v)
79-
80-
walk(algebra)
81-
return bool(found)
72+
73+
def walk(node) -> bool:
74+
if isinstance(node, CompValue): # note: CompValue is itself a dict
75+
return node.name == "ServiceGraphPattern" or any(map(walk, node.values()))
76+
if isinstance(node, (list, tuple, set)):
77+
return any(map(walk, node))
78+
if isinstance(node, dict):
79+
return any(map(walk, node.values()))
80+
return False
81+
82+
return walk(algebra)
8283

8384

8485
class GraphStore:

0 commit comments

Comments
 (0)