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:

  1. Retrieve relevant context → turn it into triples ((subject, predicate, object)).

  2. Store / update these triples in a graph backend (persistent memory).

  3. 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 type

  • EVOLVES_TO — shows evolution paths

  • NEEDS_ITEM — evolution dependency (e.g., Thunder Stone)

  • LEARNS_MOVE — captures learnable moves

  • WEAK_AGAINST, RESISTS — for type matchups

Using this schema, we’ll create two Pydantic models:

  1. Triple — represents one edge (subject, predicate, object)

  2. 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) as Edge dataclasses

  • Provides 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))
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.

🧭 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

  1. List core entities and the questions you must answer.

  2. Define relations that connect those entities (domain/range).

  3. Add attributes needed for reasoning (and keep the rest out).

  4. 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.

  1. 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.

  2. 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-large from OpenAI to create these embeddings, allowing Graphiti to find related nodes or documents efficiently.

  3. 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, and Item — 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 Episode with fields for name, episode_body, and source_description.
    A field_validator ensures titles are short and clean.

  • We create a EpisodesResult wrapper to hold multiple episodes.

  • We then use a PydanticAI Agent, episode_generator, which takes a long Pokémon text and
    splits it into coherent Episode objects.

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:

  1. Initialize Graphiti with:

    • graph_driver → our FalkorDB backend

    • llm_client, embedder, and cross_encoder → for triple extraction, embedding, and reranking

    • store_raw_episode_content=False → skips saving large text blobs to keep storage light

  2. Prepare the environment:

    • clear_data() wipes any existing graph data.

    • build_indices_and_constraints() sets up indexes and schema-level constraints in the database.

  3. 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 Episode is passed to graphiti.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()

image

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:

  1. Connects to FalkorDB to query stored nodes and relationships.

  2. Uses graphiti.search() to fetch the most relevant EntityEdge objects.

  3. For each edge, it retrieves the source and destination node details from the graph.

  4. 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:

  1. 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.

  2. 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 response

  • used_graph: flag for whether graph data was used

  • evidence: 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:

  1. Search the existing graph for relevant information using graph_search_tool.

  2. Detect knowledge gaps — when the graph lacks the facts required to answer.

  3. Read and segment raw .md files (e.g., bulbasaur.md, pikachu.md) to locate the missing context.

  4. Generate new, schema-compliant episodes containing just the necessary information.

  5. Add those new episodes back into the graph, expanding it dynamically.

  6. 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 relationship Bulbasaur EVOLVES_TO and 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 that Bulbasaur EVOLVES_TO Ivysaur (and further to Venusaur) now exists, complete with level-based evolution attributes.

  • 📚 Structured extraction:
    The final Extracted JSON shows 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:

  1. graph_augment() tool

    • Calls the contextual_episode_generator agent you built earlier.

    • If the current graph lacks facts needed to answer a query, it automatically creates new episodes (from the raw .md files), extracts triples, and inserts them into the knowledge graph.

  2. graph_augment_agent

    • Tries 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 QAResult with 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

  1. 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.

  2. Graph Augmentation Triggered:
    Realizing the graph lacked the answer, it invoked the graph_augment tool. This tool, in turn, called the contextual_episode_generator, which:

    • Read the relevant .md source (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.

  3. 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.

  4. 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 QAResult includes 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 .md files
    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.