05. Knowledge Graphs and GraphRAG#
So far in this course, we’ve explored traditional RAG (Retrieval-Augmented Generation) — a paradigm where large language models retrieve unstructured text chunks from vector databases (like FAISS or LanceDB) and synthesize answers on-the-fly.
While RAG works well for surface-level question answering, it struggles with structure, reasoning, and relationships. It treats text as isolated passages — not as entities linked by meaning or causality. That’s where Knowledge Graphs and GraphRAG step in.
🧠 Why Knowledge Graphs?#
A Knowledge Graph (KG) represents knowledge as nodes (entities) and edges (relationships), creating a structured and interpretable memory.
In contrast to flat vector retrieval, KGs allow an agent to:
Reason symbolically — follow explicit paths like “Pichu → Pikachu → Raichu”.
Disambiguate entities — distinguish Thunderbolt (move) vs Thunderbolt (item).
Fuse multi-source facts — merging structured and unstructured evidence.
Explain answers — show the exact graph edges used in reasoning.
This yields an AI system that is not only more precise but also auditable and less hallucinatory.
GraphRAG blends the strengths of retrieval and structured reasoning:
Retrieve relevant context → turn it into triples (
(subject, predicate, object)).Store / update these triples in a graph backend (persistent memory).
Reason on the graph to answer complex or multi-hop queries.
In essence, GraphRAG = RAG + Knowledge Graph Reasoning. Instead of searching documents, we query the graph — traversing relationships explicitly. More details in the paper by Han et al. (2024)
🧩 Enter Graphiti + FalkorDB#
We’ll use the Graphiti library — a lightweight, production-grade framework for building temporal knowledge graphs that integrate directly with LLMs.
FalkorDB is a high-performance graph database built on Redis, which we use as the backend for Graphiti. It combines the speed of in-memory databases with Cypher-style graph queries, making it perfect for real-time AI agents that need to evolve their graph dynamically.
Graphiti uses structured outputs from LLMs to extract triples, store them as graph edges, and enable reasoning through its built-in query APIs and MCP server. Together, Graphiti + FalkorDB create the ideal playground for GraphRAG agents — ones that can remember, reason, and adapt.
However, let’s first start with what it takes to create graphical data.
import os, json
from typing import List, Optional, Literal, Tuple, Dict
from dotenv import load_dotenv
import numpy as np
from pydantic_ai import Agent, RunContext
from openai import OpenAI
load_dotenv() # expects OPENROUTER_API_KEY in your environment
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
CHAT_MODEL = "openrouter:google/gemini-2.5-flash"
EMBED_MODEL = "openai/text-embedding-3-large"
openai = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=OPENROUTER_API_KEY,
)
EPISODES = [
"""Ash meets a timid Pichu that later evolves into Pikachu using friendship.
Pikachu is an Electric-type and often uses Thunderbolt against Team Rocket.""",
"""During a gym battle, Pikachu faces a Ground-type opponent and struggles due to type disadvantage.
Raichu appears later as Pikachu's evolution with a Thunder Stone.""",
"""Pikachu practices Quick Attack in the forest. Trainers discuss that Electric resists Flying and Steel."""
]
🧩 Define the Schema and Create a Triple-Extraction Agent#
Before we can build a knowledge graph, we need to define what relationships are allowed. We’ll describe our Pokémon world using a small, fixed schema of predicates such as:
HAS_TYPE— connects a Pokémon to its elemental typeEVOLVES_TO— shows evolution pathsNEEDS_ITEM— evolution dependency (e.g., Thunder Stone)LEARNS_MOVE— captures learnable movesWEAK_AGAINST,RESISTS— for type matchups
Using this schema, we’ll create two Pydantic models:
Triple— represents one edge (subject,predicate,object)BuildKGResult— wraps the list of extracted entities and triples
Finally, we’ll define a PydanticAI Agent called builder that takes raw episode text and returns structured triples according to our schema. This mimics how an information-extraction LLM in Graphiti works under the hood — but here we do it manually for clarity.
from typing import List, Optional, Literal, Tuple
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
ValidPredicates = Literal[
"HAS_TYPE", "EVOLVES_TO", "NEEDS_ITEM", "LEARNS_MOVE", "WEAK_AGAINST", "RESISTS"
]
class Triple(BaseModel):
subject: str
predicate: ValidPredicates
object: str
fact: Optional[str] = None
confidence: float = Field(0.9, ge=0.0, le=1.0)
class BuildKGResult(BaseModel):
entities: List[str]
triples: List[Triple]
builder = Agent[None, BuildKGResult](
model=CHAT_MODEL,
system_prompt=(
"You are a precise IE system. Extract schema-conformant triples ONLY from the provided episode text.\n"
"Schema:\n"
"- Entities: Pokemon/Type/Move/Item are plain strings (e.g., 'Pikachu', 'Electric', 'Thunderbolt', 'Thunder Stone').\n"
"- Relations: HAS_TYPE(Pokemon→Type), EVOLVES_TO(Pokemon→Pokemon), NEEDS_ITEM(Pokemon→Item), "
"LEARNS_MOVE(Pokemon→Move), WEAK_AGAINST(Pokemon→Type), RESISTS(Pokemon→Type)\n"
"Return a JSON with 'entities' and 'triples'."
),
output_type=BuildKGResult,
)
🕸️ Build a Minimal In-Memory Graph#
Now that we can extract structured triples from episode text, we need a simple data structure to store them as a graph — with nodes and edges.
Here we’ll implement a lightweight MiniGraph class that:
Keeps track of nodes (unique entity names like Pikachu, Electric, Thunderbolt)
Stores edges (
subject → predicate → object) asEdgedataclassesProvides helper methods to generate text corpora of nodes and edges for embedding later
We’ll also use logfire to instrument PydanticAI for observability and apply nest_asyncio so that async agents can run smoothly inside notebooks.
Finally, we’ll loop through our Pokémon episode texts, extract triples using the builder agent, and populate the graph.
This gives us an interpretable knowledge graph memory — before we move on to embedding and semantic search.
from dataclasses import dataclass
import logfire
import nest_asyncio
nest_asyncio.apply()
logfire.configure(send_to_logfire=False) # set to true if you want to use logfire console
logfire.instrument_pydantic_ai()
@dataclass
class Edge:
subject: str
predicate: str
object: str
fact: str = ""
confidence: float = 1.0
class MiniGraph:
def __init__(self):
self.nodes = set()
self.edges: List[Edge] = []
# embedding indexes
self.node_texts: List[str] = [] # e.g., node labels like "Pikachu"
self.node_vecs: List[List[float]] = []
self.edge_texts: List[str] = [] # e.g., "(Pikachu)-[HAS_TYPE]->(Electric)"
self.edge_vecs: List[List[float]] = []
def add_triple(self, t: Triple):
self.nodes.add(t.subject); self.nodes.add(t.object)
self.edges.append(Edge(t.subject, t.predicate, t.object, t.fact or "", t.confidence))
def node_corpus(self) -> List[str]:
return sorted(self.nodes)
def edge_corpus(self) -> List[str]:
return [f"({e.subject})-[{e.predicate}]->({e.object}) :: {e.fact}" for e in self.edges]
GRAPH = MiniGraph()
def add_episode_to_graph(text: str):
res = builder.run_sync(f"Episode:\n{text}").output
for t in res.triples:
GRAPH.add_triple(t)
return res
for ep in EPISODES:
add_episode_to_graph(ep)
print("Nodes:", len(GRAPH.nodes))
print("Edges:", len(GRAPH.edges))
Logfire project URL: https://logfire-eu.pydantic.dev/shreshthtuli/agenticai
12:52:33.493 builder run
12:52:33.499 chat google/gemini-2.5-flash
12:52:35.946 builder run
12:52:35.946 chat google/gemini-2.5-flash
12:52:37.915 builder run
12:52:37.915 chat google/gemini-2.5-flash
Nodes: 10
Edges: 9
Let’s see what graph was generated!
from rich import print as rprint
def pretty_print_graph(graph: MiniGraph):
rprint(f"[purple]\n🕸️ Knowledge Graph Summary[/]")
rprint(f"[green]Nodes[/] ({len(graph.nodes)}): {', '.join(sorted(graph.nodes))}\n")
rprint(f"[red]Edges[/] ({len(graph.edges)}):")
for e in graph.edges:
print(f" ({e.subject}) -[{e.predicate}]-> ({e.object})"
+ (f" | fact: {e.fact}" if e.fact else "")
+ (f" [conf={e.confidence:.2f}]" if e.confidence != 1.0 else ""))
print("-" * 60)
pretty_print_graph(GRAPH)
🕸️ Knowledge Graph Summary
Nodes (10): Electric, Flying, Ground, Pichu, Pikachu, Quick Attack, Raichu, Steel, Thunder Stone, Thunderbolt
Edges (9):
(Pichu) -[EVOLVES_TO]-> (Pikachu) | fact: Pichu that later evolves into Pikachu using friendship [conf=0.90]
(Pikachu) -[HAS_TYPE]-> (Electric) | fact: Pikachu is an Electric-type [conf=0.90]
(Pikachu) -[LEARNS_MOVE]-> (Thunderbolt) | fact: Pikachu ... often uses Thunderbolt against Team Rocket [conf=0.90]
(Pikachu) -[WEAK_AGAINST]-> (Ground) | fact: Pikachu faces a Ground-type opponent and struggles due to type disadvantage. [conf=0.90]
(Pikachu) -[EVOLVES_TO]-> (Raichu) | fact: Raichu appears later as Pikachu's evolution with a Thunder Stone. [conf=0.90]
(Pikachu) -[NEEDS_ITEM]-> (Thunder Stone) | fact: Raichu appears later as Pikachu's evolution with a Thunder Stone. [conf=0.90]
(Pikachu) -[LEARNS_MOVE]-> (Quick Attack) [conf=0.90]
(Electric) -[RESISTS]-> (Flying) [conf=0.90]
(Electric) -[RESISTS]-> (Steel) [conf=0.90]
------------------------------------------------------------
from copy import deepcopy
def reflect_and_expand(graph: MiniGraph, max_iters: int = 3):
prev_edge_count = -1
iteration = 0
while iteration < max_iters:
iteration += 1
current_count = len(graph.edges)
if current_count == prev_edge_count:
print(f"No new edges found after iteration {iteration-1}. Stopping reflection.")
break
prev_edge_count = current_count
graph_state = json.dumps([e.__dict__ for e in graph.edges], indent=2)
# Ask the same builder agent if new relationships can be added
prompt = (
f"Here is the current knowledge graph:\n{graph_state}\n\n"
"Reflect on it and see if any *implicit* or *missing* relationships "
"can be derived from this graph. Add only valid new triples, if any. "
"Return empty if nothing new can be inferred."
)
reflection = builder.run_sync(prompt).output
# Add new edges (if any)
added = 0
for t in reflection.triples:
logfire.info(f"New edge added: {(t.subject, t.predicate, t.object)}")
key = (t.subject, t.predicate, t.object)
existing = {(e.subject, e.predicate, e.object) for e in graph.edges}
if key not in existing:
graph.add_triple(t)
added += 1
print(f"Iteration {iteration}: added {added} new edges. Total now {len(graph.edges)}.")
return graph
NEWGRAPH = reflect_and_expand(deepcopy(GRAPH))
pretty_print_graph(NEWGRAPH)
20:33:44.100 builder run
20:33:44.102 chat google/gemini-2.5-flash
20:33:45.476 New edge added: ('Raichu', 'HAS_TYPE', 'Electric')
20:33:45.476 New edge added: ('Raichu', 'WEAK_AGAINST', 'Ground')
20:33:45.476 New edge added: ('Raichu', 'RESISTS', 'Flying')
20:33:45.476 New edge added: ('Raichu', 'RESISTS', 'Steel')
Iteration 1: added 4 new edges. Total now 13.
20:33:45.476 builder run
20:33:45.484 chat google/gemini-2.5-flash
20:33:46.974 New edge added: ('Pikachu', 'RESISTS', 'Flying')
20:33:46.974 New edge added: ('Pikachu', 'RESISTS', 'Steel')
Iteration 2: added 2 new edges. Total now 15.
20:33:46.974 builder run
20:33:46.974 chat google/gemini-2.5-flash
Iteration 3: added 0 new edges. Total now 15.
🕸️ Knowledge Graph Summary
Nodes (10): Electric, Flying, Ground, Pichu, Pikachu, Quick Attack, Raichu, Steel, Thunder Stone, Thunderbolt
Edges (15):
(Pichu) -[EVOLVES_TO]-> (Pikachu) [conf=0.90]
(Pikachu) -[HAS_TYPE]-> (Electric) [conf=0.90]
(Pikachu) -[LEARNS_MOVE]-> (Thunderbolt) [conf=0.90]
(Pikachu) -[WEAK_AGAINST]-> (Ground) | fact: Pikachu faces a Ground-type opponent and struggles due to type disadvantage. [conf=0.90]
(Pikachu) -[EVOLVES_TO]-> (Raichu) | fact: Raichu appears later as Pikachu's evolution. [conf=0.90]
(Pikachu) -[NEEDS_ITEM]-> (Thunder Stone) | fact: Raichu appears later as Pikachu's evolution with a Thunder Stone. [conf=0.90]
(Pikachu) -[LEARNS_MOVE]-> (Quick Attack) [conf=0.90]
(Electric) -[RESISTS]-> (Flying) [conf=0.90]
(Electric) -[RESISTS]-> (Steel) [conf=0.90]
(Raichu) -[HAS_TYPE]-> (Electric) [conf=0.90]
(Raichu) -[WEAK_AGAINST]-> (Ground) [conf=0.90]
(Raichu) -[RESISTS]-> (Flying) [conf=0.90]
(Raichu) -[RESISTS]-> (Steel) [conf=0.90]
(Pikachu) -[RESISTS]-> (Flying) | fact: As an Electric type, Pikachu resists Flying type moves. [conf=0.90]
(Pikachu) -[RESISTS]-> (Steel) | fact: As an Electric type, Pikachu resists Steel type moves. [conf=0.90]
------------------------------------------------------------
In this example, our builder agent initially extracted direct facts from the Pokémon episodes — things like “Pikachu evolves to Raichu” or “Pikachu has type Electric.”
When we introduced the reflection loop, the agent began to review its own graph output and infer missing or implicit relations. For instance, it noticed that Pikachu also resists Flying and Steel — facts implied by its Electric typing but not explicitly mentioned in the text.
This reflective step acts as a lightweight self-consistency check:
It helps the model fill small gaps in knowledge by reasoning over the structure it already built.
It can correct omissions or low-confidence facts without requiring another dataset.
It converges automatically — once the graph stabilizes (no new edges are added), the loop stops.
In a larger system, this is the foundation of agentic knowledge refinement — the same principle used by Graphiti and other GraphRAG frameworks to keep the knowledge graph both complete and consistent over time.
🎯 Embedding and Semantic Search#
Now that our mini knowledge graph is built and refined, the next step is to make it searchable. We’ll embed both nodes (entity names) and edges (relationships) into vector space using the OpenRouter embedding API (text-embedding-3-large by OpenAI, accessed via OpenRouter).
This lets us perform semantic search over the graph — so instead of keyword lookups, we can find conceptually related entities and relationships.
In this section:
We define helper functions to embed text and compute cosine similarity.
Build vector indexes for all nodes and edges.
Implement simple search functions that return the top-K most semantically similar nodes or edges for any query.
This is conceptually similar to what happens in traditional RAG, except here we are embedding graph elements instead of text chunks — a key building block for GraphRAG reasoning.
from pprint import pprint
def embed_texts(texts: List[str]) -> List[List[float]]:
if not texts:
return []
resp = openai.embeddings.create(model=EMBED_MODEL, input=texts)
return [d.embedding for d in resp.data]
def normalize(v: np.ndarray) -> np.ndarray:
n = np.linalg.norm(v) + 1e-12
return v / n
def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(normalize(a), normalize(b)))
def build_vector_indexes():
GRAPH.node_texts = GRAPH.node_corpus()
GRAPH.node_vecs = embed_texts(GRAPH.node_texts)
GRAPH.edge_texts = GRAPH.edge_corpus()
GRAPH.edge_vecs = embed_texts(GRAPH.edge_texts)
def search_nodes(query: str, k: int = 5) -> List[Tuple[str, float]]:
if not GRAPH.node_texts:
return []
qv = embed_texts([query])[0]
sims = [cosine_sim(np.array(qv), np.array(v)) for v in GRAPH.node_vecs]
ranked = sorted(zip(GRAPH.node_texts, sims), key=lambda x: x[1], reverse=True)
return ranked[:k]
def search_edges(query: str, k: int = 5) -> List[Tuple[str, float]]:
if not GRAPH.edge_texts:
return []
qv = embed_texts([query])[0]
sims = [cosine_sim(np.array(qv), np.array(v)) for v in GRAPH.edge_vecs]
ranked = sorted(zip(GRAPH.edge_texts, sims), key=lambda x: x[1], reverse=True)
return ranked[:k]
build_vector_indexes()
print("Top nodes for 'Pikachu evolution item':")
pprint(search_nodes("Pikachu evolution item"))
print("Top edges for 'type disadvantage against Ground':")
pprint(search_edges("type disadvantage against Ground"))
Top nodes for 'Pikachu evolution item':
[('Pikachu', 0.6038109820985103),
('Pichu', 0.5137344454239156),
('Raichu', 0.49602330682813606),
('Thunder Stone', 0.4325935945057776),
('Quick Attack', 0.34751407732015965)]
Top edges for 'type disadvantage against Ground':
[('(Pikachu)-[WEAK_AGAINST]->(Ground) :: Pikachu faces a Ground-type opponent '
'and struggles due to type disadvantage.',
0.6646366743822216),
('(Electric)-[RESISTS]->(Flying) :: ', 0.4096363073497873),
('(Electric)-[RESISTS]->(Steel) :: ', 0.38054139684246147),
('(Pikachu)-[LEARNS_MOVE]->(Thunderbolt) :: ', 0.32694458619268363),
('(Pikachu)-[HAS_TYPE]->(Electric) :: ', 0.325167775411372)]
🧭 What is an Ontology (and why it matters for Graph/GraphRAG)?#
An ontology is a formal, shared specification of the concepts (classes) in a domain, their attributes (properties), and the relationships among them.
In graph terms, it defines:
Entity types (e.g.,
Pokemon,Type,Move,Item)Attributes on entities (e.g.,
Pokemon.stage,Move.power)Relation types (e.g.,
HAS_TYPE,EVOLVES_TO,LEARNS_MOVE)Domain/Range constraints (what can connect to what) and sometimes cardinalities (e.g.,
Pokemon HAS_TYPE Type)
A good ontology:
Reduces hallucinations by constraining what can be asserted
Improves explainability because answers refer to explicit entities/relations
Enables reusable reasoning across tasks (querying, validation, analytics)
See more on FalkorDB’s blog.
Pragmatic recipe to design one
List core entities and the questions you must answer.
Define relations that connect those entities (domain/range).
Add attributes needed for reasoning (and keep the rest out).
Start small; iterate with real data; add constraints as you go.
Allthough ontologies can be created by LLMs like below:
class OntologyAttribute(BaseModel):
name: str
dtype: Literal["string","int","float","bool","datetime","enum","id"] = "string"
description: Optional[str] = None
required: bool = False
class OntologyClass(BaseModel):
name: str
description: Optional[str] = None
attributes: List[OntologyAttribute] = Field(default_factory=list)
class OntologyRelation(BaseModel):
name: str
description: Optional[str] = None
domain: str # class name
range: str # class name
class OntologyProposal(BaseModel):
classes: List[OntologyClass]
relations: List[OntologyRelation]
notes: Optional[str] = None
ontology_suggester = Agent(
model=CHAT_MODEL,
system_prompt=(
"You are an ontology engineer. Given example domain text, propose a SMALL, "
"pragmatic ontology capturing key classes, attributes, and relations. "
"Keep it minimal but sufficient for QA and reasoning. Prefer concise names. "
"Return structured JSON matching OntologyProposal."
),
output_type=OntologyProposal,
)
def suggest_ontology_from_examples(texts: List[str]) -> OntologyProposal:
corpus = "\n\n---\n\n".join(texts)
prompt = (
"Domain examples:\n"
f"{corpus}\n\n"
"Requirements:\n"
"- Classes should include Pokemon, Type, Move, and Item if present.\n"
"- Add minimal attributes that are useful for Q&A (e.g., power for moves, stage for pokemon).\n"
"- Add relations like HAS_TYPE, EVOLVES_TO, NEEDS_ITEM, LEARNS_MOVE, WEAK_AGAINST, RESISTS.\n"
"- You may add brief descriptions.\n"
"- Keep it compact. Avoid unnecessary ontology."
)
return ontology_suggester.run_sync(prompt).output
proposal = suggest_ontology_from_examples(EPISODES)
print("=== Ontology Proposal ===")
print(json.dumps(proposal.model_dump(), indent=2))
12:52:48.819 ontology_suggester run
12:52:48.821 chat google/gemini-2.5-flash
=== Ontology Proposal ===
{
"classes": [
{
"name": "Pokemon",
"description": "A creature in the Pokemon world.",
"attributes": [
{
"name": "name",
"dtype": "string",
"description": "The name of the Pokemon",
"required": false
},
{
"name": "stage",
"dtype": "int",
"description": "Evolutionary stage of the Pokemon",
"required": false
}
]
},
{
"name": "Type",
"description": "A category that defines a Pokemon's and Move's elemental properties.",
"attributes": [
{
"name": "name",
"dtype": "string",
"description": "The name of the Pokemon type (e.g., Electric, Ground).",
"required": false
}
]
},
{
"name": "Move",
"description": "An action a Pokemon can perform in battle.",
"attributes": [
{
"name": "name",
"dtype": "string",
"description": "The name of the move.",
"required": false
},
{
"name": "power",
"dtype": "int",
"description": "The base power of the move, if applicable.",
"required": false
}
]
},
{
"name": "Item",
"description": "An object that can be used by or on a Pokemon.",
"attributes": [
{
"name": "name",
"dtype": "string",
"description": "The name of the item.",
"required": false
}
]
}
],
"relations": [
{
"name": "HAS_TYPE",
"description": "Indicates the type of a Pokemon.",
"domain": "Pokemon",
"range": "Type"
},
{
"name": "HAS_TYPE",
"description": "Indicates the type of a Move.",
"domain": "Move",
"range": "Type"
},
{
"name": "EVOLVES_TO",
"description": "Indicates that one Pokemon evolves into another.",
"domain": "Pokemon",
"range": "Pokemon"
},
{
"name": "NEEDS_ITEM",
"description": "Indicates that a Pokemon needs a specific item for evolution.",
"domain": "Pokemon",
"range": "Item"
},
{
"name": "LEARNS_MOVE",
"description": "Indicates a move that a Pokemon can learn.",
"domain": "Pokemon",
"range": "Move"
},
{
"name": "WEAK_AGAINST",
"description": "Indicates a type that is super effective against the domain type.",
"domain": "Type",
"range": "Type"
},
{
"name": "RESISTS",
"description": "Indicates a type that is not very effective against the domain type.",
"domain": "Type",
"range": "Type"
}
],
"notes": "This ontology captures key entities and relationships for basic Pokemon knowledge, allowing for queries about types, evolutions, and move effectiveness. The 'stage' attribute for Pokemon helps track evolutionary progress without explicit evolution events."
}
🧩 Graphiti abstraction#
All these embedding and search operations are automatically handled inside Graphiti.
It provides:
Configurable embedders and cross-encoders for reranking
Persistent vector indexes linked to graph nodes
Integration with real graph backends (e.g., FalkorDB, Neo4j)
APIs to search, rank, and traverse the graph directly
So while we’re writing these utilities manually here to understand the mechanics, in the next section we’ll switch to Graphiti, which abstracts away all this boilerplate and provides a much more powerful, production-ready interface.
Let’s first create the FalkorDB as a backend for Graphiti.
from graphiti_core import Graphiti
from graphiti_core.cross_encoder.openai_reranker_client import OpenAIRerankerClient
from graphiti_core.llm_client.openai_client import OpenAIClient
from graphiti_core.embedder.openai import OpenAIEmbedder, OpenAIEmbedderConfig
from graphiti_core.driver.falkordb_driver import FalkorDriver
from graphiti_core.llm_client.config import LLMConfig
from dotenv import load_dotenv
import os
from src.falkordb_setup import run_falkordb, save_db
load_dotenv()
falkordb_container = run_falkordb()
⏬ Pulling falkordb/falkordb:latest (if needed)...
🚀 Starting FalkorDB with persistence at C:\Users\SHRESHTH\Desktop\build-your-own-super-agents\db\falkordb_data
- Container: a8c68e3101ab
- UI: http://localhost:3000 | Redis: localhost:6379
In Graphiti, these three components play the same roles as in a traditional RAG pipeline — but for graph-based reasoning instead of plain text retrieval.
LLM Client (
OpenAIGenericClient)This wraps the language model endpoint (in our case, Gemini 2.5 Flash via OpenRouter).
It’s used for all generative tasks inside Graphiti — such as extracting triples, summarizing nodes, or generating context-aware graph queries.
Embedder (
OpenAIEmbedder)Similar to the vector embedder in RAG, it converts text, entity names, or relationships into dense embeddings for semantic similarity search within the graph.
We use
text-embedding-3-largefrom OpenAI to create these embeddings, allowing Graphiti to find related nodes or documents efficiently.
Cross-Encoder / Re-ranker (
OpenAIRerankerClient)After retrieval, multiple candidate nodes or subgraphs may be found.
The reranker uses a small LLM to score and reorder these candidates based on their semantic relevance to the query, improving precision.
This is analogous to the reranking step in advanced RAG setups.
Together, these components form the reasoning and retrieval core of Graphiti. The embedder finds relevant graph pieces, the reranker prioritizes them, and the LLM client performs reasoning over the final context.
from graphiti_core.utils.maintenance.graph_data_operations import clear_data
from datetime import datetime
llm_config = LLMConfig(api_key=os.getenv("OPENROUTER_API_KEY"),
base_url="https://openrouter.ai/api/v1",
model="x-ai/grok-4-fast",
small_model="x-ai/grok-4-fast")
client = OpenAIClient(config=llm_config, reasoning='medium')
embedder_config = OpenAIEmbedderConfig(api_key=os.getenv("OPENROUTER_API_KEY"),
base_url="https://openrouter.ai/api/v1",
embedding_model="openai/text-embedding-3-large")
embedder = OpenAIEmbedder(embedder_config)
reranker = OpenAIRerankerClient(llm_config)
driver = FalkorDriver()
🧱 Defining Entity and Relationship Schemas for Graphiti#
Now that we understand how knowledge graphs can be built manually, let’s formalize our Pokémon world using Graphiti’s structured schema definitions.
We define:
Entity types like
Pokemon,Type,Move, andItem— each with optional attributes (e.g.,stage,power,effect).Edge types like
HAS_TYPE,EVOLVES_TO,LEARNS_MOVE, etc. — describing allowed relationships between entities.
The edge_type_map explicitly specifies which relationships are permitted between each entity pair (e.g., Pokemon → Type can have HAS_TYPE, WEAK_AGAINST, or RESISTS).
Finally, we initialize a Graphiti instance connected to the FalkorDB driver,
and load our Pokémon episode data into it using add_episode().
This automatically handles:
LLM-based triple extraction
Schema validation
Embedding and storage in the graph backend
Essentially, this is the Graphiti abstraction over everything we built manually earlier — offering schema-aware KG construction, persistence, and reasoning in one unified interface.
For our use-case, we define a fixed ontology as follows. We will use Graphiti’s custom entities and edges to encode this ontology for our knowledge graph.
from graphiti_core.nodes import EpisodeType
from pathlib import Path
import os
# Entities
class Pokemon(BaseModel):
"""A Pokemon species or evolutionary form."""
stage: Optional[int] = Field(None, description="Evolution stage number (e.g., Pichu is 1, Pikachu is 2, Raichu is 3)")
class Type(BaseModel):
"""Elemental typing such as Electric, Ground, Flying."""
category: Optional[str] = Field(None, description="Damage class or grouping if applicable")
class Move(BaseModel):
"""A move a Pokemon can learn or use."""
power: Optional[int] = Field(None, description="Base power if applicable")
move_type: Optional[str] = Field(None, description="Type of the move, e.g., Electric")
class Item(BaseModel):
"""An evolution or battle item."""
effect: Optional[str] = Field(None, description="Short description of the item effect")
# Edges
class HasType(BaseModel):
"""Pokemon → Type"""
pass
class EvolvesTo(BaseModel):
"""Pokemon → Pokemon"""
method: Optional[str] = Field(None, description="Evolution method (friendship, level, etc.)")
level: Optional[int] = Field(None, description="Level when evolves")
class NeedsItem(BaseModel):
"""Pokemon → Item"""
reason: Optional[str] = Field(None, description="Why the item is required (e.g., evolve)")
class LearnsMove(BaseModel):
"""Pokemon → Move"""
learn_method: Optional[str] = Field(None, description="TM/TR/Level-up/etc.")
level: Optional[int] = Field(None, description="Level when learned, if applicable")
class WeakAgainst(BaseModel):
"""Pokemon → Type"""
note: Optional[str] = Field(None, description="Context note")
class Resists(BaseModel):
"""Pokemon → Type"""
note: Optional[str] = Field(None, description="Context note")
# Entity and edge registries
entity_types: Dict[str, type] = {
"Pokemon": Pokemon,
"Type": Type,
"Move": Move,
"Item": Item,
}
edge_types: Dict[str, type] = {
"HAS_TYPE": HasType,
"EVOLVES_TO": EvolvesTo,
"NEEDS_ITEM": NeedsItem,
"LEARNS_MOVE": LearnsMove,
"WEAK_AGAINST": WeakAgainst,
"RESISTS": Resists,
}
# Which edge types are allowed between which entity pairs
edge_type_map: Dict[Tuple[str, str], List[str]] = {
("Pokemon", "Type"): ["HAS_TYPE", "WEAK_AGAINST", "RESISTS"],
("Pokemon", "Pokemon"): ["EVOLVES_TO"],
("Pokemon", "Item"): ["NEEDS_ITEM"],
("Pokemon", "Move"): ["LEARNS_MOVE"],
}
🎬 Splitting Text into Self-Contained Episodes#
Before adding data into the knowledge graph, we need to break long Pokémon narratives
(such as full transcripts or story summaries) into smaller, coherent segments called episodes.
Each Episode should represent a complete scene or event — containing enough context
for the LLM to extract entities and relationships without depending on other segments.
In this step:
We define a Pydantic model
Episodewith fields forname,episode_body, andsource_description.
Afield_validatorensures titles are short and clean.We create a
EpisodesResultwrapper to hold multiple episodes.We then use a PydanticAI Agent,
episode_generator, which takes a long Pokémon text and
splits it into coherentEpisodeobjects.
This segmentation step ensures the graph builder later works on focused, semantically consistent chunks,
just like scene segmentation in a movie — enabling better entity extraction and cleaner graph structure.
from pydantic import BaseModel, Field, field_validator
class Episode(BaseModel):
"""A coherent segment suitable for graph extraction."""
name: str = Field(..., description="Short, unique episode title (e.g., 'Charizard learns move Slash').")
episode_body: str = Field(..., min_length=120, description="Self-contained text of the episode.")
source_description: Optional[str] = Field("episode", description="Provenance label (default: 'episode').")
@field_validator("name")
@classmethod
def strip_name(cls, v: str) -> str:
v = v.strip()
if len(v) > 120:
v = v[:117] + "..."
return v
class EpisodesResult(BaseModel):
episodes: List[Episode]
episode_generator = Agent(
model="openrouter:openai/gpt-5",
system_prompt=(
"""
You are an expert segmenter. Split a long Pokémon-related context into coherent EPISODES.
Rules:
- Prioritize coherence over exact length.
- Each episode must be self-contained: enough detail so downstream IE can extract entities/relations without cross-episode references.
- Prefer semantic boundaries: scene changes, locations, battles, new characters/pokemon, or topic shifts.
- Titles should be short, unique, and descriptive.
- Respect chronology if provided; otherwise, group by topical coherence.
- Keep `source_description="episode"` unless the input explicitly suggests otherwise (e.g., 'movie recap', 'blog post', etc.).
- NEVER fabricate content beyond the given text. If info is uncertain, omit it.
- Technical Machines (TMs) are items. Moves can be learnt by TMs or level-ups.
- Include all information required to create ontological entities and edges, in an episode in precise natural language.
Ontology (Schema to follow strictly)
- **Entities**
- `Pokemon` — fields: `stage` (int, e.g., Pichu=1, Pikachu=2, Raichu=3)
- `Type` — fields: `category` (str, optional damage class/group)
- `Move` — fields: `power` (int, optional), `move_type` (str, e.g., Electric)
- `Item` — fields: `effect` (str, short description)
- **Edges (directed)**
- `HAS_TYPE` : `Pokemon → Type`
- `EVOLVES_TO` : `Pokemon → Pokemon` (attr: `method` e.g., friendship/level, `level`=int)
- `NEEDS_ITEM` : `Pokemon → Item` (attr: `reason`, e.g., evolve)
- `LEARNS_MOVE` : `Pokemon → Move` (attrs: `learn_method`=TM/TR/Level-up, `level`=int)
- `WEAK_AGAINST` : `Pokemon → Type` (attr: `note`)
- `RESISTS` : `Pokemon → Type` (attr: `note`)
- **Allowed pairs**
- `(Pokemon, Type) → {HAS_TYPE, WEAK_AGAINST, RESISTS}`
- `(Pokemon, Pokemon) → {EVOLVES_TO}`
- `(Pokemon, Item) → {NEEDS_ITEM}`
- `(Pokemon, Move) → {LEARNS_MOVE}`
**Constraints**
- Use only the predicates listed above.
- Subjects/objects must match the domain/range shown.
- Use **exact surface names** from the text (no fabrication).
- Prefer concise `fact` strings; omit if redundant.
- If uncertain, omit rather than guess.
**Output contract**
- Extract entities and edges that conform to this ontology.
- Return JSON with:
- `entities`: list of unique entity names (strings)
- `triples`: list of objects with fields
- `subject` (str), `predicate` (one of the allowed), `object` (str)
- optional: `fact` (str), `confidence` (0..1)
Output strictly as EpisodesResult. Episode body should be in natural language text, not json. Include information for all fields where possible.
"""
),
output_type=EpisodesResult
)
response = episode_generator.run_sync(EPISODES[0])
rprint(response.output)
12:55:28.192 episode_generator run
12:55:28.194 chat openai/gpt-5
EpisodesResult( episodes=[ Episode( name='Ash meets timid Pichu; evolves via friendship', episode_body='Ash encounters a timid Pichu and spends time with it. As their bond grows, the text states that Pichu later evolves into Pikachu using friendship. This explicitly identifies friendship as the cause of Pichu’s evolution, linking the evolution event directly to the relationship Pichu develops with Ash.', source_description='episode' ), Episode( name='Pikachu’s type and Thunderbolt against Team Rocket', episode_body='Pikachu is an Electric-type Pokémon. In battles against Team Rocket, Pikachu often uses the move Thunderbolt against them. The repeated use of Thunderbolt against Team Rocket is described as a frequent tactic, emphasizing Pikachu’s Electric-type identity and its reliance on Thunderbolt in those encounters.', source_description='episode' ) ] )
⚙️ Loading and Processing Pokémon Episodes into Graphiti#
Now that we’ve defined our episode segmentation agent and graph schema,
we can put everything together to build a complete knowledge graph using Graphiti.
In this step:
Initialize Graphiti with:
graph_driver→ our FalkorDB backendllm_client,embedder, andcross_encoder→ for triple extraction, embedding, and rerankingstore_raw_episode_content=False→ skips saving large text blobs to keep storage light
Prepare the environment:
clear_data()wipes any existing graph data.build_indices_and_constraints()sets up indexes and schema-level constraints in the database.
Process local Pokémon markdown files:
Each file in
data/pokemon_md/contains a text segment describing Pokémon interactions or battles.We pass each file through the
episode_generator, which splits it into coherent episodes.Then each
Episodeis passed tographiti.add_episode(), which:Extracts entities and relationships,
Embeds and links them,
Inserts them into the graph database with timestamps and group metadata.
🔁 This creates a structured, queryable knowledge graph from unstructured Pokémon text — and demonstrates how Graphiti unifies the full workflow (segmentation → extraction → embedding → persistence) that we previously built manually in separate steps.
from tqdm.notebook import tqdm
graphiti = Graphiti(graph_driver=driver, llm_client=client, embedder=embedder, cross_encoder=reranker, store_raw_episode_content=False)
""" Helper functions for first time load (if you want to recreate graph from scratch) """
# await clear_data(graphiti.driver)
# await graphiti.build_indices_and_constraints(delete_existing=True)
DB_FILES = "data/pokemon_md/"
# If graph is empty create it from the files
if len(await graphiti.search('Charizard')) == 0:
episodes = []
for filename in os.listdir(DB_FILES):
episodes.append(Path(DB_FILES + filename).read_text(encoding='utf-8'))
for episode in tqdm(episodes, "Processing Files"):
response = episode_generator.run_sync(episode)
for gep in tqdm(response.output.episodes, desc="Processing episodes"):
print(gep.episode_body)
await graphiti.add_episode(name=gep.name,
episode_body=gep.episode_body,
source_description=gep.source_description,
source=EpisodeType.text,
reference_time=datetime.now(),
group_id="pokemon_data_tmp",
entity_types=entity_types,
edge_types=edge_types,
edge_type_map=edge_type_map)
save_db(falkordb_container)
Let’s visualize the graph.
from src.graphiti_utils import save_graph
save_graph()

You can also view the full interactive knowledge graph here.
🔎 Building a Graph Search Tool for Agents#
With our Pokémon knowledge graph stored in FalkorDB via Graphiti, we can now build a reusable graph search tool — an interface that retrieves the most relevant entities and relationships for any natural language query.
Here’s what this step does:
Connects to FalkorDB to query stored nodes and relationships.
Uses
graphiti.search()to fetch the most relevantEntityEdgeobjects.For each edge, it retrieves the source and destination node details from the graph.
Formats the combined results into a human- and LLM-readable JSON string, so downstream agents can reason directly over structured graph results.
This tool can be wrapped inside a PydanticAI Agent tool or used in an LLM chain, allowing the model to “see” structured knowledge graph data instead of plain text.
from graphiti_core.edges import EntityEdge
from falkordb import FalkorDB
# Connect to your FalkorDB instance
db = FalkorDB()
g = db.select_graph("default_db")
async def get_node_by_uuid(uuid_value: str):
cypher = f"""
MATCH (n)
WHERE n.uuid = '{uuid_value}'
RETURN id(n) AS node_id, labels(n) AS labels, properties(n) AS properties
"""
res = g.query(cypher)
_, _, properties = res.result_set[0]
for k in ['name_embedding', 'uuid', 'group_id', 'created_at']:
properties.pop(k, None)
properties['labels'].remove('Entity')
properties['label'] = properties.pop('labels')[0] if properties['labels'] else None
return properties
async def pretty_print(entity_edge: EntityEdge):
e_dict = entity_edge.model_dump()
source_properties = await get_node_by_uuid(e_dict['source_node_uuid'])
dest_properties = await get_node_by_uuid(e_dict['target_node_uuid'])
return {'source_node': source_properties, 'fact': e_dict['fact'], 'relation': e_dict['name'], 'dest_node': dest_properties}
async def graph_search_tool(query: str, top_k: int = 5) -> str:
results = await graphiti.search(query, num_results=top_k)
logfire.info(f"Graph search for '{query}' returned {len(results)} results.")
tool_outputs = []
for i, result in enumerate(results):
tool_outputs.append(await pretty_print(result))
logfire.info(f"Result {i + 1}: {tool_outputs[-1]['fact']}")
return json.dumps(tool_outputs, indent=2, ensure_ascii=False)
Example usage:
response = await graph_search_tool("Pikachu evolution item", 1)
print(response)
15:19:45.936 Graph search for 'Pikachu evolution item' returned 1 results.
15:19:45.941 Result 1: Thunder Stone is required for Pikachu to evolve into either Raichu or Alolan Raichu depending on the region.
[
{
"source_node": {
"name": "Pikachu",
"summary": "Pikachu (#0025, Electric) is a 0.4m, 6kg Mouse Pokémon that stores electricity in cheek sacs, accumulating charge overnight while sleeping. Discharges mildly when dozy or powerfully like lightning when threatened.",
"stage": 2,
"label": "Pokemon"
},
"fact": "Thunder Stone is required for Pikachu to evolve into either Raichu or Alolan Raichu depending on the region.",
"relation": "NEEDS_ITEM",
"dest_node": {
"name": "Thunder Stone",
"summary": "Thunder Stone is an evolution item that evolves Pikachu (#0025, Electric) into Raichu (#0026, Electric) outside Alola or Alolan Raichu (Electric · Psychic) in Alola.",
"effect": "Evolves Pikachu into Raichu (Electric) outside Alola or Alolan Raichu (Electric · Psychic) in Alola.",
"label": "Item"
}
}
]
🤖 Comparing a Baseline LLM vs. a Graph-Augmented Agent#
Now that we can search the Pokémon knowledge graph, let’s see how graph access changes the quality of answers.
We define two agents:
Baseline Agent — a plain language model that answers questions directly from its internal knowledge. It can hallucinate or be uncertain if the fact isn’t well-represented in its training data.
Graph Agent — an LLM augmented with our graph search tool. It retrieves structured evidence from the FalkorDB + Graphiti knowledge graph and uses those results to ground its response.
Both agents output a structured QAResult object:
answer: the final text responseused_graph: flag for whether graph data was usedevidence: a list of{source_node, relation, dest_node}triples supporting the answer
By comparing their outputs, we can observe how GraphRAG reasoning reduces hallucinations and increases factual precision — for example, correctly identifying that Pikachu needs a Thunder Stone to evolve.
class QAResult(BaseModel):
answer: str
used_graph: bool
evidence: List[str] = Field(description="list of relations/facts from the graph used to answer the question")
# Baseline agent without graph access
baseline_agent = Agent(
model="openrouter:openai/gpt-5",
system_prompt=(
"Answer the user's Pokémon question as best as you can. "
"Be concise. If unsure, say you are unsure."
),
output_type=QAResult,
)
# Agent with graph access
graph_agent = Agent(
model="openrouter:openai/gpt-5",
system_prompt=(
"You have access to a Pokémon knowledge graph. Use it to answer the user's question. "
"If the graph does not have the information, say you are unsure. "
"Be concise. Populate the 'evidence' field with relevant graph facts used."
),
tools=[graph_search_tool],
output_type=QAResult
)
baseline_agent_response = baseline_agent.run_sync("What item does Pikachu need to evolve?")
rprint("Baseline Agent Response:", baseline_agent_response.output)
graph_agent_response = graph_agent.run_sync("What item does Pikachu need to evolve?")
rprint("Graph Agent Response:", graph_agent_response.output)
14:22:22.897 baseline_agent run
baseline_agent run
14:22:22.963 chat openai/gpt-5
Baseline Agent Response: QAResult(answer='A Thunder Stone.', used_graph=False, evidence=[])
14:22:38.257 graph_agent run
14:22:38.257 chat openai/gpt-5
graph_agent run
14:22:52.487 running 1 tool
14:22:52.488 running tool: graph_search_tool
graph_agent run
running 1 tool
running tool: graph_search_tool
14:22:53.403 Graph search for 'Pikachu evolve item' returned 5 results.
14:22:53.412 Result 1: Pichu evolves into Pikachu with high friendship without needing an item.
14:22:53.421 Result 2: Pikachu uses Thunder Stone to evolve into Alolan Raichu in Alola
14:22:53.427 Result 3: Pikachu provides 2 EV yield in Speed
14:22:53.432 Result 4: Thunder Stone is required for Pikachu to evolve into either Raichu or Alolan Raichu depending on the region.
14:22:53.437 Result 5: Pikachu uses Thunder Stone to evolve into Raichu outside Alola
14:22:53.439 chat openai/gpt-5
Graph Agent Response: QAResult( answer='Thunder Stone.', used_graph=True, evidence=['Pikachu needs a Thunder Stone to evolve into Raichu (or Alolan Raichu in Alola).'] )
By comparing the two outputs, we can clearly see the advantage of GraphRAG: it enables the model to cite real facts from the knowledge graph, leading to more precise, verifiable, and explainable answers — a crucial step toward trustworthy agentic reasoning.
Let’s see an example where multiple Graph searches are required.
baseline_agent_response = baseline_agent.run_sync("Which Pokémon that evolves from Pichu is weak against Ground-type opponents, and what move does it commonly use to counter this weakness?")
rprint("Baseline Agent Response:", baseline_agent_response.output)
graph_agent_response = graph_agent.run_sync("Which Pokémon that evolves from Pichu is weak against Ground-type opponents, and what move does it commonly use to counter this weakness?")
rprint("Graph Agent Response:", graph_agent_response.output)
14:23:03.705 baseline_agent run
baseline_agent run
14:23:03.744 chat openai/gpt-5
Baseline Agent Response: QAResult( answer='Pikachu (which evolves from Pichu) is weak to Ground types. It commonly uses Grass Knot to hit Ground-type foes (and sometimes Surf in games where it’s available).', used_graph=False, evidence=[] )
14:24:20.527 graph_agent run
14:24:20.530 chat openai/gpt-5
14:24:54.681 running 5 tools
14:24:54.681 running tool: graph_search_tool
14:24:54.681 running tool: graph_search_tool
14:24:54.681 running tool: graph_search_tool
14:24:54.681 running tool: graph_search_tool
14:24:54.681 running tool: graph_search_tool
running tool: graph_search_tool
14:24:55.814 Graph search for 'Pikachu type' returned 5 results.
14:24:55.815 Result 1: Pikachu is of Electric type
14:24:55.821 Result 2: Partner Pikachu is of Electric type
14:24:55.821 Result 3: Pikachu is an Electric type Mouse Pokémon.
14:24:55.832 Result 4: Partner Pikachu resists Electric type
14:24:55.832 Result 5: Pichu is an Electric type Pokémon
running tool: graph_search_tool
14:24:55.923 Graph search for 'Type effectiveness Grass vs Ground' returned 5 results.
14:24:55.933 Result 1: Charizard double-resists Grass type moves with quarter effectiveness
14:24:55.933 Result 2: Earth Power is a Ground type move.
14:24:55.943 Result 3: Pichu is weak to Ground
14:24:55.948 Result 4: Partner Pikachu is weak against Ground type
14:24:55.949 Result 5: Raichu is weak against Ground type attacks which deal double damage
running tool: graph_search_tool
14:24:56.027 Graph search for 'Pikachu weaknesses Ground' returned 5 results.
14:24:56.036 Result 1: Pichu is weak to Ground
14:24:56.040 Result 2: Pikachu is weak to Ground type attacks.
14:24:56.044 Result 3: Partner Pikachu is weak against Ground type
14:24:56.049 Result 4: Raichu is weak against Ground type attacks which deal double damage
14:24:56.054 Result 5: Alolan Raichu is weak to Ground type attacks
running tool: graph_search_tool
14:24:56.055 Graph search for 'Pikachu learns Grass Knot' returned 5 results.
14:24:56.059 Result 1: Pikachu can learn Grass Knot via TM in Pokémon Scarlet and Violet.
14:24:56.065 Result 2: Pichu learns Grass Knot by using TM81 in Pokémon Scarlet & Violet
14:24:56.068 Result 3: TM81 teaches Grass Knot to Raichu in Pokémon Scarlet & Violet
14:24:56.068 Result 4: Pichu is compatible with TM81 to learn Grass Knot in Pokémon Scarlet & Violet
14:24:56.078 Result 5: Alolan Raichu can learn Grass Knot using TM81 in Pokémon Scarlet & Violet.
running tool: graph_search_tool
14:24:56.082 Graph search for 'Pikachu evolves from Pichu' returned 5 results.
14:24:56.087 Result 1: Pichu is the baby form in the Pikachu evolutionary line
14:24:56.091 Result 2: Pichu evolves into Pikachu with high friendship without needing an item.
14:24:56.096 Result 3: Pikachu uses Thunder Stone to evolve into Alolan Raichu in Alola
14:24:56.099 Result 4: Pichu is an Electric type Pokémon
14:24:56.099 Result 5: Pikachu evolves into Alolan Raichu by using a Thunder Stone in Alola.
14:24:56.099 chat openai/gpt-5
14:25:57.806 running 1 tool
14:25:57.806 running tool: graph_search_tool
14:25:58.992 Graph search for 'Grass type is super effective against Ground type' returned 5 results.
14:25:59.007 Result 1: Raichu is weak against Ground type attacks which deal double damage
14:25:59.013 Result 2: Earth Power is a Ground type move.
14:25:59.013 Result 3: Pikachu is weak to Ground type attacks.
14:25:59.023 Result 4: Energy Ball is a Grass type move.
14:25:59.025 Result 5: Grass Knot is a Grass type move.
14:25:59.029 chat openai/gpt-5
Graph Agent Response: QAResult( answer='Pikachu (which evolves from Pichu) is weak to Ground-type foes. A common way it counters this is by using the Grass-type move Grass Knot, which hits Ground types super effectively.', used_graph=True, evidence=[ 'Pichu EVOLVES_TO Pikachu', 'Pikachu HAS_TYPE Electric', 'Pikachu WEAK_AGAINST Ground', 'Pikachu LEARNS_MOVE Grass Knot', 'Grass Knot RELATES_TO Grass' ] )
However, there might be information that got missed when creating the knowledge graph. If the agent only depends on this graph, it might be unable to respond to specific queries.
Let’s see that in practice.
graph_agent_response = graph_agent.run_sync("What does bulbasaur evolve into?")
rprint("Graph Agent Response:", graph_agent_response.output)
15:05:37.985 graph_agent run
15:05:37.986 chat openai/gpt-5
15:05:53.216 running 1 tool
15:05:53.216 running tool: graph_search_tool
15:05:54.451 Graph search for 'Bulbasaur evolves into' returned 5 results.
15:05:54.461 Result 1: Charmander evolves into Charmeleon at level 16
15:05:54.467 Result 2: Charmeleon evolves into Charizard at level 36
15:05:54.471 Result 3: Slowpoke evolves into Slowbro at level 37
15:05:54.471 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:05:54.481 Result 5: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:05:54.485 chat openai/gpt-5
graph_agent run
15:06:04.062 running 1 tool
15:06:04.063 running tool: graph_search_tool
graph_agent run
running 1 tool
running tool: graph_search_tool
15:06:05.478 Graph search for 'Bulbasaur evolves into' returned 10 results.
15:06:05.483 Result 1: Charmander evolves into Charmeleon at level 16
15:06:05.488 Result 2: Charmeleon evolves into Charizard at level 36
15:06:05.492 Result 3: Slowpoke evolves into Slowbro at level 37
15:06:05.497 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:06:05.502 Result 5: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:05.506 Result 6: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:06:05.512 Result 7: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:06:05.515 Result 8: Galarian Slowpoke evolves into Galarian Slowking when Galarica Wreath is used on it
15:06:05.520 Result 9: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:05.525 Result 10: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
graph_agent run
15:06:05.773 chat openai/gpt-5
graph_agent run
15:06:21.814 running 1 tool
graph_agent run
running 1 tool
15:06:22.070 running tool: graph_search_tool
graph_agent run
running 1 tool
running tool: graph_search_tool
15:06:22.970 Graph search for 'Bulbasaur evolves into Ivysaur' returned 5 results.
15:06:22.970 Result 1: Charmander evolves into Charmeleon at level 16
15:06:22.980 Result 2: Charmeleon evolves into Charizard at level 36
15:06:22.980 Result 3: Slowpoke evolves into Slowbro at level 37
15:06:22.991 Result 4: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:22.996 Result 5: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:22.996 chat openai/gpt-5
15:06:32.411 running 1 tool
15:06:32.411 running tool: graph_search_tool
15:06:33.395 Graph search for 'Bulbasaur' returned 10 results.
15:06:33.395 Result 1: Charmander has the Fire type
15:06:33.405 Result 2: Pichu is an Electric type Pokémon
15:06:33.405 Result 3: Energy Ball is a Grass type move.
15:06:33.415 Result 4: Pichu learns Present as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:06:33.420 Result 5: Pichu appears in Pokémon Scarlet & Violet
15:06:33.420 Result 6: Pichu learns Grass Knot by using TM81 in Pokémon Scarlet & Violet
15:06:33.426 Result 7: Pichu is compatible with TM81 to learn Grass Knot in Pokémon Scarlet & Violet
15:06:33.426 Result 8: Charizard has the Fire type
15:06:33.439 Result 9: Pikachu is an Electric-type Pokemon
15:06:33.444 Result 10: Pikachu has Present as an egg move via breeding or picnics
15:06:33.447 chat openai/gpt-5
graph_agent run
15:06:48.201 running 6 tools
15:06:48.201 running tool: graph_search_tool
15:06:48.201 running tool: graph_search_tool
15:06:48.201 running tool: graph_search_tool
15:06:48.201 running tool: graph_search_tool
15:06:48.201 running tool: graph_search_tool
15:06:48.201 running tool: graph_search_tool
15:06:49.202 Graph search for 'Bulbasaur' returned 10 results.
15:06:49.210 Result 1: Charmander has the Fire type
15:06:49.217 Result 2: Pichu is an Electric type Pokémon
15:06:49.220 Result 3: Energy Ball is a Grass type move.
15:06:49.220 Result 4: Pichu learns Present as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:06:49.230 Result 5: Pichu appears in Pokémon Scarlet & Violet
15:06:49.238 Result 6: Pichu learns Grass Knot by using TM81 in Pokémon Scarlet & Violet
15:06:49.241 Result 7: Pichu is compatible with TM81 to learn Grass Knot in Pokémon Scarlet & Violet
15:06:49.241 Result 8: Charizard has the Fire type
15:06:49.252 Result 9: Pikachu is an Electric-type Pokemon
15:06:49.259 Result 10: Pikachu has Present as an egg move via breeding or picnics
running tool: graph_search_tool
15:06:49.261 Graph search for 'Bulbasaur evolves into' returned 10 results.
15:06:49.267 Result 1: Charmander evolves into Charmeleon at level 16
15:06:49.272 Result 2: Charmeleon evolves into Charizard at level 36
15:06:49.277 Result 3: Slowpoke evolves into Slowbro at level 37
15:06:49.281 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:06:49.287 Result 5: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.292 Result 6: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:06:49.298 Result 7: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:06:49.304 Result 8: Galarian Slowpoke evolves into Galarian Slowking when Galarica Wreath is used on it
15:06:49.310 Result 9: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.315 Result 10: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:06:49.317 Graph search for 'Bulbasaur -> Ivysaur' returned 10 results.
15:06:49.322 Result 1: Charmander evolves into Charmeleon at level 16
15:06:49.327 Result 2: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.332 Result 3: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
15:06:49.337 Result 4: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.341 Result 5: Pichu learns Play Nice by level-up at level 4 in Pokémon Scarlet & Violet
15:06:49.345 Result 6: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:06:49.349 Result 7: Slowpoke learns Water Gun by level-up at level 6 in Pokémon Scarlet & Violet
15:06:49.354 Result 8: Galarian Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:06:49.358 Result 9: Pichu learns Thunder Shock by level-up at level 1 in Pokémon Scarlet & Violet
15:06:49.363 Result 10: Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:06:49.508 Graph search for 'Ivysaur evolves from' returned 10 results.
15:06:49.508 Result 1: Charmander evolves into Charmeleon at level 16
15:06:49.519 Result 2: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.526 Result 3: Charmeleon evolves into Charizard at level 36
15:06:49.529 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:06:49.535 Result 5: Pichu learns Nasty Plot by level-up at level 16 and by using TM140 in Pokémon Scarlet & Violet
15:06:49.539 Result 6: Pichu learns Sweet Kiss by level-up at level 8 in Pokémon Scarlet & Violet
15:06:49.539 Result 7: Slowpoke evolves into Slowbro at level 37
15:06:49.549 Result 8: Pichu learns Present as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:06:49.556 Result 9: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:06:49.561 Result 10: Galarian Slowpoke learns Rain Dance by level-up at level 42 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:06:49.563 Graph search for 'Bulbasaur evolution' returned 10 results.
15:06:49.568 Result 1: Charmander evolves into Charmeleon at level 16
15:06:49.570 Result 2: Charmeleon evolves into Charizard at level 36
15:06:49.570 Result 3: Slowpoke evolves into Slowbro at level 37
15:06:49.580 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:06:49.588 Result 5: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.590 Result 6: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.590 Result 7: Slowpoke evolves into Slowking when traded while holding Kings Rock
15:06:49.603 Result 8: Pichu is the baby form in the Pikachu evolutionary line
15:06:49.605 Result 9: Pichu learns Present as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:06:49.611 Result 10: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:06:49.688 Graph search for 'Ivysaur' returned 10 results.
15:06:49.695 Result 1: Energy Ball is a Grass type move.
15:06:49.695 Result 2: TM140 teaches Nasty Plot to Raichu in Pokémon Scarlet & Violet
15:06:49.705 Result 3: Galarian Slowpoke learns Yawn by level-up at level 9 in Pokémon Scarlet & Violet
15:06:49.705 Result 4: Galarian Slowpoke learns Rain Dance by level-up at level 42 in Pokémon Scarlet & Violet
15:06:49.705 Result 5: Pichu learns Play Nice by level-up at level 4 in Pokémon Scarlet & Violet
15:06:49.719 Result 6: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.719 Result 7: In Pokémon Scarlet & Violet, Charizard learns Helping Hand.
15:06:49.728 Result 8: Galarian Slowpoke learns Psych Up by level-up at level 39 in Pokémon Scarlet & Violet
15:06:49.735 Result 9: Galarian Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:06:49.738 Result 10: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:06:49.738 chat openai/gpt-5
graph_agent run
15:09:18.316 running 8 tools
15:09:18.316 running tool: graph_search_tool
15:09:18.316 running tool: graph_search_tool
15:09:18.316 running tool: graph_search_tool
15:09:18.316 running tool: graph_search_tool
15:09:18.324 running tool: graph_search_tool
15:09:18.326 running tool: graph_search_tool
15:09:18.326 running tool: graph_search_tool
15:09:18.326 running tool: graph_search_tool
running tool: graph_search_tool
15:09:19.291 Graph search for 'Bulbasaur -> Ivysaur' returned 10 results.
15:09:19.297 Result 1: Charmander evolves into Charmeleon at level 16
15:09:19.297 Result 2: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:09:19.308 Result 3: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
15:09:19.308 Result 4: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:09:19.318 Result 5: Pichu learns Play Nice by level-up at level 4 in Pokémon Scarlet & Violet
15:09:19.318 Result 6: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:09:19.324 Result 7: Slowpoke learns Water Gun by level-up at level 6 in Pokémon Scarlet & Violet
15:09:19.328 Result 8: Galarian Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:09:19.328 Result 9: Pichu learns Thunder Shock by level-up at level 1 in Pokémon Scarlet & Violet
15:09:19.341 Result 10: Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:09:19.369 Graph search for 'Bulbasaur evolves into' returned 10 results.
15:09:19.378 Result 1: Charmander evolves into Charmeleon at level 16
15:09:19.383 Result 2: Charmeleon evolves into Charizard at level 36
15:09:19.387 Result 3: Slowpoke evolves into Slowbro at level 37
15:09:19.391 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:09:19.396 Result 5: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:09:19.401 Result 6: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:09:19.408 Result 7: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:09:19.414 Result 8: Galarian Slowpoke evolves into Galarian Slowking when Galarica Wreath is used on it
15:09:19.418 Result 9: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:09:19.423 Result 10: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:09:19.476 Graph search for 'Bulbasaur evolution line' returned 10 results.
15:09:19.481 Result 1: Pichu is the baby form in the Pikachu evolutionary line
15:09:19.485 Result 2: Charmander evolves into Charmeleon at level 16
15:09:19.489 Result 3: Pichu evolves into Pikachu with high friendship without needing an item.
15:09:19.493 Result 4: Charmeleon evolves into Charizard at level 36
15:09:19.498 Result 5: Slowpoke can learn Belch via egg move in Pokémon Scarlet & Violet
15:09:19.502 Result 6: Galarian Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:09:19.507 Result 7: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:09:19.511 Result 8: Pichu learns Thunder Shock by level-up at level 1 in Pokémon Scarlet & Violet
15:09:19.516 Result 9: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
15:09:19.520 Result 10: Slowpoke evolves into Slowbro at level 37
running tool: graph_search_tool
15:09:19.565 Graph search for 'Venusaur evolves from Ivysaur' returned 10 results.
15:09:19.575 Result 1: Charmander evolves into Charmeleon at level 16
15:09:19.576 Result 2: Charmeleon evolves into Charizard at level 36
15:09:19.576 Result 3: Slowpoke evolves into Slowbro at level 37
15:09:19.590 Result 4: Galarian Slowpoke evolves into Galarian Slowking when Galarica Wreath is used on it
15:09:19.595 Result 5: Slowpoke evolves into Slowking when traded while holding Kings Rock
15:09:19.599 Result 6: Pikachu learns Spark by level-up at level 20 in Pokémon Scarlet & Violet.
15:09:19.604 Result 7: Pichu learns Sweet Kiss by level-up at level 8 in Pokémon Scarlet & Violet
15:09:19.608 Result 8: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:09:19.612 Result 9: Pichu evolves into Pikachu with high friendship without needing an item.
15:09:19.617 Result 10: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:09:19.835 Graph search for 'Bulbasaur level 16 evolves' returned 10 results.
15:09:19.841 Result 1: Charmander evolves into Charmeleon at level 16
15:09:19.845 Result 2: Charmeleon evolves into Charizard at level 36
15:09:19.845 Result 3: Slowpoke evolves into Slowbro at level 37
15:09:19.858 Result 4: Pikachu learns Spark by level-up at level 16 in Pokémon Legends: Z-A
15:09:19.858 Result 5: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:09:19.866 Result 6: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:09:19.866 Result 7: Pichu learns Nasty Plot by level-up at level 16 and by using TM140 in Pokémon Scarlet & Violet
15:09:19.877 Result 8: Pikachu learns Spark by level-up at level 20 in Pokémon Scarlet & Violet.
15:09:19.880 Result 9: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:09:19.880 Result 10: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Legends: Z-A.
running tool: graph_search_tool
15:09:19.886 Graph search for 'Ivysaur evolves from' returned 10 results.
15:09:19.892 Result 1: Charmander evolves into Charmeleon at level 16
15:09:19.897 Result 2: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:09:19.897 Result 3: Charmeleon evolves into Charizard at level 36
15:09:19.897 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:09:19.909 Result 5: Pichu learns Nasty Plot by level-up at level 16 and by using TM140 in Pokémon Scarlet & Violet
15:09:19.909 Result 6: Pichu learns Sweet Kiss by level-up at level 8 in Pokémon Scarlet & Violet
15:09:19.917 Result 7: Slowpoke evolves into Slowbro at level 37
15:09:19.924 Result 8: Pichu learns Present as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:09:19.929 Result 9: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:09:19.934 Result 10: Galarian Slowpoke learns Rain Dance by level-up at level 42 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:09:19.934 Graph search for 'Bulbasaur Ivysaur' returned 10 results.
15:09:19.941 Result 1: Charmander evolves into Charmeleon at level 16
15:09:19.945 Result 2: Pichu learns Grass Knot by using TM81 in Pokémon Scarlet & Violet
15:09:19.949 Result 3: Energy Ball is a Grass type move.
15:09:19.949 Result 4: Pichu learns Thunder Shock by level-up at level 1 in Pokémon Scarlet & Violet
15:09:19.960 Result 5: Slowpoke learns Tackle by level-up at level 1 in Pokémon Scarlet & Violet
15:09:19.961 Result 6: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
15:09:19.961 Result 7: Pichu learns Play Nice by level-up at level 4 in Pokémon Scarlet & Violet
15:09:19.970 Result 8: Galarian Slowpoke learns Heal Pulse by level-up at level 45 in Pokémon Scarlet & Violet
15:09:19.977 Result 9: Galarian Slowpoke learns Water Pulse by level-up at level 18 in Pokémon Scarlet & Violet
15:09:19.981 Result 10: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:09:20.059 Graph search for 'Ivysaur -> Venusaur' returned 10 results.
15:09:20.063 Result 1: TM229 teaches Upper Hand to Raichu in Pokémon Scarlet & Violet
15:09:20.063 Result 2: TM140 teaches Nasty Plot to Raichu in Pokémon Scarlet & Violet
15:09:20.078 Result 3: TM103 teaches Substitute to Raichu in Pokémon Scarlet & Violet
15:09:20.079 Result 4: TM81 teaches Grass Knot to Raichu in Pokémon Scarlet & Violet
15:09:20.083 Result 5: Energy Ball is a Grass type move.
15:09:20.091 Result 6: Pichu learns Grass Knot by using TM81 in Pokémon Scarlet & Violet
15:09:20.097 Result 7: Galarian Slowpoke learns Yawn by level-up at level 9 in Pokémon Scarlet & Violet
15:09:20.102 Result 8: Galarian Slowpoke learns Rain Dance by level-up at level 42 in Pokémon Scarlet & Violet
15:09:20.106 Result 9: TM205 teaches Endeavor to Raichu in Pokémon Scarlet & Violet
15:09:20.108 Result 10: TM130 teaches Helping Hand to Raichu in Pokémon Scarlet & Violet
15:09:20.114 chat openai/gpt-5
graph_agent run
15:10:04.186 running 4 tools
15:10:04.187 running tool: graph_search_tool
15:10:04.189 running tool: graph_search_tool
15:10:04.190 running tool: graph_search_tool
15:10:04.192 running tool: graph_search_tool
running tool: graph_search_tool
15:10:05.380 Graph search for 'Bulbasaur evolves into' returned 20 results.
15:10:05.384 Result 1: Charmander evolves into Charmeleon at level 16
15:10:05.390 Result 2: Charmeleon evolves into Charizard at level 36
15:10:05.390 Result 3: Slowpoke evolves into Slowbro at level 37
15:10:05.401 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:10:05.401 Result 5: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:10:05.410 Result 6: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:10:05.417 Result 7: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:10:05.421 Result 8: Galarian Slowpoke evolves into Galarian Slowking when Galarica Wreath is used on it
15:10:05.426 Result 9: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:10:05.433 Result 10: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
15:10:05.438 Result 11: Pichu learns Sweet Kiss by level-up at level 8 in Pokémon Scarlet & Violet
15:10:05.442 Result 12: Pichu learns Thunder Shock by level-up at level 1 in Pokémon Scarlet & Violet
15:10:05.447 Result 13: Galarian Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:10:05.452 Result 14: Charmander has the Fire type
15:10:05.453 Result 15: Pichu learns Tera Blast by using TM171 in Pokémon Scarlet & Violet
15:10:05.462 Result 16: Pikachu learns Nasty Plot by level-up at level 1 in Pokémon Scarlet & Violet.
15:10:05.467 Result 17: Pichu learns Play Nice by level-up at level 4 in Pokémon Scarlet & Violet
15:10:05.472 Result 18: Slowpoke evolves into Slowking when traded while holding Kings Rock
15:10:05.476 Result 19: Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:10:05.482 Result 20: Slowpoke learns Tackle by level-up at level 1 in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:10:05.486 Graph search for 'Ivysaur evolves from' returned 20 results.
15:10:05.491 Result 1: Charmander evolves into Charmeleon at level 16
15:10:05.495 Result 2: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:10:05.496 Result 3: Charmeleon evolves into Charizard at level 36
15:10:05.503 Result 4: Pichu evolves into Pikachu with high friendship without needing an item.
15:10:05.503 Result 5: Pichu learns Nasty Plot by level-up at level 16 and by using TM140 in Pokémon Scarlet & Violet
15:10:05.503 Result 6: Pichu learns Sweet Kiss by level-up at level 8 in Pokémon Scarlet & Violet
15:10:05.517 Result 7: Slowpoke evolves into Slowbro at level 37
15:10:05.517 Result 8: Pichu learns Present as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:10:05.527 Result 9: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:10:05.527 Result 10: Galarian Slowpoke learns Rain Dance by level-up at level 42 in Pokémon Scarlet & Violet
15:10:05.536 Result 11: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:10:05.541 Result 12: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:10:05.545 Result 13: Galarian Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:10:05.550 Result 14: Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:10:05.555 Result 15: Slowpoke learns Yawn by level-up at level 9 in Pokémon Scarlet & Violet
15:10:05.559 Result 16: Slowpoke learns Rain Dance by level-up at level 42 in Pokémon Scarlet & Violet
15:10:05.559 Result 17: Galarian Slowpoke learns Curse by level-up at level 1 in Pokémon Scarlet & Violet
15:10:05.569 Result 18: Galarian Slowpoke learns Yawn by level-up at level 9 in Pokémon Scarlet & Violet
15:10:05.571 Result 19: Slowpoke learns Curse by level-up at level 1 in Pokémon Scarlet & Violet
15:10:05.579 Result 20: Pichu learns Tickle as an egg move via breeding or picnics in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:10:05.785 Graph search for 'Ivysaur' returned 20 results.
15:10:05.785 Result 1: Energy Ball is a Grass type move.
15:10:05.792 Result 2: TM140 teaches Nasty Plot to Raichu in Pokémon Scarlet & Violet
15:10:05.801 Result 3: Galarian Slowpoke learns Yawn by level-up at level 9 in Pokémon Scarlet & Violet
15:10:05.802 Result 4: Galarian Slowpoke learns Rain Dance by level-up at level 42 in Pokémon Scarlet & Violet
15:10:05.802 Result 5: Pichu learns Play Nice by level-up at level 4 in Pokémon Scarlet & Violet
15:10:05.813 Result 6: Galarian Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:10:05.818 Result 7: In Pokémon Scarlet & Violet, Charizard learns Helping Hand.
15:10:05.823 Result 8: Galarian Slowpoke learns Psych Up by level-up at level 39 in Pokémon Scarlet & Violet
15:10:05.823 Result 9: Galarian Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:10:05.835 Result 10: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:10:05.841 Result 11: TM130 teaches Helping Hand to Raichu in Pokémon Scarlet & Violet
15:10:05.845 Result 12: Pichu is compatible with TM140 to learn Nasty Plot in Pokémon Scarlet & Violet
15:10:05.850 Result 13: Pichu learns Tickle as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:10:05.854 Result 14: Galarian Slowpoke learns Growl by level-up at level 3 in Pokémon Scarlet & Violet
15:10:05.854 Result 15: Pichu learns Grass Knot by using TM81 in Pokémon Scarlet & Violet
15:10:05.864 Result 16: Slowpoke learns Zen Headbutt by level-up at level 24 in Pokémon Scarlet & Violet
15:10:05.869 Result 17: In Pokémon Scarlet & Violet, Charizard learns Tera Blast.
15:10:05.875 Result 18: Pichu learns Tera Blast by using TM171 in Pokémon Scarlet & Violet
15:10:05.875 Result 19: Slowpoke learns Yawn by level-up at level 9 in Pokémon Scarlet & Violet
15:10:05.884 Result 20: TM81 teaches Grass Knot to Raichu in Pokémon Scarlet & Violet
running tool: graph_search_tool
15:10:05.970 Graph search for 'Bulbasaur' returned 20 results.
15:10:05.977 Result 1: Charmander has the Fire type
15:10:05.977 Result 2: Pichu is an Electric type Pokémon
15:10:05.987 Result 3: Energy Ball is a Grass type move.
15:10:05.992 Result 4: Pichu learns Present as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:10:05.996 Result 5: Pichu appears in Pokémon Scarlet & Violet
15:10:06.002 Result 6: Pichu learns Grass Knot by using TM81 in Pokémon Scarlet & Violet
15:10:06.007 Result 7: Pichu is compatible with TM81 to learn Grass Knot in Pokémon Scarlet & Violet
15:10:06.010 Result 8: Charizard has the Fire type
15:10:06.016 Result 9: Pikachu is an Electric-type Pokemon
15:10:06.021 Result 10: Pikachu has Present as an egg move via breeding or picnics
15:10:06.026 Result 11: Pichu learns Thunder Shock by level-up at level 1 in Pokémon Scarlet & Violet
15:10:06.029 Result 12: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
15:10:06.035 Result 13: Pikachu belongs to the Field egg group
15:10:06.040 Result 14: Pichu learns Tickle as an egg move via breeding or picnics in Pokémon Scarlet & Violet
15:10:06.044 Result 15: Slowpoke is weak against Grass
15:10:06.048 Result 16: Pikachu is of Electric type
15:10:06.051 Result 17: Pichu learns Play Nice by level-up at level 4 in Pokémon Scarlet & Violet
15:10:06.051 Result 18: Partner Pikachu resists Electric type
15:10:06.059 Result 19: Slowpoke learns Tackle by level-up at level 1 in Pokémon Scarlet & Violet
15:10:06.068 Result 20: Pikachu can be encountered in Viridian Forest in Red/Blue
15:10:06.070 chat openai/gpt-5
Graph Agent Response: QAResult( answer='I’m unsure — the knowledge graph doesn’t show Bulbasaur’s evolution.', used_graph=False, evidence=[] )
🌱 Building a Context-Aware Graph Augmentation Agent#
So far, our knowledge graph has been static — it only knows what we explicitly extracted earlier. But in real-world scenarios, users often ask questions that require knowledge not yet stored in the graph
(e.g., “What does Bulbasaur evolve into?” if that evolution chain wasn’t previously extracted).
To solve this, we’ll create a contextual episode generator agent that can:
Search the existing graph for relevant information using
graph_search_tool.Detect knowledge gaps — when the graph lacks the facts required to answer.
Read and segment raw
.mdfiles (e.g.,bulbasaur.md,pikachu.md) to locate the missing context.Generate new, schema-compliant episodes containing just the necessary information.
Add those new episodes back into the graph, expanding it dynamically.
Re-query the graph to verify the new knowledge is now present.
This demonstrates the concept of query-aware graph augmentation — a powerful pattern where agents can read, reason, and write back into the graph in real time.
⚙️ We will later use this agent as a tool within a higher-level Graph Augmentation Agent, allowing an LLM to autonomously expand the Pokémon knowledge graph whenever it encounters missing information.
from pydantic import BaseModel, Field, field_validator
from pydantic_ai import ModelRetry
ALLOWED_MD_FILES = ['charizard.md', 'pikachu.md', 'pichu.md', 'mewtwo.md', 'bulbasaur.md']
def read_md_file(file_path: str) -> str:
if file_path not in ALLOWED_MD_FILES:
raise ModelRetry("Invalid file path. Allowed files: " + ", ".join(ALLOWED_MD_FILES))
logfire.info(f"Reading markdown file: {file_path}")
file_path = ("data/pokemon_md/" if file_path != "bulbasaur.md" else "data/pokemon_md_extended/") + file_path
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
async def add_episode_to_graph(gep: Episode):
logfire.info(f"Adding episode to graph: {gep.episode_body}")
await graphiti.add_episode(name=gep.name,
episode_body=gep.episode_body,
source_description=gep.source_description,
source=EpisodeType.text,
reference_time=datetime.now(),
group_id="pokemon_data_tmp",
entity_types=entity_types,
edge_types=edge_types,
edge_type_map=edge_type_map)
contextual_episode_generator = Agent(
model="openrouter:openai/gpt-5",
system_prompt=(
"""
You are an expert segmenter. Generate a Pokémon-related context into a coherent episode. You have access to the raw .md files and also the graph search tool.
You need to only generate episode for the parts that are required to provide responses to the user query.
First search the graph, if info absent, generate episodes for the missing info that are added to the graph and then verify by searching the graph again.
Rules:
- Prioritize coherence over exact length.
- Each episode must be self-contained: enough detail so downstream IE can extract entities/relations without cross-episode references.
- Prefer semantic boundaries: scene changes, locations, battles, new characters/pokemon, or topic shifts.
- Titles should be short, unique, and descriptive.
- Respect chronology if provided; otherwise, group by topical coherence.
- Keep `source_description="episode"` unless the input explicitly suggests otherwise (e.g., 'movie recap', 'blog post', etc.).
- NEVER fabricate content beyond the given text. If info is uncertain, omit it.
- Technical Machines (TMs) are items. Moves can be learnt by TMs or level-ups.
- Include all information required to create ontological entities and edges, in an episode in precise natural language.
Ontology (Schema to follow strictly)
- **Entities**
- `Pokemon` — fields: `stage` (int, e.g., Pichu=1, Pikachu=2, Raichu=3)
- `Type` — fields: `category` (str, optional damage class/group)
- `Move` — fields: `power` (int, optional), `move_type` (str, e.g., Electric)
- `Item` — fields: `effect` (str, short description)
- **Edges (directed)**
- `HAS_TYPE` : `Pokemon → Type`
- `EVOLVES_TO` : `Pokemon → Pokemon` (attr: `method` e.g., friendship/level, `level`=int)
- `NEEDS_ITEM` : `Pokemon → Item` (attr: `reason`, e.g., evolve)
- `LEARNS_MOVE` : `Pokemon → Move` (attrs: `learn_method`=TM/TR/Level-up, `level`=int)
- `WEAK_AGAINST` : `Pokemon → Type` (attr: `note`)
- `RESISTS` : `Pokemon → Type` (attr: `note`)
- **Allowed pairs**
- `(Pokemon, Type) → {HAS_TYPE, WEAK_AGAINST, RESISTS}`
- `(Pokemon, Pokemon) → {EVOLVES_TO}`
- `(Pokemon, Item) → {NEEDS_ITEM}`
- `(Pokemon, Move) → {LEARNS_MOVE}`
**Constraints**
- Use only the predicates listed above.
- Subjects/objects must match the domain/range shown.
- Use **exact surface names** from the text (no fabrication).
- Prefer concise `fact` strings; omit if redundant.
- If uncertain, omit rather than guess.
**Output contract**
- Extract entities and edges that conform to this ontology.
- Return JSON with:
- `entities`: list of unique entity names (strings)
- `triples`: list of objects with fields
- `subject` (str), `predicate` (one of the allowed), `object` (str)
- optional: `fact` (str), `confidence` (0..1)
Episode body should be in natural language text, not json. Include information for all fields where possible. Output your working steps.
"""
),
tools=[graph_search_tool, read_md_file, add_episode_to_graph]
)
response = contextual_episode_generator.run_sync("What does bulbasaur evolve into?")
rprint(response.output)
15:21:29.334 contextual_episode_generator run
15:21:29.336 chat openai/gpt-5
15:21:41.732 running 1 tool
15:21:41.732 running tool: graph_search_tool
15:21:42.663 Graph search for 'Bulbasaur EVOLVES_TO' returned 5 results.
15:21:42.665 Result 1: Charmander evolves into Charmeleon at level 16
15:21:42.674 Result 2: Slowpoke evolves into Slowbro at level 37
15:21:42.674 Result 3: Charmeleon evolves into Charizard at level 36
15:21:42.684 Result 4: Mewtwo can mega evolve into Mega Mewtwo X in battle
15:21:42.684 Result 5: Pikachu provides 2 EV yield in Speed
15:21:42.684 chat openai/gpt-5
15:22:05.027 running 1 tool
15:22:05.027 running tool: graph_search_tool
15:22:06.247 Graph search for 'Bulbasaur EVOLVES_TO' returned 5 results.
15:22:06.254 Result 1: Charmander evolves into Charmeleon at level 16
15:22:06.258 Result 2: Slowpoke evolves into Slowbro at level 37
15:22:06.263 Result 3: Charmeleon evolves into Charizard at level 36
15:22:06.268 Result 4: Mewtwo can mega evolve into Mega Mewtwo X in battle
15:22:06.274 Result 5: Pikachu provides 2 EV yield in Speed
15:22:06.276 chat openai/gpt-5
15:22:27.314 running 1 tool
15:22:27.314 running tool: add_episode_to_graph
15:22:27.315 Adding episode to graph: Bulbasaur is a stage 1 Pokémon. It has the Grass and Poison ty...cur through leveling and do not require any evolutionary item.
15:23:04.381 chat openai/gpt-5
15:23:05.894 running 1 tool
15:23:05.894 running tool: graph_search_tool
15:23:06.787 Graph search for 'Bulbasaur evolves into Ivysaur' returned 5 results.
15:23:06.793 Result 1: Bulbasaur evolves into Ivysaur by level-up at level 16
15:23:06.798 Result 2: Ivysaur evolves into Venusaur by level-up at level 32
15:23:06.803 Result 3: Bulbasaur has the Grass type
15:23:06.808 Result 4: Bulbasaur has the Poison type
15:23:06.813 Result 5: Charmander evolves into Charmeleon at level 16
15:23:06.813 chat openai/gpt-5
Short answer: Bulbasaur evolves into Ivysaur (at level 16). Working steps - Searched the graph for “Bulbasaur EVOLVES_TO” and found no entry. - Created a concise, self-contained episode describing Bulbasaur’s evolution line and key details (types, stages, levels). - Verified by searching the graph again; the Bulbasaur → Ivysaur relation was present with levels. Episode title: Bulbasaur evolves into Ivysaur Episode body: Bulbasaur is a stage 1 Pokémon. It has the Grass and Poison types. Bulbasaur evolves into Ivysaur by level-up when it reaches level 16. Ivysaur is a stage 2 Pokémon. Ivysaur evolves into Venusaur by level-up when it reaches level 32. Venusaur is a stage 3 Pokémon. These evolutions occur through leveling and do not require any evolutionary item. Extracted JSON { "entities": [ "Bulbasaur", "Ivysaur", "Venusaur", "Grass", "Poison" ], "triples": [ { "subject": "Bulbasaur", "predicate": "HAS_TYPE", "object": "Grass", "fact": "Bulbasaur has the Grass type", "confidence": 0.95 }, { "subject": "Bulbasaur", "predicate": "HAS_TYPE", "object": "Poison", "fact": "Bulbasaur has the Poison type", "confidence": 0.95 }, { "subject": "Bulbasaur", "predicate": "EVOLVES_TO", "object": "Ivysaur", "fact": "Bulbasaur evolves into Ivysaur by level-up at level 16", "confidence": 0.95 }, { "subject": "Ivysaur", "predicate": "EVOLVES_TO", "object": "Venusaur", "fact": "Ivysaur evolves into Venusaur by level-up at level 32", "confidence": 0.95 } ] }
The agent successfully demonstrated query-aware graph augmentation:
🧩 Reasoning process:
The model first searched the graph for the relationshipBulbasaur EVOLVES_TOand found nothing. Realizing the gap, it retrieved and read the relevant markdown file (bulbasaur.md), generated a concise natural-language episode describing Bulbasaur’s full evolution chain, and then added that episode back into the graph.⚙️ Verification:
After augmentation, the agent re-ran a graph search and confirmed thatBulbasaur → EVOLVES_TO → Ivysaur(and further toVenusaur) now exists, complete with level-based evolution attributes.📚 Structured extraction:
The finalExtracted JSONshows how Graphiti (through the agent) derived clean, ontology-compliant triples — precisely aligned with the schema (HAS_TYPE,EVOLVES_TO, etc.), each with a confidence score.
This illustrates how GraphRAG systems can go beyond static knowledge: they can detect missing information, retrieve supporting context, and evolve their own graph — a key capability for self-improving, knowledge-grounded agents.
🧠 Creating the Graph Augmentation Agent#
We now bring everything together by defining a Graph Augment Agent — an intelligent agent that can query, detect missing knowledge, and expand the graph dynamically when needed.
Here’s what happens under the hood:
graph_augment()toolCalls the
contextual_episode_generatoragent you built earlier.If the current graph lacks facts needed to answer a query, it automatically creates new episodes (from the raw
.mdfiles), extracts triples, and inserts them into the knowledge graph.
graph_augment_agentTries to answer the user’s question using the existing graph first.
If it detects missing relations or entities, it invokes the augmentation tool to enrich the graph in real time, and then retries the query.
Returns a structured
QAResultwith the final answer and the evidence triples used.
This design demonstrates query-aware reasoning and self-improving graphs — where an LLM not only consumes knowledge but also curates and expands it as it answers.
In this example, we ask:
“At what level does Bulbasaur learn Double-Edge?”
If that relation isn’t present in the graph initially, the agent will augment the graph from the Pokémon markdown files, extract the missing learning relation, and then return a grounded, evidence-backed answer.
async def graph_augment(query: str):
response = await contextual_episode_generator.run(query)
return "Working: " + response.output
graph_augment_agent = Agent(
model="openrouter:openai/gpt-5",
system_prompt=(
"You have access to a Pokémon knowledge graph. Use it to answer the user's question. "
"If the graph does not have the information, you can augment the graph for a given query using the graph_augment tool. "
"Be concise. Populate the 'evidence' field with relevant graph facts used."
),
tools=[graph_search_tool, graph_augment],
output_type=QAResult
)
graph_augment_agent_response = graph_augment_agent.run_sync("At what level does bulbasaur learn double-edge?")
rprint("Graph Augment Agent Response:", graph_augment_agent_response.output)
15:34:28.096 graph_augment_agent run
15:34:28.098 chat openai/gpt-5
15:34:39.998 running 1 tool
15:34:39.998 running tool: graph_search_tool
15:34:41.257 Graph search for 'Bulbasaur Double-Edge level up learn level' returned 5 results.
15:34:41.262 Result 1: Bulbasaur evolves into Ivysaur by level-up at level 16
15:34:41.267 Result 2: Ivysaur evolves into Venusaur by level-up at level 32
15:34:41.272 Result 3: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:34:41.277 Result 4: Pikachu learns Double Team by level-up at level 8 in Pokémon Scarlet & Violet.
15:34:41.282 Result 5: Slowpoke learns Tackle by level-up at level 1 in Pokémon Scarlet & Violet
15:34:41.284 chat openai/gpt-5
15:34:47.830 running 1 tool
15:34:47.831 running tool: graph_augment
15:34:47.834 contextual_episode_generator run
15:34:47.838 chat openai/gpt-5
15:34:55.004 running 1 tool
15:34:55.005 running tool: graph_search_tool
15:34:55.791 Graph search for 'Bulbasaur learns Double-Edge level' returned 5 results.
15:34:55.795 Result 1: Bulbasaur evolves into Ivysaur by level-up at level 16
15:34:55.803 Result 2: In Pokémon Scarlet & Violet, Charizard learns Double-Edge.
15:34:55.806 Result 3: Pikachu learns Double Team by level-up at level 8 in Pokémon Scarlet & Violet.
15:34:55.806 Result 4: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:34:55.820 Result 5: Ivysaur evolves into Venusaur by level-up at level 32
15:34:55.822 chat openai/gpt-5
15:35:03.930 running 1 tool
15:35:03.931 running tool: graph_search_tool
15:35:05.079 Graph search for 'Bulbasaur Double-Edge level learnset' returned 10 results.
15:35:05.083 Result 1: Bulbasaur evolves into Ivysaur by level-up at level 16
15:35:05.083 Result 2: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Scarlet & Violet
15:35:05.094 Result 3: Pikachu learns Double Team by level-up at level 8 in Pokémon Scarlet & Violet.
15:35:05.097 Result 4: Pichu learns Tail Whip by level-up at level 1 in Pokémon Scarlet & Violet
15:35:05.103 Result 5: Slowpoke learns Tackle by level-up at level 1 in Pokémon Scarlet & Violet
15:35:05.103 Result 6: In Pokémon Scarlet & Violet, Charizard learns Double-Edge.
15:35:05.113 Result 7: Pichu learns Nuzzle by level-up at level 12 in Pokémon Scarlet & Violet
15:35:05.113 Result 8: Pichu learns Thunder Shock by level-up at level 1 in Pokémon Scarlet & Violet
15:35:05.123 Result 9: Slowpoke learns Headbutt by level-up at level 21 in Pokémon Legends: Z-A.
15:35:05.128 Result 10: Ivysaur evolves into Venusaur by level-up at level 32
15:35:05.130 chat openai/gpt-5
15:35:11.310 running 1 tool
15:35:11.318 running tool: read_md_file
15:35:11.321 chat openai/gpt-5
15:35:13.119 running 1 tool
15:35:13.119 running tool: read_md_file
15:35:13.119 Reading markdown file: bulbasaur.md
15:35:13.146 chat openai/gpt-5
15:35:44.683 running 1 tool
15:35:44.683 running tool: add_episode_to_graph
15:35:44.683 Adding episode to graph: In Pokémon Legends: Z-A, Bulbasaur learns the move Double-Edge...evel 45 in Legends: Z-A) or by TM (TM204 in Scarlet & Violet).
15:36:40.520 chat openai/gpt-5
15:36:42.463 running 1 tool
15:36:42.464 running tool: graph_search_tool
15:36:43.270 Graph search for 'Bulbasaur learns Double-Edge level 45 Legends: Z-A TM204 Scarlet Violet' returned 5 results.
15:36:43.277 Result 1: Bulbasaur learns Double-Edge by level-up at level 45 in Pokémon Legends: Z-A
15:36:43.286 Result 2: Bulbasaur can learn Double-Edge via Technical Machine TM204 in Pokémon Scarlet & Violet
15:36:43.289 Result 3: Charizard learns Double-Edge via Technical Machine TM56 in Pokémon Legends: Z-A.
15:36:43.289 Result 4: In Pokémon Scarlet & Violet, Charizard learns Double-Edge.
15:36:43.298 Result 5: Mewtwo can use TM56 to learn Double-Edge in Pokémon Legends: Z-A
15:36:43.302 chat openai/gpt-5
15:37:16.688 chat openai/gpt-5
Graph Augment Agent Response: QAResult( answer='Level 45 (in Pokémon Legends: Z-A). Note: In Scarlet & Violet, Bulbasaur doesn’t learn Double-Edge by level-up—it learns it via TM204.', used_graph=True, evidence=[ 'Bulbasaur learns Double-Edge by level-up at level 45 in Pokémon Legends: Z-A.', 'In Pokémon Scarlet & Violet, Bulbasaur learns Double-Edge via TM204 (not by level-up).' ] )
This output showcases the full reasoning cycle of our Graph Augment Agent — an autonomous system capable of searching, detecting gaps, retrieving new knowledge, and updating the graph before answering.
🧭 Step-by-step Breakdown
Initial Graph Search:
The agent first queried the Pokémon knowledge graph for “Bulbasaur Double-Edge level up learn level.” It found related facts (e.g., Pikachu and Charizard moves) but no entry for Bulbasaur. → The agent correctly inferred that the required information was missing.Graph Augmentation Triggered:
Realizing the graph lacked the answer, it invoked thegraph_augmenttool. This tool, in turn, called thecontextual_episode_generator, which:Read the relevant
.mdsource (bulbasaur.md)Extracted structured triples like
Bulbasaur → LEARNS_MOVE → Double-Edge (level=45)and
Bulbasaur → LEARNS_MOVE → Double-Edge (learn_method=TM204)Added them back into the graph.
Verification Pass:
After augmentation, the agent re-ran graph searches — this time retrieving the newly added facts about Bulbasaur’s move learnsets from Pokémon Legends: Z-A and Scarlet & Violet.Final Answer (Grounded in Graph):
The agent then synthesized the verified graph facts into a concise, factual answer:“Level 45 (in Pokémon Legends: Z-A).
Note: In Scarlet & Violet, Bulbasaur doesn’t learn Double-Edge by level-up—it learns it via TM204.”
📚 Key Takeaways
✅ Graph Reasoning: The agent first reasoned over existing graph facts.
⚙️ Self-Expansion: Upon detecting missing info, it autonomously augmented the graph.
🔁 Verification Loop: Rechecked the updated graph to ensure correctness.
🔎 Grounded Answer: The final
QAResultincludes both the factual answer and the supporting evidence
— fully derived from the graph’s current state.
This example demonstrates query-aware graph augmentation in practice — a core feature of GraphRAG systems like Graphiti, where LLMs evolve their own knowledge graph in response to user queries, achieving continual learning without retraining.
# await graphiti.close()
Conclusion: Toward Advanced GraphRAG Systems#
In this tutorial, we built a complete GraphRAG pipeline — starting from scratch and ending with
a self-improving knowledge graph that can evolve in response to new queries.
We covered:
✅ Schema-based knowledge extraction using PydanticAI
✅ Dynamic reflection loops to refine and expand the graph
✅ Embedding and semantic search for entity and edge retrieval
✅ Integration with Graphiti + FalkorDB for real persistence and querying
✅ Query-aware graph augmentation, where the LLM autonomously reads
.mdfiles
and expands the graph when information is missing
🚀 Beyond This Tutorial: Advanced GraphRAG Techniques#
If you want to go further, here are some next-generation approaches used in research and production GraphRAG systems:
Graph-R1 (Reasoning-First GraphRAG):
Proposed by Microsoft Research — this method first reasons over retrieved subgraphs, generating structured reasoning traces before synthesizing final answers. It improves interpretability and multi-hop consistency.Graph-Agent Collaboration:
Multiple agents handle retrieval, augmentation, and validation — one extracts, another verifies, and a third merges results into the evolving graph.Temporal GraphRAG:
Maintains time-aware edges and supports temporal reasoning (e.g., “Which Pokémon knew Thunderbolt before evolving?”).Graph Neural Networks for RAG Using GNNs to handle the complex graph information stored in the KG.
Hybrid GraphRAG + VectorRAG Pipelines:
Combines graph traversal with semantic document retrieval, letting agents reason jointly over structured and unstructured sources.
In the next tutorial, we’ll focus on evaluation — how to measure the accuracy, consistency, and factual grounding of GraphRAG agents. You’ll learn to design evaluation metrics, benchmark queries, and test harnesses that assess how well your graph-augmented system reasons, retrieves, and explains.