Skip to main content
Version: 0.0.5 (stable)

Retrieve and answer

Use SearchEngine when your application needs retrieved entities or chunks. Use build_agent when you want the system to search, combine evidence, and return a cited answer. The direct-search example needs no agent LLM; the agent examples do.

Prerequisites​

Install the storage, embedding, and extraction extras. The agent layer needs the agents extra:

pip install 'agentic-graphrag[neo4j,extract]'
# For the agent layer:
pip install 'agentic-graphrag[agents]'

Each code block on this page is complete. It opens a graph with DEFAULT_SCHEMA, adds two sentences, and closes the graph at the end. The agent examples read LLM_BASE_URL, LLM_MODEL_ID, and LLM_API_KEY from the environment or from .env.

Search directly​

SearchEngine searches entity labels that exist in your graph. The example below ingests its data with DEFAULT_SCHEMA, so its labels match. If you ingested with another schema, pass that schema's labels in entity_labels.

import asyncio

from agrag.retrieval.recipes import HYBRID
from agrag.retrieval.search_engine import SearchEngine
from agrag.common.data_models import DEFAULT_SCHEMA
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import Graph
from agrag.ingestion.extract import GlinerExtractor


async def main() -> None:
graph_store = build_graph_store("neo4j")
embedder = build_embedder("ibm-granite/granite-embedding-small-english-r2")
graph = await Graph.open(
schema=DEFAULT_SCHEMA,
graph_store=graph_store,
embedder=embedder,
extractor=GlinerExtractor(),
)
try:
await graph.add(text="Aspirin treats headaches. Ibuprofen also treats headaches.")
engine = SearchEngine(
graph_store=graph_store,
embedder=embedder,
entity_labels=[entity.label for entity in DEFAULT_SCHEMA.entities],
)

results = await engine.search("What treats headaches?", HYBRID)

for result in results:
print(f"{result.item} (score={result.score:.3f})")
finally:
await graph_store.close()


asyncio.run(main())

An empty result is not an error. Check that the search labels match the schema used for ingestion, then confirm that the graph has indexed the documents. See Troubleshoot common errors for other causes.

Answer with an agent​

import asyncio

from agrag.agents.build import build_agent
from agrag.agents.settings import AgentLLMSettings
from agrag.retrieval.search_engine import SearchEngine
from agrag.common.data_models import DEFAULT_SCHEMA
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import Graph
from agrag.ingestion.extract import GlinerExtractor


async def main() -> None:
graph_store = build_graph_store("neo4j")
embedder = build_embedder("ibm-granite/granite-embedding-small-english-r2")
graph = await Graph.open(
schema=DEFAULT_SCHEMA,
graph_store=graph_store,
embedder=embedder,
extractor=GlinerExtractor(),
)
try:
await graph.add(text="Aspirin treats headaches. Ibuprofen also treats headaches.")
engine = SearchEngine(graph_store=graph_store, embedder=embedder)

agent = build_agent(
engine=engine,
llm_settings=AgentLLMSettings.from_openai_compatible_env(),
graph_schema=DEFAULT_SCHEMA,
)

result = await agent.ainvoke({
"messages": [
{"role": "user", "content": "What treats headaches?"}
]
})

last_message = result["messages"][-1]
print(last_message["content"] if isinstance(last_message, dict) else last_message.content)
finally:
await graph_store.close()


asyncio.run(main())

result["ledger"] holds the citation keys the agent assigned during the run. Call result["ledger"].resolve("C1") to get the SearchResult behind a key in the answer. It returns None for a key the run never assigned. Each call to ainvoke returns a new ledger.

The deep-agent path returns LangChain message objects, so the last message's text is .content. The optional-dependency fallback returns dictionaries, as shown above. graph_schema tells the researcher's prompt and the generated-query tool which labels and relationship types the graph uses. Omit it to use the engine's own schema, which falls back to DEFAULT_SCHEMA when the engine was built without one. If you pass a schema that differs from the engine's schema, build_agent raises ValueError. The prompt can never describe a graph that the engine does not search.

By default, the agent's tools search the whole graph. Pass SearchFilters to limit every tool search by document set, tenant, or label:

import asyncio

from agrag.agents.build import build_agent
from agrag.agents.settings import AgentLLMSettings
from agrag.retrieval.filters import SearchFilters
from agrag.retrieval.search_engine import SearchEngine
from agrag.common.data_models import DEFAULT_SCHEMA
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import Graph
from agrag.ingestion.extract import GlinerExtractor


async def main() -> None:
graph_store = build_graph_store("neo4j")
embedder = build_embedder("ibm-granite/granite-embedding-small-english-r2")
graph = await Graph.open(
schema=DEFAULT_SCHEMA,
graph_store=graph_store,
embedder=embedder,
extractor=GlinerExtractor(),
)
try:
await graph.add(text="Aspirin treats headaches. Ibuprofen also treats headaches.")
engine = SearchEngine(graph_store=graph_store, embedder=embedder)

agent = build_agent(
engine=engine,
llm_settings=AgentLLMSettings.from_openai_compatible_env(),
filters=SearchFilters(document_ids=["doc-1", "doc-2"]),
)
finally:
await graph_store.close()


asyncio.run(main())

Set the filters when your application builds the agent. The tools enforce the filters. The model cannot see or change them.

Build-time filters and per-call tool parameters have different roles. The caller sets filters, and the model cannot see them. The model chooses the tool parameters for each call, such as labels, document_ids, limit, min_score, relation_type, and direction. These parameters can only narrow the build-time scope. They cannot widen it. The tools refuse requests outside the scope.

The document_ids filter applies to the five discovery tools and entity lookup. It also limits neighbors from find_related_entities and traverse_from_entity. The labels and properties filters apply to entity lookup and traversal neighbors. relation_types is a traversal allowlist. list_relationship_types uses only this allowlist. The eleventh tool, query_graph_directly, is outside both groups. Unscoped agents receive it. Caller-scoped agents do not.

Researcher tools​

The researcher subagent receives eleven tools when the agent is unscoped, ten when caller-set base filters are non-empty. The tools fall into three groups plus a calculator.

Discovery tools find entry points into the graph:

ToolPurposeKey parameters
search_source_textFind passages of source text that mention the query termsquery, limit, document_ids
look_up_entityFind entities that match a name or descriptionquery, limit, labels
explore_relatedSearch entities and source text together for a topicquery, limit, labels, document_ids
answer_from_graph_structureAnswer from reranked entity and passage evidencequery, limit, min_score
answer_thematic_questionAnswer a broad question from community summariesquery, limit

Structural navigation tools read what the graph stores about one entity:

ToolPurposeKey parameters
describe_entityShow an entity's own recorded properties, not its neighborsentity
list_relationship_typesList the relationship types attached to an entityentity, relation_type_filter

Relational tools follow relationships between entities:

ToolPurposeKey parameters
find_related_entitiesFind the entities one relationship type away from an entityentity, relation_type, direction, community_expand
traverse_from_entityWalk outward from an entity, with a fallback to relationship types when the neighborhood is too wideentity, relation_type, direction, depth
query_graph_directlyAsk a structural question in one generated read-only Cypher query; present only for unscoped agentsquery

If the generated query fails, query_graph_directly returns an error message, not "No results found.", so the agent can try another tool.

compute_over_evidence stands outside the three groups. It computes a count, a sum, or a comparison over numbers the researcher already read from earlier tool results. It never queries the graph.

Available recipes​

RecipeMethodsBFSReranker
ENTITYentityNoNone
CHUNKchunkNoNone
HYBRIDentity, chunkNoNone
HYBRID_RERANKEDentity, chunkNocross_encoder
GRAPH_EXPANDentityYesNone
THEMATICcommunityNoNone
TEXT2CYPHERtext2cypherNoNone

Configuration​

Set environment variables with the RETRIEVAL_ prefix:

RETRIEVAL_ENTITY_LABELS='["Drug","Condition"]'
RETRIEVAL_ENTITY_TOP_K=10
RETRIEVAL_CHUNK_TOP_K=10
RETRIEVAL_HYBRID_ALPHA=0.5
RETRIEVAL_TRAVERSAL_DEPTH=2

RETRIEVAL_ENTITY_LABELS (or the entity_labels argument) names the schema entity labels that entity search runs against. Ingestion creates one native vector index per label, so search needs the labels themselves. The RETRIEVAL_ENTITY_COLLECTION name does not apply here. It applies only when you configure a separate VectorStore.