ContextGraph is a thread-safe, in-memory property graph with temporal validity windows on every node and edge, built-in Breadth-First Search (BFS) traversal, a FAISS vector index for semantic search, and proximity-blended retrieval through AgentContext. Use it when multiple agents or threads write to a shared knowledge base while analysts query it in real time.

What Is a Context Graph?

A context graph is a property graph that stores entities as nodes and relationships as edges, enriched with metadata and temporal validity. Nodes represent entities in your domain — threat actors, vulnerabilities, companies, people, or any concept you want to track. Each node has an ID, a type, optional content text, and metadata properties. Edges represent relationships between entities — “APT29 uses SUNBURST”, “Alice works_for Acme Corp”, or “CVE-2024-3400 affects SolarWinds”. Each edge has a type, weight, and optional metadata. Metadata stores additional properties on nodes and edges as key-value pairs — geographic origin, confidence scores, timestamps, or any domain-specific attributes. Property graphs like ContextGraph differ from simple networks by supporting rich metadata on both nodes and edges, making them suitable for complex real-world domains where relationships need context and attributes.

Why Use a Context Graph?

Relationship analysis. Graph structure reveals how entities connect — who targets whom, what exploits what, which decisions led to which outcomes. Relationships that aren’t obvious from individual documents become clear when connected. Multi-hop reasoning. Answer questions like “what can APT29 reach within 3 steps?” or “which vulnerabilities affect our critical systems?” by traversing the graph rather than keyword matching. Context preservation. Unlike vector search alone, graphs preserve the relationships between entities. When you find a relevant threat actor, you can immediately see their tools, targets, and infrastructure. Temporal state tracking. Track when relationships were valid, when infrastructure was active, or when decisions were made. Query historical states or filter to current information only.

When To Use / When Not To Use

Use context graphs for:
  • Complex domains with rich relationships between entities
  • Multi-hop reasoning and traversal queries
  • Temporal tracking of entity relationships and states
  • Integration with structured data that has clear entity relationships
  • Collaborative environments where multiple sources contribute connected data
Simple vector search may be sufficient for:
  • Pure document retrieval where relationships don’t matter
  • Single-hop similarity searches
  • Exploratory research where structure isn’t well-defined
  • Read-only analysis of unstructured text without entity relationships
Graphs may be unnecessary for:
  • Simple keyword or semantic search tasks
  • Static document collections without evolving relationships
  • Single-user, short-term analysis projects
  • Cases where setup complexity exceeds the relationship complexity
ContextGraph is an in-memory data structure. All nodes, edges, and metadata are stored in Python dictionaries and lists. For standalone graphs, persist state with save_to_file(). When using AgentContext, call AgentContext.save() instead — it saves the graph, the FAISS vector index, and memory in one step. For analytical operations on top of a populated graph — centrality rankings, community detection, node embeddings, link prediction — see the Graph Analytics guide. For recording and querying decisions stored as nodes, see the Decision Intelligence guide.

Constructing the Graph

You can construct a ContextGraph in two ways: Manual construction — Add nodes and edges programmatically using add_node() and add_edge(). This gives you complete control over the graph structure and is ideal when you have structured data or want to build specific relationship patterns. Automated extraction workflows — Pass a list of documents to AgentContext.store() with extract_entities=True or extract_relationships=True. This requires knowledge_graph= to be set in the AgentContext constructor; the extraction process then creates nodes for detected entities and edges for discovered relationships. Single strings passed to store() are stored as memory items only and do not trigger graph construction. The simplest possible graph needs no arguments:
For a threat intelligence workload that will also run analytics, enable the sub-components at construction time — they initialize lazily but must be declared upfront:
The graph is backed entirely by Python dicts and a re-entrant lock (threading.RLock). No external service, no database connection, no network call. You can stand up a fully functional intelligence graph in a unit test with a single import.

Adding Your First Entities

Every entity goes in as a node with a type, optional content string, and any number of metadata kwargs:
There is no properties={} parameter. Pass all metadata fields as direct keyword arguments. Calling add_node("x", "t", properties={"k": "v"}) would store the dict under a key literally named properties in metadata — not what you want.
Now connect them with typed, weighted edges:
Check what you have:

Temporal Validity — Intel Has an Expiry Date

Use valid_from and valid_until to mark nodes and edges with activity windows so temporal queries exclude stale data:
Now ask: which nodes were active on December 1, 2020 (during the campaign)?
This is how you prevent a query today from returning “APT29 currently operates 45.142.212.100” — the edge is outside its validity window and won’t appear in temporal queries.

Finding Nodes

find_node() retrieves by ID, and find_nodes() filters by type or metadata:

Traversing the Graph

BFS traversal answers reachability questions directly:
Filter to only follow specific edge types — useful when you want to trace just the exploitation chain without noise from other relationship types:
When you need to understand how confident a connection is based on graph distance, enable distance metadata. Each result gains a confidence_decay multiplier — nodes further away are weighted down:
To trace the route from a starting node to a specific target, use get_neighbors() with include_distance_metadata=True. Each result includes a path_to_anchor list showing the exact sequence of node IDs from source to that neighbor:

Handling Concurrent Writes

ContextGraph handles concurrent writes with a re-entrant lock (threading.RLock) that wraps every mutation — you do not need to add your own synchronization:
The lock is re-entrant, so internal calls that themselves acquire the lock (for example, add_edge() calling find_node() internally) won’t deadlock.

Semantic Search via AgentContext

AgentContext wraps the graph with a FAISS vector index and lets you retrieve by semantic similarity, with optional blending of graph proximity:
proximity_weight is a per-call parameter on retrieve(), not a constructor setting. This means different queries can use different blending ratios on the same context object — a broad semantic search uses proximity_weight=0.0, while a neighborhood-focused traversal uses proximity_weight=0.5.

Cross-Graph Navigation

link_graph() connects two separate graphs, and cross_graph_path() finds paths that span the boundary:

Serialization and Persistence

After each ingest cycle, save the graph to disk. On restart, restore it — the entire node and edge set is preserved:
If the graph had cross-graph links created with link_graph(), call resolve_links() after loading to restore live navigation — object references cannot be serialized, so they must be reconnected manually:
For full session persistence (graph + FAISS vector index + memory), use AgentContext.save() / AgentContext.load():

Common Pitfalls

Duplicate entities. Adding “APT-29”, “APT29”, and “Cozy Bear” as separate nodes fragments the graph when they should be one entity. Use consistent naming conventions upfront, or use detect_duplicates() and EntityMerger from the Deduplication guide to merge them after ingestion. Inconsistent naming conventions. Mixing “ThreatActor”, “threat_actor”, and “Threat-Actor” as node types breaks queries that filter by type. Pick one convention and enforce it across all data sources. Over-connecting nodes. Creating edges between every entity mentioned in the same document adds noise. Focus on meaningful relationships — direct causation, membership, or functional dependencies rather than co-occurrence. Storing unnecessary information. Adding every field from source data as metadata bloats memory usage. Include only properties needed for queries, filtering, or downstream analysis. Failing to persist important graph state. Since ContextGraph is in-memory, shutting down your application loses all nodes and edges unless you call save_to_file() or AgentContext.save(). Persist regularly during long-running ingestion processes. ContextGraph structure and vector search serve complementary purposes:
  • Graph structure captures explicit relationships and enables traversal, reachability analysis, and multi-hop reasoning
  • Vector search enables semantic similarity queries and fuzzy matching based on content
When used together via AgentContext, you can blend both approaches — find semantically similar content while boosting results that are structurally close to your starting point in the graph.

Domain Examples

Three separate ingest workers write to a shared ContextGraph simultaneously (MISP, NVD, classified STIX). Temporal validity prevents stale campaign data from appearing in current-threat queries.
  • Graph Analytics — centrality rankings, community detection, node embeddings, and link prediction on a populated ContextGraph
  • Decision Intelligence — recording decisions as typed nodes, causal chain analysis, precedent search, and policy enforcement
  • Ingest — loading data from PDFs, APIs, databases, STIX bundles, and RSS feeds into the graph
  • Deduplication — detecting and merging near-duplicate nodes before insertion to prevent graph fragmentation
  • Reasoning — temporal interval algebra (Allen relations), forward/backward chaining, and SPARQL over the knowledge graph
  • Ontology Management — deriving formal OWL ontologies from graph.to_dict() for downstream reasoning engines
  • Context Module Reference — full API for AgentContext, ContextGraph, ContextNode, ContextEdge