Skip to content

Commit e7b5c18

Browse files
jackaldenryancursoragent
andcommitted
feat(eval-harness): default retrieval to auto search via strategy module
Move context retrieval into retrieval_strategy.build_context_block so evals use scope=auto with a 10k character budget, prepend the user-node summary, and drop per-type search limit constants. Co-authored-by: Cursor <[email protected]>
1 parent 0d54106 commit e7b5c18

5 files changed

Lines changed: 166 additions & 355 deletions

File tree

zep-eval-harness/.claude/commands/zep-eval-harness/SKILL.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The pipeline has four steps:
1515

1616
### Scope: Single-Shot Retrieval
1717

18-
**The harness evaluates single-shot retrieval only.** Every test case issues one fixed batch of scoped searches from the raw test question (nodes + edges, optionally episodes, across the user graph and any document graph), then hands the resulting context block to the response model in a single turn — no second retrieval round, no query reformulation. This mirrors deterministic/programmatic retrieval, not the tool-based pattern where an agent is handed Zep search tools (e.g. `search_graph` from the Zep MCP server) and decides when and what to search.
18+
**The harness evaluates single-shot retrieval only.** Every test case issues one retrieval from the raw test question via auto search (`config/evaluation_config/retrieval_strategy.py`), then hands the resulting context block to the response model in a single turn — no second retrieval round, no query reformulation. This mirrors deterministic/programmatic retrieval, not the tool-based pattern where an agent is handed Zep search tools (e.g. `search_graph` from the Zep MCP server) and decides when and what to search.
1919

2020
That makes the harness a clean instrument for the ingestion and search configuration, but it says nothing about agent tool-use behavior. A tool-based agent may do better (several targeted searches, reformulating after a weak result) or worse (never searching, poorly phrased queries, running out of turns). When reporting results, scope conclusions to the config under test and never present them as a prediction of production agent performance.
2121

@@ -28,7 +28,7 @@ That makes the harness a clean instrument for the ingestion and search configura
2828

2929
This is the metric that matters most. It directly measures Zep's retrieval quality — whether the knowledge graph and search surface the right facts, entities, and relationships.
3030

31-
When completeness is low, the key diagnostic question is: **does the graph contain the right information but search failed to retrieve it, or is the information missing from the graph entirely?** Use `zep_graph_inspect.py` to examine what's actually in the graph. If the information is there but not retrieved, the issue is search configuration (limits, reranker, query phrasing). If the information is absent from the graph, the issue is upstream — ingestion, ontology, or custom instructions need adjustment.
31+
When completeness is low, the key diagnostic question is: **does the graph contain the right information but search failed to retrieve it, or is the information missing from the graph entirely?** Use `zep_graph_inspect.py` to examine what's actually in the graph. If the information is there but not retrieved, the issue is retrieval strategy (auto-search character budget, query phrasing). If the information is absent from the graph, the issue is upstream — ingestion, ontology, or custom instructions need adjustment.
3232

3333
**Answer Accuracy (SECONDARY)** — Did the LLM produce a correct answer from the retrieved context?
3434
- **CORRECT**: Answer conveys the same key information as the golden answer
@@ -38,15 +38,13 @@ This measures whether the response model uses the context well. It depends on th
3838

3939
Metrics are calculated in aggregate, per-category (based on test case `category` field), and per-user.
4040

41-
### Context Block: Edges vs Nodes vs Episodes
41+
### Context Block: Auto Search
4242

43-
The evaluation script constructs a context block from graph search results. Understanding what each component contributes:
43+
The evaluation script retrieves context via `build_context_block()` in `config/evaluation_config/retrieval_strategy.py`. The default strategy uses `scope="auto"` with `MAX_CHARACTERS = 10000`, and prepends the user-node summary (fetched separately — auto search does not include it).
4444

45-
- **Edges (facts)**: Relationships extracted by Zep between entities — e.g., "Sarah WORKS_FOR TechCorp", "Biscuit IS_OWNED_BY Sarah". These are the primary source of structured knowledge. Controlled by `USER_FACTS_LIMIT` / `DOC_FACTS_LIMIT`.
46-
- **Nodes (entities)**: Entity summaries — e.g., a Person node with name, relationship type, and a description synthesized from all conversations mentioning them. Controlled by `USER_ENTITIES_LIMIT` / `DOC_ENTITIES_LIMIT`.
47-
- **Episodes (raw data)**: The original messages, document chunks, or JSON data that was ingested. These provide verbatim source text but are bulkier. Disabled by default (`*_EPISODES_LIMIT = 0`). Enable by setting limits > 0 in `config/evaluation_config/constants.py`.
45+
Auto search packs relevant edges (facts), nodes (entities), episodes, observations, and thread summaries into a pre-assembled context string. ``limit`` and ``reranker`` do not apply under auto — volume is controlled by the character budget.
4846

49-
Most evaluations work best with edges + nodes (structured, concise). Enable episodes when verbatim source text is needed for answering questions that require exact quotes or details not captured in the graph extraction.
47+
To change retrieval (e.g. manual multi-scope searches with per-type limits and a reranker), edit `build_context_block` in that module. That function is the source of truth for the strategy.
5048

5149
All commands run from `zep-eval-harness/` using `uv run`. Depending on the user's request, either run the scripts directly or provide the terminal commands for the user.
5250

@@ -76,7 +74,8 @@ config/
7674
├── document_chunking_config/
7775
│ └── constants.py # CHUNK_SIZE, CHUNK_OVERLAP, LLM_CONTEXTUALIZATION_MODEL
7876
└── evaluation_config/
79-
├── constants.py # Search limits, LLM_RESPONSE_MODEL, LLM_JUDGE_MODEL
77+
├── constants.py # LLM_RESPONSE_MODEL, LLM_JUDGE_MODEL
78+
├── retrieval_strategy.py # build_context_block() — retrieval + context assembly
8079
└── response_prompt.py # get_response_system_prompt() — AI persona for eval
8180
```
8281

zep-eval-harness/README.md

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -174,20 +174,20 @@ Rate limits are handled automatically — if you hit limits, the retry backoff w
174174

175175
### Pipeline Steps (automated in zep_evaluate.py)
176176

177-
1. **Search**: Query Zep's knowledge graph (nodes, edges) using cross-encoder reranker
177+
1. **Search**: Query Zep's knowledge graph with ``scope="auto"`` (character-budgeted context block)
178178
2. **Evaluate Context**: Assess whether retrieved context contains sufficient information (PRIMARY METRIC)
179179
3. **Generate Response**: Use LLM with retrieved context to answer questions
180180
4. **Grade Answer**: Evaluate answers against golden answers using LLM judge (SECONDARY METRIC)
181181

182182
### Scope: Single-Shot Retrieval
183183

184-
**This harness evaluates single-shot retrieval only.** Each test case issues one fixed batch of searches — the raw test question against nodes and edges (plus episodes if enabled), across the user graph and any document graph, all in parallelassembles the results into a context block, and hands it to the response model in a single turn. There is no second retrieval round and no query reformulation. That mirrors *deterministic/programmatic* retrieval, where your application searches on every turn and injects the context itself.
184+
**This harness evaluates single-shot retrieval only.** Each test case issues one retrieval — the raw test question via auto search on the user graph (and any document graph), using the strategy in `config/evaluation_config/retrieval_strategy.py`and hands the resulting context block to the response model in a single turn. There is no second retrieval round and no query reformulation. That mirrors *deterministic/programmatic* retrieval, where your application searches on every turn and injects the context itself.
185185

186186
Production agents are frequently built the other way: Zep is exposed to the model as **tools** — for example `search_graph` from the [Zep MCP server](../mcp/zep-mcp-server/), or your own tool definitions — and the LLM decides when to search, how to phrase each query, and whether to search again after seeing results.
187187

188188
Read the scores accordingly:
189189

190-
- **What they measure**: whether your ingestion and search configuration (ontology, custom instructions, chunking, search limits, reranker) puts the right facts within reach of one well-formed query. Pinning retrieval to a single deterministic search is what keeps runs comparable — the config stays the only variable.
190+
- **What they measure**: whether your ingestion and retrieval strategy (ontology, custom instructions, chunking, auto-search character budget) puts the right facts within reach of one well-formed query. Pinning retrieval to a single deterministic search is what keeps runs comparable — the config stays the only variable.
191191
- **What they do not measure**: agent behavior. A tool-based agent can beat these numbers by issuing several targeted searches and reformulating after a weak result, or fall short of them by not searching at all, phrasing a query poorly, or running out of turns. Tool choice, query formulation, and multi-turn dynamics are untested here.
192192

193193
If your production path exposes Zep through tools, treat a strong result here as a prerequisite rather than a verdict, and evaluate the agent end-to-end as well. See [Evaluate Zep for your use case](https://help.getzep.com/evaluate-zep-for-your-use-case).
@@ -210,11 +210,12 @@ config/
210210
├── document_chunking_config/
211211
│ └── constants.py # CHUNK_SIZE, CHUNK_OVERLAP, LLM_CONTEXTUALIZATION_MODEL
212212
└── evaluation_config/
213-
├── constants.py # Search limits, LLM_RESPONSE_MODEL, LLM_JUDGE_MODEL
213+
├── constants.py # LLM_RESPONSE_MODEL, LLM_JUDGE_MODEL
214+
├── retrieval_strategy.py # build_context_block() — retrieval + context assembly
214215
└── response_prompt.py # get_response_system_prompt() — the system prompt for AI responses
215216
```
216217

217-
Each script imports only from its relevant config subfolder. The response prompt used during evaluation is defined in `config/evaluation_config/response_prompt.py` and can be customized independently from the evaluation logic.
218+
Each script imports only from its relevant config subfolder. The response prompt used during evaluation is defined in `config/evaluation_config/response_prompt.py` and can be customized independently from the evaluation logic. Retrieval behavior lives entirely in `retrieval_strategy.py`.
218219

219220
## Run Tracking
220221

@@ -390,15 +391,16 @@ To add more users:
390391

391392
## Advanced Evaluation
392393

393-
### Tune Zep Search Parameters
394+
### Tune Zep Retrieval Strategy
394395

395-
The evaluation script uses `cross_encoder` reranker by default for best accuracy. Search parameters and LLM models are configured in `config/evaluation_config/constants.py`:
396-
- `USER_FACTS_LIMIT = 20`: Number of facts (edges) from user graph
397-
- `USER_ENTITIES_LIMIT = 10`: Number of entities (nodes) from user graph
398-
- `USER_EPISODES_LIMIT = 0`: User episodes disabled by default (set >0 to enable)
399-
- `DOC_FACTS_LIMIT = 10`: Number of facts from document graph
400-
- `DOC_ENTITIES_LIMIT = 5`: Number of entities from document graph
401-
- `DOC_EPISODES_LIMIT = 0`: Document episodes disabled by default
396+
Retrieval is defined by a single module: `config/evaluation_config/retrieval_strategy.py`. Its `build_context_block()` function is the source of truth — edit that function (and the constants it uses) to change how context is retrieved and assembled.
397+
398+
Default strategy:
399+
- `SCOPE = "auto"`: Zep packs edges, nodes, episodes, observations, and thread summaries into a pre-assembled context string
400+
- `MAX_CHARACTERS = 10000`: character budget for auto search (``limit`` and ``reranker`` do not apply under auto)
401+
- User-node summary is fetched via `user.get_node()` and prepended (auto search does not include it)
402+
403+
LLM models remain in `config/evaluation_config/constants.py`:
402404
- `LLM_RESPONSE_MODEL`: Model for generating responses
403405
- `LLM_JUDGE_MODEL`: Model for grading answers
404406

@@ -407,20 +409,15 @@ Chunking-specific constants are in `config/document_chunking_config/constants.py
407409
- `CHUNK_OVERLAP`: Characters of overlap between consecutive chunks (default: 100)
408410
- `LLM_CONTEXTUALIZATION_MODEL`: Model for document chunk contextualization
409411

410-
You can experiment with different rerankers by modifying the `reranker` parameter in `perform_graph_search()`:
411-
- `cross_encoder`: Best accuracy, slower (default)
412-
- `rrf`: Reciprocal Rank Fusion, balanced
413-
- `mmr`: Maximal Marginal Relevance, diversity-focused
414-
415-
For guidance, check out the [Searching the Graph documentation](https://help.getzep.com/searching-the-graph).
412+
For guidance, check out the [Searching the Graph documentation](https://help.getzep.com/searching-the-graph) (including Auto Search).
416413

417414
### Customize Response Prompt
418415

419416
The system prompt used when generating AI responses during evaluation is defined in `config/evaluation_config/response_prompt.py`. Edit the `get_response_system_prompt()` function to customize the AI's persona, response style, or instructions for your use case. This is snapshotted into each evaluation run for reproducibility.
420417

421-
### Customize Context Block
418+
### Customize Context Block / Retrieval
422419

423-
The harness constructs a custom context block from graph search results. You can modify the `construct_context_block()` function in `zep_evaluate.py` to format results differently. See the [Customize Your Context Block documentation](https://help.getzep.com/cookbook/customize-your-context-block) for best practices.
420+
Change retrieval by editing `build_context_block()` in `config/evaluation_config/retrieval_strategy.py`. For example, you can switch to manual multi-scope searches with per-type limits and a chosen reranker, or adjust the auto-search character budget. See the [Customize Your Context Block documentation](https://help.getzep.com/cookbook/customize-your-context-block) and [Searching the Graph](https://help.getzep.com/searching-the-graph) for best practices.
424421

425422
### Add JSON/Text Data
426423

@@ -485,12 +482,9 @@ Results are saved to `runs/evaluations/{run_number}_{timestamp}/results.json` wi
485482
"document_run": { "run_number": 1, "run_dir": "runs/documents/1_20260331T222500", "graph_id": "zep_eval_shared_documents_1d4d9a28" }
486483
},
487484
"search_configuration": {
488-
"user_facts_limit": 20,
489-
"user_entities_limit": 10,
490-
"user_episodes_limit": 0,
491-
"doc_facts_limit": 10,
492-
"doc_entities_limit": 5,
493-
"doc_episodes_limit": 0
485+
"strategy": "auto_search",
486+
"scope": "auto",
487+
"max_characters": 10000
494488
},
495489
"model_configuration": {
496490
"response_model": "gemini-2.5-flash-lite",
Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
# Search configuration — user graphs
2-
USER_FACTS_LIMIT = 20 # Number of facts (edges) to return
3-
USER_ENTITIES_LIMIT = 10 # Number of entities (nodes) to return
4-
USER_EPISODES_LIMIT = 0 # Number of episodes to return (when enabled)
5-
6-
# Search configuration — standalone document graph
7-
DOC_FACTS_LIMIT = 10 # Number of facts (edges) to return
8-
DOC_ENTITIES_LIMIT = 5 # Number of entities (nodes) to return
9-
DOC_EPISODES_LIMIT = 0 # Number of episodes to return (when enabled)
10-
111
# LLM models for evaluation
122
LLM_RESPONSE_MODEL = "gemini-3-flash-preview" # Model used for generating responses
133
LLM_JUDGE_MODEL = "gemini-3-flash-preview" # Model used for grading responses
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
Retrieval strategy for evaluation context blocks.
3+
4+
This module is the source of truth for how the harness retrieves and assembles
5+
context. Edit ``build_context_block`` (and the constants it closes over) to
6+
change search behavior — there is no separate per-scope limit/reranker config.
7+
8+
Default: ``scope="auto"`` with a 10k character budget. Auto search packs edges,
9+
nodes, episodes, observations, and thread summaries into a pre-assembled
10+
``result.context`` string. ``limit`` and ``reranker`` do not apply under auto.
11+
The user-node summary is fetched separately and prepended (auto search does
12+
not include it).
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import asyncio
18+
from typing import TYPE_CHECKING
19+
20+
from retry import retry_with_backoff
21+
22+
if TYPE_CHECKING:
23+
from zep_cloud.client import AsyncZep
24+
25+
STRATEGY_NAME = "auto_search"
26+
SCOPE = "auto"
27+
MAX_CHARACTERS = 10_000
28+
29+
30+
def get_search_configuration() -> dict:
31+
"""Snapshot of the active retrieval strategy for evaluation result files."""
32+
return {
33+
"strategy": STRATEGY_NAME,
34+
"scope": SCOPE,
35+
"max_characters": MAX_CHARACTERS,
36+
}
37+
38+
39+
async def _fetch_user_summary(zep_client: AsyncZep, user_id: str) -> str | None:
40+
"""Fetch the user-node summary, or None if unavailable."""
41+
try:
42+
user_node_response = await retry_with_backoff(
43+
zep_client.user.get_node,
44+
user_id=user_id,
45+
description=f"get user node [{user_id}]",
46+
)
47+
node = getattr(user_node_response, "node", None)
48+
summary = getattr(node, "summary", None) if node else None
49+
if summary and str(summary).strip():
50+
return str(summary).strip()
51+
except Exception as e:
52+
print(f" Could not retrieve user summary for [{user_id}]: {e}")
53+
return None
54+
55+
56+
async def build_context_block(
57+
zep_client: AsyncZep,
58+
*,
59+
user_id: str,
60+
query: str,
61+
doc_graph_id: str | None = None,
62+
) -> str:
63+
"""
64+
Retrieve a context block for ``query`` using the configured strategy.
65+
66+
Fetches the user-node summary, runs auto search on the user graph, and
67+
optionally auto-searches a standalone document graph in parallel. Assembles
68+
summary + Zep's materialized ``context`` string(s).
69+
"""
70+
print(f"Searching [{user_id}]: '{query}' (scope={SCOPE}, max_characters={MAX_CHARACTERS})")
71+
72+
summary_task = _fetch_user_summary(zep_client, user_id)
73+
user_task = retry_with_backoff(
74+
zep_client.graph.search,
75+
user_id=user_id,
76+
query=query,
77+
scope=SCOPE,
78+
max_characters=MAX_CHARACTERS,
79+
description=f"auto search user [{user_id}]",
80+
)
81+
82+
if doc_graph_id:
83+
doc_task = retry_with_backoff(
84+
zep_client.graph.search,
85+
graph_id=doc_graph_id,
86+
query=query,
87+
scope=SCOPE,
88+
max_characters=MAX_CHARACTERS,
89+
description=f"auto search doc [{doc_graph_id}]",
90+
)
91+
user_summary, user_result, doc_result = await asyncio.gather(
92+
summary_task, user_task, doc_task
93+
)
94+
else:
95+
user_summary, user_result = await asyncio.gather(summary_task, user_task)
96+
doc_result = None
97+
98+
parts: list[str] = []
99+
100+
if user_summary:
101+
parts.append(
102+
"# High-level summary of the user\n"
103+
"<USER_SUMMARY>\n"
104+
f"{user_summary}\n"
105+
"</USER_SUMMARY>"
106+
)
107+
108+
user_context = getattr(user_result, "context", None) or ""
109+
if user_context.strip():
110+
parts.append(user_context.strip())
111+
112+
if doc_result is not None:
113+
doc_context = getattr(doc_result, "context", None) or ""
114+
if doc_context.strip():
115+
parts.append(
116+
"The following context is from shared reference documents.\n\n"
117+
+ doc_context.strip()
118+
)
119+
120+
if not parts:
121+
return "No relevant context found."
122+
123+
return "\n\n".join(parts)

0 commit comments

Comments
 (0)