LYRA Knowledge Graph Integration¶
Duration: 90 minutes
Level: Advanced
Module: fcc.knowledge.lyra_bridge, fcc.api.knowledge
Bridge: v1.5.0 stable promotion
This tutorial teaches you how to integrate with LYRA — the graph-of-thought
retrieval service in the FCC ecosystem — through the stable fcc.api.knowledge
surface. You will learn the three-method protocol (graph_of_thought_query,
vocab_lookup, harmonic_expand), how the functional mock uses SKOS-proxy
edges over a seed KnowledgeGraph, how to supply your own KG to the mock for
testing, and how GraphRAG consumes LYRA for augmentation.
What LYRA is (and is not)¶
LYRA (Linked Yields for Reasoning + Argumentation) is the graph-of-thought navigation service in the FCC ecosystem. Where POLARIS preserves artifacts, LYRA navigates them — given a starting concept, LYRA returns reasoned paths through neighboring nodes with argumentation traces explaining why each hop was chosen.
The lyra.api package is not yet published; lyra_available() returns
False in every current environment. The sentinel is wired in advance so the
real adapter can slot in without any import-site churn. Until then,
get_lyra_bridge() returns a functional LyraMockBridge backed by a minimal
KnowledgeGraph seed. That means your code works today against the mock,
and will seamlessly upgrade to live LYRA when it publishes.
LYRA is a read service. It does not mutate the knowledge graph; it
traverses one. Mutations live in your ordinary KnowledgeGraph and
FederatedKnowledgeGraph APIs.
The stable surface — fcc.api.knowledge¶
v1.5.0 promoted the LYRA bridge from fcc.plugins.v1_5_preview into the
canonical fcc.api.knowledge surface. Public symbols carry the same 1.x
SemVer backward-compatibility guarantee as the rest of fcc.api.*.
# Preferred — stable top-level API
from fcc.api import knowledge
# Equivalent — direct module import
from fcc.knowledge.lyra_bridge import (
get_lyra_bridge,
LyraBridgeProtocol,
LyraMockBridge,
NoOpLyraBridge,
lyra_available,
)
The preview path fcc.plugins.v1_5_preview.lyra_bridge still works through
v1.5.x with a DeprecationWarning emitted on import. The preview path is
removed in v1.6.0.
The three-method protocol¶
@runtime_checkable
class LyraBridgeProtocol(Protocol):
def graph_of_thought_query(self, query: str, depth: int = 3) -> dict: ...
def vocab_lookup(self, term: str, namespace: str = "") -> list[str]: ...
def harmonic_expand(self, concept: str) -> list[str]: ...
Three methods. The production adapter, the functional mock, and the no-op fallback all implement exactly this surface.
graph_of_thought_query — the BFS walker¶
graph_of_thought_query(query, depth) walks the backing graph BFS from every
node matching query up to depth hops. It returns a dict with four keys:
{
"query": str,
"depth": int,
"nodes": list[dict], # visited node descriptors
"edges": list[dict], # traversed edges
"paths": list[list], # [source_id, target_id] pairs
}
This is the exact shape GraphRAG consumes in its LYRA-augmentation stage.
from fcc.api import knowledge
bridge = knowledge.get_lyra_bridge()
result = bridge.graph_of_thought_query("persona", depth=2)
print(f"visited {len(result['nodes'])} nodes")
print(f"traversed {len(result['edges'])} edges")
for edge in result["edges"][:5]:
print(f" {edge['source_id']} --{edge['edge_type']}--> {edge['target_id']}")
vocab_lookup — term resolution¶
vocab_lookup(term, namespace) returns node IDs whose id or label contains
term (case-insensitive). The optional namespace argument filters to that
namespace only.
from fcc.api import knowledge
bridge = knowledge.get_lyra_bridge()
# Search globally
ids = bridge.vocab_lookup("persona")
print(f"global matches: {ids}")
# Search within the seed namespace only
ids_seed = bridge.vocab_lookup("persona", namespace="lyra-seed")
print(f"seed namespace matches: {ids_seed}")
Use this to resolve user-supplied strings into canonical graph ids before handing them to a downstream consumer like GraphRAG or a KG query.
harmonic_expand — SKOS-proxy expansion¶
harmonic_expand(concept) returns related concept IDs linked to the input
via SKOS-proxy edges. The mock implementation maps the SKOS hierarchy onto
the FCC EdgeType enum:
| SKOS role | FCC EdgeType proxy |
|---|---|
| broader | DEPENDS_ON |
| related | MAPS_TO |
| narrower | INTERACTS_WITH |
These mappings live in the mock bridge as module constants — when the real
LYRA publishes, it will use native SKOS.broader/narrower/related but the
method signature is the same.
from fcc.api import knowledge
bridge = knowledge.get_lyra_bridge()
related = bridge.harmonic_expand("persona")
print(f"related concepts: {related}")
# ['riscear', 'discernment', 'fcc'] — per the seed graph
If no matching concept exists, the method returns [] (graceful degradation,
never raises).
The seed graph¶
LyraMockBridge() with no arguments builds a minimal seed KnowledgeGraph
with seven concepts and six edges:
persona --depends_on--> riscear (broader: persona is a subtype of R.I.S.C.E.A.R.)
persona --maps_to--> discernment (related)
discernment --interacts_with--> curiosity (narrower)
discernment --interacts_with--> humility (narrower)
persona --depends_on--> fcc
workflow --maps_to--> fcc
This seed is sufficient to exercise every method of the protocol. For realistic demos, pass your own KG:
from fcc.knowledge.lyra_bridge import LyraMockBridge
from fcc.knowledge.builders import build_full_fcc_graph
full_graph = build_full_fcc_graph()
bridge = LyraMockBridge(graph=full_graph)
result = bridge.graph_of_thought_query("research crafter", depth=2)
print(f"walked {len(result['nodes'])} nodes of the full FCC KG")
Mock vs real — the factory¶
get_lyra_bridge() returns the live adapter when lyra.api is importable
and the functional LyraMockBridge otherwise.
from fcc.api.knowledge import get_lyra_bridge, lyra_available
print(f"LYRA installed? {lyra_available()}") # False until lyra publishes
bridge = get_lyra_bridge() # works regardless
print(f"bridge: {type(bridge).__name__}") # LyraMockBridge today
For tests that want explicit control, construct the mock directly with a specific graph:
from fcc.knowledge.lyra_bridge import LyraMockBridge
from fcc.knowledge.graph import KnowledgeGraph
from fcc.knowledge.models import EdgeType, KnowledgeEdge, KnowledgeNode, NodeType
g = KnowledgeGraph()
g.add_node(KnowledgeNode(
node_id="a", node_type=NodeType.CONCEPT, label="Alpha", namespace="test",
))
g.add_node(KnowledgeNode(
node_id="b", node_type=NodeType.CONCEPT, label="Beta", namespace="test",
))
g.add_edge(KnowledgeEdge(source_id="a", target_id="b", edge_type=EdgeType.DEPENDS_ON))
bridge = LyraMockBridge(graph=g)
assert bridge.vocab_lookup("alpha", namespace="test") == ["a"]
assert "b" in bridge.harmonic_expand("alpha")
The NoOpLyraBridge is the empty-behaviour fallback for verifying that your
code tolerates no-op responses — use it only when you specifically need the
non-functional stand-in.
GraphRAG augmentation — how it integrates¶
GraphRAG's fifth stage calls lyra_bridge.graph_of_thought_query(text,
depth=max_hops) and folds the returned nodes, edges, and paths into its
result. The LYRA call happens even when you do not explicitly pass a bridge —
GraphRAG resolves the default bridge lazily on the first query.
from fcc.knowledge.builders import build_full_fcc_graph
from fcc.rag.graphrag import GraphRAG
from fcc.knowledge.lyra_bridge import LyraMockBridge
# Without explicit bridge — lazy default
pipe = GraphRAG(knowledge_graph=build_full_fcc_graph())
# With explicit bridge (e.g. custom-seeded for deterministic tests)
seeded = LyraMockBridge(graph=build_full_fcc_graph())
pipe_explicit = GraphRAG(
knowledge_graph=build_full_fcc_graph(),
lyra_bridge=seeded,
)
LYRA-sourced nodes enter the result at a baseline score of 0.25 — below
every Zachman-filter survivor and persona-boosted node, but above the
defensive "no info" floor. This preserves the invariant that KG-sourced
answers outrank external augmentation when the KG has relevant information.
To disable LYRA augmentation entirely (useful for A/B comparison):
class NullBridge:
def graph_of_thought_query(self, q, depth=3):
return {} # empty -> extra_nodes empty -> no augmentation
def vocab_lookup(self, t, ns=""): return []
def harmonic_expand(self, c): return []
pipe_no_lyra = GraphRAG(
knowledge_graph=build_full_fcc_graph(),
lyra_bridge=NullBridge(),
)
See the GraphRAG deep dive for the full GraphRAG pipeline and how augmentation composes with the other stages.
A realistic cross-namespace walk¶
A GraphRAG query typically surfaces persona and workflow nodes from the local FCC KG. LYRA adds cross-namespace concepts from linked vocabularies. The mock exercises the same path locally:
from fcc.knowledge.lyra_bridge import LyraMockBridge
from fcc.knowledge.builders import build_full_fcc_graph
from fcc.knowledge.graph import KnowledgeGraph
from fcc.knowledge.models import EdgeType, KnowledgeEdge, KnowledgeNode, NodeType
# 1. Start with the full FCC KG
fcc_graph = build_full_fcc_graph()
# 2. Add a few "external" concepts that LYRA might surface
external = KnowledgeGraph()
for node in fcc_graph.all_nodes():
external.add_node(node)
for edge in fcc_graph.all_edges():
external.add_edge(edge)
# External namespace concepts
external.add_node(KnowledgeNode(
node_id="ext-governance",
node_type=NodeType.CONCEPT,
label="Enterprise Governance",
namespace="ext-vocab",
))
external.add_node(KnowledgeNode(
node_id="ext-audit",
node_type=NodeType.CONCEPT,
label="Third-Party Audit",
namespace="ext-vocab",
))
external.add_edge(KnowledgeEdge(
source_id="ext-governance",
target_id="ext-audit",
edge_type=EdgeType.MAPS_TO,
))
# 3. LYRA walks the combined graph
bridge = LyraMockBridge(graph=external)
result = bridge.graph_of_thought_query("governance", depth=2)
for node in result["nodes"]:
print(f" [{node['namespace']}] {node['node_id']}: {node['label']}")
When live LYRA lands, this same call routes to the real lyra.api.LyraAdapter
and surfaces concepts from every federated namespace LYRA has indexed.
Combining with bidirectional federation¶
LYRA reads from a graph; BidirectionalSync writes to one. A typical pattern
wires them together: federation pushes vocabulary changes across namespaces,
and LYRA surfaces the newly-shared terms via vocab_lookup and
harmonic_expand.
# Pseudocode — actual wiring depends on your FederationRegistry setup
from fcc.api import knowledge
from fcc.federation.bidirectional import BidirectionalSync
def on_term_added(change_event, lyra_bridge, sync):
# Let bidirectional sync propagate the new term
sync.tracker_for(change_event.namespace).track_change(change_event.model_change)
sync.sync_to(target_namespace="partner",
source_namespace=change_event.namespace)
# Immediately verify LYRA can see the term (via its vocab)
hits = lyra_bridge.vocab_lookup(change_event.term)
assert change_event.entity_id in hits
See the bidirectional federation patterns tutorial for the write path and the GraphRAG deep dive for the read path.
Graceful degradation¶
Every LYRA bridge method must be tolerant of:
- Empty inputs.
graph_of_thought_query("")returns a result with emptynodes,edges,paths.vocab_lookup("")returns[]. - Unknown terms.
vocab_lookup("does-not-exist")returns[]. - No matching concept.
harmonic_expand("does-not-exist")returns[]. - Missing bridge. When LYRA is not installed,
get_lyra_bridge()returnsLyraMockBridgerather than raising.
GraphRAG depends on this graceful degradation — it wraps the LYRA call in a
try/except and degrades to a KG-only result on any exception. Your own code
should do the same.
Troubleshooting¶
harmonic_expand returns empty on a concept that exists. The concept
node is probably not connected via a SKOS-proxy edge. Inspect the graph's
edges for DEPENDS_ON, MAPS_TO, and INTERACTS_WITH from the concept.
If the seed graph has no such edges, the method correctly returns [].
graph_of_thought_query returns zero nodes. Either the query matches no
node labels (_nodes_matching is case-insensitive substring — verify the
term is actually present), or the matched nodes are isolated and depth=0
returns only the seeds. Try depth=3 to walk further.
LYRA results appear in GraphRAG but not in the KG. That is correct.
LYRA-augmented nodes come from the bridge's backing graph (which may differ
from GraphRAG's primary KG). Use result.nodes to get the full merged set.
Tests break when lyra.api is installed. The factory switches to the
live adapter. Pin tests to the mock by constructing it explicitly:
LyraMockBridge(graph=my_test_graph) instead of calling get_lyra_bridge().
Warnings from NoOpLyraBridge. Someone instantiated NoOpLyraBridge
explicitly (which emits UserWarning on every call). Switch to
LyraMockBridge if you want a functional mock; keep NoOpLyraBridge only
for the narrow case of verifying degraded-mode behavior.
Summary¶
In this tutorial you learned how to:
- Use the stable
fcc.api.knowledgesurface for LYRA integration - Call the three protocol methods:
graph_of_thought_query,vocab_lookup,harmonic_expand - Understand the SKOS-proxy edge mapping used by
LyraMockBridge - Seed the mock with a custom
KnowledgeGraphfor realistic tests - Compose LYRA with
GraphRAGfor graph-of-thought augmentation - Combine LYRA with
BidirectionalSyncfor read + write ecosystem patterns - Handle graceful degradation so downstream code never crashes on empty inputs
Next steps¶
- Build a
FederatedKnowledgeGraphthat spans FCC + partner namespaces, then seedLyraMockBridgewith it to preview what live LYRA will return - Write integration tests that assert
lyra_available() is Truein environments where you expect the live bridge — catches accidental mock-in-production - Watch for the
lyra.apipackage publication — once it lands, no code changes are required on the FCC side
Related tutorials¶
- GraphRAG Deep Dive — how GraphRAG uses LYRA
- Knowledge Graphs — the KG substrate
- POLARIS Archive Integration — the partner bridge
- Bidirectional Federation Patterns — the write path
- Federation — cross-project namespace plumbing
Related references¶
- Codename decoder —
docs/ecosystem/codename-decoder.md - LYRA bridge chapter stub —
publications/chapters/integ/lyra-bridge.qmd - Source —
src/fcc/knowledge/lyra_bridge.py,src/fcc/plugins/v1_5_preview/lyra_bridge.py - Class diagram —
docs/architecture/class-diagrams/lyra-bridge.md - GraphRAG integration —
src/fcc/rag/graphrag.py(stages 5–6)