AI GitHub Knowledge Base: Codebase Intelligence with RAG and Graph

TL;DR — AI GitHub knowledge base transforms code repositories into queryable knowledge graphs using RAG and AST analysis. CodeRAG: "Creates a semantic vector database (RAG) from your source code, then exposes it as MCP tools that give AI agents deep understanding of your entire codebase." CogniRepo: "On Python repos >= 15K LOC, cuts AI coding agent token usage by 50-80% compared to raw file reads. Works with Claude Code, Cursor, and Gemini CLI. Fully offline." VortexRAG: "Integrates vector storage (Qdrant) with structural AST call-graphs (Neo4j) — understands not only what code exists, but how classes, methods, and decorators communicate." Central Code Knowledge Graph: "One Neo4j graph spans all registered repos — Functions, classes, files, imports, calls stored as labeled nodes + typed relationships." Key patterns: AST chunking (tree-sitter, 15-26 languages), GraphRAG (Neo4j call graphs), Hybrid search (vector + BM25 with Reciprocal Rank Fusion), MCP tools (coderag_search, context_pack, lookup_symbol), Incremental sync (Git diffs, 1.8s delta, webhooks). Integrate with Google Drive, company knowledge, Nango, and RAG pipeline.

CogniRepo states the problem: "AI coding tools re-read your entire codebase on every task. CogniRepo fixes that. One server indexes every repo in your org with Tree-sitter across 26 languages, stores the structural map as a Neo4j property graph, keeps it fresh via incremental ingest + webhooks, and serves precise context to your AI assistant via MCP so it reads only what matters."

GitHub Smart Agent describes the capability: "Turn any repository into an interactive AI assistant for code understanding, debugging, and optimization. Instead of reading files one-by-one, you can ask architecture, bug, and optimization questions and get context-aware answers grounded in your code."

Codebase Intelligence Architecture

flowchart TD subgraph GitHub["GitHub Repositories"] Repo1["Repository 1"] Repo2["Repository 2"] Webhook["Webhook
(push events)"] end subgraph Ingestion["Ingestion Pipeline"] Clone["Clone / Fetch
(git)"] Diff["Git Diff
(changed files only)"] Parse["AST Parsing
(tree-sitter, 26 languages)"] Chunk["Semantic Chunking
(functions, classes, methods)"] Summarize["Generate NL Summaries
(per chunk)"] end subgraph Vector["Vector Store"] Embed["Generate Embeddings
(embedding model)"] Qdrant["Qdrant / pgvector
(dense vectors)"] BM25["BM25 Index
(sparse keyword)"] RRF["Reciprocal Rank Fusion
(hybrid search)"] end subgraph Graph["Knowledge Graph"] Neo4j["Neo4j
(property graph)"] Nodes["Nodes: File, Class, Function"] Edges["Edges: calls, imports, contains"] Queries["Graph Queries: callers_of,
callees_of, blast_radius"] end subgraph MCP["MCP Tools"] Search["coderag_search
(hybrid search)"] Context["context_pack
(token-budget context)"] Lookup["lookup_symbol
(O(1) symbol lookup)"] WhoCalls["who_calls
(caller trace)"] Explain["coderag_explain
(symbol explanation)"] end subgraph Agents["AI Agents"] Claude["Claude Code"] Cursor["Cursor"] Gemini["Gemini CLI"] end Repo1 --> Clone Repo2 --> Clone Webhook --> Diff Clone --> Parse Diff --> Parse Parse --> Chunk Chunk --> Summarize Summarize --> Embed Embed --> Qdrant Embed --> BM25 Qdrant --> RRF BM25 --> RRF Parse --> Neo4j Neo4j --> Nodes Neo4j --> Edges Edges --> Queries RRF --> Search Queries --> WhoCalls Chunk --> Lookup Search --> Context Lookup --> Explain Search --> Claude Context --> Claude Lookup --> Cursor WhoCalls --> Cursor Explain --> Gemini

AST Chunking vs Line Chunking

Aspect AST Chunking (tree-sitter) Line Chunking (naive)
Chunk boundary Function, class, method boundaries Arbitrary line counts
Semantic completeness Complete functions, no splits May split mid-function
Metadata Name, class, file, line range, params Line numbers only
Language support 26 languages via tree-sitter grammars Language-agnostic
NL enrichment Summary per chunk (what does this function do?) None
Retrieval quality 10x better (complete semantic units) Poor (partial context)
Import context Included with chunk Missing
Decorator info Preserved Lost

GraphRAG Query Types

Query What It Returns Use Case
callers_of(function) All functions that call this function Impact analysis, refactoring
callees_of(function) All functions this function calls Dependency mapping
imports_of(file) All imports for a file Dependency tracking
blast_radius(file) Files affected if this file changes Change impact assessment
downstream_dependencies(file) Files this file depends on Dependency graph
entry_points(repo) Most-connected functions Architecture analysis
file_symbols(file) All symbols in a file Code navigation
semantic_search(query) Semantically relevant code chunks Code search

Implementation

import tree_sitter
from tree_sitter import Parser
import hashlib

class GitHubKnowledgeBase:
    """AI knowledge base for GitHub repositories with AST + GraphRAG."""

    SUPPORTED_LANGUAGES = {
        "python": "tree_sitter_python",
        "javascript": "tree_sitter_javascript",
        "typescript": "tree_sitter_typescript",
        "go": "tree_sitter_go",
        "rust": "tree_sitter_rust",
        "java": "tree_sitter_java",
        "c": "tree_sitter_c",
        "cpp": "tree_sitter_cpp",
    }

    FUNCTION_NODE_TYPES = {
        "python": ["function_definition", "decorated_definition"],
        "javascript": ["function_declaration", "method_definition", "arrow_function"],
        "typescript": ["function_declaration", "method_definition", "arrow_function"],
        "go": ["function_declaration", "method_declaration"],
    }

    def __init__(self, vector_db, neo4j, embedding_model, llm, audit):
        self.vector_db = vector_db
        self.neo4j = neo4j
        self.embeddings = embedding_model
        self.llm = llm
        self.audit = audit
        self.parsers = {}

    async def ingest_repository(self, tenant_id: str, repo_url: str,
                                branch: str = "main") -> dict:
        """Ingest a GitHub repository into the knowledge base."""
        # 1. Clone repository
        repo_path = await self._clone_repo(repo_url, branch)

        # 2. Get last indexed commit (for incremental sync)
        last_commit = await self._get_last_commit(tenant_id, repo_url)
        current_commit = await self._get_current_commit(repo_path)

        if last_commit == current_commit:
            return {"status": "up_to_date", "commit": current_commit}

        # 3. Get changed files (Git diff)
        if last_commit:
            changed_files = await self._get_changed_files(repo_path, last_commit)
            # Delete old vectors and graph nodes for changed files
            for file_path in changed_files:
                await self._delete_file_vectors(tenant_id, repo_url, file_path)
                await self._delete_file_graph_nodes(tenant_id, repo_url, file_path)
        else:
            changed_files = await self._list_all_files(repo_path)

        # 4. Parse and chunk each changed file
        chunks_created = 0
        for file_path in changed_files:
            language = self._detect_language(file_path)
            if language not in self.SUPPORTED_LANGUAGES:
                continue

            chunks = await self._parse_file(repo_path, file_path, language)
            for chunk in chunks:
                await self._store_chunk(tenant_id, repo_url, file_path, chunk)
                chunks_created += 1

            # Build graph nodes and edges
            await self._build_graph(tenant_id, repo_url, file_path, chunks)

        # 5. Update commit hash
        await self._update_commit(tenant_id, repo_url, current_commit)

        # 6. Audit log
        await self.audit.log(
            tenant_id=tenant_id,
            action="github_repo_ingested",
            entity_type="repository",
            entity_id=repo_url,
            after={
                "commit": current_commit,
                "files_processed": len(changed_files),
                "chunks_created": chunks_created,
            },
        )

        return {
            "status": "ingested",
            "commit": current_commit,
            "files_processed": len(changed_files),
            "chunks_created": chunks_created,
        }

    async def _parse_file(self, repo_path: str, file_path: str,
                          language: str) -> list:
        """Parse a source file using tree-sitter AST."""
        parser = self._get_parser(language)

        with open(f"{repo_path}/{file_path}", "rb") as f:
            source_code = f.read()

        tree = parser.parse(source_code)

        # Extract semantic chunks (functions, classes, methods)
        chunks = []
        function_types = self.FUNCTION_NODE_TYPES.get(language, [])

        def traverse(node, class_name=None):
            if node.type in function_types:
                # Extract function/method chunk
                name_node = node.child_by_field_name("name")
                func_name = source_code[name_node.start_byte:name_node.end_byte].decode() if name_node else "anonymous"

                chunk_text = source_code[node.start_byte:node.end_byte].decode()

                # Generate natural language summary
                summary = await self._generate_summary(chunk_text, func_name, language)

                chunks.append({
                    "type": "function",
                    "name": func_name,
                    "class": class_name,
                    "file": file_path,
                    "start_line": node.start_point[0] + 1,
                    "end_line": node.end_point[0] + 1,
                    "text": chunk_text,
                    "summary": summary,
                    "language": language,
                })

            elif node.type == "class_definition" or node.type == "class_declaration":
                name_node = node.child_by_field_name("name")
                cls_name = source_code[name_node.start_byte:name_node.end_byte].decode() if name_node else "anonymous"

                chunks.append({
                    "type": "class",
                    "name": cls_name,
                    "file": file_path,
                    "start_line": node.start_point[0] + 1,
                    "end_line": node.end_point[0] + 1,
                    "text": source_code[node.start_byte:node.end_byte].decode(),
                    "language": language,
                })

                # Traverse children with class context
                for child in node.children:
                    traverse(child, class_name=cls_name)
            else:
                for child in node.children:
                    traverse(child, class_name=class_name)

        traverse(tree.root_node)
        return chunks

    async def _store_chunk(self, tenant_id: str, repo_url: str,
                           file_path: str, chunk: dict):
        """Store a code chunk in vector database with embedding."""
        # Combine code + summary for embedding
        embed_text = f"{chunk['summary']}\n\n{chunk['text']}"
        embedding = await self.embeddings.embed(embed_text)

        content_hash = hashlib.sha256(chunk["text"].encode()).hexdigest()

        await self.vector_db.insert({
            "tenant_id": tenant_id,
            "repo_url": repo_url,
            "file_path": file_path,
            "chunk_type": chunk["type"],
            "chunk_name": chunk["name"],
            "class_name": chunk.get("class"),
            "start_line": chunk["start_line"],
            "end_line": chunk["end_line"],
            "text": chunk["text"],
            "summary": chunk["summary"],
            "language": chunk["language"],
            "embedding": embedding,
            "content_hash": content_hash,
        })

    async def _build_graph(self, tenant_id: str, repo_url: str,
                           file_path: str, chunks: list):
        """Build Neo4j knowledge graph nodes and edges."""
        for chunk in chunks:
            if chunk["type"] == "class":
                await self.neo4j.execute(
                    "CREATE (c:Class {tenant_id: $tenant, repo: $repo, "
                    "file: $file, name: $name, line: $line})",
                    tenant=tenant_id, repo=repo_url, file=file_path,
                    name=chunk["name"], line=chunk["start_line"],
                )
            elif chunk["type"] == "function":
                await self.neo4j.execute(
                    "CREATE (f:Function {tenant_id: $tenant, repo: $repo, "
                    "file: $file, name: $name, class: $class, line: $line})",
                    tenant=tenant_id, repo=repo_url, file=file_path,
                    name=chunk["name"], class=chunk.get("class"),
                    line=chunk["start_line"],
                )

        # Create CONTAINS edges (file → functions/classes)
        await self.neo4j.execute(
            "MATCH (f:File {tenant_id: $tenant, path: $path}), "
            "(s {tenant_id: $tenant, file: $path}) "
            "CREATE (f)-[:CONTAINS]->(s)",
            tenant=tenant_id, path=file_path,
        )

    async def hybrid_search(self, tenant_id: str, query: str,
                            top_k: int = 10) -> list:
        """Hybrid search: vector similarity + BM25 with Reciprocal Rank Fusion."""
        # Vector search
        query_embedding = await self.embeddings.embed(query)
        vector_results = await self.vector_db.search(
            embedding=query_embedding,
            filter={"tenant_id": tenant_id},
            top_k=top_k * 3,
        )

        # BM25 keyword search
        keyword_results = await self.vector_db.bm25_search(
            query=query,
            filter={"tenant_id": tenant_id},
            top_k=top_k * 3,
        )

        # Reciprocal Rank Fusion
        fused = self._reciprocal_rank_fusion(vector_results, keyword_results)

        return fused[:top_k]

    def _reciprocal_rank_fusion(self, vector_results: list,
                                keyword_results: list, k: int = 60) -> list:
        """Fuse ranked lists using Reciprocal Rank Fusion."""
        scores = {}

        for rank, result in enumerate(vector_results):
            chunk_id = result["id"]
            scores[chunk_id] = scores.get(chunk_id, 0) + 1 / (k + rank + 1)

        for rank, result in enumerate(keyword_results):
            chunk_id = result["id"]
            scores[chunk_id] = scores.get(chunk_id, 0) + 1 / (k + rank + 1)

        # Sort by fused score
        all_results = {r["id"]: r for r in vector_results + keyword_results}
        sorted_ids = sorted(scores, key=scores.get, reverse=True)

        return [
            {**all_results[chunk_id], "fused_score": scores[chunk_id]}
            for chunk_id in sorted_ids
        ]

MCP Tools for AI Agents

Tool Description Transport Use Case
coderag_search Hybrid search (semantic + keyword) stdio / SSE Find relevant code
context_pack Token-budget code context stdio / SSE Provide context to LLM
lookup_symbol O(1) symbol → file + line stdio / SSE Fast code navigation
who_calls Trace callers of a function stdio / SSE Impact analysis
coderag_explain Explain a code symbol stdio / SSE Code understanding
coderag_status Index health and stats stdio / SSE Monitoring
coderag_docs Search documentation stdio / SSE Doc search
coderag_backlog Query project backlog (Jira, ClickUp) stdio / SSE Task context

AI GitHub Knowledge Base Checklist

  • [ ] Set up GitHub App or OAuth for repository access
  • [ ] Configure webhook for push events (trigger incremental sync)
  • [ ] Clone/fetch repositories via git
  • [ ] Track last indexed commit hash per repository
  • [ ] Implement incremental sync using git diff (only changed files)
  • [ ] Delete old vectors and graph nodes for changed files before re-indexing
  • [ ] Install tree-sitter with grammars for 15-26 programming languages
  • [ ] Parse source files using tree-sitter AST (not line-based chunking)
  • [ ] Chunk by semantic units: functions, classes, methods
  • [ ] Preserve metadata: name, class, file path, line range, parameters
  • [ ] Generate natural language summaries for each chunk
  • [ ] Combine code + summary for embedding (better semantic matching)
  • [ ] Store embeddings in vector database (Qdrant, pgvector, Pinecone)
  • [ ] Build BM25 sparse index for keyword search
  • [ ] Implement Reciprocal Rank Fusion for hybrid search
  • [ ] Build Neo4j knowledge graph: File, Class, Function nodes
  • [ ] Create graph edges: calls, imports, contains
  • [ ] Implement graph queries: callers_of, callees_of, blast_radius
  • [ ] Implement entry_points query (most-connected functions)
  • [ ] Expose MCP tools: coderag_search, context_pack, lookup_symbol
  • [ ] Support MCP stdio transport for local agents (Claude Code, Cursor)
  • [ ] Support MCP SSE transport for remote agents
  • [ ] Implement context_pack with token budget management
  • [ ] Implement lookup_symbol for O(1) symbol → file + line lookup
  • [ ] Implement who_calls for caller tracing and impact analysis
  • [ ] Support multi-repo indexing (one graph spans all repos)
  • [ ] Index documentation (Markdown, Confluence, SharePoint)
  • [ ] Integrate with project backlog (Jira, ClickUp, Azure DevOps)
  • [ ] Connect to company knowledge RAG pipeline
  • [ ] Integrate with RAG to actuation pipeline
  • [ ] Use Nango for GitHub OAuth management
  • [ ] Log all ingestion and query events in audit logs
  • [ ] Apply RBAC to repository access
  • [ ] Apply zero-trust architecture to code data
  • [ ] Apply OWASP LLM Top 10 security controls
  • [ ] Set up platform monitoring for ingestion
  • [ ] Monitor: files indexed, sync latency, query latency, graph size
  • [ ] Handle large repositories (15K+ LOC) with efficient parsing
  • [ ] Implement retry logic for git operations and API failures
  • [ ] Consider multi-tenant repo isolation
  • [ ] Support private repositories (GitHub App permissions)
  • [ ] Consider semantic answer caching for queries
  • [ ] Test: AST parsing across all supported languages
  • [ ] Test: incremental sync detects changes correctly
  • [ ] Test: hybrid search returns relevant results
  • [ ] Test: graph queries return correct callers/callees
  • [ ] Test: MCP tools work with Claude Code, Cursor, Gemini CLI
  • [ ] Document supported languages and MCP tool usage
  • [ ] Consider AI incident response for ingestion failures

FAQ

What is an AI GitHub knowledge base?

An AI GitHub knowledge base is a system that ingests code repositories and transforms them into a queryable knowledge graph using RAG and AST analysis. CodeRAG defines it: "CodeRAG creates a semantic vector database (RAG) from your source code, documentation, and project backlog, then exposes it as MCP tools that give AI agents deep understanding of your entire codebase." CogniRepo reports: "On Python repos >= 15K LOC, CogniRepo cuts AI coding agent token usage by 50-80% compared to raw file reads." Key capabilities: (1) AST-based code parsing with tree-sitter — chunks by functions, classes, methods, not arbitrary line splits. (2) GraphRAG — Neo4j call graphs showing how classes, methods, and decorators communicate. (3) Hybrid search — vector similarity (semantic) + BM25 (keyword) with Reciprocal Rank Fusion. (4) MCP tools — coderag_search, coderag_context, coderag_explain. (5) Incremental sync — Git diffs to re-index only changed files.

How does AST-based code chunking work?

AST-based code chunking uses tree-sitter to parse source code into an Abstract Syntax Tree and split it into semantically meaningful units — functions, classes, methods — instead of arbitrary line splits. CodeRAG: "Tree-sitter parses source code into semantically meaningful chunks (functions, classes, methods), not arbitrary line splits." VortexRAG: "AST-based semantic chunking using tree-sitter." Benefits: (1) Each chunk is a complete semantic unit — no half-functions. (2) Chunks include metadata: function name, class name, file path, line range, parameters. (3) Natural language summaries are generated for each chunk, enriching the embedding. (4) Supports 15-26 programming languages via tree-sitter grammars. (5) Chunks can be enriched with import context and decorator information. This produces 10x better retrieval quality compared to arbitrary line-based chunking.

What is GraphRAG for code repositories?

GraphRAG for code repositories combines vector search with structural code analysis using a knowledge graph (Neo4j). VortexRAG: "By integrating vector storage (Qdrant) with structural AST call-graphs (Neo4j), the application understands not only what code exists, but how classes, methods, and decorators communicate with each other." Central Code Knowledge Graph: "One Neo4j graph spans all registered repos — Functions, classes, files, imports, calls all stored as labeled nodes + typed relationships." Graph queries enable: (1) callers_of — who calls this function? (2) callees_of — what does this function call? (3) blast_radius — what files are affected if this file changes? (4) downstream_dependencies — what does this file depend on? (5) entry_points — most-connected functions. Vector search finds semantically relevant code; graph traversal adds structural context (sibling functions, call paths, containing classes).

How do you keep a code knowledge base current?

Keep a code knowledge base current with incremental sync using Git diffs. VortexRAG: "Smart Delta Ingestion uses Git diffs to delete, chunk, and upsert only modified files, completing ingestion in seconds. Full ingestion (150 large files): ~45 seconds. Smart Delta Sync (2 modified files): 1.8 seconds." CodeRAG: "Subsequent coderag index runs are incremental — only changed files are re-processed." Central Code Knowledge Graph: "keeps it fresh via incremental ingest + webhooks." Implementation: (1) Track last indexed commit hash. (2) On sync, run git diff to find changed files. (3) Delete vectors and graph nodes for changed files. (4) Re-parse, re-chunk, re-embed, and re-insert only changed files. (5) Update commit hash. (6) Use GitHub webhooks to trigger sync on push events.

How do MCP tools enable AI agents to use a code knowledge base?

MCP (Model Context Protocol) tools expose the code knowledge base to AI agents like Claude Code, Cursor, and Gemini CLI. CodeRAG provides 6 MCP tools: (1) coderag_search — semantic + keyword hybrid search across the codebase. (2) coderag_context — assemble relevant context within a token budget. (3) coderag_explain — explain a code symbol with full surrounding context. (4) coderag_status — check index health and statistics. (5) coderag_docs — search indexed documentation (Markdown, Confluence, SharePoint). (6) coderag_backlog — query project backlog items (Jira, ClickUp, Azure DevOps). CogniRepo provides 34 MCP tools including lookup_symbol (O(1) symbol lookup), who_calls (trace callers), and context_pack (token-budget code context). MCP tools work with stdio and SSE transport, enabling both local and remote agent connections.


Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →