How to Build a Context Graph for Agent Memory with Structured, Queryable Entities
Semantica stores agent long-term memory in a ContextGraph—an in-memory, thread-safe graph structure that organizes entities as typed nodes and relationships as weighted edges, supporting temporal validity, multi-hop traversal, and cross-graph linking.
A robust memory system for AI agents requires more than simple key-value storage; it demands a structure that captures semantic relationships, temporal bounds, and modularity. The ContextGraph class in the semantica-agi/semantica repository provides exactly this capability, implementing a queryable graph layer that agents use to retain and retrieve structured knowledge across conversation turns and tasks.
Core Architecture and Data Model
At the foundation of the memory system are two data classes defined in semantica/context/context_graph.py: ContextNode and ContextEdge. These structures store unique identifiers, semantic types, content payloads, arbitrary metadata dictionaries, and optional temporal validity windows (valid_from and valid_until ISO timestamps). The graph maintains these objects in internal dictionaries while exposing them through a high-level API.
Thread Safety and Indexing
All mutating operations—including add_node(), add_edge(), add_nodes(), and add_edges()—acquire a re-entrant lock (self._lock) to guarantee thread-safe concurrent access. For fast retrieval, ContextGraph maintains three indices: node_type_index and edge_type_index for type-based filtering, and _adjacency for storing the underlying adjacency list structure.
Optional Knowledge Graph Analytics
When initialized with advanced_analytics=True, the graph instantiates additional components from the semantica/kg/* modules. According to the source code in lines 151–159 of context_graph.py, this enables centrality analysis, community detection, and node embeddings via self.kg_components, allowing the agent to compute importance scores and semantic clusters over its memory.
Initializing the ContextGraph
Creating a memory graph requires importing the class and optionally enabling analytics suites. The constructor accepts boolean flags that determine whether the graph calculates PageRank-style centrality, detects community structures, or generates vector embeddings for nodes.
from semantica.context import ContextGraph
graph = ContextGraph(
advanced_analytics=True,
centrality_analysis=True,
community_detection=True,
node_embeddings=True,
)
Populating the Graph with Structured Entities
Bulk Insertion
For efficiency when hydrating memory from external sources, use add_nodes() and add_edges() to insert lists of dictionaries. Each dictionary must contain an id field and a type field; additional keys like content, metadata, valid_from, and weight are optional.
nodes = [
{"id": "Python", "type": "language", "content": "Python programming language"},
{"id": "Programming", "type": "concept"},
{"id": "AI", "type": "field", "valid_from": "2022-01-01"},
]
graph.add_nodes(nodes)
edges = [
{"source": "Python", "target": "Programming", "type": "related_to"},
{"source": "Python", "target": "AI", "type": "applies_to", "weight": 0.8},
]
graph.add_edges(edges)
Single Entity Helpers
For incremental updates during agent execution, use the convenience methods add_node() and add_edge(). These wrap the bulk methods internally but provide a simpler signature for interactive construction.
graph.add_node("ChatGPT", "agent", content="Large language model")
graph.add_edge("ChatGPT", "Python", "uses")
Querying Agent Memory
Neighbor Retrieval with Pagination
The get_neighbors() method performs breadth-first traversal to retrieve connected entities. It supports pagination through skip and limit parameters and respects the hops argument for controlling traversal depth.
first_neighbors = graph.get_neighbors("Python", hops=1, limit=5, skip=0)
# → [{'id': 'Programming', 'type': 'concept', ...}, ...]
Multi-Hop Weighted Traversal
For relationship-aware retrieval, get_neighbor_distances() (implemented in lines 210–289) returns neighbors with distance metadata, confidence decay calculations, and path reconstruction. You can filter by minimum confidence thresholds and specific relationship types.
neighbors = graph.get_neighbor_distances(
"Python",
hops=3,
min_confidence=0.5,
relationship_types=["related_to", "applies_to"],
)
# Each entry contains hop, distance_band, confidence_decay, path_to_anchor
Keyword Search
The query() method provides simple keyword search over node contents, returning ranked results with relevance scores. This is useful for fuzzy retrieval when the exact entity ID is unknown.
results = graph.query("language model", limit=3)
# → [{'node': {...}, 'score': 0.667, 'content': 'ChatGPT'}, ...]
Advanced Memory Features
Temporal Validity
Nodes and edges can carry time-bound metadata. The helper methods _parse_iso_dt_ and _closing_valid_until_ (lines 42–73) handle ISO string parsing, while is_active() checks the current system time against valid_from and valid_until bounds to determine if a memory entry should be considered current.
Cross-Graph Linking for Modular Memory
The link_graph() method (lines 283–327) creates a "bridge" node and edge connecting the current ContextGraph to another instance. This enables hierarchical or domain-specific memory modules. The navigate_to() method allows traversal across these bridges during query execution.
other = ContextGraph()
other.add_node("Finance", "domain")
link_id = graph.link_graph(other, source_node_id="ChatGPT", target_node_id="Finance")
# Later navigation:
other_graph, entry_node = graph.navigate_to(link_id)
Entity Resolution
When extract_entities is enabled, ContextGraph lazily instantiates an EntityLinker from semantica/context/entity_linker.py. This component resolves raw text mentions to canonical node IDs before insertion, preventing duplicate entities for the same real-world concept.
Persistence and State Management
Agent memory can survive process restarts through JSON serialization. The save_to_file() method exports the entire graph structure, indices, and metadata, while load_from_file() reconstructs the object graph including temporal validity windows and edge weights.
graph.save_to_file("/tmp/agent_memory.json")
# Later reconstruction:
new_graph = ContextGraph()
new_graph.load_from_file("/tmp/agent_memory.json")
Summary
- ContextGraph in
semantica/context/context_graph.pyprovides the core in-memory storage for agent memory, using ContextNode and ContextEdge objects with temporal validity support. - All write operations are thread-safe via a re-entrant lock (
self._lock), and fast lookups are enabled by type indices and adjacency lists. - The query API supports BFS neighborhood retrieval (
get_neighbors), weighted multi-hop traversal (get_neighbor_distances), and keyword search (query). - Advanced features include optional KG analytics (centrality, embeddings), cross-graph linking via
link_graph(), and entity resolution through EntityLinker. - Full persistence is available through
save_to_file()andload_from_file()for maintaining state across sessions.
Frequently Asked Questions
What data structure does Semantica use for agent memory?
Semantica uses the ContextGraph class, which implements an in-memory graph structure composed of ContextNode objects (representing entities, concepts, or decisions) and ContextEdge objects (representing relationships or causal links). This structure is defined in semantica/context/context_graph.py and supports typed nodes, weighted edges, and temporal validity metadata.
How does ContextGraph handle concurrent access from multiple threads?
All mutating methods—including add_node(), add_edge(), and their bulk counterparts—acquire a re-entrant lock stored in self._lock. This guarantees thread-safe concurrent reads and writes without requiring external synchronization, as implemented in the core graph file.
Can I query relationships across multiple isolated memory graphs?
Yes. The link_graph() method creates a bridge between two ContextGraph instances, producing a special node and edge pair that reference the external graph. You can then use navigate_to() to traverse into the linked graph during query execution, enabling modular memory architectures where different domains or tasks maintain separate but connected graph instances.
How do I ensure old memories expire automatically?
Assign valid_from and valid_until ISO timestamp strings when creating nodes or edges. The graph stores these values and exposes an is_active() method (which internally uses _parse_iso_dt_) to check if the current time falls within the validity window. When querying, you can filter results based on this temporal metadata to exclude expired memories.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →