RepoLens: Enhancing GitHub Repository Understanding using Graph RAG
One URL in. A full technical tutorial out — plus a chat interface that actually understands your codebase.

RepoLens: Enhancing GitHub Repository Understanding using Graph RAG
One URL in. A full technical tutorial out — plus a chat interface that actually understands your codebase.
The Problem
You’ve cloned a new repo. Maybe it’s a library you want to contribute to, a project your team inherited, or an open-source tool you’re evaluating. You open the README. It’s either too vague (“a fast ML training framework!”) or an exhaustive wall of text that still doesn’t tell you where anything lives or how the pieces connect.
So you start grepping. You trace imports. You read functions in the wrong order. An hour later you have a rough mental model — fragile, incomplete, probably wrong in a few places.
Repo Lens is my answer to this problem. You paste a GitHub URL. It spins up an AI agent that reads the whole repository — README, file tree, every core source file — and streams back a structured, 2000+ word technical tutorial. Then, when the tutorial is done, a chat interface opens where you can ask deep questions about the codebase and get answers grounded in actual code, not hallucinations.
The stack: OpenAI Agents SDK, Qdrant (vector DB), Neo4j (knowledge graph), FastAPI, and a Graph RAG pipeline for the chat. This article is the complete build tutorial for the GenAI pipeline.
Architecture Overview
Before diving into code, here’s the high-level flow:
GitHub URL
│
▼
┌─────────────────────────────────────────┐
│ PARENT AGENT (GPT-4o) │
│ ┌──────────┐ ┌──────────────────────┐ │
│ │get_readme│ │return_file_structure │ │
│ └──────────┘ └──────────────────────┘ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │navigate_repo │ │ create_chunks │ │
│ └──────────────┘ └──────────────────┘ │
└─────────────┬───────────────────────────┘
│ chunks + tutorial markdown
▼
┌─────────┐
│ Qdrant │◄── raw code chunks (text-embedding-3-small)
└────┬────┘
│ when user opens chat
▼
┌─────────────────────────────────────────┐
│ KNOWLEDGE GRAPH BUILDER │
│ LLMGraphTransformer (gpt-4o-mini) │
│ → Graph Documents (nodes + edges) │
└──────┬────────────────────┬────────────┘
▼ ▼
┌───────┐ ┌─────────┐
│ Neo4j │ │ Qdrant │◄── graph doc embeddings
└───────┘ └─────────┘
│ │
└─────────┬──────────┘
▼
┌──────────────────────┐
│ CHAT AGENT │
│ Query_VectorDB tool │
│ Graph RAG retrieval │
└──────────────────────┘
Two distinct phases: analysis (the agent reads the repo and generates a tutorial) and chat (Graph RAG answers follow-up questions). Let’s build both.
Part 1 — The Parent Agent: Repo Analysis Pipeline
Step 1.1 — The Four Function Tools
The Parent Agent has exactly four tools. Each one is a @function_tool decorated function from the OpenAI Agents SDK. The agent decides when to call them and in what order — your job is to write them with clear docstrings that act as the tool's contract.
Tool 1: get_readme
Always called first. Fetches and base64-decodes the README from the GitHub Contents API.
from agents import function_tool
import requests, base64
HEADERS = {"Authorization": os.getenv('Github_access_token')}
@function_tool
def get_readme(user: str, repository_name: str):
'''
When to call->
Always call this FIRST before any other tool.
What to pass in->
1. user : GitHub username or org name
2. repository_name : exact repository name as on GitHub
What does it return->
str : full decoded README content, or 'Readme not found' if absent.
What to do next->
Call return_file_structure to map the full repo, using README
as context for which files matter.
'''
url = f'https://api.github.com/repos/{user}/{repository_name}/contents/'
response = requests.get(url, headers=HEADERS)
Data = response.json()
content = 'Readme not found'
for data in Data:
if data['name'].lower() == 'readme.md':
result = requests.get(data['url'], headers=HEADERS).json()
content = base64.b64decode(result['content']).decode("utf-8")
return content
The docstring is not documentation for you — it’s the prompt the LLM reads before deciding how to use this tool. Be explicit about sequencing ("Always call this FIRST"), what to pass, and what to do next. The Agents SDK surfaces the docstring directly to the model as the tool description.
Tool 2: return_file_structure
Calls the GitHub Trees API with ?recursive=1 to get the full repo structure in one request. Returns a flat string of paths and types (blob or tree).
@function_tool
def return_file_structure(user: str, repository_name: str, branch: str):
'''
When to call this function->
After reading the Readme of the repository
What to pass in this function->
1. user : owner of the repository
2. repository_name : repository name as given on GitHub
3. branch : e.g. (main or master)
** try running for main if it returns error try running for master
What does the function return->
returns the string of the repository structure e.g. (Readme.md Blob \n asset tree)
What to do next after calling this function->
Use the tool Navigate_repo to explore and understand the repo
'''
repo_url = (
f'https://api.github.com/repos/{user}/{repository_name}'
f'/git/trees/{branch}?recursive=1'
)
response = requests.get(repo_url, headers=HEADERS)
data = response.json()
structure_string = ''
for item in data["tree"]:
structure_string += f"{item['path']} {item['type']} \n"
return structure_string
The ?recursive=1 flag is key — GitHub's Trees API returns the entire file tree in a single call. Without it you'd have to paginate through each directory. The output is a flat string that the agent parses to decide which paths to navigate next.
Tool 3: navigate_repo
This is the workhorse. It takes a GitHub Contents API URL and a file type (blob or tree). For trees it returns child URLs; for blobs it base64-decodes and returns the file content.
from pydantic import BaseModel
from typing import Optional
class Navigate_repo_class(BaseModel):
list_url: Optional[list[dict]]
content: Optional[str]
@function_tool
def Navigate_repo(url: str, file_type: str) -> Navigate_repo_class:
'''
When to call? ->
Once you have to navigate a tree or get the content of a blob
When not to call ->
Do NOT call this tool for: README.md, LICENSE, .gitignore,
images (.png, .jpg), PDFs, videos, binaries, or docs.
What to pass in the function? ->
1. url : GitHub API contents URL only. Must follow this format:
https://api.github.com/repos/{user}/{repo}/contents/{path}
NOT the HTML url (https://github.com/...)
2. file_type : 'blob' or 'tree'
What does the function return->
list_url : list of child URLs (for trees)
content : file content (for blobs)
After calling this function? ->
file_type='tree' → make parallel Navigate_repo calls on each URL
file_type='blob' → call create_chunks() immediately with the content
'''
response = requests.get(url, headers=HEADERS)
Data = response.json()
content = None
response_list = []
if file_type == 'tree':
for data in Data:
response_list.append({'name': data.get('name'), 'URL': data.get('url')})
if file_type == 'blob':
content = base64.b64decode(Data['content']).decode("utf-8")
return Navigate_repo_class(list_url=response_list, content=content)
The Pydantic return type matters here. The Agents SDK serializes it back to the model as structured JSON, so the agent reliably parses list_url and content from the response. Without the typed model, the agent might misparse a raw dict.
The docstring explicitly tells the agent not to call this on binary or documentation files — without that instruction, agents tend to dutifully try to base64-decode PNGs and then get confused.
Tool 4: create_chunks
Takes file content, language, and filename. Uses chunk_tree (a tree-sitter-based AST chunker) to split the file at natural code boundaries — function-level, class-level — then stores the chunks in Qdrant.
from chunk_ast import chunk_tree
from Qdrant_db import store_in_Qdrant
@function_tool
def create_chunks(content: str, language: str, filename: str):
'''
When to call:
After Navigate_repo returns blob content — call this IMMEDIATELY.
What to pass:
1. content : the content returned by Navigate_repo
2. language : the coding language (python, java, cpp, javascript...)
3. filename : name of the file e.g. 'run.py'
After calling the function:
Continue Navigate_repo to explore the rest of the repository
'''
try:
chunks = chunk_tree(content, language.lower(), file_name=filename)
store_in_Qdrant(chunks=chunks)
return 'Created a chunk'
except Exception as e:
return f'Error {e} occurred'
chunk_tree uses tree-sitter to parse the AST and split at meaningful boundaries. This means each vector in Qdrant represents a complete function or class body — not an arbitrary 512-token window. This is important for retrieval quality later: when the chat agent asks about a specific function, it retrieves that function's chunk, not a fragment that cuts off mid-logic.
Step 1.2 — Storing Chunks in Qdrant
# Qdrant_db.py
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
from openai import OpenAI
import uuid
client_openai = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
client = QdrantClient(
url=os.getenv('QDRANT_CLUSTER'),
api_key=os.getenv('QDRANT_API_KEY')
)
def store_in_Qdrant(chunks: list, collection_name="documents"):
if not client.collection_exists(collection_name):
client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(size=1536, distance=Distance.COSINE)
)
points = []
for chunk in chunks:
response = client_openai.embeddings.create(
model="text-embedding-3-small",
input=chunk['text']
)
points.append(PointStruct(
id=str(uuid.uuid4()),
vector=response.data[0].embedding,
payload=chunk
))
client.upsert(collection_name=collection_name, points=points)
A few deliberate choices here: text-embedding-3-small at 1536 dimensions hits a good balance between cost and retrieval quality for code. Each point's payload is the full chunk dict — this means at retrieval time you get back not just the text but also metadata like file, node_type, start_line, and end_line without a second lookup.
Step 1.3 — The Parent Agent: System Prompt and Wiring
The system prompt is the most important piece of the pipeline. It doesn’t just describe what the agent should do — it enforces a strict, ordered workflow and defines the exact output format.
Here is the core structure (abbreviated — the full prompt is ~150 lines):
parent_agent_instruction = '''
You are a technical educator who creates in-depth tutorials for GitHub repositories.
You have tools available to explore a repository and store its content in a Vector DB.
You MUST follow this exact workflow — do not skip any step.
STEP 1 — Read the README (MANDATORY FIRST STEP)
Call get_readme() first, always.
STEP 2 — Get the full file structure (MANDATORY SECOND STEP)
Call return_file_structure() immediately after reading the README.
STEP 3 — Navigate ALL relevant files AND store chunks (MANDATORY)
a) For every relevant directory (type=tree) → call Navigate_repo(url, 'tree')
b) For every relevant file (type=blob):
STEP 3b-i → call Navigate_repo(url, 'blob') to get the file content
STEP 3b-ii → IMMEDIATELY call create_chunks() with the file content
CRITICAL: If you do not call create_chunks() after EVERY blob, the Vector DB
will be empty and the chat will produce answers with no code references.
After calling Navigate_repo on a blob, your very next action MUST be create_chunks().
c) Prioritize files in this order:
1. Entry point files (main.py, app.py, index.py, run.py)
2. Core logic files referenced in the README
3. Configuration files (requirements.txt, config.py)
4. Skip: LICENSE, .gitignore, __pycache__, test files, images, binaries
STEP 4 — Write the tutorial (ONLY after Step 3 is fully complete)
Write a tutorial of AT LEAST 2000 words with these exact sections:
SECTION 1 — OVERVIEW
SECTION 2 — REPOSITORY STRUCTURE
SECTION 3 — INSTALLATION
SECTION 4 — RUNNING THE PROJECT
SECTION 5 — CODE ARCHITECTURE
SECTION 6 — KEY FILES EXPLANATION
SECTION 7 — EXAMPLE WORKFLOW
SECTION 8 — CUSTOMIZATION
SECTION 9 — TROUBLESHOOTING
'''
A few things worth noting about this prompt design:
Explicit step ordering beats implicit expectations. LLM agents will skip steps if you don’t make them mandatory. The ⚠️ CRITICAL warning about create_chunks() exists because without it, early testing showed the agent would read all files, then generate the tutorial, and only call create_chunks at the very end — or sometimes not at all.
Prioritization lists prevent waste. Without the “skip: LICENSE, .gitignore, images” list, agents happily try to read and chunk binary files. The prioritization list also keeps the agent focused on files that actually matter for understanding the codebase.
Section structure in the prompt becomes section structure in the output. The exact nine sections are specified so the tutorial is predictable enough to display and export consistently.
Now wire it up:
from agents import Agent, Runner, trace, OpenAIChatCompletionsModel
from openai import AsyncOpenAI
from openai.types.responses import ResponseTextDeltaEvent
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
model = OpenAIChatCompletionsModel(
model="gpt-4o",
openai_client=client
)
tools = [get_readme, return_file_structure, Navigate_repo, create_chunks]
Parent_Agent = Agent(
name='Parent Agent',
instructions=parent_agent_instruction,
tools=tools,
model=model
)
async def parent_agent(message: str):
with trace('GitHub Repo Explainer'):
result = Runner.run_streamed(starting_agent=Parent_Agent, input=message)
async for event in result.stream_events():
if event.type == 'raw_response_event' and isinstance(event.data, ResponseTextDeltaEvent):
yield event.data.delta
Runner.run_streamed gives you an async generator of events. You filter for ResponseTextDeltaEvent to get just the text tokens — everything else (tool calls, tool results, metadata) is available if you want to surface tool call logs to the UI, but for the tutorial stream you only want the final text output.
The trace() context manager integrates with OpenAI's tracing dashboard — indispensable for debugging why the agent skipped a file or called tools in the wrong order.
Step 1.4 — The FastAPI Streaming Endpoint
@app.post('/analyze/{session_id}')
async def analyze(session_id: str, repo_input: RepoInput):
message = (
f'Explain this repo---> '
f'url:{repo_input.url}, '
f'owner:{repo_input.owner}, '
f'repo_name:{repo_input.repo_name}, '
f'branch:{repo_input.branch}'
)
async def stream_and_store():
chunks = []
async for chunk in parent_agent(message=message):
chunks.append(chunk)
yield chunk
report_store[session_id] = ''.join(chunks)
return StreamingResponse(stream_and_store(), media_type='text/markdown')
StreamingResponse with an async generator lets the tutorial tokens stream to the client as they're generated — the user sees the tutorial being written in real time rather than waiting for GPT-4o to finish the full 2000+ word output. report_store is an in-memory dict keyed by session ID that holds the completed markdown for the PDF download endpoint.
Part 2 — The Chat Pipeline: Graph RAG
When the user clicks “Chat”, the analysis phase is already done — Qdrant has all the raw code chunks. Now we build the knowledge graph on top of those chunks, store it in both Neo4j and Qdrant, and wire up the chat agent.
Step 2.1 — Why Graph RAG?
Standard RAG retrieves the top-k most similar chunks to the question and feeds them to the LLM. For code, this misses a crucial dimension: relationships. If you ask “how does the authentication flow connect to the database layer?”, a pure vector search might return the auth function and the DB function separately — but it doesn’t tell you that auth.py imports db.connection, which calls pool.get(), which is defined in utils/db.py.
Graph RAG adds a second retrieval step: after finding relevant nodes via vector search, it traverses the knowledge graph to find connected nodes — crossing file boundaries, surfacing relationships the embedding similarity alone would miss.
Step 2.2 — Building the Knowledge Graph
LLMGraphTransformer from langchain-experimental takes a Document and uses an LLM to extract entities (nodes) and relationships (edges). Here's the full implementation:
from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_openai import ChatOpenAI
from langchain_core.documents import Document
from langchain_neo4j import Neo4jGraph
import asyncio
llm = ChatOpenAI(model='gpt-4o-mini', api_key=OPENAI_KEY, temperature=0)
llm_transformer = LLMGraphTransformer(llm=llm)
semaphore = asyncio.Semaphore(3)
async def Create_KG(collection_name: str = 'documents'):
# Step 1: scroll all chunks from Qdrant
points = []
offset = None
while True:
result, offset = client.scroll(
collection_name=collection_name,
limit=50,
with_payload=True,
with_vectors=False,
offset=offset
)
points.extend(result)
if offset is None:
break
# Step 2: convert chunks to LangChain Documents
documents = []
for point in points:
payload = point.payload
text = payload.get('text')
if not text:
continue
doc = Document(
page_content=text,
metadata={
'file': payload.get('file', 'unknown'),
'node_type': payload.get('node_type', 'unknown'),
'name': payload.get('name', 'unknown'),
'start_line': payload.get('start_line', 0),
'end_line': payload.get('end_line', 0),
'Source_type': 'Graph_Document',
}
)
documents.append(doc)
# Step 3: extract graph structure with concurrency limit
async def semaphore_doc_processing(doc):
async with semaphore:
return await llm_transformer.aconvert_to_graph_documents([doc])
tasks = [semaphore_doc_processing(doc) for doc in documents]
graph_documents = await asyncio.gather(*tasks)
return [graph[0] for graph in graph_documents]
The semaphore with limit 3 is critical — asyncio.gather would otherwise fire all API calls simultaneously. For a repo with 50+ chunks that's 50+ concurrent GPT requests, which will immediately hit rate limits. The semaphore caps concurrency at 3 tasks at a time.
gpt-4o-mini is used here deliberately. Graph extraction is a structured extraction task — identifying "this function CALLS that function", "this class INHERITS from that class" — and mini handles it well at a fraction of the cost of GPT-4o.
Step 2.3 — Storing the Graph
Neo4j stores the actual graph structure for traversal. Qdrant stores embeddings of the serialized graph documents for semantic search.
def Store_graph_Neo4j(documents):
graph.add_graph_documents(
documents,
baseEntityLabel=True,
include_source=True
)
baseEntityLabel=True adds a universal __Entity__ label to every node, making cross-type traversal queries simpler. include_source=True links each node back to its source Document, so you can always trace a relationship back to the exact code chunk it came from.
For Qdrant, we first serialize each graph document into a rich text string:
def create_string_payload(graph_docs: list):
lists = []
for graph_doc in graph_docs:
payload = {}
temp_string = ''
source = graph_doc.source
# Include source metadata
for key, value in source.metadata.items():
payload[key] = value
temp_string += f'{key.upper()} : {value}\n'
temp_string += f'CODE : {source.page_content}\n'
payload['Code'] = source.page_content
# Include nodes
nodes_list = [{'node_id': n.id, 'type': n.type} for n in graph_doc.nodes]
temp_string += f'NODES : {nodes_list}'
payload['Nodes'] = nodes_list
# Include relationships
relationships_list = [
f'{r.source.id} {r.type} {r.target.id}'
for r in graph_doc.relationships
]
temp_string += f'NODES_RELATIONSHIP : {relationships_list}'
payload['Node_Relationship'] = relationships_list
payload['Source_type'] = 'Graph_Document'
payload['String_GraphDoc'] = temp_string
lists.append({'TEXT': temp_string, 'PAYLOAD': payload})
return lists
This serialized string — combining the code, node identities, and relationship triples — is what gets embedded. The idea is that a question like “how does module A connect to module B” will semantically match this combined representation better than matching against code text alone, because the relationship triples are part of the embedding input.
Step 2.4 — Graph RAG Retrieval
At query time, Graph RAG is a two-step process: vector search to find relevant graph nodes, then Cypher traversal to find their neighbors.
# Graph_RAG.py
def Graph_Query_Qdrant(message: str):
embedding = client_openai.embeddings.create(
model="text-embedding-3-small",
input=message
)
# Filter for graph documents only
client.create_payload_index(
collection_name="documents",
field_name="Source_type",
field_schema=PayloadSchemaType.KEYWORD
)
results = client.query_points(
collection_name="documents",
query=embedding.data[0].embedding,
query_filter=Filter(
must=[FieldCondition(
key="Source_type",
match=MatchValue(value="Graph_Document")
)]
),
limit=3
)
return results
The Source_type payload index is important. Qdrant holds two kinds of vectors: raw code chunks (from the analysis phase) and graph document embeddings (from the KG phase). The filter ensures that chat retrieval only hits graph documents — which carry the richer context of code + nodes + relationships — not the raw chunks.
Then the traversal:
def traversal_query(results, message: str):
traversal_results = []
code_results = []
graph_data = []
for result in results.points:
graph_data.append(f"file name : {result.payload['file']}")
graph_data.append(f"name : {result.payload['name']}")
code_results.append(result.payload['Code'])
for node_ID in result.payload['Nodes']:
node_id = node_ID.get('node_id')
# 1-2 hop traversal in Neo4j
result_query = graph.query("""
MATCH (n {id: $node_id})-[r*1..2]-(neighbor)
RETURN n.id AS source,
type(r[-1]) AS relationship,
neighbor.id AS target,
labels(neighbor) AS target_labels
""", params={"node_id": node_id})
traversal_results.extend(result_query)
traversal_lines = [
f"{row['source']} --[{row['relationship']}]--> {row['target']}"
for row in traversal_results
]
final_prompt = build_prompt(
question=message,
doc_context='\n'.join(code_results),
graph_context=',\n'.join(graph_data),
traversal_text='\n'.join(traversal_lines)
)
return final_prompt
The Cypher query MATCH (n {id: $node_id})-[r*1..2]-(neighbor) does a 1-to-2-hop traversal from each retrieved node, finding all directly and indirectly connected entities. A function node might connect to: the class it belongs to (1 hop), the module that class is in (2 hops), other functions it calls (1 hop), and the data types it returns (1 hop). This traversal output — rendered as source --[RELATIONSHIP]--> target lines — gives the chat LLM a structural map of the code it's reasoning about.
Step 2.5 — The Chat Agent
from agents import Agent, Runner, trace, function_tool
from openai.types.responses import ResponseTextDeltaEvent
@function_tool
def Query_VectorDB(message: str):
'''
When to call:
If the history isn't sufficient to answer user's question,
use the tool to query VectorDB (Qdrant) and retrieve relevant docs.
What to pass:
message : user's question
What does the function return:
Retrieved information as a string ready for the LLM to ingest.
'''
results = Graph_Query_Qdrant(message)
final_string = traversal_query(results, message)
return final_string
Chat_agent = Agent(
name='Chat_Agent',
instructions=CHAT_AGENT_INSTRUCTION,
tools=[Query_VectorDB],
model='gpt-4o-mini'
)
async def get_answer(message: str, history: list):
with trace(workflow_name="Github Repo Chat"):
result = Runner.run_streamed(
starting_agent=Chat_agent,
input=(history + [{"role": "user", "content": message}]),
context=history
)
async for event in result.stream_events():
if event.type == 'raw_response_event' and isinstance(event.data, ResponseTextDeltaEvent):
yield event.data.delta
The chat agent receives the full conversation history on every call — the Agents SDK is stateless so history management is explicit. gpt-4o-mini is used here (cheaper than GPT-4o, sufficient for answering code questions with retrieved context).
The chat system prompt is equally deliberate:
CHAT_AGENT_INSTRUCTION = '''
You are an expert code assistant helping developers understand a GitHub repository.
## Decision Rules
Call Query_VectorDB when:
- The user asks about a specific function, file, class or behaviour
- The conversation history does not already contain enough information
- The user introduces a new topic not yet covered in history
Do NOT call Query_VectorDB when:
- The answer is already present in a previous retrieval in the history
- The user is asking a follow-up or clarification on something already discussed
## How to Use Retrieved Information
The tool returns three sources — reason across all three:
- Code Chunks: use for exact implementation details, treat as ground truth
- Graph Descriptions: use for understanding structure and purpose
- Graph Traversal: use for understanding how things connect across files
- If sources contradict each other, always prefer Code Chunks
## How to Answer
- Be concise and technical
- Always mention the specific file name and function name
- When explaining a function: state input, what it does, what it returns
- When explaining a flow: trace it step by step across files and functions
- Never make up code or function names not present in retrieved context
'''
The decision rules for when not to call Query_VectorDB are as important as the rules for when to call it. Without them, the agent re-retrieves context on every single message — even follow-ups where the answer is sitting right in the history — burning tokens unnecessarily and introducing latency.
Step 2.6 — Orchestrating the Chat Phase in FastAPI
@app.websocket("/chat/{session_id}")
async def websocket_chat(websocket: WebSocket, session_id: str):
await websocket.accept()
await websocket.send_text("__LOADING__")
# Build knowledge graph from Qdrant chunks
list_graph_docs = await Create_KG()
# Reset Qdrant collection (replace raw chunks with graph docs)
try:
client.delete_collection(collection_name="documents")
except Exception:
pass
if list_graph_docs:
# Clear and repopulate Neo4j
graph.query("MATCH (n) DETACH DELETE n")
Store_graph_Neo4j(list_graph_docs)
# Store graph doc embeddings in Qdrant
Store_graph_Qdrant(list_graph_docs)
await websocket.send_text("__READY__")
await run_chat_loop(websocket, session_id)
There’s a deliberate reset here: when the user opens chat, we delete the raw chunk collection and replace it with graph document embeddings. The raw chunks served their purpose — they fed into Create_KG(). Now the graph documents (which contain code + nodes + relationships) are the retrieval source. The chat phase doesn't need the raw chunks anymore.
WebSocket progress messages (__LOADING__, __LOG__:..., __READY__) let the frontend show meaningful progress during the 30-60 seconds it takes to build the knowledge graph.
Environment Setup
OPENAI_API_KEY=sk-...
Github_access_token=Bearer ghp_...
QDRANT_CLUSTER=https://your-cluster.qdrant.io
QDRANT_API_KEY=your-qdrant-key
NEO4J_URI=neo4j+s://your-instance.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your-password
pip install openai-agents fastapi qdrant-client langchain-experimental \
langchain-neo4j langchain-openai tree-sitter-languages \
python-dotenv uvicorn
Run:
uvicorn main:app --reload
What You’ve Built
The complete GenAI pipeline behind Repo Lens:
- A 4-tool agentic system that reads a GitHub repository autonomously, following a strict ordered workflow enforced through prompt engineering
- AST-aware chunking that splits code at function and class boundaries instead of arbitrary token windows
- A knowledge graph builder that extracts entities and relationships from code chunks using
LLMGraphTransformer - A dual-store architecture — Neo4j for graph traversal, Qdrant for semantic search
- A Graph RAG chat agent that combines vector similarity with 1–2 hop Cypher traversal to answer questions that cross file boundaries
The full source is available on GitHub github.com/soumil2334/RepoLens. If you have questions about specific parts of the pipeline, feel free to drop them in the comments.
Built with: OpenAI Agents SDK · Qdrant · Neo4j · LangChain · FastAPI · tree-sitter(code-parser)
메타데이터
- post_id
- 080e48eedc0c
- slug
- repolens-enhancing-github-repository-understanding-using-graph-rag-080e48eedc0c
- url
- https://medium.com/@soumil2433/repolens-enhancing-github-repository-understanding-using-graph-rag-080e48eedc0c
- canonical_url
- https://medium.com/@soumil2433/repolens-enhancing-github-repository-understanding-using-graph-rag-080e48eedc0c
- author_url
- https://medium.com/@soumil2433
- status
- ok
- fetched_at
- 2026-06-09 15:37:30