Turn code review noise into searchable team knowledge. This tutorial walks through how the GitHub PR Memory example ingests pull request comments from public repos, synthesizes them into high-signal memories with an LLM, and stores them in Exabase for chat-based retrieval.
What it does
You paste a public GitHub repo URL. The app fetches discussion and review comments from the 5 most recently updated PRs, runs them through an LLM that filters out the noise (LGTMs, scheduling chatter, trivial nits) and extracts durable team knowledge: review conventions, things to avoid, structural preferences, test expectations. Those distilled insights are stored as Exabase memories. Then you chat with them.
The ingest pipeline is the interesting part — it's an ETL from GitHub → LLM synthesis → Exabase memories.
How Exabase fits in
The ingest pipeline
First, the app fetches raw PR comments via the GitHub REST API. Then it passes the combined text to an LLM with a structured output schema:
constprSynthesisSchema = z.object({memories:z.array(z.string().min(1).max(3_500).describe("A single, self-contained memory: durable insight a developer should recall later")).min(0).max(50),});const{object} = awaitgenerateObject({model:openai(resolveOpenAiModel()),schema:prSynthesisSchema,temperature:0.25,system:SYNTHESIS_SYSTEM,// detailed prompt for filtering noiseprompt:`Repository: ${owner}/${repo}\n...\n${threadText}`,});
constprSynthesisSchema = z.object({memories:z.array(z.string().min(1).max(3_500).describe("A single, self-contained memory: durable insight a developer should recall later")).min(0).max(50),});const{object} = awaitgenerateObject({model:openai(resolveOpenAiModel()),schema:prSynthesisSchema,temperature:0.25,system:SYNTHESIS_SYSTEM,// detailed prompt for filtering noiseprompt:`Repository: ${owner}/${repo}\n...\n${threadText}`,});
constprSynthesisSchema = z.object({memories:z.array(z.string().min(1).max(3_500).describe("A single, self-contained memory: durable insight a developer should recall later")).min(0).max(50),});const{object} = awaitgenerateObject({model:openai(resolveOpenAiModel()),schema:prSynthesisSchema,temperature:0.25,system:SYNTHESIS_SYSTEM,// detailed prompt for filtering noiseprompt:`Repository: ${owner}/${repo}\n...\n${threadText}`,});
The system prompt is specific about what to keep and what to drop. Here's the key section:
Only emit memories that capture:
- Recurring review themes or team preferences(API shape,error handling,naming)
- Explicit guidance on code structure,layering,or what belongs where
- Things reviewers asked to avoid,deprecate,or not merge without
- Shared conventions,tooling,or CI / release expectations
- Cross-cutting decisions that will matter on future PRsDo NOT create memories for:
- Pure sign-offs,thanks,"LGTM","merged",or scheduling chatter
- Duplicates;merge similar points into one clear memory
- Trivial nits(typos)unless they encode a standing rule
Only emit memories that capture:
- Recurring review themes or team preferences(API shape,error handling,naming)
- Explicit guidance on code structure,layering,or what belongs where
- Things reviewers asked to avoid,deprecate,or not merge without
- Shared conventions,tooling,or CI / release expectations
- Cross-cutting decisions that will matter on future PRsDo NOT create memories for:
- Pure sign-offs,thanks,"LGTM","merged",or scheduling chatter
- Duplicates;merge similar points into one clear memory
- Trivial nits(typos)unless they encode a standing rule
Only emit memories that capture:
- Recurring review themes or team preferences(API shape,error handling,naming)
- Explicit guidance on code structure,layering,or what belongs where
- Things reviewers asked to avoid,deprecate,or not merge without
- Shared conventions,tooling,or CI / release expectations
- Cross-cutting decisions that will matter on future PRsDo NOT create memories for:
- Pure sign-offs,thanks,"LGTM","merged",or scheduling chatter
- Duplicates;merge similar points into one clear memory
- Trivial nits(typos)unless they encode a standing rule
This is what makes the pipeline useful — 200 raw comments might produce 8–12 focused memories. The LLM is acting as a noise filter, not a transcriber.
Rate limits and safety rails
The pipeline has hard caps to keep things manageable: it processes at most 5 PRs, caps raw comment blocks at 400, truncates the combined thread text to 100K characters, and limits the LLM output to 50 memories. These constraints are set as constants at the top of the ingest module:
The infer: false flag stores the text verbatim — the LLM already did the synthesis work, so there's no need for Exabase to run additional processing.
Chat with the same tool pattern
Once memories exist, the chat interface uses the same searchMemory / addMemory tool pattern as the Personal Assistant example:
searchMemory:tool({description:'Search saved PR/review-style memories. Phrase the query as a full question.',inputSchema:z.object({query:z.string().min(1),limit:z.number().min(1).max(20).optional(),}),execute:async({query,limit})=>{constres = awaitapi.memories.search({query,limit:limit ?? 8},{baseId},);return{ok:true,total:res.total,hits:/* ... */};},}),
searchMemory:tool({description:'Search saved PR/review-style memories. Phrase the query as a full question.',inputSchema:z.object({query:z.string().min(1),limit:z.number().min(1).max(20).optional(),}),execute:async({query,limit})=>{constres = awaitapi.memories.search({query,limit:limit ?? 8},{baseId},);return{ok:true,total:res.total,hits:/* ... */};},}),
searchMemory:tool({description:'Search saved PR/review-style memories. Phrase the query as a full question.',inputSchema:z.object({query:z.string().min(1),limit:z.number().min(1).max(20).optional(),}),execute:async({query,limit})=>{constres = awaitapi.memories.search({query,limit:limit ?? 8},{baseId},);return{ok:true,total:res.total,hits:/* ... */};},}),
So you can ask things like "What conventions does this team follow for error handling?" and get answers grounded in actual review discussions.
Note how the system prompt for this chat is different from the Personal Assistant — it's tailored for code review context:
You are a pair-programming and code review assistant. Preferciting what wassaidinthe repo's PR and review context when you have it.
You are a pair-programming and code review assistant. Preferciting what wassaidinthe repo's PR and review context when you have it.
You are a pair-programming and code review assistant. Preferciting what wassaidinthe repo's PR and review context when you have it.
Same tools, same Exabase APIs, but the personality and grounding instructions are domain-specific. This is a good pattern to follow: keep the Exabase integration generic and customize behavior through the system prompt.
Key Exabase APIs used in this example:
API
Purpose
bases.create
Create an isolated workspace per session
memories.create
Store each synthesized insight as a memory
memories.search
Semantic search over the knowledge base
memories.list
Browse ingested memories in the sidebar
memories.delete
Remove individual memories
Run it yourself
git clone https://github.com/futurebrowser/exabase-examples.git
cd
git clone https://github.com/futurebrowser/exabase-examples.git
cd
git clone https://github.com/futurebrowser/exabase-examples.git
cd
Add EXABASE_API_KEY and OPENAI_API_KEY to .env.local. Optionally add GITHUB_TOKEN if you hit rate limits (public repos don't require a token, but unauthenticated limits are low). Open http://localhost:3000 and click New base.
FAQ
Why synthesize instead of storing raw comments?
Raw PR comments are noisy — most are "LGTM," "fixed," or one-off nits. Storing them all would pollute search results. The LLM acts as a filter, keeping only insights that will be useful across future PRs: team conventions, recurring feedback themes, architectural decisions.
How many memories does a typical ingest produce?
It depends on the repo. A repo with active, substantive reviews might produce 10–15 memories from 5 PRs. A repo where reviews are mostly approvals might produce 2–3 or none.
Can I ingest private repos?
The example uses the public GitHub REST API. For private repos, add a GITHUB_TOKEN with repo access. The Exabase integration stays exactly the same — it doesn't care where the text comes from.
How is this different from just searching GitHub?
GitHub search finds comments by keywords. Exabase search finds memories by meaning. "What does this team think about error handling?" won't work well as a GitHub search, but it works against synthesized memories because they're written as self-contained insights, not scattered across 50 comment threads.
Can I try this without a GitHub repo?
Yes — the app includes a "Populate sample PR memories" button that seeds a Base with pre-written demo memories (things like "PR #112 — @jordan-lee: the auth middleware test should also cover 401 for expired token"). This lets you test the chat and search flow without connecting to GitHub at all.
Could I use this pattern with other sources besides GitHub?
The Exabase side is completely source-agnostic — it just stores and searches text. The GitHub-specific code is isolated in the ingest module. You could swap in Jira comments, Slack threads, Confluence pages, or any other text source. The pattern stays the same: fetch raw text → synthesize with an LLM → store as Exabase memories → chat against them.