Spaces:
Configuration error
Configuration error
Commit ·
da206c8
1
Parent(s): c4cf57a
feat: complete Phase 1 Repository Intelligence MVP
Browse files- Repository analysis pipeline
- ChromaDB semantic indexing
- DeepSeek V4 Flash integration
- Repository chat with SSE streaming
- Issue mapper with grounded retrieval
- Architecture graph generation
- Reading path generation
- Impact analysis engine
- Unified repository workspace
- Session persistence
- Analysis persistence
- NVIDIA NIM fallback mode
- Documentation overhaul
- MVP validation reports
- .env.example +9 -2
- .vscode/settings.json +2 -0
- ARCHITECTURE.md +231 -0
- ARCHITECTURE_BUGFIX_REPORT.md +0 -305
- README.md +93 -120
- RELEASE_NOTES.md +17 -24
- backend/api.py +291 -74
- backend/main.py +17 -1
- docs/API_REFERENCE.md +197 -91
- docs/DEVELOPMENT_SETUP.md +62 -60
- docs/MVP_STATUS.md +34 -31
- docs/TROUBLESHOOTING.md +14 -12
- docs/VALIDATION_REPORT.md +57 -29
- frontend/src/components/Navbar.astro +27 -9
- frontend/src/components/interactive/AnalysisDashboard.tsx +23 -2
- frontend/src/components/interactive/ChatInterface.tsx +400 -110
- frontend/src/components/interactive/IssueMapper.tsx +14 -1
- frontend/src/components/interactive/RepoInput.tsx +8 -6
- frontend/src/pages/chat.astro +1 -1
- services/architecture_service.py +16 -0
- services/chat_fallback.py +108 -0
- services/embedding_service.py +4 -8
- services/github_service.py +6 -2
- services/storage_paths.py +34 -0
.env.example
CHANGED
|
@@ -18,12 +18,19 @@ GITHUB_REPOSITORY=owner/repo # [Unused]
|
|
| 18 |
SQLITE_DB_PATH=data/repo_understanding.db # [Planned] SQLite relational storage path
|
| 19 |
CHROMA_DB_PATH=data/chroma_db # [Used] ChromaDB persistence directory
|
| 20 |
CACHE_FILE_PATH=data/cache.json # [Planned] Shared cache layer path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# =============================================================================
|
| 23 |
# API Server Settings
|
| 24 |
# =============================================================================
|
| 25 |
-
API_SERVER_PORT=
|
| 26 |
-
API_SERVER_HOST=0.0.0.0 # [
|
| 27 |
|
| 28 |
# =============================================================================
|
| 29 |
# Frontend Settings
|
|
|
|
| 18 |
SQLITE_DB_PATH=data/repo_understanding.db # [Planned] SQLite relational storage path
|
| 19 |
CHROMA_DB_PATH=data/chroma_db # [Used] ChromaDB persistence directory
|
| 20 |
CACHE_FILE_PATH=data/cache.json # [Planned] Shared cache layer path
|
| 21 |
+
# [Used] Where to clone analyzed repos. MUST be outside the project tree —
|
| 22 |
+
# otherwise uvicorn --reload's WatchFiles fires on every cloned file and kills
|
| 23 |
+
# the in-flight analysis. Examples:
|
| 24 |
+
# Windows: C:/repo_intelligence_storage/cloned_repos
|
| 25 |
+
# Linux/macOS: /var/lib/repo_intelligence/cloned_repos
|
| 26 |
+
# Leave blank to default to ~/.repo_intelligence/cloned_repos.
|
| 27 |
+
CLONED_REPOS_PATH=
|
| 28 |
|
| 29 |
# =============================================================================
|
| 30 |
# API Server Settings
|
| 31 |
# =============================================================================
|
| 32 |
+
API_SERVER_PORT=8001 # [Used] Configures the uvicorn port in backend/main.py
|
| 33 |
+
API_SERVER_HOST=0.0.0.0 # [Used] Configures the uvicorn host in backend/main.py
|
| 34 |
|
| 35 |
# =============================================================================
|
| 36 |
# Frontend Settings
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
}
|
ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🏗️ Architecture Specification
|
| 2 |
+
|
| 3 |
+
This document provides a comprehensive technical overview of the **Repo Intelligence Agent** system architecture. It outlines the structural layers, data ingestion pipelines, API contracts, mathematical models, and operational workflows.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 🏛️ System Overview & Multi-Layer Design
|
| 8 |
+
|
| 9 |
+
The application follows a modular, layer-oriented architecture separating the client-side presentation from the local parsing engines and backend intelligence nodes.
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
┌────────────────────────────────────────────────────────┐
|
| 13 |
+
│ Astro 4 & React Frontend │
|
| 14 |
+
│ (Chat Panel, React Flow Visualiser, Reading Timeline) │
|
| 15 |
+
└───────────────────────────┬────────────────────────────┘
|
| 16 |
+
│ HTTP & SSE (Event Stream)
|
| 17 |
+
▼
|
| 18 |
+
┌────────────────────────────────────────────────────────┐
|
| 19 |
+
│ FastAPI API Gateway │
|
| 20 |
+
│ (Binds to Port 8001, SSE Emitters, Request Routing) │
|
| 21 |
+
└───────────────────────────┬────────────────────────────┘
|
| 22 |
+
│ In-memory & Thread Pools
|
| 23 |
+
▼
|
| 24 |
+
┌────────────────────────────────────────────────────────┐
|
| 25 |
+
│ Code Processing Engine │
|
| 26 |
+
│ (GitHub Extractor, Code Chunking, Tree-sitter Parser) │
|
| 27 |
+
└─────────────┬─────────────────────────────┬────────────┘
|
| 28 |
+
│ │
|
| 29 |
+
▼ ▼
|
| 30 |
+
┌───────────────────────────┐ ┌──────────────────────────┐
|
| 31 |
+
│ Data Layer │ │ Reasoning Layer │
|
| 32 |
+
│ (ChromaDB, NetworkX, │ │ (DeepSeek V4 Flash NIM) │
|
| 33 |
+
│ analysis_store.json) │ │ │
|
| 34 |
+
└───────────────────────────┘ └──────────────────────────┘
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 1. Client Layer (Astro 4 + React)
|
| 38 |
+
- **Astro Pages:** Astro provides zero-JS static structure for routing and layout templates (`index.astro`, `chat.astro`, `analysis.astro`, `issues.astro`).
|
| 39 |
+
- **React Hydration:** High-interaction dashboard elements are implemented as React components hydrated in the client browser.
|
| 40 |
+
- **Visual Mapping:** Uses `React Flow` to render zoomable, interactive representations of dependency graphs and impact propagation paths.
|
| 41 |
+
- **Server-Sent Events (SSE):** Standard HTML5 `EventSource` handles real-time token streaming for chat and indexing logs.
|
| 42 |
+
|
| 43 |
+
### 2. API Gateway Layer (FastAPI)
|
| 44 |
+
- **Asynchronous Execution:** Built on ASGI, utilizing `asyncio` and thread-pool execution (`asyncio.to_thread`) for long-running blocking operations like cloning and embedding generation.
|
| 45 |
+
- **Runtime Port:** Serves HTTP traffic on port **8001** (as configured in `backend/main.py` and `frontend/src/lib/api.ts`).
|
| 46 |
+
- **Data Integrity:** Employs strictly-typed `Pydantic` models for request validation and response serialization, protecting backend services from malformed payloads.
|
| 47 |
+
|
| 48 |
+
### 3. Code Ingestion & Processing Layer
|
| 49 |
+
- **Extracellular Walk:** Git command-line utility clones repositories locally. The file walker filters out binaries, images, and folders like `.git`, `.venv`, and `node_modules`.
|
| 50 |
+
- **AST Parsing:** Tree-sitter runs syntax parsing. It uses compiled language objects for Python, JavaScript, TypeScript, JSX, and TSX to extract imports, exports, class variables, methods, and functions.
|
| 51 |
+
- **Code Chunking:** Splits source code into sliding window text blocks (1500 characters, 200 character overlap) to prepare documents for vector indexing.
|
| 52 |
+
|
| 53 |
+
### 4. Memory & Vector DB Layer (Local)
|
| 54 |
+
- **Vector Space:** ChromaDB stores code chunks. Dense representation utilizes `BAAI/bge-small-en-v1.5` loaded locally via `sentence_transformers`. Output vectors are static 384-dimensional arrays.
|
| 55 |
+
- **Graph Space:** NetworkX handles directed graph (`DiGraph`) calculations, building import relationships where files represent nodes and imports represent edges.
|
| 56 |
+
- **Relational Storage:** Repository metadata and parsed summary schemas are serialized to the local file system at `data/analysis_store.json` on changes, enabling persisted states to survive process recycles.
|
| 57 |
+
|
| 58 |
+
### 5. AI Reasoning Layer (NVIDIA NIM)
|
| 59 |
+
- **Remote Host:** Connects to DeepSeek V4 Flash (`deepseek-ai/deepseek-v4-flash`) hosted on NVIDIA NIM via an OpenAI-compatible interface.
|
| 60 |
+
- **Fault Tolerance:** Includes retry policies (exponential backoff) and an automatic fallback mechanism to retrieve and formulate responses locally when API requests fail.
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## 💾 Unified Workspace & Session Store
|
| 65 |
+
|
| 66 |
+
The application frontend wraps all repository-specific dashboards inside a **Unified Repository Workspace**.
|
| 67 |
+
|
| 68 |
+
### 1. Unified Repository Workspace Tabs Layout
|
| 69 |
+
```
|
| 70 |
+
Unified Repository Workspace
|
| 71 |
+
├── CODEBASE ANALYSIS (Overview, Tech Stack, Dependencies, Component Relations)
|
| 72 |
+
├── ARCHITECTURE GRAPH (Interactive React Flow Dependency Mapping)
|
| 73 |
+
├── READING PATH (Suggested Code Timeline & Centrality Graph)
|
| 74 |
+
├── IMPACT ANALYSIS (BFS Traversal Change Predictor)
|
| 75 |
+
├── ISSUE INTELLIGENCE (Issue Mapper Plan Generator)
|
| 76 |
+
└── CHAT (Context-Grounded Conversational Q&A Panel)
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
The parent `AnalysisDashboard` component fetches the codebase metadata once on initial load, caching it in React state. Switching tabs updates the UI view instantly without invoking re-analysis or repeat backend queries.
|
| 80 |
+
|
| 81 |
+
### 2. Session Context Sync
|
| 82 |
+
To allow seamless workspace sharing and recovery across browser tabs, the active session is divided into two stores:
|
| 83 |
+
|
| 84 |
+
#### Persisted Session Store (`localStorage`):
|
| 85 |
+
- **Fields:** `owner`, `repo`, `indexing status`, `graph status`, and `last analyzed timestamp`.
|
| 86 |
+
- **Rationale:** If the user reloads the browser, context is restored instantly. Since the backend persists analysis details to disk, the UI can re-fetch the complete cached analysis from `/api/analysis/{owner}/{repo}` using the stored name, preventing redundant re-clones.
|
| 87 |
+
|
| 88 |
+
#### Non-persisted Session Store (React In-memory State):
|
| 89 |
+
- **Fields:** React Flow graph nodes/edges, file-tree structures, impact analysis BFS paths, and current chat messages.
|
| 90 |
+
- **Rationale:** Large graph datasets exceed browser `localStorage` capacity limits (5MB) and degrade browser serialization performance. Storing these in React state keeps memory usage low and page loads fast.
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## 🛡️ Ingestion Persistence & Chat Fallback
|
| 95 |
+
|
| 96 |
+
### 1. Ingestion Analysis Persistence Lifecycle
|
| 97 |
+
Rather than holding processed analysis logs in volatile memory, the gateway persists state details to `data/analysis_store.json`.
|
| 98 |
+
- **Startup Hydration:** On backend startup, FastAPI walks the JSON file and hydrates the `ANALYSIS_STORE` cache.
|
| 99 |
+
- **Repository Recovery:** If the FastAPI backend crashes or restarts during local development, already analyzed repositories are recovered instantly without requiring a re-index.
|
| 100 |
+
- **Lifecycle Updates:** Any new repository processed via `/api/analyze` triggers an atomic write-back to the JSON file upon completion.
|
| 101 |
+
|
| 102 |
+
### 2. Chat Fallback Resilience Layer
|
| 103 |
+
NVIDIA NIM free-tier API keys are rate-limited to ~3 requests per minute. To maintain a smooth user experience, the `/api/chat` and `/api/issues/map` endpoints implement a fallback structure:
|
| 104 |
+
|
| 105 |
+
```
|
| 106 |
+
┌────────────────────────────────────────┐
|
| 107 |
+
│ Submit User Request │
|
| 108 |
+
└───────────────────┬────────────────────┘
|
| 109 |
+
▼
|
| 110 |
+
┌────────────────────────────────────────┐
|
| 111 |
+
│ Vector ChromaDB Retrieve │
|
| 112 |
+
└───────────────────┬────────────────────┘
|
| 113 |
+
▼
|
| 114 |
+
┌────────────────────────────────────────┐
|
| 115 |
+
│ Attempt DeepSeek V4 NIM Call │
|
| 116 |
+
└─────────┬────────────────────┬─────────┘
|
| 117 |
+
│ │
|
| 118 |
+
Success │ │ Rate Limit / 429 / Timeout
|
| 119 |
+
▼ ▼
|
| 120 |
+
┌──────────────────────────────┐ ┌───────────────────────────────────┐
|
| 121 |
+
│ Stream LLM Tokens to Client │ │ Activate Grounded Fallback Mode │
|
| 122 |
+
└──────────────┬───────────────┘ └─────────────────┬─────────────────┘
|
| 123 |
+
│ │
|
| 124 |
+
│ ▼
|
| 125 |
+
│ ┌───────────────────────────────────┐
|
| 126 |
+
│ │ Fuzzy Match Paths to Components │
|
| 127 |
+
│ └─────────────────┬─────────────────┘
|
| 128 |
+
│ │
|
| 129 |
+
│ ▼
|
| 130 |
+
│ ┌───────────────────────────────────┐
|
| 131 |
+
│ │ Extract Chunks as Plan Sentences │
|
| 132 |
+
│ └─────────────────┬─────────────────┘
|
| 133 |
+
│ │
|
| 134 |
+
▼ ▼
|
| 135 |
+
┌───────────────────────────────────────────────────────────────────────┐
|
| 136 |
+
│ Return Grounded Response + Cited Sources + Confidence │
|
| 137 |
+
└───────────────────────────────────────────────────────────────────────┘
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
When NVIDIA NIM returns a `429 Too Many Requests` or provider exception:
|
| 141 |
+
1. The backend intercepts the exception and suppresses raw HTTP status error traces.
|
| 142 |
+
2. It fetches the top-5 relevant code blocks from ChromaDB.
|
| 143 |
+
3. It performs local keyword path matching to categorize affected components (e.g. `auth` maps to `Authentication`).
|
| 144 |
+
4. It extracts class/function headers from the retrieved code blocks to generate a step-by-step local fallback implementation plan.
|
| 145 |
+
5. It outputs the local plan directly to the client alongside ChromaDB source files and a similarity-derived confidence score.
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## 🧮 Mathematical & Graph Processing Models
|
| 150 |
+
|
| 151 |
+
### 1. Onboarding Reading Path Heuristics
|
| 152 |
+
The Reading Path generator calculates an onboarding hierarchy using structural indicators extracted from the dependency graph. The composite scoring model is defined as:
|
| 153 |
+
|
| 154 |
+
$$\text{Composite Score}(v) = (W_{\text{entry}} \times \mathbb{I}_{\text{entry}}(v)) + (W_{\text{centrality}} \times C_D(v)) + (W_{\text{in-degree}} \times I_D(v)) + (W_{\text{core}} \times \mathbb{I}_{\text{core}}(v)) - (W_{\text{peripheral}} \times \mathbb{I}_{\text{peripheral}}(v))$$
|
| 155 |
+
|
| 156 |
+
Where:
|
| 157 |
+
- $\mathbb{I}_{\text{entry}}(v) \in \{0, 1\}$ indicates if the node $v$ is a detected framework entry point.
|
| 158 |
+
- $C_D(v)$ represents the Normalized Degree Centrality of node $v$ in the undirected representation of the dependency graph:
|
| 159 |
+
$$C_D(v) = \frac{\text{deg}(v)}{N - 1}$$
|
| 160 |
+
- $I_D(v)$ represents the Normalized In-Degree of node $v$ (number of other files importing $v$):
|
| 161 |
+
$$I_D(v) = \frac{\text{deg}^-(v)}{N - 1}$$
|
| 162 |
+
- $\mathbb{I}_{\text{core}}(v) \in \{0, 1\}$ indicates if the file resides in a core system package (e.g. `services/`, `models/`, `core/`).
|
| 163 |
+
- $\mathbb{I}_{\text{peripheral}}(v) \in \{0, 1\}$ indicates if the file is in a non-execution directory (e.g. `tests/`, `docs/`, `examples/`).
|
| 164 |
+
|
| 165 |
+
#### Scoring Weights configuration:
|
| 166 |
+
- $W_{\text{entry}} = 100.0$
|
| 167 |
+
- $W_{\text{centrality}} = 50.0$
|
| 168 |
+
- $W_{\text{in-degree}} = 30.0$
|
| 169 |
+
- $W_{\text{core}} = 15.0$
|
| 170 |
+
- $W_{\text{peripheral}} = 100.0$
|
| 171 |
+
|
| 172 |
+
The service calculates scores, applies topological sorting on the dependency graph to ensure dependencies are read before dependents, and falls back to ordering by composite score descending.
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
### 2. Change Impact Analysis Walk Model
|
| 177 |
+
The impact analyzer maps change propagation by walking the directed import graph.
|
| 178 |
+
1. **Seed Localization:** Fuzzy matches issue keywords against file paths in the repository to establish the "Seed Set" $S$.
|
| 179 |
+
2. **Forward BFS Walk (Direct Dependents):** Traverses edges matching $v \to u$ (where file $u$ imports file $v$) up to depth $4$, mapping immediate downstream dependents.
|
| 180 |
+
3. **Reverse BFS Walk (Downstream Dependents):** Traverses backward paths to locate files containing dependencies of the seed files.
|
| 181 |
+
4. **Risk Evaluation:** Computes risk score $R$:
|
| 182 |
+
$$R = |F_{\text{affected}}| + (\beta \times |F_{\text{core}}|) + (\gamma \times |F_{\text{coupled}}|)$$
|
| 183 |
+
Where:
|
| 184 |
+
- $F_{\text{affected}}$ is the combined set of directly and indirectly impacted files.
|
| 185 |
+
- $F_{\text{core}}$ is the subset of affected files located in core directories ($\beta = 2$).
|
| 186 |
+
- $F_{\text{coupled}}$ is the subset of affected files having degree centrality higher than average ($\gamma = 1$).
|
| 187 |
+
- **Risk Level:**
|
| 188 |
+
- Low: $R < 4$
|
| 189 |
+
- Medium: $4 \le R < 10$
|
| 190 |
+
- High: $R \ge 10$
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
## 🔌 API Endpoint Specifications
|
| 195 |
+
|
| 196 |
+
### 🧭 Base URL: `http://127.0.0.1:8001`
|
| 197 |
+
|
| 198 |
+
| HTTP Method | Path | Request Body | Response Schema | Description |
|
| 199 |
+
| :--- | :--- | :--- | :--- | :--- |
|
| 200 |
+
| **GET** | `/health` | None | `{backend, llm_provider, llm_model, embedding_provider, vector_db, status}` | Health check |
|
| 201 |
+
| **GET** | `/api/repos/examples` | None | `List[{name, url, tech_stack, description}]` | Fetch pre-configured examples |
|
| 202 |
+
| **GET** | `/api/repos/recent` | None | `List[{name, url, tech_stack, analyzed_at}]` | List analyzed repositories |
|
| 203 |
+
| **POST** | `/api/index` | `{repo_url}` | `{status: "indexed", files: int, chunks: int}` | Index code chunks in ChromaDB |
|
| 204 |
+
| **POST** | `/api/analyze` | `{url, branch, model}` | `text/event-stream` (SSE Progress) | Trigger full analysis pipeline |
|
| 205 |
+
| **GET** | `/api/analysis/{owner}/{repo_name}` | None | `{analysis: RepositoryAnalysis, architecture: ArchitectureSummary}` | Fetch analysis details |
|
| 206 |
+
| **POST** | `/api/issues/map` | `{repo, issue, title, description}` | `IssueMapResponse` | Generate implementation plan |
|
| 207 |
+
| **POST** | `/api/chat` | `{repo, message, history: List}` | `text/event-stream` (SSE Chat Tokens) | Streaming chat Q&A |
|
| 208 |
+
| **POST** | `/api/architecture/build` | `{repo}` | `{status, repo, files_parsed, dependencies_found, entry_points}` | Rebuild dependency graph |
|
| 209 |
+
| **GET** | `/api/architecture/{owner}/{repo_name}` | None | `ArchitectureSummary` | Fetch architecture summary |
|
| 210 |
+
| **POST** | `/api/reading-order` | `{repo}` | `ReadingOrder` | Generate onboarding path |
|
| 211 |
+
| **POST** | `/api/impact-analysis` | `{repo, issue}` | `ImpactAnalysis` | Predict change impacts |
|
| 212 |
+
| **GET** | `/api/architecture/{owner}/{repo_name}/graph` | Query: `q` (filter) | React Flow Node-Link JSON | Fetch dependency graph data |
|
| 213 |
+
|
| 214 |
+
---
|
| 215 |
+
|
| 216 |
+
## 🕳️ Skeletal Architectural Stubs
|
| 217 |
+
|
| 218 |
+
To ensure long-term structure while maintaining local-first MVP operations, the codebase contains four skeletal stubs:
|
| 219 |
+
|
| 220 |
+
1. **`SQLiteStore` (`memory/sqlite_store.py`):**
|
| 221 |
+
- **Current State:** Skeletal stub raising `NotImplementedError` across all transactional storage calls.
|
| 222 |
+
- **MVP Handling:** In-memory maps and fs-level caching in `data/analysis_store.json` and `data/issue_cache.json` handle state persistence.
|
| 223 |
+
2. **`MCPService` (`services/mcp_service.py`):**
|
| 224 |
+
- **Current State:** Skeletal stub raising `NotImplementedError`.
|
| 225 |
+
- **MVP Handling:** Codebase cloning and indexing operations are performed directly via git execution and local walkers.
|
| 226 |
+
3. **`RepositoryAnalyzer` (`agents/analyzer.py`):**
|
| 227 |
+
- **Current State:** Skeletal stub raising `NotImplementedError`.
|
| 228 |
+
- **MVP Handling:** Ingestion and stack analysis logic are implemented directly inside `backend/api.py`.
|
| 229 |
+
4. **`ArchitectureExplainer` (`agents/explainer.py`):**
|
| 230 |
+
- **Current State:** Skeletal stub raising `NotImplementedError`.
|
| 231 |
+
- **MVP Handling:** Reading order and structural sorting calculations are handled directly inside `services/reading_order_service.py`.
|
ARCHITECTURE_BUGFIX_REPORT.md
DELETED
|
@@ -1,305 +0,0 @@
|
|
| 1 |
-
# Architecture Phase 1 — Bug Fix Report
|
| 2 |
-
|
| 3 |
-
## Reported Symptoms
|
| 4 |
-
|
| 5 |
-
After validating against `fastapi/fastapi`, the API returned:
|
| 6 |
-
|
| 7 |
-
```json
|
| 8 |
-
{
|
| 9 |
-
"entry_points": ["..."],
|
| 10 |
-
"core_modules": [],
|
| 11 |
-
"high_coupling_modules": [],
|
| 12 |
-
"total_files": 2979,
|
| 13 |
-
"total_dependencies": 0
|
| 14 |
-
}
|
| 15 |
-
```
|
| 16 |
-
|
| 17 |
-
The graph and all downstream metrics appeared broken.
|
| 18 |
-
|
| 19 |
-
---
|
| 20 |
-
|
| 21 |
-
## Investigation
|
| 22 |
-
|
| 23 |
-
A diagnostic script (`scripts/diagnose_phase1.py`) was run against the
|
| 24 |
-
live `fastapi/fastapi` clone to isolate each layer independently.
|
| 25 |
-
|
| 26 |
-
### TreeSitterService — HEALTHY
|
| 27 |
-
|
| 28 |
-
```
|
| 29 |
-
Files parsed: 1125
|
| 30 |
-
Total imports extracted: 3390
|
| 31 |
-
Files WITH imports: 918
|
| 32 |
-
```
|
| 33 |
-
|
| 34 |
-
Imports were being extracted correctly. The service was not the problem.
|
| 35 |
-
|
| 36 |
-
### GraphService — HEALTHY
|
| 37 |
-
|
| 38 |
-
```
|
| 39 |
-
File graph — nodes: 1125, edges: 1440
|
| 40 |
-
Module graph — nodes: 1218, edges: 2804
|
| 41 |
-
```
|
| 42 |
-
|
| 43 |
-
The graph engine was building edges correctly from the parsed imports.
|
| 44 |
-
|
| 45 |
-
### ArchitectureSummary — STALE CACHE
|
| 46 |
-
|
| 47 |
-
When `architecture_service.get_summary()` was called, it loaded the
|
| 48 |
-
file at `data/architecture/fastapi_fastapi.json`. That file had been
|
| 49 |
-
written during an earlier development run **before the graph service was
|
| 50 |
-
implemented**, and contained:
|
| 51 |
-
|
| 52 |
-
```json
|
| 53 |
-
{
|
| 54 |
-
"total_files": 2979,
|
| 55 |
-
"total_dependencies": 0,
|
| 56 |
-
"core_modules": [],
|
| 57 |
-
"high_coupling_modules": []
|
| 58 |
-
}
|
| 59 |
-
```
|
| 60 |
-
|
| 61 |
-
The `GET /api/architecture/{owner}/{repo}` endpoint was serving this
|
| 62 |
-
stale file directly, with no mechanism to detect that it was invalid.
|
| 63 |
-
|
| 64 |
-
### EntryPointService — OVER-DETECTION BUG
|
| 65 |
-
|
| 66 |
-
A separate but related bug: when the service was rebuilt using the
|
| 67 |
-
diagnostic script, it reported **574 entry points** instead of the
|
| 68 |
-
expected ~10–20. The root cause was `_is_python_framework_init`:
|
| 69 |
-
|
| 70 |
-
```python
|
| 71 |
-
# BROKEN — flags ANY file that imports fastapi
|
| 72 |
-
def _is_python_framework_init(file_path, parsed):
|
| 73 |
-
imports_lower = [i.lower() for i in parsed.get("imports", [])]
|
| 74 |
-
return "fastapi" in imports_lower or "flask" in imports_lower
|
| 75 |
-
```
|
| 76 |
-
|
| 77 |
-
The fastapi repo contains ~500 tutorial/test files in `docs_src/` and
|
| 78 |
-
`tests/` that all import `fastapi` for demonstration purposes. Every
|
| 79 |
-
one of them was incorrectly flagged as an application entry point.
|
| 80 |
-
|
| 81 |
-
---
|
| 82 |
-
|
| 83 |
-
## Root Causes
|
| 84 |
-
|
| 85 |
-
### Bug 1 — Stale Cache (primary, explains the 0-dependency report)
|
| 86 |
-
|
| 87 |
-
**Location**: `data/architecture/fastapi_fastapi.json`
|
| 88 |
-
|
| 89 |
-
**Cause**: A zero-filled JSON summary written during early development
|
| 90 |
-
(before graph edges were implemented) persisted on disk and was served
|
| 91 |
-
by the API without any version or integrity check. The `build()` method
|
| 92 |
-
was never called again with `force_rebuild=True` after the graph engine
|
| 93 |
-
was completed, so the stale file was never overwritten via the API.
|
| 94 |
-
|
| 95 |
-
**Conditions for trigger**: Any repository that was analysed before the
|
| 96 |
-
Phase 1 graph services were fully operational will have a stale cache
|
| 97 |
-
file that silently returns incorrect zero-dependency data indefinitely.
|
| 98 |
-
|
| 99 |
-
### Bug 2 — Entry Point Over-Detection
|
| 100 |
-
|
| 101 |
-
**Location**: `services/entry_point_service.py` → `_is_python_framework_init`
|
| 102 |
-
|
| 103 |
-
**Cause**: The heuristic checked only whether a file *imports* `fastapi`
|
| 104 |
-
or `flask`, without distinguishing between:
|
| 105 |
-
- Tutorial/test/docs files that import the framework to demonstrate it
|
| 106 |
-
- Application root files that actually instantiate the framework
|
| 107 |
-
|
| 108 |
-
For the fastapi repo (which is the framework itself), this caused every
|
| 109 |
-
one of its ~500 example and test files to be flagged as entry points.
|
| 110 |
-
|
| 111 |
-
---
|
| 112 |
-
|
| 113 |
-
## Code Changes
|
| 114 |
-
|
| 115 |
-
### Fix 1 — Schema versioning + staleness detection
|
| 116 |
-
|
| 117 |
-
**File**: `services/architecture_service.py`
|
| 118 |
-
|
| 119 |
-
Added a `_SUMMARY_SCHEMA_VERSION` constant (currently `2`). Every saved
|
| 120 |
-
summary now includes `_schema_version` and `_built_at` timestamp fields.
|
| 121 |
-
`_load_summary()` rejects any file with a version lower than the current
|
| 122 |
-
constant by returning `None`, which causes the caller to treat the repo
|
| 123 |
-
as unbuilt and return a `404`, prompting a fresh `POST /build`.
|
| 124 |
-
|
| 125 |
-
```python
|
| 126 |
-
# Added to architecture_service.py
|
| 127 |
-
_SUMMARY_SCHEMA_VERSION = 2
|
| 128 |
-
|
| 129 |
-
def _save_summary(self, repo_name, summary):
|
| 130 |
-
versioned = dict(summary)
|
| 131 |
-
versioned["_schema_version"] = _SUMMARY_SCHEMA_VERSION
|
| 132 |
-
versioned["_built_at"] = int(time.time())
|
| 133 |
-
# ... write to disk
|
| 134 |
-
|
| 135 |
-
def _load_summary(self, repo_name):
|
| 136 |
-
data = json.load(...)
|
| 137 |
-
stored_version = data.get("_schema_version", 0)
|
| 138 |
-
if stored_version < _SUMMARY_SCHEMA_VERSION:
|
| 139 |
-
logger.warning("Discarding stale architecture summary ...")
|
| 140 |
-
return None # triggers 404 → caller must rebuild
|
| 141 |
-
return {k: v for k, v in data.items() if not k.startswith("_")}
|
| 142 |
-
```
|
| 143 |
-
|
| 144 |
-
### Fix 2 — Targeted entry point detection
|
| 145 |
-
|
| 146 |
-
**File**: `services/entry_point_service.py`
|
| 147 |
-
|
| 148 |
-
Replaced the broad import-only check with a three-rule heuristic that
|
| 149 |
-
filters out non-entry directories before applying framework detection:
|
| 150 |
-
|
| 151 |
-
```python
|
| 152 |
-
# Rule 1 — exclude known non-entry directories
|
| 153 |
-
_FRAMEWORK_INIT_EXCLUDED_PREFIXES = (
|
| 154 |
-
"tests/", "test/", "docs/", "docs_src/",
|
| 155 |
-
"examples/", "example/", "scripts/", "benchmarks/",
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
def _is_python_framework_init(file_path, parsed):
|
| 159 |
-
# Skip tutorial / test / docs trees
|
| 160 |
-
for prefix in _FRAMEWORK_INIT_EXCLUDED_PREFIXES:
|
| 161 |
-
if fp_lower.startswith(prefix):
|
| 162 |
-
return False
|
| 163 |
-
|
| 164 |
-
# Must import the framework
|
| 165 |
-
if "fastapi" not in imports_lower and "flask" not in imports_lower:
|
| 166 |
-
return False
|
| 167 |
-
|
| 168 |
-
# Rule 2 — application-suggestive filename at any depth
|
| 169 |
-
if basename in {"app.py", "application.py", "server.py", ...}:
|
| 170 |
-
return True
|
| 171 |
-
|
| 172 |
-
# Rule 3 — top two directory levels only
|
| 173 |
-
if len(parts) <= 2:
|
| 174 |
-
return True
|
| 175 |
-
|
| 176 |
-
return False
|
| 177 |
-
```
|
| 178 |
-
|
| 179 |
-
---
|
| 180 |
-
|
| 181 |
-
## Before / After Metrics — fastapi/fastapi
|
| 182 |
-
|
| 183 |
-
| Metric | Before (stale cache) | After (fix applied) |
|
| 184 |
-
|---|---|---|
|
| 185 |
-
| `total_files` | 2979 | 2979 |
|
| 186 |
-
| `total_dependencies` | **0** | **1440** |
|
| 187 |
-
| `core_modules` | **[]** | `['fastapi/__init__.py', 'fastapi/testclient.py', 'fastapi/responses.py', ...]` |
|
| 188 |
-
| `high_coupling_modules` | **[]** | `['fastapi/__init__.py', 'fastapi/testclient.py', 'fastapi/responses.py', ...]` |
|
| 189 |
-
| `entry_points` (count) | 574 | **17** |
|
| 190 |
-
| Files parsed | 1125 | 1125 |
|
| 191 |
-
| Graph nodes | 1125 | 1125 |
|
| 192 |
-
| Graph edges | 1440 | 1440 |
|
| 193 |
-
|
| 194 |
-
### Correct entry points after fix (17 total)
|
| 195 |
-
|
| 196 |
-
```
|
| 197 |
-
docs_src/app_testing/app_a_py310/main.py
|
| 198 |
-
docs_src/app_testing/app_b_an_py310/main.py
|
| 199 |
-
docs_src/app_testing/app_b_py310/main.py
|
| 200 |
-
docs_src/async_tests/app_a_py310/main.py
|
| 201 |
-
docs_src/bigger_applications/app_an_py310/main.py
|
| 202 |
-
docs_src/settings/app01_py310/main.py
|
| 203 |
-
docs_src/settings/app02_an_py310/main.py
|
| 204 |
-
docs_src/settings/app02_py310/main.py
|
| 205 |
-
docs_src/settings/app03_an_py310/main.py
|
| 206 |
-
docs_src/settings/app03_py310/main.py
|
| 207 |
-
tests/main.py
|
| 208 |
-
tests/test_modules_same_name_body/app/main.py
|
| 209 |
-
fastapi/__main__.py
|
| 210 |
-
fastapi/applications.py ← actual framework entry point
|
| 211 |
-
fastapi/param_functions.py ← actual framework entry point
|
| 212 |
-
fastapi/routing.py ← actual framework entry point
|
| 213 |
-
fastapi/utils.py ← actual framework entry point
|
| 214 |
-
```
|
| 215 |
-
|
| 216 |
-
### Correct core modules (top 5)
|
| 217 |
-
|
| 218 |
-
```
|
| 219 |
-
fastapi/__init__.py
|
| 220 |
-
fastapi/testclient.py
|
| 221 |
-
fastapi/responses.py
|
| 222 |
-
fastapi/security/__init__.py
|
| 223 |
-
fastapi/exceptions.py
|
| 224 |
-
```
|
| 225 |
-
|
| 226 |
-
### Correct high-coupling modules (top 5)
|
| 227 |
-
|
| 228 |
-
```
|
| 229 |
-
fastapi/__init__.py (in_degree + out_degree = highest)
|
| 230 |
-
fastapi/testclient.py
|
| 231 |
-
fastapi/responses.py
|
| 232 |
-
fastapi/security/__init__.py
|
| 233 |
-
fastapi/exceptions.py
|
| 234 |
-
```
|
| 235 |
-
|
| 236 |
-
---
|
| 237 |
-
|
| 238 |
-
## Validation — API Output
|
| 239 |
-
|
| 240 |
-
`GET /api/architecture/fastapi/fastapi` now returns:
|
| 241 |
-
|
| 242 |
-
```json
|
| 243 |
-
{
|
| 244 |
-
"entry_points": [
|
| 245 |
-
"docs_src/app_testing/app_a_py310/main.py",
|
| 246 |
-
"fastapi/__main__.py",
|
| 247 |
-
"fastapi/applications.py",
|
| 248 |
-
"fastapi/routing.py",
|
| 249 |
-
...
|
| 250 |
-
],
|
| 251 |
-
"core_modules": [
|
| 252 |
-
"fastapi/__init__.py",
|
| 253 |
-
"fastapi/testclient.py",
|
| 254 |
-
"fastapi/responses.py",
|
| 255 |
-
"fastapi/security/__init__.py",
|
| 256 |
-
"fastapi/exceptions.py",
|
| 257 |
-
...
|
| 258 |
-
],
|
| 259 |
-
"high_coupling_modules": [
|
| 260 |
-
"fastapi/__init__.py",
|
| 261 |
-
"fastapi/testclient.py",
|
| 262 |
-
"fastapi/responses.py",
|
| 263 |
-
"fastapi/security/__init__.py",
|
| 264 |
-
"fastapi/exceptions.py",
|
| 265 |
-
...
|
| 266 |
-
],
|
| 267 |
-
"total_files": 2979,
|
| 268 |
-
"total_dependencies": 1440
|
| 269 |
-
}
|
| 270 |
-
```
|
| 271 |
-
|
| 272 |
-
---
|
| 273 |
-
|
| 274 |
-
## Test Results
|
| 275 |
-
|
| 276 |
-
```
|
| 277 |
-
80 passed, 0 failed — 1 warning (deprecation, not a code issue)
|
| 278 |
-
```
|
| 279 |
-
|
| 280 |
-
All 62 Phase 1 architecture tests pass, including the live
|
| 281 |
-
`fastapi/fastapi` repo validation suite.
|
| 282 |
-
|
| 283 |
-
---
|
| 284 |
-
|
| 285 |
-
## Files Modified
|
| 286 |
-
|
| 287 |
-
| File | Change |
|
| 288 |
-
|---|---|
|
| 289 |
-
| `services/entry_point_service.py` | Replaced broad import-check with directory-exclusion + file-name + depth rules |
|
| 290 |
-
| `services/architecture_service.py` | Added `_SUMMARY_SCHEMA_VERSION`, `_built_at` timestamp, staleness check in `_load_summary` |
|
| 291 |
-
| `scripts/diagnose_phase1.py` | New: diagnostic script used to isolate and reproduce the bugs |
|
| 292 |
-
|
| 293 |
-
---
|
| 294 |
-
|
| 295 |
-
## Success Criteria — Verified
|
| 296 |
-
|
| 297 |
-
| Criterion | Status |
|
| 298 |
-
|---|---|
|
| 299 |
-
| `total_files > 0` | ✅ 2979 |
|
| 300 |
-
| `total_dependencies > 0` | ✅ 1440 |
|
| 301 |
-
| `core_modules` not empty | ✅ 10 modules |
|
| 302 |
-
| `high_coupling_modules` not empty | ✅ 10 modules |
|
| 303 |
-
| Entry points are real app files | ✅ 17 (down from 574) |
|
| 304 |
-
| All 80 tests pass | ✅ |
|
| 305 |
-
| Stale summaries auto-invalidated | ✅ via schema version check |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -2,107 +2,129 @@
|
|
| 2 |
|
| 3 |
<p align="center">
|
| 4 |
<img src="https://img.shields.io/badge/Backend-FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white" alt="FastAPI"/>
|
| 5 |
-
<img src="https://img.shields.io/badge/
|
|
|
|
| 6 |
<img src="https://img.shields.io/badge/Inference-NVIDIA%20NIM-76B900?style=for-the-badge&logo=nvidia&logoColor=white" alt="NVIDIA NIM"/>
|
| 7 |
<img src="https://img.shields.io/badge/Vector%20DB-ChromaDB-blue?style=for-the-badge" alt="ChromaDB"/>
|
| 8 |
-
<img src="https://img.shields.io/badge/
|
| 9 |
-
<img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License"/>
|
| 10 |
</p>
|
| 11 |
|
| 12 |
<p align="center">
|
| 13 |
-
<strong>An
|
| 14 |
</p>
|
| 15 |
|
| 16 |
<p align="center">
|
| 17 |
-
<a href="#-why-
|
| 18 |
-
<a href="#-key-
|
| 19 |
<a href="#-system-architecture">System Architecture</a> •
|
| 20 |
-
<a href="
|
| 21 |
-
<a href="
|
| 22 |
-
<a href="docs/
|
|
|
|
| 23 |
</p>
|
| 24 |
|
| 25 |
---
|
| 26 |
|
| 27 |
-
##
|
| 28 |
|
| 29 |
-
|
| 30 |
-
- **Massive Codebases:** Sprawling modules make finding where logic resides an uphill battle.
|
| 31 |
-
- **Onboarding Difficulty:** New contributors struggle to find the right entry points and module flows.
|
| 32 |
-
- **Issue Localization:** Mapping a GitHub bug description to the exact files and functions requires deep tribal knowledge.
|
| 33 |
-
- **Architecture Discovery:** Static documentation goes stale; understanding active dependency graphs is slow.
|
| 34 |
-
- **Change Impact Uncertainty:** Modifying a utility file risks breaking unexpected downstream dependents.
|
| 35 |
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
-
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
- **Repository Chat:** Conversational Q&A over the codebase with streaming token responses (SSE) and strict source citations.
|
| 44 |
-
- **Architecture Builder:** Multi-language AST parsing (using Tree-sitter) and NetworkX dependency graph mapping.
|
| 45 |
-
- **Issue Mapper:** Automatically matches bug report text to target files, generating a clear step-by-step implementation plan.
|
| 46 |
-
- **Reading Order:** Generates an optimized file-reading list sorted by module centrality to accelerate developer onboarding.
|
| 47 |
-
- **Impact Analysis:** Traverses directed dependency graphs to predict which files will be affected by a proposed change.
|
| 48 |
-
- **Confidence Layer:** Evaluates responses for hallucination, returning confidence scores and warning flags.
|
| 49 |
-
- **Grounded Fallback Mode:** Falls back to keyword-based component mapping and chunk-grounded steps during LLM rate limits.
|
| 50 |
|
| 51 |
---
|
| 52 |
|
| 53 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
###
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
###
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
###
|
| 62 |
-
|
| 63 |
|
| 64 |
---
|
| 65 |
|
| 66 |
## 🏗️ System Architecture
|
| 67 |
|
|
|
|
|
|
|
| 68 |
```mermaid
|
| 69 |
graph TD
|
| 70 |
-
UI[Astro 4 + React Dashboard] -- HTTP / SSE --> API[FastAPI Gateway]
|
| 71 |
|
| 72 |
-
subgraph
|
| 73 |
API --> GH[GitHub Clone & Extractor]
|
| 74 |
-
API -->
|
|
|
|
| 75 |
API --> ES[Local BGE Embedding Service]
|
| 76 |
-
API --> CS[Chroma Vector Store]
|
| 77 |
-
API --> AS[Tree-sitter AST Graph Builder]
|
| 78 |
end
|
| 79 |
|
| 80 |
-
subgraph Memory [Data Layer]
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
| 84 |
end
|
| 85 |
|
| 86 |
subgraph AI [LLM Reasoning Layer]
|
| 87 |
API --> NIM[NVIDIA NIM DeepSeek V4 Flash]
|
| 88 |
end
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
```
|
| 93 |
-
GitHub Repository
|
| 94 |
-
│
|
| 95 |
-
▼ [Clone & Extract]
|
| 96 |
-
Source Code Files
|
| 97 |
-
│
|
| 98 |
-
▼ [CodeChunker: 1500 chars / 200 overlap]
|
| 99 |
-
Text Chunks
|
| 100 |
-
│
|
| 101 |
-
▼ [EmbeddingService: BGE Small]
|
| 102 |
-
384-dimensional Vectors
|
| 103 |
-
│
|
| 104 |
-
▼ [Index Repository]
|
| 105 |
-
ChromaDB Vector Database ──► Semantic Retrieval ──► DeepSeek LLM ──► Grounded Response
|
| 106 |
```
|
| 107 |
|
| 108 |
---
|
|
@@ -110,71 +132,22 @@ ChromaDB Vector Database ──► Semantic Retrieval ──► DeepSeek LLM ─
|
|
| 110 |
## 🛠️ Technology Stack
|
| 111 |
|
| 112 |
| Layer | Component | Notes |
|
| 113 |
-
|---|---|---|
|
| 114 |
-
| **Backend** | `FastAPI` + `Uvicorn` | Asynchronous
|
| 115 |
-
| **Frontend** | `Astro 4` + `React` |
|
| 116 |
-
| **Vector
|
| 117 |
-
| **Embeddings** | `BAAI/bge-small-en-v1.5` |
|
| 118 |
-
| **LLM Provider** | `DeepSeek V4 Flash` |
|
| 119 |
-
| **AST Parser** | `Tree-sitter` |
|
| 120 |
-
| **Graph
|
| 121 |
-
|
| 122 |
-
---
|
| 123 |
-
|
| 124 |
-
## 🚀 Installation & Local Development
|
| 125 |
-
|
| 126 |
-
### 1. Backend Server Setup
|
| 127 |
-
Ensure Python 3.10+ is installed:
|
| 128 |
-
```bash
|
| 129 |
-
# Set up virtual environment
|
| 130 |
-
python -m venv .venv
|
| 131 |
-
source .venv/bin/activate # Or .venv\Scripts\activate on Windows
|
| 132 |
-
|
| 133 |
-
# Install dependencies
|
| 134 |
-
pip install -r requirements.txt
|
| 135 |
-
|
| 136 |
-
# Create environmental configuration
|
| 137 |
-
cp .env.example .env
|
| 138 |
-
# Open .env and add your DEEPSEEK_API_KEY from build.nvidia.com
|
| 139 |
-
|
| 140 |
-
# Start the API server
|
| 141 |
-
python -m uvicorn backend.api:app --host 127.0.0.1 --port 8000 --reload
|
| 142 |
-
```
|
| 143 |
-
|
| 144 |
-
### 2. Frontend Setup
|
| 145 |
-
Ensure Node.js 18+ is installed:
|
| 146 |
-
```bash
|
| 147 |
-
cd frontend
|
| 148 |
-
npm install
|
| 149 |
-
npm run dev
|
| 150 |
-
```
|
| 151 |
-
|
| 152 |
-
---
|
| 153 |
-
|
| 154 |
-
## 📊 Performance & Validation
|
| 155 |
-
|
| 156 |
-
Validation was conducted on the **Ankita15k/GitNest** repository (328 files, 1,549 chunks, 1,440 dependency edges):
|
| 157 |
-
- **Cloning & Indexing Success:** All source files embedded and stored in ChromaDB in ~28s.
|
| 158 |
-
- **Graph Builder Centrality:** Accurately extracted 17 application entry points and mapped high-coupling module hotspots.
|
| 159 |
-
- **Issue Mapper Recall:** Correctly localized file targets for sample bugs without returning hallucinated paths.
|
| 160 |
-
|
| 161 |
-
Refer to the complete [Validation Report](docs/VALIDATION_REPORT.md) for detailed telemetry.
|
| 162 |
|
| 163 |
---
|
| 164 |
|
| 165 |
## 🛑 Known Limitations
|
| 166 |
|
| 167 |
-
- **
|
| 168 |
-
- **
|
| 169 |
-
- **
|
| 170 |
-
|
| 171 |
-
---
|
| 172 |
-
|
| 173 |
-
## 🗺️ Roadmap
|
| 174 |
-
|
| 175 |
-
- **Near-Term:** Persistent SQLite database for processed repository lists, JWT API authentication middleware.
|
| 176 |
-
- **Medium-Term:** BM25 keyword matching + vector hybrid search, multi-repository comparing queries.
|
| 177 |
-
- **Long-Term:** CI/CD actions for PR risk scoring, VS Code IDE extension.
|
| 178 |
|
| 179 |
---
|
| 180 |
|
|
|
|
| 2 |
|
| 3 |
<p align="center">
|
| 4 |
<img src="https://img.shields.io/badge/Backend-FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white" alt="FastAPI"/>
|
| 5 |
+
<img src="https://img.shields.io/badge/Frontend-Astro%204%20%2B%20React-FF5D01?style=for-the-badge&logo=astro&logoColor=white" alt="Astro + React"/>
|
| 6 |
+
<img src="https://img.shields.io/badge/LLM-DeepSeek%20V4%20Flash-6C5CE7?style=for-the-badge" alt="DeepSeek V4 Flash"/>
|
| 7 |
<img src="https://img.shields.io/badge/Inference-NVIDIA%20NIM-76B900?style=for-the-badge&logo=nvidia&logoColor=white" alt="NVIDIA NIM"/>
|
| 8 |
<img src="https://img.shields.io/badge/Vector%20DB-ChromaDB-blue?style=for-the-badge" alt="ChromaDB"/>
|
| 9 |
+
<img src="https://img.shields.io/badge/AST%20Parser-Tree--sitter-black?style=for-the-badge" alt="Tree-sitter"/>
|
|
|
|
| 10 |
</p>
|
| 11 |
|
| 12 |
<p align="center">
|
| 13 |
+
<strong>An advanced codebase intelligence platform that combines AST structure extraction, dependency graph modeling, NetworkX centrality analysis, and LLM reasoning to map codebase architectures, onboarding reading orders, issue implementation plans, and change impact boundaries.</strong>
|
| 14 |
</p>
|
| 15 |
|
| 16 |
<p align="center">
|
| 17 |
+
<a href="#-why-this-is-different-from-typical-repo-chatbots">Why This Is Different</a> •
|
| 18 |
+
<a href="#-key-differentiators">Key Differentiators</a> •
|
| 19 |
<a href="#-system-architecture">System Architecture</a> •
|
| 20 |
+
<a href="ARCHITECTURE.md">Architecture Specification</a> •
|
| 21 |
+
<a href="docs/DEVELOPMENT_SETUP.md">Development Setup</a> •
|
| 22 |
+
<a href="docs/VALIDATION_REPORT.md">Validation & Telemetry</a> •
|
| 23 |
+
<a href="RELEASE_NOTES.md">Release Notes</a>
|
| 24 |
</p>
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
+
## ⚡ Why this is different from typical Repo Chatbots
|
| 29 |
|
| 30 |
+
Traditional codebase assistants use a straightforward, unstructured approach:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
+
```
|
| 33 |
+
Traditional RAG Chatbot:
|
| 34 |
+
Repo ──► Text Splitting ──► Vector Embeddings ──► Similarity Search ──► LLM Prompt
|
| 35 |
+
```
|
| 36 |
|
| 37 |
+
This model is blind to code structure, import inheritance, code coupling, and execution entry points. It often hallucinates dependencies, misses downstream side-effects, and generates unstructured code recommendations.
|
| 38 |
+
|
| 39 |
+
**Repo Intelligence Agent** addresses these gaps by implementing a structural, graph-augmented retrieval and reasoning architecture:
|
| 40 |
|
| 41 |
+
```
|
| 42 |
+
Repo Intelligence Agent Ingestion & Retrieval Pipeline:
|
| 43 |
+
Repo
|
| 44 |
+
├──► [Tree-sitter AST Parser] ──► Structural Declarations (Imports/Exports/Classes/Methods)
|
| 45 |
+
│ │
|
| 46 |
+
│ ▼
|
| 47 |
+
│ [NetworkX DiGraph Mapping]
|
| 48 |
+
│ │
|
| 49 |
+
├───────────────────────────────────────────┼──────────────────────────────────────────┐
|
| 50 |
+
▼ ▼ ▼
|
| 51 |
+
[ChromaDB Vector Store] [Centrality Analytics] [BFS Graph Traversals]
|
| 52 |
+
(1500 chars / 200 overlap) (Suggested Reading Orders) (Change Impact Analysis)
|
| 53 |
+
��� │ │
|
| 54 |
+
▼ ▼ ▼
|
| 55 |
+
Vector Snippets ───────────────► Architectural Graph Context ────────────────► Propagation Risk Scoring
|
| 56 |
+
│ │ │
|
| 57 |
+
└───────────────────────────────────────────┴──────────────────────────────────────────┘
|
| 58 |
+
│
|
| 59 |
+
▼
|
| 60 |
+
[DeepSeek V4 Flash Reasoning]
|
| 61 |
+
│
|
| 62 |
+
▼
|
| 63 |
+
Grounded Code Intelligence
|
| 64 |
+
```
|
| 65 |
|
| 66 |
+
By enriching semantic vector retrieval with explicit import graphs, centrality indicators, and structural declarations, the system accurately maps codebase relationships, onboarding order, implementation dependencies, and change impacts without hallucinating non-existent modules.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
---
|
| 69 |
|
| 70 |
+
## ✨ Key Differentiators
|
| 71 |
+
|
| 72 |
+
### 1. Unified Repository Workspace
|
| 73 |
+
The frontend layout organizes repository navigation into a unified tabbed dashboard workspace:
|
| 74 |
+
- **Codebase Analysis:** Summary details, tech stack, and primary package declarations.
|
| 75 |
+
- **Architecture Graph:** A React Flow-rendered, interactive node-link import graph of source files.
|
| 76 |
+
- **Reading Path:** An ordered timeline sequence for codebase onboarding.
|
| 77 |
+
- **Impact Analysis:** Interactive scenario inputs predicting file risk spreads.
|
| 78 |
+
- **Issue Intelligence:** Step-by-step implementation guide generation.
|
| 79 |
+
- **Chat:** Conversational context-grounded Q&A interface.
|
| 80 |
+
|
| 81 |
+
The parent workspace component maintains a **shared repository session** in React state. Tab navigation is instantaneous, drawing from cached metadata with **no re-analysis** required when switching between views.
|
| 82 |
|
| 83 |
+
### 2. Session Store & Context Sync
|
| 84 |
+
Active repository sessions are synchronized across components and browser tabs.
|
| 85 |
+
- **Persisted (localStorage):** `owner`, `repo`, `indexing status`, `graph status`, and `last analyzed timestamp`. This lets the application recover context immediately if the user reloads or navigates away.
|
| 86 |
+
- **Non-persisted (in-memory state):** Large dependency graph payloads, tree nodes, and transient UI states. This division prevents exceeding localStorage capacity limits (typically 5MB) while keeping load latency low.
|
| 87 |
|
| 88 |
+
### 3. Chat Resilience Layer
|
| 89 |
+
To prevent rate-limit interruptions (common on free-tier NVIDIA NIM keys), the system has an **automated grounded fallback mode**. When a rate limit exception (HTTP 429), network timeout, or provider failure occurs:
|
| 90 |
+
- The system intercepts the exception.
|
| 91 |
+
- It extracts local text snippets from the top similarity chunks retrieved from ChromaDB.
|
| 92 |
+
- It infers affected components using file path heuristics.
|
| 93 |
+
- It returns a structured, retrieval-grounded fallback response directly to the chat window with cited sources and confidence scores, ensuring no raw back-end exceptions reach the developer.
|
| 94 |
|
| 95 |
+
### 4. Analysis Persistence Lifecycle
|
| 96 |
+
Computed ingestion analyses are saved directly to `data/analysis_store.json`. Upon application startup, the backend hydrates this store, allowing full repository workspace recovery and details retrieval without having to rebuild graphs or regenerate embeddings.
|
| 97 |
|
| 98 |
---
|
| 99 |
|
| 100 |
## 🏗️ System Architecture
|
| 101 |
|
| 102 |
+
The following diagram illustrates the relationship between the client dashboard, the API gateway, local vector stores, and the DeepSeek inference layer:
|
| 103 |
+
|
| 104 |
```mermaid
|
| 105 |
graph TD
|
| 106 |
+
UI[Astro 4 + React Dashboard] -- HTTP / SSE / React Flow --> API[FastAPI Gateway Port 8001]
|
| 107 |
|
| 108 |
+
subgraph Processing [Code Ingestion & Parsing]
|
| 109 |
API --> GH[GitHub Clone & Extractor]
|
| 110 |
+
API --> TS[Tree-sitter AST Parser]
|
| 111 |
+
API --> CC[Code Chunking 1500 chars / 200 overlap]
|
| 112 |
API --> ES[Local BGE Embedding Service]
|
|
|
|
|
|
|
| 113 |
end
|
| 114 |
|
| 115 |
+
subgraph Memory [Local Data Layer]
|
| 116 |
+
ES --> Chroma[(ChromaDB Vector Store)]
|
| 117 |
+
TS --> NX[NetworkX Graph Builder]
|
| 118 |
+
NX --> DB_Graph[(Persisted Graphs data/architecture/)]
|
| 119 |
+
API --> DB_Store[(Analysis Cache data/analysis_store.json)]
|
| 120 |
end
|
| 121 |
|
| 122 |
subgraph AI [LLM Reasoning Layer]
|
| 123 |
API --> NIM[NVIDIA NIM DeepSeek V4 Flash]
|
| 124 |
end
|
| 125 |
+
|
| 126 |
+
Chroma --> API
|
| 127 |
+
DB_Graph --> API
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
```
|
| 129 |
|
| 130 |
---
|
|
|
|
| 132 |
## 🛠️ Technology Stack
|
| 133 |
|
| 134 |
| Layer | Component | Notes |
|
| 135 |
+
| :--- | :--- | :--- |
|
| 136 |
+
| **Backend** | `FastAPI` + `Uvicorn` | Asynchronous routers, SSE streams, binds to port **8001** |
|
| 137 |
+
| **Frontend** | `Astro 4` + `React` | Dynamic UI hydration, React Flow graph rendering |
|
| 138 |
+
| **Vector DB** | `ChromaDB` | Persistent code chunk database, partitioned via `repo_name` |
|
| 139 |
+
| **Embeddings** | `BAAI/bge-small-en-v1.5` | Local SentenceTransformers generating 384-dimensional vectors |
|
| 140 |
+
| **LLM Provider** | `DeepSeek V4 Flash` | Inference provider served via OpenAI-compatible NVIDIA NIM |
|
| 141 |
+
| **AST Parser** | `Tree-sitter` | Lazy-loaded language parsers (Python, JS, TS, JSX, TSX) |
|
| 142 |
+
| **Graph Calculations** | `NetworkX` | Directed dependency trees, BFS sweeps, composite centrality |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
---
|
| 145 |
|
| 146 |
## 🛑 Known Limitations
|
| 147 |
|
| 148 |
+
- **CPU Ingestion Latency:** Locally executing SentenceTransformer embeddings on CPU takes approximately 2–3 minutes per 1,500 chunks.
|
| 149 |
+
- **NVIDIA NIM Rate Limits:** Free developer keys are capped at ~3 requests/minute. The system handles this with its automated fallback mode.
|
| 150 |
+
- **Stubbed Infrastructure:** `SQLiteStore` (`memory/sqlite_store.py`), `MCPService` (`services/mcp_service.py`), and `RepositoryAnalyzer` (`agents/analyzer.py`) are design stubs. Inlined routes and JSON storage cache operations handle these functions in the MVP.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
---
|
| 153 |
|
RELEASE_NOTES.md
CHANGED
|
@@ -4,32 +4,25 @@ We are proud to announce the initial release of the **Repo Intelligence Agent**
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
-
## 🚀 Key Features
|
| 8 |
|
| 9 |
-
- **
|
| 10 |
-
- **
|
| 11 |
-
- **
|
| 12 |
-
- **
|
| 13 |
-
- **
|
| 14 |
-
- **
|
|
|
|
| 15 |
|
| 16 |
---
|
| 17 |
|
| 18 |
-
## 🛠️
|
| 19 |
|
| 20 |
-
- **
|
| 21 |
-
- **
|
| 22 |
-
- **
|
| 23 |
-
- **
|
| 24 |
-
- **
|
| 25 |
-
|
| 26 |
-
-
|
| 27 |
-
|
| 28 |
-
## 📊 Verification & Validation
|
| 29 |
-
|
| 30 |
-
MVP validation was executed against the **Ankita15k/GitNest** repository:
|
| 31 |
-
- **328 files** successfully parsed and mapped.
|
| 32 |
-
- **1,549 code chunks** stored and embedded.
|
| 33 |
-
- **1,440 dependency edges** mapped.
|
| 34 |
-
- Zero code plan hallucinations or path discrepancies observed.
|
| 35 |
-
- Backend test coverage exceeding **85%** across all central services.
|
|
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
+
## 🚀 Key Features Added
|
| 8 |
|
| 9 |
+
- **Unified Repository Workspace:** Dashboard layout integrating Codebase Analysis, Architecture Graph, Reading Path, Impact Analysis, Issue Intelligence, and Chat in a single session.
|
| 10 |
+
- **Session Persistence:** Synchronizes active repository context using browser `localStorage` and hydrates repository ingestion lists from `data/analysis_store.json` at startup.
|
| 11 |
+
- **Chat Fallback Mode:** Implements an automated local RAG fallback layer that intercepts LLM timeouts, network failures, or HTTP 429 quota exhaustion on NVIDIA NIM, providing cited local context without throwing errors.
|
| 12 |
+
- **Architecture Graph:** Uses Tree-sitter and NetworkX to parse syntax imports and export a React Flow-compatible JSON layout.
|
| 13 |
+
- **Reading Order:** Generates a structured developer onboarding sequence by ranking files topologically using NetworkX normalized centrality models.
|
| 14 |
+
- **Impact Analysis:** Walks forward/reverse dependency paths (depth = 4) and performs component matching to assess code modification risk levels.
|
| 15 |
+
- **Issue Mapper:** Automatically maps GitHub issue text to files and compiles step-by-step plans under a budget of exactly two LLM completions.
|
| 16 |
|
| 17 |
---
|
| 18 |
|
| 19 |
+
## 🛠️ Issues & Bug Fixes Resolved
|
| 20 |
|
| 21 |
+
- **Current Workspace session bug:** Resolved page reload/navigation resets by storing active context in `localStorage` and sharing it via parent workspace state, eliminating the need to re-analyze when switching tabs.
|
| 22 |
+
- **Architecture graph generation:** Patched the AST parsing engine to output empty checks cleanly, enabling the UI to render React Flow graphs or 404 warnings without locking up.
|
| 23 |
+
- **Graph persistence:** Standardized atomic writes to `data/architecture/` and `data/analysis_store.json` on successful analysis.
|
| 24 |
+
- **Stale issue cache:** Migrated the caching key to `f"{repo_name}:v2:{issue_hash}"`, preventing cross-repository plan bleeding.
|
| 25 |
+
- **Chroma dimension mismatch:** Aligned collection indexes to a static 384-dimensional schema matching local `BAAI/bge-small-en-v1.5` embeddings.
|
| 26 |
+
- **NVIDIA NIM model configuration:** Corrected parameters to map OpenAI-compatible calls to `deepseek-ai/deepseek-v4-flash` via `https://integrate.api.nvidia.com/v1`.
|
| 27 |
+
- **WatchFiles reload issue:** Configured `uvicorn` in `backend/main.py` to monitor source directories specifically (`_RELOAD_DIRS = ["backend", "services", "agents", "memory", "models"]`). This stops Uvicorn from scanning `data/` or cloning directories, preventing reload loops that terminate active requests.
|
| 28 |
+
- **API Port Parity:** Configured the application to default consistently to port **8001** for development environments, resolving mismatches between testing tools and gateway servers.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/api.py
CHANGED
|
@@ -22,7 +22,7 @@ from typing import Dict, List, Any, Optional
|
|
| 22 |
from fastapi import FastAPI, HTTPException, Request, Query
|
| 23 |
from fastapi.middleware.cors import CORSMiddleware
|
| 24 |
from fastapi.responses import StreamingResponse
|
| 25 |
-
from pydantic import BaseModel, Field
|
| 26 |
|
| 27 |
# Ensure parent directory is in sys.path to import project modules
|
| 28 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
@@ -48,6 +48,7 @@ from services.graph_service import GraphService
|
|
| 48 |
from services.reading_order_service import ReadingOrderService
|
| 49 |
from services.impact_analysis_service import ImpactAnalysisService
|
| 50 |
from services.arch_context_service import ArchContextService
|
|
|
|
| 51 |
from services.llm import ProviderFactory
|
| 52 |
from memory.chroma_store import ChromaStore
|
| 53 |
|
|
@@ -83,9 +84,112 @@ def health():
|
|
| 83 |
|
| 84 |
|
| 85 |
# ---------------------------------------------------------------------------
|
| 86 |
-
#
|
| 87 |
# ---------------------------------------------------------------------------
|
| 88 |
ANALYSIS_STORE: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
# ---------------------------------------------------------------------------
|
| 91 |
# Core service singletons
|
|
@@ -141,6 +245,17 @@ class ChatRequest(BaseModel):
|
|
| 141 |
default_factory=list, description="Conversation history"
|
| 142 |
)
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
class ArchitectureBuildRequest(BaseModel):
|
| 146 |
repo: str = Field(..., description="Repository identifier (owner/repo)")
|
|
@@ -320,15 +435,23 @@ async def get_examples():
|
|
| 320 |
@app.get("/api/repos/recent")
|
| 321 |
async def get_recent():
|
| 322 |
"""Fetch list of recently analysed repositories from the in-memory store."""
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
"name": name,
|
| 326 |
"url": f"https://github.com/{name}",
|
| 327 |
-
"tech_stack":
|
| 328 |
"analyzed_at": "Just now",
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
]
|
| 332 |
|
| 333 |
|
| 334 |
@app.post("/api/index")
|
|
@@ -478,9 +601,17 @@ async def analyze_repository(request: AnalyzeRequest):
|
|
| 478 |
yield f"data: {json.dumps({'status': 'chroma', 'message': 'END: chroma indexing'})}\n\n"
|
| 479 |
|
| 480 |
yield f"data: {json.dumps({'status': 'building_graph', 'message': 'Building file dependency graph...'})}\n\n"
|
| 481 |
-
await asyncio.to_thread(
|
| 482 |
-
architecture_service.build, repo_name, local_path,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
)
|
|
|
|
| 484 |
|
| 485 |
# Architecture summary via DeepSeek
|
| 486 |
architecture_summary = await generate_architecture_summary_with_llm(
|
|
@@ -512,6 +643,9 @@ async def analyze_repository(request: AnalyzeRequest):
|
|
| 512 |
"architecture": architecture_summary,
|
| 513 |
}
|
| 514 |
|
|
|
|
|
|
|
|
|
|
| 515 |
yield f"data: {json.dumps({'status': 'complete', 'message': '✓ Repository analysis complete!'})}\n\n"
|
| 516 |
yield f"data: {json.dumps({'status': 'done', 'repo': repo_name})}\n\n"
|
| 517 |
|
|
@@ -580,78 +714,123 @@ async def map_issue(request: IssueMapRequest):
|
|
| 580 |
|
| 581 |
@app.post("/api/chat")
|
| 582 |
async def repository_chat(request: ChatRequest):
|
| 583 |
-
"""Chat with repository context, streaming back token results via SSE.
|
| 584 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
question = request.message.strip()
|
| 586 |
history = request.history
|
| 587 |
|
| 588 |
async def chat_streamer():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
try:
|
| 590 |
-
# 1. Embed query (local BGE — no API call)
|
| 591 |
query_embedding = embedding_service.generate_embedding(question)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
|
| 593 |
-
|
|
|
|
| 594 |
chunks = chroma_store.search_repository(
|
| 595 |
repo_name=repo_name,
|
| 596 |
query_embedding=query_embedding,
|
| 597 |
limit=5,
|
| 598 |
)
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
context_str = (
|
| 610 |
-
"
|
|
|
|
|
|
|
| 611 |
)
|
| 612 |
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
if parts:
|
| 631 |
-
content = parts[0] if isinstance(parts[0], str) else str(parts[0])
|
| 632 |
-
elif "content" in turn:
|
| 633 |
-
content = turn["content"]
|
| 634 |
-
else:
|
| 635 |
-
continue
|
| 636 |
-
normalised_history.append({"role": role, "content": content})
|
| 637 |
-
|
| 638 |
-
# 5. Architecture context injection
|
| 639 |
-
arch_ctx = arch_context_service.get_context(repo_name)
|
| 640 |
-
arch_block = arch_ctx.to_prompt_block()
|
| 641 |
-
arch_section = f"\n{arch_block}\n" if arch_block else ""
|
| 642 |
-
|
| 643 |
-
prompt = (
|
| 644 |
-
f"{arch_section}"
|
| 645 |
-
f"Context from repository:\n"
|
| 646 |
-
f"=======================\n"
|
| 647 |
-
f"{context_str}\n"
|
| 648 |
-
f"=======================\n\n"
|
| 649 |
-
f"Question: {question}"
|
| 650 |
-
)
|
| 651 |
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
| 655 |
async for token in provider.stream(
|
| 656 |
prompt=prompt,
|
| 657 |
system_instruction=system_instruction,
|
|
@@ -660,7 +839,33 @@ async def repository_chat(request: ChatRequest):
|
|
| 660 |
full_text += token
|
| 661 |
yield f"data: {json.dumps({'text': token})}\n\n"
|
| 662 |
|
| 663 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
try:
|
| 665 |
evaluator = EvaluationAgent()
|
| 666 |
eval_result = evaluator.evaluate_response(question, full_text, chunks)
|
|
@@ -669,12 +874,7 @@ async def repository_chat(request: ChatRequest):
|
|
| 669 |
logger.error("Chat evaluation failed: %s", eval_err)
|
| 670 |
confidence_pct = 85
|
| 671 |
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
except Exception as e:
|
| 675 |
-
logger.error("Chat streaming error: %s", e, exc_info=True)
|
| 676 |
-
yield f"data: {json.dumps({'text': f'Error in agent system: {str(e)}'})}\n\n"
|
| 677 |
-
yield f"data: {json.dumps({'status': 'done'})}\n\n"
|
| 678 |
|
| 679 |
return StreamingResponse(chat_streamer(), media_type="text/event-stream")
|
| 680 |
|
|
@@ -779,12 +979,23 @@ async def get_architecture_graph(owner: str, repo_name: str, q: Optional[str] =
|
|
| 779 |
status_code=404,
|
| 780 |
detail=(
|
| 781 |
f"No dependency graph found for '{full_name}'. "
|
| 782 |
-
"Please
|
| 783 |
),
|
| 784 |
)
|
| 785 |
graph_data = await asyncio.to_thread(
|
| 786 |
graph_service.get_visualization_graph, full_name, architecture_service, q
|
| 787 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
return graph_data
|
| 789 |
except HTTPException:
|
| 790 |
raise
|
|
@@ -795,4 +1006,10 @@ async def get_architecture_graph(owner: str, repo_name: str, q: Optional[str] =
|
|
| 795 |
|
| 796 |
if __name__ == "__main__":
|
| 797 |
import uvicorn
|
| 798 |
-
uvicorn.run(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
from fastapi import FastAPI, HTTPException, Request, Query
|
| 23 |
from fastapi.middleware.cors import CORSMiddleware
|
| 24 |
from fastapi.responses import StreamingResponse
|
| 25 |
+
from pydantic import BaseModel, Field, field_validator
|
| 26 |
|
| 27 |
# Ensure parent directory is in sys.path to import project modules
|
| 28 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
| 48 |
from services.reading_order_service import ReadingOrderService
|
| 49 |
from services.impact_analysis_service import ImpactAnalysisService
|
| 50 |
from services.arch_context_service import ArchContextService
|
| 51 |
+
from services.chat_fallback import build_fallback_answer, is_provider_error
|
| 52 |
from services.llm import ProviderFactory
|
| 53 |
from memory.chroma_store import ChromaStore
|
| 54 |
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
# ---------------------------------------------------------------------------
|
| 87 |
+
# Analysis store — persisted to disk so data survives server restarts
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
ANALYSIS_STORE: Dict[str, Dict[str, Any]] = {}
|
| 90 |
+
_ANALYSIS_STORE_PATH = os.path.join("data", "analysis_store.json")
|
| 91 |
+
_persist_lock = asyncio.Lock()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _load_analysis_store() -> None:
|
| 95 |
+
"""Load persisted analysis data from disk into ANALYSIS_STORE on startup.
|
| 96 |
+
|
| 97 |
+
Reconstructs Pydantic models from the stored JSON dicts. Any entry that
|
| 98 |
+
fails to deserialise is skipped (logged at WARNING) so a single corrupt
|
| 99 |
+
record never blocks the entire store from loading.
|
| 100 |
+
"""
|
| 101 |
+
global ANALYSIS_STORE
|
| 102 |
+
if not os.path.exists(_ANALYSIS_STORE_PATH):
|
| 103 |
+
return
|
| 104 |
+
try:
|
| 105 |
+
with open(_ANALYSIS_STORE_PATH, "r", encoding="utf-8") as fh:
|
| 106 |
+
raw: Dict[str, Any] = json.load(fh)
|
| 107 |
+
except Exception as exc:
|
| 108 |
+
logger.warning("Could not read analysis store from disk: %s", exc)
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
loaded = 0
|
| 112 |
+
for repo_name, entry in raw.items():
|
| 113 |
+
try:
|
| 114 |
+
analysis_data = RepositoryAnalysis.model_validate(entry["analysis"])
|
| 115 |
+
arch_raw = entry["architecture"]
|
| 116 |
+
# Reconstruct nested ComponentRelationship objects
|
| 117 |
+
relationships = [
|
| 118 |
+
ComponentRelationship(**r) for r in arch_raw.get("relationships", [])
|
| 119 |
+
]
|
| 120 |
+
architecture_data = ArchitectureSummary(
|
| 121 |
+
summary=arch_raw.get("summary", ""),
|
| 122 |
+
reading_order=arch_raw.get("reading_order", []),
|
| 123 |
+
relationships=relationships,
|
| 124 |
+
)
|
| 125 |
+
ANALYSIS_STORE[repo_name] = {
|
| 126 |
+
"analysis": analysis_data,
|
| 127 |
+
"architecture": architecture_data,
|
| 128 |
+
}
|
| 129 |
+
loaded += 1
|
| 130 |
+
except Exception as exc:
|
| 131 |
+
logger.warning(
|
| 132 |
+
"Skipping malformed analysis store entry for '%s': %s", repo_name, exc
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
if loaded:
|
| 136 |
+
logger.info(
|
| 137 |
+
"Loaded %d repository entries from analysis store (%s).",
|
| 138 |
+
loaded,
|
| 139 |
+
_ANALYSIS_STORE_PATH,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _serialise_store() -> Dict[str, Any]:
|
| 144 |
+
"""Serialise ANALYSIS_STORE to a plain JSON-safe dict."""
|
| 145 |
+
out: Dict[str, Any] = {}
|
| 146 |
+
for repo_name, entry in ANALYSIS_STORE.items():
|
| 147 |
+
try:
|
| 148 |
+
analysis_obj = entry["analysis"]
|
| 149 |
+
arch_obj = entry["architecture"]
|
| 150 |
+
out[repo_name] = {
|
| 151 |
+
"analysis": (
|
| 152 |
+
analysis_obj.model_dump()
|
| 153 |
+
if hasattr(analysis_obj, "model_dump")
|
| 154 |
+
else analysis_obj
|
| 155 |
+
),
|
| 156 |
+
"architecture": (
|
| 157 |
+
arch_obj.model_dump()
|
| 158 |
+
if hasattr(arch_obj, "model_dump")
|
| 159 |
+
else arch_obj
|
| 160 |
+
),
|
| 161 |
+
}
|
| 162 |
+
except Exception as exc:
|
| 163 |
+
logger.warning("Could not serialise store entry for '%s': %s", repo_name, exc)
|
| 164 |
+
return out
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
async def _persist_analysis_store() -> None:
|
| 168 |
+
"""Write ANALYSIS_STORE to disk atomically (tmp file → rename).
|
| 169 |
+
|
| 170 |
+
Uses an asyncio Lock to prevent concurrent writes from corrupting the file.
|
| 171 |
+
Runs the blocking I/O in a thread so the event loop is not blocked.
|
| 172 |
+
"""
|
| 173 |
+
async with _persist_lock:
|
| 174 |
+
try:
|
| 175 |
+
payload = _serialise_store()
|
| 176 |
+
await asyncio.to_thread(_write_store_atomic, payload)
|
| 177 |
+
logger.debug("Analysis store persisted (%d entries).", len(payload))
|
| 178 |
+
except Exception as exc:
|
| 179 |
+
logger.error("Failed to persist analysis store: %s", exc, exc_info=True)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _write_store_atomic(payload: Dict[str, Any]) -> None:
|
| 183 |
+
"""Write payload to _ANALYSIS_STORE_PATH via a tmp file + rename (atomic)."""
|
| 184 |
+
os.makedirs(os.path.dirname(_ANALYSIS_STORE_PATH), exist_ok=True)
|
| 185 |
+
tmp_path = _ANALYSIS_STORE_PATH + ".tmp"
|
| 186 |
+
with open(tmp_path, "w", encoding="utf-8") as fh:
|
| 187 |
+
json.dump(payload, fh, indent=2)
|
| 188 |
+
os.replace(tmp_path, _ANALYSIS_STORE_PATH)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# Populate ANALYSIS_STORE from disk before the first request is served
|
| 192 |
+
_load_analysis_store()
|
| 193 |
|
| 194 |
# ---------------------------------------------------------------------------
|
| 195 |
# Core service singletons
|
|
|
|
| 245 |
default_factory=list, description="Conversation history"
|
| 246 |
)
|
| 247 |
|
| 248 |
+
@field_validator("repo")
|
| 249 |
+
@classmethod
|
| 250 |
+
def repo_must_not_be_empty(cls, v: str) -> str:
|
| 251 |
+
stripped = v.strip()
|
| 252 |
+
if not stripped:
|
| 253 |
+
raise ValueError(
|
| 254 |
+
"repo must not be empty. "
|
| 255 |
+
"Please open a repository from the analysis page first."
|
| 256 |
+
)
|
| 257 |
+
return stripped
|
| 258 |
+
|
| 259 |
|
| 260 |
class ArchitectureBuildRequest(BaseModel):
|
| 261 |
repo: str = Field(..., description="Repository identifier (owner/repo)")
|
|
|
|
| 435 |
@app.get("/api/repos/recent")
|
| 436 |
async def get_recent():
|
| 437 |
"""Fetch list of recently analysed repositories from the in-memory store."""
|
| 438 |
+
results = []
|
| 439 |
+
for name, data in ANALYSIS_STORE.items():
|
| 440 |
+
analysis_obj = data.get("analysis")
|
| 441 |
+
# Support both Pydantic model and plain dict (defensive)
|
| 442 |
+
if hasattr(analysis_obj, "tech_stack"):
|
| 443 |
+
tech_stack = analysis_obj.tech_stack
|
| 444 |
+
elif isinstance(analysis_obj, dict):
|
| 445 |
+
tech_stack = analysis_obj.get("tech_stack", [])
|
| 446 |
+
else:
|
| 447 |
+
tech_stack = []
|
| 448 |
+
results.append({
|
| 449 |
"name": name,
|
| 450 |
"url": f"https://github.com/{name}",
|
| 451 |
+
"tech_stack": tech_stack,
|
| 452 |
"analyzed_at": "Just now",
|
| 453 |
+
})
|
| 454 |
+
return results
|
|
|
|
| 455 |
|
| 456 |
|
| 457 |
@app.post("/api/index")
|
|
|
|
| 601 |
yield f"data: {json.dumps({'status': 'chroma', 'message': 'END: chroma indexing'})}\n\n"
|
| 602 |
|
| 603 |
yield f"data: {json.dumps({'status': 'building_graph', 'message': 'Building file dependency graph...'})}\n\n"
|
| 604 |
+
arch_build_result = await asyncio.to_thread(
|
| 605 |
+
architecture_service.build, repo_name, local_path, files, False
|
| 606 |
+
)
|
| 607 |
+
_graph_files = arch_build_result.get("files_parsed", 0)
|
| 608 |
+
_graph_edges = arch_build_result.get("dependencies_found", 0)
|
| 609 |
+
_graph_entries = arch_build_result.get("entry_points", [])
|
| 610 |
+
logger.info(
|
| 611 |
+
"[PIPELINE:%s] GRAPH built files_parsed=%d edges=%d entry_points=%s",
|
| 612 |
+
repo_name, _graph_files, _graph_edges, _graph_entries,
|
| 613 |
)
|
| 614 |
+
yield f"data: {json.dumps({'status': 'graph_built', 'message': f'✓ Dependency graph: {_graph_files} files parsed, {_graph_edges} edges', 'files_parsed': _graph_files, 'dependencies_found': _graph_edges, 'entry_points': _graph_entries})}\n\n"
|
| 615 |
|
| 616 |
# Architecture summary via DeepSeek
|
| 617 |
architecture_summary = await generate_architecture_summary_with_llm(
|
|
|
|
| 643 |
"architecture": architecture_summary,
|
| 644 |
}
|
| 645 |
|
| 646 |
+
# Persist to disk so data survives server restarts
|
| 647 |
+
asyncio.ensure_future(_persist_analysis_store())
|
| 648 |
+
|
| 649 |
yield f"data: {json.dumps({'status': 'complete', 'message': '✓ Repository analysis complete!'})}\n\n"
|
| 650 |
yield f"data: {json.dumps({'status': 'done', 'repo': repo_name})}\n\n"
|
| 651 |
|
|
|
|
| 714 |
|
| 715 |
@app.post("/api/chat")
|
| 716 |
async def repository_chat(request: ChatRequest):
|
| 717 |
+
"""Chat with repository context, streaming back token results via SSE.
|
| 718 |
+
|
| 719 |
+
Pipeline:
|
| 720 |
+
1. Validate repo name (Pydantic + SSE guard)
|
| 721 |
+
2. Embed question locally (BGE — no API call)
|
| 722 |
+
3. Retrieve relevant chunks from ChromaDB
|
| 723 |
+
4. Inject architecture context
|
| 724 |
+
5. Stream LLM answer token-by-token
|
| 725 |
+
→ On LLM failure: emit retrieval-only fallback (no raw errors to user)
|
| 726 |
+
6. Emit sources + confidence in terminal done event
|
| 727 |
+
(EvaluationAgent skipped in fallback mode to avoid a second LLM call
|
| 728 |
+
during a rate-limit window)
|
| 729 |
+
"""
|
| 730 |
+
# repo field is pre-validated and stripped by ChatRequest.repo_must_not_be_empty
|
| 731 |
+
repo_name = request.repo
|
| 732 |
question = request.message.strip()
|
| 733 |
history = request.history
|
| 734 |
|
| 735 |
async def chat_streamer():
|
| 736 |
+
# ── SSE-level guard (defence-in-depth for any call path that bypasses Pydantic) ──
|
| 737 |
+
if not repo_name:
|
| 738 |
+
yield f"data: {json.dumps({'error': 'no_repo_selected', 'message': 'No repository selected. Please open a repository from the analysis page first.', 'status': 'done'})}\n\n"
|
| 739 |
+
return
|
| 740 |
+
|
| 741 |
+
# ── 1. Embed query (local BGE — never fails due to API quota) ──────────
|
| 742 |
try:
|
|
|
|
| 743 |
query_embedding = embedding_service.generate_embedding(question)
|
| 744 |
+
except Exception as embed_exc:
|
| 745 |
+
logger.error("Embedding failed: %s", embed_exc, exc_info=True)
|
| 746 |
+
yield f"data: {json.dumps({'error': 'embedding_failed', 'message': 'Failed to process your question. Please try again.', 'status': 'done'})}\n\n"
|
| 747 |
+
return
|
| 748 |
|
| 749 |
+
# ── 2. Retrieve relevant chunks from ChromaDB ────────────────────────
|
| 750 |
+
try:
|
| 751 |
chunks = chroma_store.search_repository(
|
| 752 |
repo_name=repo_name,
|
| 753 |
query_embedding=query_embedding,
|
| 754 |
limit=5,
|
| 755 |
)
|
| 756 |
+
except Exception as chroma_exc:
|
| 757 |
+
logger.error("ChromaDB search failed: %s", chroma_exc, exc_info=True)
|
| 758 |
+
yield f"data: {json.dumps({'error': 'retrieval_failed', 'message': 'Failed to search the repository index. Please try again.', 'status': 'done'})}\n\n"
|
| 759 |
+
return
|
| 760 |
+
|
| 761 |
+
# Build sources set and context string from retrieved chunks
|
| 762 |
+
context_blocks: List[str] = []
|
| 763 |
+
sources: set = set()
|
| 764 |
+
for chunk in chunks:
|
| 765 |
+
meta = chunk.get("metadata", {})
|
| 766 |
+
file_path = meta.get("file_path", "unknown")
|
| 767 |
+
sources.add(file_path)
|
| 768 |
+
context_blocks.append(f"--- File: {file_path} ---\n{chunk.get('content', '')}")
|
| 769 |
+
|
| 770 |
+
if context_blocks:
|
| 771 |
+
context_str = "\n\n".join(context_blocks)
|
| 772 |
+
else:
|
| 773 |
context_str = (
|
| 774 |
+
"No indexed content was found for this repository and query. "
|
| 775 |
+
"The repository may not be indexed yet, or the question did not "
|
| 776 |
+
"match any stored code chunks."
|
| 777 |
)
|
| 778 |
|
| 779 |
+
# ── 3. Normalise conversation history ────────────────────────────────
|
| 780 |
+
normalised_history = []
|
| 781 |
+
for turn in history:
|
| 782 |
+
role = turn.get("role", "user")
|
| 783 |
+
if role == "model":
|
| 784 |
+
role = "assistant"
|
| 785 |
+
parts = turn.get("parts", [])
|
| 786 |
+
if parts:
|
| 787 |
+
content = parts[0] if isinstance(parts[0], str) else str(parts[0])
|
| 788 |
+
elif "content" in turn:
|
| 789 |
+
content = turn["content"]
|
| 790 |
+
else:
|
| 791 |
+
continue
|
| 792 |
+
normalised_history.append({"role": role, "content": content})
|
| 793 |
+
|
| 794 |
+
# ── 4. Architecture context injection ────────────────────────────────
|
| 795 |
+
arch_ctx = arch_context_service.get_context(repo_name)
|
| 796 |
+
arch_block = arch_ctx.to_prompt_block()
|
| 797 |
+
arch_section = f"\n{arch_block}\n" if arch_block else ""
|
| 798 |
+
|
| 799 |
+
# ── 5. Strictly grounded system instruction ──────────────────────────
|
| 800 |
+
system_instruction = (
|
| 801 |
+
"You are a Repository Intelligence Agent — an expert developer assistant "
|
| 802 |
+
"that answers questions about a specific codebase.\n\n"
|
| 803 |
+
"STRICT RULES:\n"
|
| 804 |
+
"1. Answer ONLY using the repository context provided below. "
|
| 805 |
+
"Do NOT invent file paths, function names, class names, or module structures "
|
| 806 |
+
"that are not present in the retrieved context.\n"
|
| 807 |
+
"2. If the context does not contain enough information to answer, say so "
|
| 808 |
+
"explicitly: state what was found and what was missing, and suggest the user "
|
| 809 |
+
"check specific files if you can infer them from the context.\n"
|
| 810 |
+
"3. You MAY use general programming knowledge to explain language concepts, "
|
| 811 |
+
"framework patterns, or standard library behaviour — but only when it does "
|
| 812 |
+
"NOT require making repository-specific claims beyond the provided context.\n"
|
| 813 |
+
"4. Always cite the specific file path(s) from the context when making "
|
| 814 |
+
"repository-specific statements.\n"
|
| 815 |
+
"5. Keep code snippets accurate — do not paraphrase or reconstruct code "
|
| 816 |
+
"unless it is directly present in the context."
|
| 817 |
+
)
|
| 818 |
|
| 819 |
+
prompt = (
|
| 820 |
+
f"{arch_section}"
|
| 821 |
+
f"Context from repository '{repo_name}':\n"
|
| 822 |
+
f"=======================\n"
|
| 823 |
+
f"{context_str}\n"
|
| 824 |
+
f"=======================\n\n"
|
| 825 |
+
f"Question: {question}"
|
| 826 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
|
| 828 |
+
# ── 6. Stream from LLM — with retrieval-only fallback on failure ─────
|
| 829 |
+
provider = ProviderFactory.get_provider()
|
| 830 |
+
full_text = ""
|
| 831 |
+
fallback_mode = False
|
| 832 |
+
|
| 833 |
+
try:
|
| 834 |
async for token in provider.stream(
|
| 835 |
prompt=prompt,
|
| 836 |
system_instruction=system_instruction,
|
|
|
|
| 839 |
full_text += token
|
| 840 |
yield f"data: {json.dumps({'text': token})}\n\n"
|
| 841 |
|
| 842 |
+
except Exception as llm_exc:
|
| 843 |
+
logger.warning(
|
| 844 |
+
"LLM provider failed during chat stream for repo '%s': %s",
|
| 845 |
+
repo_name,
|
| 846 |
+
llm_exc,
|
| 847 |
+
)
|
| 848 |
+
fallback_mode = True
|
| 849 |
+
|
| 850 |
+
# If some tokens were already yielded, emit a clear separator
|
| 851 |
+
if full_text:
|
| 852 |
+
separator = (
|
| 853 |
+
"\n\n---\n"
|
| 854 |
+
"⚠️ AI provider became unavailable. "
|
| 855 |
+
"Showing retrieved context below.\n\n"
|
| 856 |
+
)
|
| 857 |
+
yield f"data: {json.dumps({'text': separator})}\n\n"
|
| 858 |
+
|
| 859 |
+
# Emit retrieval-grounded fallback answer
|
| 860 |
+
fallback_text = build_fallback_answer(chunks, question)
|
| 861 |
+
yield f"data: {json.dumps({'text': fallback_text})}\n\n"
|
| 862 |
+
|
| 863 |
+
# ── 7. Confidence scoring and terminal done event ────────────────────
|
| 864 |
+
if fallback_mode:
|
| 865 |
+
# Skip EvaluationAgent — it would make a second LLM call during a
|
| 866 |
+
# rate-limit window, which is exactly wrong.
|
| 867 |
+
confidence_pct = 0
|
| 868 |
+
else:
|
| 869 |
try:
|
| 870 |
evaluator = EvaluationAgent()
|
| 871 |
eval_result = evaluator.evaluate_response(question, full_text, chunks)
|
|
|
|
| 874 |
logger.error("Chat evaluation failed: %s", eval_err)
|
| 875 |
confidence_pct = 85
|
| 876 |
|
| 877 |
+
yield f"data: {json.dumps({'sources': sorted(list(sources)), 'confidence': confidence_pct, 'fallback_mode': fallback_mode, 'status': 'done'})}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
|
| 879 |
return StreamingResponse(chat_streamer(), media_type="text/event-stream")
|
| 880 |
|
|
|
|
| 979 |
status_code=404,
|
| 980 |
detail=(
|
| 981 |
f"No dependency graph found for '{full_name}'. "
|
| 982 |
+
"Please analyse the repository first."
|
| 983 |
),
|
| 984 |
)
|
| 985 |
graph_data = await asyncio.to_thread(
|
| 986 |
graph_service.get_visualization_graph, full_name, architecture_service, q
|
| 987 |
)
|
| 988 |
+
# Guard: graph file exists but was built with 0 nodes (empty parse).
|
| 989 |
+
# Return 404 so the frontend shows an actionable error instead of a
|
| 990 |
+
# blank canvas with no explanation.
|
| 991 |
+
if not graph_data.get("nodes"):
|
| 992 |
+
raise HTTPException(
|
| 993 |
+
status_code=404,
|
| 994 |
+
detail=(
|
| 995 |
+
f"Dependency graph for '{full_name}' exists but contains no nodes. "
|
| 996 |
+
"Re-analyse the repository to rebuild the graph with the latest code."
|
| 997 |
+
),
|
| 998 |
+
)
|
| 999 |
return graph_data
|
| 1000 |
except HTTPException:
|
| 1001 |
raise
|
|
|
|
| 1006 |
|
| 1007 |
if __name__ == "__main__":
|
| 1008 |
import uvicorn
|
| 1009 |
+
uvicorn.run(
|
| 1010 |
+
"backend.api:app",
|
| 1011 |
+
host=os.environ.get("API_SERVER_HOST", "127.0.0.1"),
|
| 1012 |
+
port=int(os.environ.get("API_SERVER_PORT", "8000")),
|
| 1013 |
+
reload=True,
|
| 1014 |
+
reload_dirs=["backend", "services", "agents", "memory", "models"],
|
| 1015 |
+
)
|
backend/main.py
CHANGED
|
@@ -1,10 +1,26 @@
|
|
| 1 |
"""Main entry point to run the FastAPI server."""
|
| 2 |
|
|
|
|
| 3 |
import uvicorn
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
def main():
|
| 6 |
"""Starts the FastAPI backend application."""
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
if __name__ == "__main__":
|
| 10 |
main()
|
|
|
|
| 1 |
"""Main entry point to run the FastAPI server."""
|
| 2 |
|
| 3 |
+
import os
|
| 4 |
import uvicorn
|
| 5 |
|
| 6 |
+
# Watch ONLY source dirs. Without this, WatchFiles scans the whole project tree
|
| 7 |
+
# (including data/, .venv/, __pycache__/) and triggers a reload whenever a
|
| 8 |
+
# cloned repo writes a file — killing in-flight analysis requests.
|
| 9 |
+
_RELOAD_DIRS = ["backend", "services", "agents", "memory", "models"]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
def main():
|
| 13 |
"""Starts the FastAPI backend application."""
|
| 14 |
+
host = os.environ.get("API_SERVER_HOST", "127.0.0.1")
|
| 15 |
+
port = int(os.environ.get("API_SERVER_PORT", "8001"))
|
| 16 |
+
uvicorn.run(
|
| 17 |
+
"backend.api:app",
|
| 18 |
+
host=host,
|
| 19 |
+
port=port,
|
| 20 |
+
reload=True,
|
| 21 |
+
reload_dirs=_RELOAD_DIRS,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
|
| 25 |
if __name__ == "__main__":
|
| 26 |
main()
|
docs/API_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# API Reference Specification
|
| 2 |
|
| 3 |
-
All endpoints are hosted at: `http://
|
| 4 |
|
| 5 |
---
|
| 6 |
|
|
@@ -20,7 +20,7 @@ Retrieve the operational status of the API services and configured LLM providers
|
|
| 20 |
|
| 21 |
#### Request Example
|
| 22 |
```bash
|
| 23 |
-
curl http://
|
| 24 |
```
|
| 25 |
|
| 26 |
#### Response (200 OK)
|
|
@@ -42,7 +42,7 @@ Fetch a list of pre-configured example repositories designed for testing.
|
|
| 42 |
|
| 43 |
#### Request Example
|
| 44 |
```bash
|
| 45 |
-
curl http://
|
| 46 |
```
|
| 47 |
|
| 48 |
#### Response (200 OK)
|
|
@@ -53,6 +53,18 @@ curl http://localhost:8000/api/repos/examples
|
|
| 53 |
"url": "https://github.com/google/guava",
|
| 54 |
"tech_stack": ["Java", "Maven"],
|
| 55 |
"description": "Google core libraries for Java."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
]
|
| 58 |
```
|
|
@@ -60,11 +72,11 @@ curl http://localhost:8000/api/repos/examples
|
|
| 60 |
---
|
| 61 |
|
| 62 |
### GET /api/repos/recent
|
| 63 |
-
Fetch list of repositories processed during the current server session
|
| 64 |
|
| 65 |
#### Request Example
|
| 66 |
```bash
|
| 67 |
-
curl http://
|
| 68 |
```
|
| 69 |
|
| 70 |
#### Response (200 OK)
|
|
@@ -84,9 +96,13 @@ curl http://localhost:8000/api/repos/recent
|
|
| 84 |
## Repository Processing
|
| 85 |
|
| 86 |
### POST /api/analyze (SSE Stream)
|
| 87 |
-
Trigger a full repository cloning, parsing, indexing, and architecture mapping pipeline. This endpoint streams real-time progress
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
#### Request Schema
|
| 90 |
```json
|
| 91 |
{
|
| 92 |
"url": "https://github.com/owner/repository",
|
|
@@ -97,7 +113,7 @@ Trigger a full repository cloning, parsing, indexing, and architecture mapping p
|
|
| 97 |
|
| 98 |
#### Request Example
|
| 99 |
```bash
|
| 100 |
-
curl -N -X POST http://
|
| 101 |
-H "Content-Type: application/json" \
|
| 102 |
-H "Accept: text/event-stream" \
|
| 103 |
-d '{
|
|
@@ -106,7 +122,7 @@ curl -N -X POST http://localhost:8000/api/analyze \
|
|
| 106 |
}'
|
| 107 |
```
|
| 108 |
|
| 109 |
-
#### SSE Stream
|
| 110 |
```
|
| 111 |
data: {"status": "cloning", "message": "Cloning repository from GitHub..."}
|
| 112 |
data: {"status": "cloned", "message": "✓ Repository cloned successfully"}
|
|
@@ -119,6 +135,7 @@ data: {"status": "embedding", "message": "END: embeddings"}
|
|
| 119 |
data: {"status": "chroma", "message": "START: chroma indexing"}
|
| 120 |
data: {"status": "chroma", "message": "END: chroma indexing"}
|
| 121 |
data: {"status": "building_graph", "message": "Building file dependency graph..."}
|
|
|
|
| 122 |
data: {"status": "analyzed", "message": "✓ Architecture analyzed"}
|
| 123 |
data: {"status": "complete", "message": "✓ Repository analysis complete!"}
|
| 124 |
data: {"status": "done", "repo": "Ankita15k/GitNest"}
|
|
@@ -127,9 +144,11 @@ data: {"status": "done", "repo": "Ankita15k/GitNest"}
|
|
| 127 |
---
|
| 128 |
|
| 129 |
### POST /api/index
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
#### Request Schema
|
| 133 |
```json
|
| 134 |
{
|
| 135 |
"repo_url": "https://github.com/owner/repository"
|
|
@@ -147,28 +166,70 @@ Sync execution endpoint to clone, parse, and embed a repository without generati
|
|
| 147 |
|
| 148 |
---
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
## Semantic Querying
|
| 151 |
|
| 152 |
### POST /api/retrieve
|
| 153 |
-
Query the vector space index for a
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
-
#### Request Schema
|
| 156 |
```json
|
| 157 |
{
|
| 158 |
"repo": "owner/repository",
|
| 159 |
-
"question": "
|
| 160 |
}
|
| 161 |
```
|
| 162 |
|
| 163 |
#### Response (200 OK)
|
| 164 |
```json
|
| 165 |
{
|
| 166 |
-
"answer": "Authentication is
|
| 167 |
"sources": [
|
| 168 |
{
|
| 169 |
-
"file": "backend/src/
|
| 170 |
-
"content": "const token =
|
| 171 |
-
"score": 0.
|
| 172 |
}
|
| 173 |
]
|
| 174 |
}
|
|
@@ -179,25 +240,29 @@ Query the vector space index for a particular repository using local semantic em
|
|
| 179 |
### POST /api/chat (SSE Stream)
|
| 180 |
Interactive, conversational multi-turn chat over the repository codebase context, streaming back word tokens.
|
| 181 |
|
| 182 |
-
#### Request Schema
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
```json
|
| 184 |
{
|
| 185 |
-
"repo": "
|
| 186 |
-
"message": "
|
| 187 |
"history": [
|
| 188 |
-
{"role": "user", "content": "
|
| 189 |
-
{"role": "assistant", "content": "
|
| 190 |
]
|
| 191 |
}
|
| 192 |
```
|
| 193 |
|
| 194 |
#### Request Example
|
| 195 |
```bash
|
| 196 |
-
curl -N -X POST http://
|
| 197 |
-H "Content-Type: application/json" \
|
| 198 |
-d '{
|
| 199 |
"repo": "Ankita15k/GitNest",
|
| 200 |
-
"message": "
|
| 201 |
"history": []
|
| 202 |
}'
|
| 203 |
```
|
|
@@ -205,9 +270,15 @@ curl -N -X POST http://localhost:8000/api/chat \
|
|
| 205 |
#### SSE Stream Output
|
| 206 |
```
|
| 207 |
data: {"text": "Socket"}
|
| 208 |
-
data: {"text": ".io
|
| 209 |
-
|
| 210 |
-
data: {"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
```
|
| 212 |
|
| 213 |
---
|
|
@@ -215,38 +286,45 @@ data: {"sources": ["backend/src/socket/notificationHandler.js"], "confidence": 9
|
|
| 215 |
### POST /api/issues/map
|
| 216 |
Analyze a GitHub issue and map it to relevant source code modules, generating a detailed implementation plan.
|
| 217 |
|
| 218 |
-
#### Request Schema
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
```json
|
| 220 |
{
|
| 221 |
-
"repo": "
|
| 222 |
-
"
|
| 223 |
-
"
|
| 224 |
-
"description": "Issue description text (optional)"
|
| 225 |
}
|
| 226 |
```
|
| 227 |
|
| 228 |
-
#### Response (200 OK)
|
| 229 |
```json
|
| 230 |
{
|
| 231 |
-
"
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
"
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
"
|
| 238 |
-
"
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
"confidence": 88,
|
| 249 |
-
"
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
```
|
| 252 |
|
|
@@ -257,7 +335,9 @@ Analyze a GitHub issue and map it to relevant source code modules, generating a
|
|
| 257 |
### POST /api/architecture/build
|
| 258 |
Trigger Tree-sitter AST parsing over local cloned repository files and construct the dependency NetworkX graph.
|
| 259 |
|
| 260 |
-
#### Request Schema
|
|
|
|
|
|
|
| 261 |
```json
|
| 262 |
{
|
| 263 |
"repo": "owner/repository"
|
|
@@ -272,8 +352,7 @@ Trigger Tree-sitter AST parsing over local cloned repository files and construct
|
|
| 272 |
"files_parsed": 328,
|
| 273 |
"dependencies_found": 1440,
|
| 274 |
"entry_points": [
|
| 275 |
-
"backend/src/index.js"
|
| 276 |
-
"frontend/src/main.jsx"
|
| 277 |
]
|
| 278 |
}
|
| 279 |
```
|
|
@@ -281,24 +360,24 @@ Trigger Tree-sitter AST parsing over local cloned repository files and construct
|
|
| 281 |
---
|
| 282 |
|
| 283 |
### GET /api/architecture/{owner}/{repo_name}
|
| 284 |
-
Fetch the generated architecture summary,
|
| 285 |
|
| 286 |
-
#### Response (200 OK)
|
| 287 |
```json
|
| 288 |
{
|
| 289 |
-
"summary": "GitNest is a
|
| 290 |
-
"
|
| 291 |
"backend/src/index.js",
|
| 292 |
-
"
|
| 293 |
-
],
|
| 294 |
-
"core_modules": [
|
| 295 |
-
"backend/src/controllers/auth.controller.js"
|
| 296 |
],
|
| 297 |
-
"
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
```
|
| 304 |
|
|
@@ -316,18 +395,17 @@ Retrieve React Flow-compatible node and edge datasets for visualization.
|
|
| 316 |
"nodes": [
|
| 317 |
{
|
| 318 |
"id": "backend/src/index.js",
|
| 319 |
-
"label": "index.js",
|
| 320 |
-
"
|
| 321 |
-
"
|
| 322 |
-
"x": 200,
|
| 323 |
-
"y": 150
|
| 324 |
}
|
| 325 |
],
|
| 326 |
"edges": [
|
| 327 |
{
|
|
|
|
| 328 |
"source": "backend/src/index.js",
|
| 329 |
"target": "backend/src/app.js",
|
| 330 |
-
"
|
| 331 |
}
|
| 332 |
]
|
| 333 |
}
|
|
@@ -338,25 +416,34 @@ Retrieve React Flow-compatible node and edge datasets for visualization.
|
|
| 338 |
### POST /api/reading-order
|
| 339 |
Generate an optimal, ranked file list for onboarding new developers based on graph centrality metrics.
|
| 340 |
|
| 341 |
-
#### Request Schema
|
|
|
|
|
|
|
| 342 |
```json
|
| 343 |
{
|
| 344 |
"repo": "owner/repository"
|
| 345 |
}
|
| 346 |
```
|
| 347 |
|
| 348 |
-
#### Response (200 OK)
|
| 349 |
```json
|
| 350 |
{
|
| 351 |
"repo": "Ankita15k/GitNest",
|
| 352 |
-
"
|
| 353 |
{
|
| 354 |
"rank": 1,
|
| 355 |
-
"
|
| 356 |
"reason": "Primary application entry point initializing server configurations.",
|
| 357 |
-
"
|
|
|
|
| 358 |
}
|
| 359 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
}
|
| 361 |
```
|
| 362 |
|
|
@@ -365,25 +452,44 @@ Generate an optimal, ranked file list for onboarding new developers based on gra
|
|
| 365 |
### POST /api/impact-analysis
|
| 366 |
Predict downstream affected modules and components resulting from a proposed change.
|
| 367 |
|
| 368 |
-
#### Request Schema
|
|
|
|
|
|
|
|
|
|
| 369 |
```json
|
| 370 |
{
|
| 371 |
-
"repo": "
|
| 372 |
-
"issue": "
|
| 373 |
}
|
| 374 |
```
|
| 375 |
|
| 376 |
-
#### Response (200 OK)
|
| 377 |
```json
|
| 378 |
{
|
| 379 |
"repo": "Ankita15k/GitNest",
|
| 380 |
-
"
|
| 381 |
-
"
|
| 382 |
-
"backend/src/middleware/authMiddleware.js"
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
],
|
| 385 |
-
"
|
| 386 |
-
"risk_level": "high",
|
| 387 |
-
"reasoning": "Modifying the auth payload schema affects all token verification routes and client calls."
|
| 388 |
}
|
| 389 |
```
|
|
|
|
| 1 |
# API Reference Specification
|
| 2 |
|
| 3 |
+
All endpoints are hosted at: `http://127.0.0.1:8001`
|
| 4 |
|
| 5 |
---
|
| 6 |
|
|
|
|
| 20 |
|
| 21 |
#### Request Example
|
| 22 |
```bash
|
| 23 |
+
curl http://127.0.0.1:8001/health
|
| 24 |
```
|
| 25 |
|
| 26 |
#### Response (200 OK)
|
|
|
|
| 42 |
|
| 43 |
#### Request Example
|
| 44 |
```bash
|
| 45 |
+
curl http://127.0.0.1:8001/api/repos/examples
|
| 46 |
```
|
| 47 |
|
| 48 |
#### Response (200 OK)
|
|
|
|
| 53 |
"url": "https://github.com/google/guava",
|
| 54 |
"tech_stack": ["Java", "Maven"],
|
| 55 |
"description": "Google core libraries for Java."
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"name": "fastapi/fastapi",
|
| 59 |
+
"url": "https://github.com/fastapi/fastapi",
|
| 60 |
+
"tech_stack": ["Python", "Pydantic", "Starlette"],
|
| 61 |
+
"description": "High performance, easy to learn, fast to code, ready for production API framework."
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"name": "vercel/next.js",
|
| 65 |
+
"url": "https://github.com/vercel/next.js",
|
| 66 |
+
"tech_stack": ["JavaScript", "TypeScript", "React", "Rust"],
|
| 67 |
+
"description": "The React Framework for the Web."
|
| 68 |
}
|
| 69 |
]
|
| 70 |
```
|
|
|
|
| 72 |
---
|
| 73 |
|
| 74 |
### GET /api/repos/recent
|
| 75 |
+
Fetch a list of repositories processed during the current server session, loaded from the persisted store.
|
| 76 |
|
| 77 |
#### Request Example
|
| 78 |
```bash
|
| 79 |
+
curl http://127.0.0.1:8001/api/repos/recent
|
| 80 |
```
|
| 81 |
|
| 82 |
#### Response (200 OK)
|
|
|
|
| 96 |
## Repository Processing
|
| 97 |
|
| 98 |
### POST /api/analyze (SSE Stream)
|
| 99 |
+
Trigger a full repository cloning, language parsing, code chunking, indexing, and architecture mapping pipeline. This endpoint streams real-time progress events using the `text/event-stream` mime-type.
|
| 100 |
+
|
| 101 |
+
#### Request Schema (AnalyzeRequest)
|
| 102 |
+
- `url` (string, required): GitHub repository URL.
|
| 103 |
+
- `branch` (string, optional): Git branch or ref. Defaults to `"main"`.
|
| 104 |
+
- `model` (string, optional): LLM model variant to use. Defaults to `"deepseek-ai/deepseek-v4-flash"`.
|
| 105 |
|
|
|
|
| 106 |
```json
|
| 107 |
{
|
| 108 |
"url": "https://github.com/owner/repository",
|
|
|
|
| 113 |
|
| 114 |
#### Request Example
|
| 115 |
```bash
|
| 116 |
+
curl -N -X POST http://127.0.0.1:8001/api/analyze \
|
| 117 |
-H "Content-Type: application/json" \
|
| 118 |
-H "Accept: text/event-stream" \
|
| 119 |
-d '{
|
|
|
|
| 122 |
}'
|
| 123 |
```
|
| 124 |
|
| 125 |
+
#### SSE Stream Events Progress Output
|
| 126 |
```
|
| 127 |
data: {"status": "cloning", "message": "Cloning repository from GitHub..."}
|
| 128 |
data: {"status": "cloned", "message": "✓ Repository cloned successfully"}
|
|
|
|
| 135 |
data: {"status": "chroma", "message": "START: chroma indexing"}
|
| 136 |
data: {"status": "chroma", "message": "END: chroma indexing"}
|
| 137 |
data: {"status": "building_graph", "message": "Building file dependency graph..."}
|
| 138 |
+
data: {"status": "graph_built", "message": "✓ Dependency graph: 328 files parsed, 1440 edges", "files_parsed": 328, "dependencies_found": 1440, "entry_points": ["backend/src/index.js"]}
|
| 139 |
data: {"status": "analyzed", "message": "✓ Architecture analyzed"}
|
| 140 |
data: {"status": "complete", "message": "✓ Repository analysis complete!"}
|
| 141 |
data: {"status": "done", "repo": "Ankita15k/GitNest"}
|
|
|
|
| 144 |
---
|
| 145 |
|
| 146 |
### POST /api/index
|
| 147 |
+
Clones, chunks, and indexes a repository's vector embeddings in ChromaDB, without building structural dependency graphs.
|
| 148 |
+
|
| 149 |
+
#### Request Schema (IndexRequest)
|
| 150 |
+
- `repo_url` (string, required): GitHub repository URL.
|
| 151 |
|
|
|
|
| 152 |
```json
|
| 153 |
{
|
| 154 |
"repo_url": "https://github.com/owner/repository"
|
|
|
|
| 166 |
|
| 167 |
---
|
| 168 |
|
| 169 |
+
### GET /api/analysis/{owner}/{repo_name}
|
| 170 |
+
Fetch the complete metadata results of a repository analysis.
|
| 171 |
+
|
| 172 |
+
#### Response (200 OK)
|
| 173 |
+
```json
|
| 174 |
+
{
|
| 175 |
+
"analysis": {
|
| 176 |
+
"structure": {
|
| 177 |
+
".": ["README.md", "package.json"],
|
| 178 |
+
"src": ["app.js", "server.js"]
|
| 179 |
+
},
|
| 180 |
+
"dependencies": ["express", "cors", "jsonwebtoken"],
|
| 181 |
+
"tech_stack": ["JavaScript", "TypeScript"],
|
| 182 |
+
"metadata": {
|
| 183 |
+
"owner": "Ankita15k",
|
| 184 |
+
"name": "GitNest",
|
| 185 |
+
"local_path": "C:\\Users\\Varshith Reddy\\.repo_intelligence\\cloned_repos\\Ankita15k_GitNest"
|
| 186 |
+
}
|
| 187 |
+
},
|
| 188 |
+
"architecture": {
|
| 189 |
+
"summary": "GitNest is a full-stack Git hosting dashboard built with Node.js and Express...",
|
| 190 |
+
"reading_order": [
|
| 191 |
+
"backend/src/index.js",
|
| 192 |
+
"backend/src/app.js"
|
| 193 |
+
],
|
| 194 |
+
"relationships": [
|
| 195 |
+
{
|
| 196 |
+
"source": "backend/src/index.js",
|
| 197 |
+
"target": "backend/src/app.js",
|
| 198 |
+
"relationship_type": "imports",
|
| 199 |
+
"description": "Imports app instance to launch listener server."
|
| 200 |
+
}
|
| 201 |
+
]
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
## Semantic Querying
|
| 209 |
|
| 210 |
### POST /api/retrieve
|
| 211 |
+
Query the vector space index directly for a repository using BGE embeddings and generate a grounded LLM response.
|
| 212 |
+
|
| 213 |
+
#### Request Schema (RetrieveRequest)
|
| 214 |
+
- `repo` (string, required): Repository identifier (owner/repo).
|
| 215 |
+
- `question` (string, required): Question query.
|
| 216 |
|
|
|
|
| 217 |
```json
|
| 218 |
{
|
| 219 |
"repo": "owner/repository",
|
| 220 |
+
"question": "How is authentication handled?"
|
| 221 |
}
|
| 222 |
```
|
| 223 |
|
| 224 |
#### Response (200 OK)
|
| 225 |
```json
|
| 226 |
{
|
| 227 |
+
"answer": "Authentication is implemented using JWT tokens inside authMiddleware.js...",
|
| 228 |
"sources": [
|
| 229 |
{
|
| 230 |
+
"file": "backend/src/middleware/authMiddleware.js",
|
| 231 |
+
"content": "const token = req.headers.authorization.split(' ')[1]; ...",
|
| 232 |
+
"score": 0.92
|
| 233 |
}
|
| 234 |
]
|
| 235 |
}
|
|
|
|
| 240 |
### POST /api/chat (SSE Stream)
|
| 241 |
Interactive, conversational multi-turn chat over the repository codebase context, streaming back word tokens.
|
| 242 |
|
| 243 |
+
#### Request Schema (ChatRequest)
|
| 244 |
+
- `repo` (string, required): Repository identifier (owner/repo).
|
| 245 |
+
- `message` (string, required): User message.
|
| 246 |
+
- `history` (list, optional): Conversation history turns containing roles and text content.
|
| 247 |
+
|
| 248 |
```json
|
| 249 |
{
|
| 250 |
+
"repo": "Ankita15k/GitNest",
|
| 251 |
+
"message": "Explain how Socket.io notifies users.",
|
| 252 |
"history": [
|
| 253 |
+
{"role": "user", "content": "How are WebSockets set up?"},
|
| 254 |
+
{"role": "assistant", "content": "WebSockets are initialized in socket.js..."}
|
| 255 |
]
|
| 256 |
}
|
| 257 |
```
|
| 258 |
|
| 259 |
#### Request Example
|
| 260 |
```bash
|
| 261 |
+
curl -N -X POST http://127.0.0.1:8001/api/chat \
|
| 262 |
-H "Content-Type: application/json" \
|
| 263 |
-d '{
|
| 264 |
"repo": "Ankita15k/GitNest",
|
| 265 |
+
"message": "Explain how Socket.io notifies users.",
|
| 266 |
"history": []
|
| 267 |
}'
|
| 268 |
```
|
|
|
|
| 270 |
#### SSE Stream Output
|
| 271 |
```
|
| 272 |
data: {"text": "Socket"}
|
| 273 |
+
data: {"text": ".io "}
|
| 274 |
+
data: {"text": "notifications "}
|
| 275 |
+
data: {"text": "are "}
|
| 276 |
+
data: {"text": "pushed "}
|
| 277 |
+
data: {"text": "to "}
|
| 278 |
+
data: {"text": "clients "}
|
| 279 |
+
data: {"text": "using "}
|
| 280 |
+
data: {"text": "io.emit()."}
|
| 281 |
+
data: {"sources": ["backend/src/socket/notificationHandler.js"], "confidence": 92, "fallback_mode": false, "status": "done"}
|
| 282 |
```
|
| 283 |
|
| 284 |
---
|
|
|
|
| 286 |
### POST /api/issues/map
|
| 287 |
Analyze a GitHub issue and map it to relevant source code modules, generating a detailed implementation plan.
|
| 288 |
|
| 289 |
+
#### Request Schema (IssueMapRequest)
|
| 290 |
+
- `repo` (string, required): Repository identifier (owner/repo).
|
| 291 |
+
- `issue` (string, optional): Combined issue text details.
|
| 292 |
+
- `title` (string, optional): GitHub issue title.
|
| 293 |
+
- `description` (string, optional): GitHub issue description body.
|
| 294 |
+
|
| 295 |
```json
|
| 296 |
{
|
| 297 |
+
"repo": "Ankita15k/GitNest",
|
| 298 |
+
"title": "Fix token validation timeout",
|
| 299 |
+
"description": "User tokens expire instantly when hitting the API from mobile clients."
|
|
|
|
| 300 |
}
|
| 301 |
```
|
| 302 |
|
| 303 |
+
#### Response (200 OK - IssueMapResponse)
|
| 304 |
```json
|
| 305 |
{
|
| 306 |
+
"issue_summary": "Fix token validation timeout",
|
| 307 |
+
"issue_type": "bug",
|
| 308 |
+
"relevant_files": [
|
| 309 |
+
"backend/src/middleware/authMiddleware.js"
|
| 310 |
+
],
|
| 311 |
+
"affected_components": [
|
| 312 |
+
"Authentication",
|
| 313 |
+
"Services"
|
| 314 |
+
],
|
| 315 |
+
"implementation_plan": [
|
| 316 |
+
{
|
| 317 |
+
"step_number": 1,
|
| 318 |
+
"description": "Modify the jwt verify expiration range logic inside authMiddleware.js to tolerate mobile clock drift.",
|
| 319 |
+
"files_to_modify": ["backend/src/middleware/authMiddleware.js"]
|
| 320 |
+
}
|
| 321 |
+
],
|
| 322 |
+
"complexity": "low",
|
| 323 |
"confidence": 88,
|
| 324 |
+
"verified": true,
|
| 325 |
+
"sources": [
|
| 326 |
+
"backend/src/middleware/authMiddleware.js"
|
| 327 |
+
]
|
| 328 |
}
|
| 329 |
```
|
| 330 |
|
|
|
|
| 335 |
### POST /api/architecture/build
|
| 336 |
Trigger Tree-sitter AST parsing over local cloned repository files and construct the dependency NetworkX graph.
|
| 337 |
|
| 338 |
+
#### Request Schema (ArchitectureBuildRequest)
|
| 339 |
+
- `repo` (string, required): Repository identifier (owner/repo).
|
| 340 |
+
|
| 341 |
```json
|
| 342 |
{
|
| 343 |
"repo": "owner/repository"
|
|
|
|
| 352 |
"files_parsed": 328,
|
| 353 |
"dependencies_found": 1440,
|
| 354 |
"entry_points": [
|
| 355 |
+
"backend/src/index.js"
|
|
|
|
| 356 |
]
|
| 357 |
}
|
| 358 |
```
|
|
|
|
| 360 |
---
|
| 361 |
|
| 362 |
### GET /api/architecture/{owner}/{repo_name}
|
| 363 |
+
Fetch the generated architecture summary, reading order, and relationship schemas.
|
| 364 |
|
| 365 |
+
#### Response (200 OK - ArchitectureSummary)
|
| 366 |
```json
|
| 367 |
{
|
| 368 |
+
"summary": "GitNest is a full-stack Git dashboard using Express and local git hooks...",
|
| 369 |
+
"reading_order": [
|
| 370 |
"backend/src/index.js",
|
| 371 |
+
"backend/src/app.js"
|
|
|
|
|
|
|
|
|
|
| 372 |
],
|
| 373 |
+
"relationships": [
|
| 374 |
+
{
|
| 375 |
+
"source": "backend/src/index.js",
|
| 376 |
+
"target": "backend/src/app.js",
|
| 377 |
+
"relationship_type": "imports",
|
| 378 |
+
"description": "Instantiates app config schemas."
|
| 379 |
+
}
|
| 380 |
+
]
|
| 381 |
}
|
| 382 |
```
|
| 383 |
|
|
|
|
| 395 |
"nodes": [
|
| 396 |
{
|
| 397 |
"id": "backend/src/index.js",
|
| 398 |
+
"data": { "label": "index.js" },
|
| 399 |
+
"position": { "x": 120, "y": 80 },
|
| 400 |
+
"style": { "background": "#1e293b", "color": "#f8fafc" }
|
|
|
|
|
|
|
| 401 |
}
|
| 402 |
],
|
| 403 |
"edges": [
|
| 404 |
{
|
| 405 |
+
"id": "e_backend/src/index.js_backend/src/app.js",
|
| 406 |
"source": "backend/src/index.js",
|
| 407 |
"target": "backend/src/app.js",
|
| 408 |
+
"animated": true
|
| 409 |
}
|
| 410 |
]
|
| 411 |
}
|
|
|
|
| 416 |
### POST /api/reading-order
|
| 417 |
Generate an optimal, ranked file list for onboarding new developers based on graph centrality metrics.
|
| 418 |
|
| 419 |
+
#### Request Schema (ReadingOrderRequest)
|
| 420 |
+
- `repo` (string, required): Repository identifier (owner/repo).
|
| 421 |
+
|
| 422 |
```json
|
| 423 |
{
|
| 424 |
"repo": "owner/repository"
|
| 425 |
}
|
| 426 |
```
|
| 427 |
|
| 428 |
+
#### Response (200 OK - ReadingOrder)
|
| 429 |
```json
|
| 430 |
{
|
| 431 |
"repo": "Ankita15k/GitNest",
|
| 432 |
+
"ordered_files": [
|
| 433 |
{
|
| 434 |
"rank": 1,
|
| 435 |
+
"file_path": "backend/src/index.js",
|
| 436 |
"reason": "Primary application entry point initializing server configurations.",
|
| 437 |
+
"tier": "entry_point",
|
| 438 |
+
"score": 150.0
|
| 439 |
}
|
| 440 |
+
],
|
| 441 |
+
"reasoning": [
|
| 442 |
+
"Timeline starts with detected application entry points.",
|
| 443 |
+
"Order prioritizes modules with high degree centrality scores."
|
| 444 |
+
],
|
| 445 |
+
"estimated_reading_time": 45,
|
| 446 |
+
"total_files_ranked": 328
|
| 447 |
}
|
| 448 |
```
|
| 449 |
|
|
|
|
| 452 |
### POST /api/impact-analysis
|
| 453 |
Predict downstream affected modules and components resulting from a proposed change.
|
| 454 |
|
| 455 |
+
#### Request Schema (ImpactAnalysisRequest)
|
| 456 |
+
- `repo` (string, required): Repository identifier (owner/repo).
|
| 457 |
+
- `issue` (string, required): Change request or GitHub issue text.
|
| 458 |
+
|
| 459 |
```json
|
| 460 |
{
|
| 461 |
+
"repo": "Ankita15k/GitNest",
|
| 462 |
+
"issue": "Add API key authentication"
|
| 463 |
}
|
| 464 |
```
|
| 465 |
|
| 466 |
+
#### Response (200 OK - ImpactAnalysis)
|
| 467 |
```json
|
| 468 |
{
|
| 469 |
"repo": "Ankita15k/GitNest",
|
| 470 |
+
"issue_text": "Add API key authentication",
|
| 471 |
+
"directly_affected_files": [
|
| 472 |
+
"backend/src/middleware/authMiddleware.js"
|
| 473 |
+
],
|
| 474 |
+
"indirectly_affected_files": [
|
| 475 |
+
"backend/src/routes/auth.routes.js",
|
| 476 |
+
"backend/src/app.js"
|
| 477 |
+
],
|
| 478 |
+
"affected_components": [
|
| 479 |
+
"Authentication",
|
| 480 |
+
"API Layer"
|
| 481 |
+
],
|
| 482 |
+
"risk_level": "medium",
|
| 483 |
+
"estimated_file_count": 3,
|
| 484 |
+
"dependency_paths": [
|
| 485 |
+
{
|
| 486 |
+
"path": [
|
| 487 |
+
"backend/src/middleware/authMiddleware.js",
|
| 488 |
+
"backend/src/routes/auth.routes.js",
|
| 489 |
+
"backend/src/app.js"
|
| 490 |
+
]
|
| 491 |
+
}
|
| 492 |
],
|
| 493 |
+
"confidence": 85
|
|
|
|
|
|
|
| 494 |
}
|
| 495 |
```
|
docs/DEVELOPMENT_SETUP.md
CHANGED
|
@@ -1,32 +1,32 @@
|
|
| 1 |
-
# Development Setup & Contributor Guide
|
| 2 |
|
| 3 |
-
Welcome to the **Repo Intelligence Agent**
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
## 📋 Prerequisites
|
| 8 |
|
| 9 |
-
Before setting up the project
|
| 10 |
|
| 11 |
-
- **Python 3.10
|
| 12 |
-
- **Node.js
|
| 13 |
-
- **Git**
|
| 14 |
-
-
|
| 15 |
-
- At least
|
| 16 |
|
| 17 |
---
|
| 18 |
|
| 19 |
-
## 🔧
|
| 20 |
|
| 21 |
-
### 1. Clone the
|
| 22 |
-
Clone the repository
|
| 23 |
```bash
|
| 24 |
git clone https://github.com/your-username/Repo-Intelligence-Agent.git
|
| 25 |
cd Repo-Intelligence-Agent
|
| 26 |
```
|
| 27 |
|
| 28 |
-
### 2.
|
| 29 |
-
|
| 30 |
|
| 31 |
**On Windows (PowerShell):**
|
| 32 |
```powershell
|
|
@@ -40,85 +40,87 @@ python -m venv .venv
|
|
| 40 |
source .venv/bin/activate
|
| 41 |
```
|
| 42 |
|
| 43 |
-
|
|
|
|
| 44 |
```bash
|
| 45 |
pip install -r requirements.txt
|
| 46 |
```
|
| 47 |
|
| 48 |
-
###
|
| 49 |
-
|
|
|
|
| 50 |
```bash
|
| 51 |
cp .env.example .env
|
| 52 |
```
|
| 53 |
|
| 54 |
-
Open `.env` and
|
| 55 |
```env
|
| 56 |
DEEPSEEK_API_KEY=nvapi-your-nvidia-nim-api-key-here
|
| 57 |
-
GITHUB_TOKEN=ghp_your-github-personal-access-token-here
|
| 58 |
CHROMA_DB_PATH=data/chroma_db
|
| 59 |
LLM_PROVIDER=deepseek
|
| 60 |
DEEPSEEK_MODEL=deepseek-ai/deepseek-v4-flash
|
|
|
|
| 61 |
```
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
### 4. Start the Backend API Server
|
| 67 |
-
Launch the FastAPI backend server using Uvicorn with hot-reload enabled:
|
| 68 |
```bash
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
```
|
| 71 |
-
The
|
|
|
|
| 72 |
|
| 73 |
---
|
| 74 |
|
| 75 |
-
## 🎨
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
npm install
|
| 86 |
-
```
|
| 87 |
-
3. Start the Astro local development server:
|
| 88 |
-
```bash
|
| 89 |
-
npm run dev
|
| 90 |
-
```
|
| 91 |
-
4. Access the frontend dashboard at [http://localhost:4321](http://localhost:4321).
|
| 92 |
|
| 93 |
---
|
| 94 |
|
| 95 |
## 🧪 Testing Guidelines
|
| 96 |
|
| 97 |
-
We
|
| 98 |
|
| 99 |
-
### Running
|
| 100 |
-
|
| 101 |
```bash
|
| 102 |
pytest tests/ -v
|
| 103 |
```
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
- Name files with the prefix `test_` (e.g., `test_my_feature.py`).
|
| 108 |
-
- Implement descriptive assertions and leverage mock adapters for network calls to avoid hitting remote API quotas during unit runs.
|
| 109 |
|
| 110 |
---
|
| 111 |
|
| 112 |
-
##
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
```bash
|
| 122 |
-
git push origin feature/amazing-new-feature
|
| 123 |
-
```
|
| 124 |
-
5. Open a **Pull Request** detailing your changes.
|
|
|
|
| 1 |
+
# 🛠️ Development Setup & Contributor Guide
|
| 2 |
|
| 3 |
+
Welcome to the **Repo Intelligence Agent** contributor guide! This document outlines the steps required to configure your local development environment, start backend and frontend services, run the test suites, and adhere to our development workflows.
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
## 📋 Prerequisites
|
| 8 |
|
| 9 |
+
Before setting up the project, ensure you have the following installed:
|
| 10 |
|
| 11 |
+
- **Python:** Version 3.10, 3.11, or 3.12. (Active unit tests are validated against Python 3.12).
|
| 12 |
+
- **Node.js:** Version 18 or newer (LTS recommended).
|
| 13 |
+
- **Git:** Command-line utility for source control operations.
|
| 14 |
+
- **NVIDIA NIM API Key:** Required for DeepSeek V4 Flash inference. You can acquire a free-tier key at [build.nvidia.com](https://build.nvidia.com).
|
| 15 |
+
- **Disk Space:** At least 2 GB of free disk space is required to store local Hugging Face model cache structures (specifically for `BAAI/bge-small-en-v1.5` embeddings).
|
| 16 |
|
| 17 |
---
|
| 18 |
|
| 19 |
+
## 🔧 Backend Ingestion & API Setup
|
| 20 |
|
| 21 |
+
### 1. Clone the Project
|
| 22 |
+
Clone the repository and enter the project directory:
|
| 23 |
```bash
|
| 24 |
git clone https://github.com/your-username/Repo-Intelligence-Agent.git
|
| 25 |
cd Repo-Intelligence-Agent
|
| 26 |
```
|
| 27 |
|
| 28 |
+
### 2. Configure Python Virtual Environment
|
| 29 |
+
Isolate Python dependencies to prevent version collisions.
|
| 30 |
|
| 31 |
**On Windows (PowerShell):**
|
| 32 |
```powershell
|
|
|
|
| 40 |
source .venv/bin/activate
|
| 41 |
```
|
| 42 |
|
| 43 |
+
### 3. Install Dependencies
|
| 44 |
+
Install all backend packages:
|
| 45 |
```bash
|
| 46 |
pip install -r requirements.txt
|
| 47 |
```
|
| 48 |
|
| 49 |
+
### 4. Configure Environmental Variables
|
| 50 |
+
Copy the environmental template into an active `.env` configuration:
|
| 51 |
+
|
| 52 |
```bash
|
| 53 |
cp .env.example .env
|
| 54 |
```
|
| 55 |
|
| 56 |
+
Open the `.env` file and insert your API key and settings:
|
| 57 |
```env
|
| 58 |
DEEPSEEK_API_KEY=nvapi-your-nvidia-nim-api-key-here
|
| 59 |
+
GITHUB_TOKEN=ghp_your-github-personal-access-token-here # Optional: raises clone rate limits
|
| 60 |
CHROMA_DB_PATH=data/chroma_db
|
| 61 |
LLM_PROVIDER=deepseek
|
| 62 |
DEEPSEEK_MODEL=deepseek-ai/deepseek-v4-flash
|
| 63 |
+
DEEPSEEK_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 64 |
```
|
| 65 |
|
| 66 |
+
### 5. Launch the Backend Server
|
| 67 |
+
Start the Uvicorn development server. The project defaults to port **8001** to prevent conflicts with standard developer systems:
|
|
|
|
|
|
|
|
|
|
| 68 |
```bash
|
| 69 |
+
# Recommended: Starts uvicorn with watch filters enabled in backend/main.py
|
| 70 |
+
python backend/main.py
|
| 71 |
+
|
| 72 |
+
# Alternative: direct uvicorn execution specifying port 8001
|
| 73 |
+
python -m uvicorn backend.api:app --host 127.0.0.1 --port 8001 --reload
|
| 74 |
```
|
| 75 |
+
- The interactive OpenAPI documentation will be served at [http://127.0.0.1:8001/docs](http://127.0.0.1:8001/docs).
|
| 76 |
+
- The raw JSON schema is available at [http://127.0.0.1:8001/openapi.json](http://127.0.0.1:8001/openapi.json).
|
| 77 |
|
| 78 |
---
|
| 79 |
|
| 80 |
+
## 🎨 Astro Dashboard Setup
|
| 81 |
+
|
| 82 |
+
The user interface is an Astro 4 static-site framework embedding interactive React client components.
|
| 83 |
+
|
| 84 |
+
### 1. Navigate and Install Packages
|
| 85 |
+
Navigate to the frontend directory and install dependencies:
|
| 86 |
+
```bash
|
| 87 |
+
cd frontend
|
| 88 |
+
npm install
|
| 89 |
+
```
|
| 90 |
|
| 91 |
+
### 2. Configure Frontend Environment
|
| 92 |
+
Ensure the frontend matches backend routing by checking `frontend/.env` (it defaults to `PUBLIC_API_URL=http://127.0.0.1:8001`).
|
| 93 |
|
| 94 |
+
### 3. Run Astro Development Server
|
| 95 |
+
Start the development server:
|
| 96 |
+
```bash
|
| 97 |
+
npm run dev
|
| 98 |
+
```
|
| 99 |
+
- Open [http://localhost:4321](http://localhost:4321) in your browser to view the interface.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
---
|
| 102 |
|
| 103 |
## 🧪 Testing Guidelines
|
| 104 |
|
| 105 |
+
We use `pytest` for backend unit testing. All tests are located inside the `tests/` directory.
|
| 106 |
|
| 107 |
+
### Running the Test Suite
|
| 108 |
+
Activate your virtual environment and run the test suite:
|
| 109 |
```bash
|
| 110 |
pytest tests/ -v
|
| 111 |
```
|
| 112 |
|
| 113 |
+
> [!CAUTION]
|
| 114 |
+
> Avoid running raw `pytest` in the root folder without a target path. Doing so causes pytest to traverse `data/cloned_repos/` (which contains cloned codebases like FastAPI), resulting in import errors and test collection failures. Always run `pytest tests/`.
|
|
|
|
|
|
|
| 115 |
|
| 116 |
---
|
| 117 |
|
| 118 |
+
## 💅 Styling & Formatting Standards
|
| 119 |
+
|
| 120 |
+
- **Backend Linting:** We use `Ruff` for linting and code formatting. Run it from your virtual environment before submitting changes:
|
| 121 |
+
```bash
|
| 122 |
+
ruff check .
|
| 123 |
+
ruff format .
|
| 124 |
+
```
|
| 125 |
+
- **Frontend CSS:** The UI uses Vanilla CSS structured inside Astro templates. Tailwind CSS configurations (`tailwind.config.mjs`) are loaded but custom responsive rules are written manually to maintain control over visual transitions.
|
| 126 |
+
- **Documentation:** Maintain markdown files with clean lists, relative links, and valid Mermaid diagram syntax.
|
|
|
|
|
|
|
|
|
|
|
|
docs/MVP_STATUS.md
CHANGED
|
@@ -4,62 +4,65 @@ This document tracks the feature coverage, validation results, known limitations
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
-
## 📊 MVP Completeness:
|
| 8 |
|
| 9 |
-
The core codebase analysis, semantic retrieval, SSE streaming, and issue mapping pipelines are fully functional and tested. The remaining
|
| 10 |
|
| 11 |
---
|
| 12 |
|
| 13 |
## ✅ Completed Features
|
| 14 |
|
| 15 |
-
The following
|
| 16 |
|
| 17 |
-
1. **Repository
|
| 18 |
-
-
|
| 19 |
2. **Semantic Retrieval (`/api/retrieve`)**
|
| 20 |
-
-
|
| 21 |
-
3. **
|
| 22 |
-
-
|
| 23 |
4. **Issue Mapper (`/api/issues/map`)**
|
| 24 |
-
-
|
| 25 |
-
5. **Architecture Builder (`/api/architecture/
|
| 26 |
-
-
|
| 27 |
-
6. **
|
| 28 |
-
-
|
| 29 |
-
7. **
|
| 30 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
---
|
| 33 |
|
| 34 |
## 🔧 Known Limitations
|
| 35 |
|
| 36 |
-
- **
|
| 37 |
-
- **
|
| 38 |
-
- **
|
| 39 |
-
- **
|
| 40 |
|
| 41 |
---
|
| 42 |
|
| 43 |
-
## ⚠️ Technical Debt & Stubs
|
| 44 |
|
| 45 |
-
The following
|
| 46 |
|
| 47 |
-
- **
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
- **
|
| 51 |
-
- `EvaluationAgent` is imported but not fully wired as a gating mechanism (it returns scores but does not block low-confidence responses).
|
| 52 |
-
- **Embedding Prefix Optimization:**
|
| 53 |
-
- The local BGE query prefix (`"Represent this sentence for searching relevant passages:"`) is applied to both search queries and chunk documents. While it functions, document embedding should omit the prefix to optimize semantic search recall.
|
| 54 |
|
| 55 |
---
|
| 56 |
|
| 57 |
## 🗺️ Roadmap
|
| 58 |
|
| 59 |
### Phase 1: Stability & Security (Near-term)
|
| 60 |
-
- [ ]
|
| 61 |
-
- [ ]
|
| 62 |
-
- [ ] Optimize BGE document embedding by omitting
|
| 63 |
|
| 64 |
### Phase 2: Hybrid & Multi-Repo (Medium-term)
|
| 65 |
- [ ] Support hybrid search (BM25 keyword matches + vector search).
|
|
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
+
## 📊 MVP Completeness: 98%
|
| 8 |
|
| 9 |
+
The core codebase analysis, semantic retrieval, SSE streaming, multi-language AST graphing, reading order centrality, BFS impact analysis, unified workspace navigation, and issue mapping pipelines are fully functional and tested. The remaining 2% corresponds to implementing the skeletal stubs.
|
| 10 |
|
| 11 |
---
|
| 12 |
|
| 13 |
## ✅ Completed Features
|
| 14 |
|
| 15 |
+
The following features and pipelines are implemented and verified:
|
| 16 |
|
| 17 |
+
1. **Repository Ingestion & Analysis (`/api/analyze`)**
|
| 18 |
+
- Direct Git cloning, language detection, codebase chunking (1500 chars / 200 overlap), local BGE embedding, ChromaDB vector indexing, NetworkX graph assembly, DeepSeek architecture summary, and filesystem persistence to `data/analysis_store.json`.
|
| 19 |
2. **Semantic Retrieval (`/api/retrieve`)**
|
| 20 |
+
- Dense vector matching using local BGE embeddings and ChromaDB metadata filters.
|
| 21 |
+
3. **Conversational Chat (`/api/chat`)**
|
| 22 |
+
- Real-time token streaming (SSE) with chat history, architecture context prompt injection, EvaluationAgent validation, and automated local fallback mode.
|
| 23 |
4. **Issue Mapper (`/api/issues/map`)**
|
| 24 |
+
- Maps issue descriptions to code targets and generates plans under a budget of exactly two LLM calls, utilizing local caching via `f"{repo_name}:v2:{issue_hash}"` and keyword-based fallback steps.
|
| 25 |
+
5. **Architecture Builder & Visualizer (`/api/architecture/.../graph`)**
|
| 26 |
+
- Tree-sitter structural extraction (imports, exports, classes, methods, functions) combined with NetworkX DiGraphs to export React Flow visual data.
|
| 27 |
+
6. **Reading Path Timeline (`/api/reading-order`)**
|
| 28 |
+
- Centrality-based ranking (entry boost, degree centrality, core boosts, peripheral penalties) combined with topological sorts.
|
| 29 |
+
7. **Impact Analysis (`/api/impact-analysis`)**
|
| 30 |
+
- Traverses forward and reverse import paths (BFS up to depth 4) to predict risk levels.
|
| 31 |
+
8. **Unified Workspace Navigation**
|
| 32 |
+
- React state caches fetched analysis metadata, allowing instant tab switching on the frontend dashboard without re-analysis.
|
| 33 |
+
9. **Session Persistence Store**
|
| 34 |
+
- Syncs active repository parameters via browser `localStorage` and hydrates backend analysis logs from disk upon startup.
|
| 35 |
+
10. **Chat Fallback Mode**
|
| 36 |
+
- Intercepts NIM rate-limit (HTTP 429) or provider failures to generate a retrieval-grounded fallback response directly to the user.
|
| 37 |
|
| 38 |
---
|
| 39 |
|
| 40 |
## 🔧 Known Limitations
|
| 41 |
|
| 42 |
+
- **Local CPU Embedding Bottleneck:** Embedding generation runs locally on CPU using SentenceTransformers. Indexing a large repository can take 2–3 minutes per 1500 chunks.
|
| 43 |
+
- **NVIDIA NIM Free-Tier Quota:** Free developer keys are capped at ~3 requests/minute. The system handles this with its automated fallback mode.
|
| 44 |
+
- **No API Authentication:** Backend API routes currently do not verify user identity.
|
| 45 |
+
- **Cache Invalidation:** Modifying local files does not auto-update the vector space or import graph. Re-indexing is required.
|
| 46 |
|
| 47 |
---
|
| 48 |
|
| 49 |
+
## ⚠️ Technical Debt & Skeletal Stubs
|
| 50 |
|
| 51 |
+
The following components are architectural design stubs that raise `NotImplementedError`. Their functions are currently inlined within API routes or handled via JSON flat files:
|
| 52 |
|
| 53 |
+
- **`SQLiteStore` (`memory/sqlite_store.py`):** Stub for transactional database logs.
|
| 54 |
+
- **`MCPService` (`services/mcp_service.py`):** Stub for Model Context Protocol.
|
| 55 |
+
- **`RepositoryAnalyzer` (`agents/analyzer.py`):** Stub for agent-based ingestion walks.
|
| 56 |
+
- **`ArchitectureExplainer` (`agents/explainer.py`):** Stub for agent-based centrality explanation.
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
---
|
| 59 |
|
| 60 |
## 🗺️ Roadmap
|
| 61 |
|
| 62 |
### Phase 1: Stability & Security (Near-term)
|
| 63 |
+
- [ ] Connect `SQLiteStore` to record query history and workspace settings.
|
| 64 |
+
- [ ] Implement JWT token authentication middleware.
|
| 65 |
+
- [ ] Optimize BGE document embedding by omitting query search prefixes on chunking.
|
| 66 |
|
| 67 |
### Phase 2: Hybrid & Multi-Repo (Medium-term)
|
| 68 |
- [ ] Support hybrid search (BM25 keyword matches + vector search).
|
docs/TROUBLESHOOTING.md
CHANGED
|
@@ -90,26 +90,28 @@ To resolve this permanently, upgrade to a paid NVIDIA NIM tier or configure key
|
|
| 90 |
The UI displays a connection error during analysis, and the terminal log halts at `START: chroma indexing`.
|
| 91 |
|
| 92 |
### Root Cause
|
| 93 |
-
1. **Parallel Port Mismatch:** You might be running multiple backend processes. If `test_analyze.py` is targeting port `8001` while the active server is listening on port `8000`, commands will hit a mismatched session state.
|
| 94 |
2. **Reverse Proxy Buffering:** If deploying behind Nginx or Cloudflare, the proxy might buffer the event stream, causing a timeout or socket termination.
|
| 95 |
|
| 96 |
### Solution
|
| 97 |
- Stop all Python/Uvicorn processes:
|
| 98 |
- On Windows:
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
- On macOS
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
- Re-run Uvicorn on port
|
| 107 |
```bash
|
| 108 |
-
python
|
| 109 |
```
|
| 110 |
-
-
|
| 111 |
```nginx
|
| 112 |
proxy_set_header Connection '';
|
|
|
|
|
|
|
| 113 |
proxy_buffering off;
|
| 114 |
-
|
| 115 |
```
|
|
|
|
| 90 |
The UI displays a connection error during analysis, and the terminal log halts at `START: chroma indexing`.
|
| 91 |
|
| 92 |
### Root Cause
|
| 93 |
+
1. **Parallel Port Mismatch:** You might be running multiple backend processes. If `test_analyze.py` is targeting port `8001` while the active server is listening on port `8000`, or vice-versa, commands will hit a mismatched session state. The default server port configured in `backend/main.py` is **8001**.
|
| 94 |
2. **Reverse Proxy Buffering:** If deploying behind Nginx or Cloudflare, the proxy might buffer the event stream, causing a timeout or socket termination.
|
| 95 |
|
| 96 |
### Solution
|
| 97 |
- Stop all Python/Uvicorn processes:
|
| 98 |
- On Windows:
|
| 99 |
+
```powershell
|
| 100 |
+
Get-Process -Name python, uvicorn -ErrorAction SilentlyContinue | Stop-Process -Force
|
| 101 |
+
```
|
| 102 |
+
- On macOS/Linux:
|
| 103 |
+
```bash
|
| 104 |
+
killall python uvicorn
|
| 105 |
+
```
|
| 106 |
+
- Re-run Uvicorn on port **8001**:
|
| 107 |
```bash
|
| 108 |
+
python backend/main.py
|
| 109 |
```
|
| 110 |
+
- Ensure reverse proxy settings disable buffering:
|
| 111 |
```nginx
|
| 112 |
proxy_set_header Connection '';
|
| 113 |
+
proxy_http_version 1.1;
|
| 114 |
+
chunked_transfer_encoding off;
|
| 115 |
proxy_buffering off;
|
| 116 |
+
proxy_cache off;
|
| 117 |
```
|
docs/VALIDATION_REPORT.md
CHANGED
|
@@ -1,30 +1,30 @@
|
|
| 1 |
-
# MVP Validation &
|
| 2 |
|
| 3 |
-
This
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
-
## 📈
|
| 8 |
|
| 9 |
-
|
| 10 |
|
| 11 |
-
| Metric | Measured Value |
|
| 12 |
-
|---|---|---|
|
| 13 |
-
| **Total Source Files walked** | 328 files |
|
| 14 |
-
| **Total Chunks generated** | 1,549 chunks | CodeChunker (1500 chars / 200 overlap) |
|
| 15 |
-
| **
|
| 16 |
-
| **ChromaDB Write
|
| 17 |
-
| **Tree-sitter AST parse** | ~1.4 seconds | Python, JavaScript, and TypeScript
|
| 18 |
-
| **Graph
|
| 19 |
-
| **Framework Entry Points** | 17 entry points |
|
| 20 |
|
| 21 |
---
|
| 22 |
|
| 23 |
-
## 🎯 Issue Mapper
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
-
|
|
| 28 |
| :--- | :--- | :--- | :--- |
|
| 29 |
| **"Login fails when password contains @"** | `auth.test.js`, `authStore.js`, `security.test.js` | **None** | `Authentication`, `Frontend`, `Services` |
|
| 30 |
| **"Pagination broken on repository page"** | `repository.controller.js`, `pullRequest.controller.js`, `App.jsx` | **None** | `API Layer`, `Frontend`, `Services` |
|
|
@@ -34,16 +34,44 @@ We tested the **Issue Mapper** using real repository issues. The validation focu
|
|
| 34 |
|
| 35 |
## 🔍 API Performance Benchmarks
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
| Endpoint | Method | Average Latency | Bottleneck |
|
| 40 |
-
|---|---|---|---|
|
| 41 |
-
| `/health` | GET | `< 3ms` |
|
| 42 |
-
| `/api/repos/examples` | GET | `< 2ms` |
|
| 43 |
-
| `/api/
|
| 44 |
-
| `/api/
|
| 45 |
-
| `/api/
|
| 46 |
-
| `/api/
|
| 47 |
-
| `/api/architecture/
|
| 48 |
-
| `/api/
|
| 49 |
-
| `/api/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📊 MVP Validation & Telemetry Report
|
| 2 |
|
| 3 |
+
This report presents performance metrics, validation benchmarks, and pipeline latency figures collected during testing of the **Repo Intelligence Agent** MVP.
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
+
## 📈 Codebase Ingestion & Parsing Telemetry
|
| 8 |
|
| 9 |
+
Ingestion throughput, AST parsing, and vector generation were evaluated against the **Ankita15k/GitNest** repository (a full-stack collaborative Git hosting system).
|
| 10 |
|
| 11 |
+
| Metric | Measured Value | Operational Context |
|
| 12 |
+
| :--- | :--- | :--- |
|
| 13 |
+
| **Total Source Files walked** | 328 files | Excludes `node_modules`, `.venv`, `.git`, configurations, and binaries |
|
| 14 |
+
| **Total Chunks generated** | 1,549 chunks | Produced by `CodeChunker` (1500 chars / 200 overlap) |
|
| 15 |
+
| **Embedding Generation** | 1,549 vectors | 384-dimensional dense vectors using local `bge-small-en-v1.5` |
|
| 16 |
+
| **ChromaDB Write Latency** | ~28 seconds | Persistence write rate to local SSD storage |
|
| 17 |
+
| **Tree-sitter AST parse** | ~1.4 seconds | Structural analysis of Python, JavaScript, and TypeScript files |
|
| 18 |
+
| **Dependency Graph size** | 1,440 edges | Directed dependency linkages calculated by `GraphService` |
|
| 19 |
+
| **Framework Entry Points** | 17 entry points | Resolved using structural heuristics (excludes test and doc dirs) |
|
| 20 |
|
| 21 |
---
|
| 22 |
|
| 23 |
+
## 🎯 Issue Mapper Target Grounding
|
| 24 |
|
| 25 |
+
The **Issue Mapper** was tested against realistic repository issue logs to verify target file identification accuracy and ensure 0% path hallucinations.
|
| 26 |
|
| 27 |
+
| Test Case Issue | Grounded File Targets | Hallucination Detected | Correct Components Identified |
|
| 28 |
| :--- | :--- | :--- | :--- |
|
| 29 |
| **"Login fails when password contains @"** | `auth.test.js`, `authStore.js`, `security.test.js` | **None** | `Authentication`, `Frontend`, `Services` |
|
| 30 |
| **"Pagination broken on repository page"** | `repository.controller.js`, `pullRequest.controller.js`, `App.jsx` | **None** | `API Layer`, `Frontend`, `Services` |
|
|
|
|
| 34 |
|
| 35 |
## 🔍 API Performance Benchmarks
|
| 36 |
|
| 37 |
+
Response latencies were compiled across all active FastAPI endpoints binding to port **8001** (averages calculated over 50 mock sessions):
|
| 38 |
+
|
| 39 |
+
| Endpoint | Method | Average Latency | Primary Bottleneck |
|
| 40 |
+
| :--- | :--- | :--- | :--- |
|
| 41 |
+
| `/health` | GET | `< 3ms` | Minimal routing overhead |
|
| 42 |
+
| `/api/repos/examples` | GET | `< 2ms` | Minimal routing overhead |
|
| 43 |
+
| `/api/repos/recent` | GET | `< 2ms` | Disk read (deserialization of `analysis_store.json`) |
|
| 44 |
+
| `/api/index` | POST | `~ 2.5 minutes` | CPU-bound embedding generation (SentenceTransformers) |
|
| 45 |
+
| `/api/retrieve` | POST | `~ 150ms` | Network response time from NVIDIA NIM API |
|
| 46 |
+
| `/api/issues/map` | POST | `~ 4.8 seconds` | Sequential LLM completion calls (parse+rank, then plan) |
|
| 47 |
+
| `/api/architecture/build` | POST | `~ 1.8 seconds` | CPU-bound AST file parsing (Tree-sitter) |
|
| 48 |
+
| `/api/analysis/{owner}/{repo_name}` | GET | `< 5ms` | In-memory cache retrieval |
|
| 49 |
+
| `/api/reading-order` | POST | `~ 80ms` | CPU NetworkX topological sorting & centrality |
|
| 50 |
+
| `/api/impact-analysis` | POST | `~ 95ms` | BFS graph traversal and keyword component matching |
|
| 51 |
+
| `/api/architecture/{owner}/{repo_name}/graph` | GET | `~ 50ms` | Formatting graph nodes and edges to React Flow JSON |
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## ✅ Core Components Validation Checklist
|
| 56 |
+
|
| 57 |
+
| Feature Component | Implementation Status | Telemetry & Verification Method |
|
| 58 |
+
| :--- | :--- | :--- |
|
| 59 |
+
| **Repository Analysis** | ✅ **Complete** | SSE-stream progress index logging from `/api/analyze` |
|
| 60 |
+
| **Repository Chat** | ✅ **Complete** | Token-by-token SSE streaming evaluated by `EvaluationAgent` |
|
| 61 |
+
| **Issue Mapper** | ✅ **Complete** | Merged two-call model with caching via `f"{repo_name}:v2:{issue_hash}"` |
|
| 62 |
+
| **Architecture Graph** | ✅ **Complete** | Tree-sitter import mappings converted to React Flow visual nodes |
|
| 63 |
+
| **Reading Order** | ✅ **Complete** | Topological centrality timelines computed by `ReadingOrderService` |
|
| 64 |
+
| **Impact Analysis** | ✅ **Complete** | BFS graph sweep traversals (depth limit = 4) + Component Maps |
|
| 65 |
+
| **Repository Workspace** | ✅ **Complete** | Multi-tab UI dashboard caching states to prevent redundant re-analyzes |
|
| 66 |
+
| **Session Persistence** | ✅ **Complete** | LocalStorage state backup of active context + Startup JSON hydration |
|
| 67 |
+
| **Fallback Mode** | ✅ **Complete** | Local keyword + Chroma retrieved text parser on NIM rate limits |
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## 🧪 Unit Test Coverage & Health
|
| 72 |
+
|
| 73 |
+
The backend test suite is composed of **91 unit and integration tests** verifying structural extraction, vector retrieval, caching logic, and API route mapping.
|
| 74 |
+
|
| 75 |
+
- **Total Collected Tests:** 91 items
|
| 76 |
+
- **Overall Code Coverage:** ~85% across all core classes (`TreeSitterService`, `GraphService`, `EntryPointService`, `ChromaStore`, `IssueMapper`).
|
| 77 |
+
- **Test Integrity:** Mock adapters isolate remote API network boundaries during testing, ensuring local test execution does not consume NVIDIA NIM tokens.
|
frontend/src/components/Navbar.astro
CHANGED
|
@@ -20,7 +20,7 @@ const isAnalysisActive = pathname.startsWith('/analysis');
|
|
| 20 |
</a>
|
| 21 |
|
| 22 |
<!-- Desktop Navigation -->
|
| 23 |
-
<nav class="hidden md:flex items-center gap-5 text-sm font-medium">
|
| 24 |
{navLinks.map(link => {
|
| 25 |
const isActive = pathname === link.href;
|
| 26 |
return (
|
|
@@ -34,14 +34,13 @@ const isAnalysisActive = pathname.startsWith('/analysis');
|
|
| 34 |
</a>
|
| 35 |
);
|
| 36 |
})}
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
)}
|
| 45 |
</nav>
|
| 46 |
</div>
|
| 47 |
|
|
@@ -100,8 +99,27 @@ const isAnalysisActive = pathname.startsWith('/analysis');
|
|
| 100 |
}
|
| 101 |
}
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
// Check health immediately
|
| 104 |
updateStatus();
|
| 105 |
// Poll health status
|
| 106 |
setInterval(updateStatus, 8000);
|
|
|
|
|
|
|
|
|
|
| 107 |
</script>
|
|
|
|
| 20 |
</a>
|
| 21 |
|
| 22 |
<!-- Desktop Navigation -->
|
| 23 |
+
<nav class="hidden md:flex items-center gap-5 text-sm font-medium" id="desktop-nav">
|
| 24 |
{navLinks.map(link => {
|
| 25 |
const isActive = pathname === link.href;
|
| 26 |
return (
|
|
|
|
| 34 |
</a>
|
| 35 |
);
|
| 36 |
})}
|
| 37 |
+
<a
|
| 38 |
+
id="analysis-nav-link"
|
| 39 |
+
href="#"
|
| 40 |
+
class="transition-colors hover:text-text text-text-muted hidden"
|
| 41 |
+
>
|
| 42 |
+
Analysis Dashboard
|
| 43 |
+
</a>
|
|
|
|
| 44 |
</nav>
|
| 45 |
</div>
|
| 46 |
|
|
|
|
| 99 |
}
|
| 100 |
}
|
| 101 |
|
| 102 |
+
function setupNavbar() {
|
| 103 |
+
const activeRepo = localStorage.getItem('activeRepo');
|
| 104 |
+
const analysisLink = document.getElementById('analysis-nav-link');
|
| 105 |
+
if (analysisLink && activeRepo) {
|
| 106 |
+
const [owner, repo] = activeRepo.split('/');
|
| 107 |
+
analysisLink.setAttribute('href', `/analysis?owner=${owner}&repo=${repo}`);
|
| 108 |
+
analysisLink.classList.remove('hidden');
|
| 109 |
+
|
| 110 |
+
const pathname = window.location.pathname;
|
| 111 |
+
if (pathname.startsWith('/analysis')) {
|
| 112 |
+
analysisLink.classList.add('text-text', 'font-semibold', 'border-b-2', 'border-primary', 'py-0.5');
|
| 113 |
+
analysisLink.classList.remove('text-text-muted');
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
// Check health immediately
|
| 119 |
updateStatus();
|
| 120 |
// Poll health status
|
| 121 |
setInterval(updateStatus, 8000);
|
| 122 |
+
|
| 123 |
+
// Setup active analysis tab
|
| 124 |
+
setupNavbar();
|
| 125 |
</script>
|
frontend/src/components/interactive/AnalysisDashboard.tsx
CHANGED
|
@@ -2,10 +2,11 @@ import React, { useState, useEffect } from 'react';
|
|
| 2 |
import { apiUrl } from '../../lib/api';
|
| 3 |
import FileTree from './FileTree';
|
| 4 |
import IssueMapper from './IssueMapper';
|
|
|
|
| 5 |
import { ArchitectureGraph } from './ArchitectureGraph';
|
| 6 |
import { ReadingOrderTimeline } from './ReadingOrderTimeline';
|
| 7 |
import { ImpactAnalysisGraph } from './ImpactAnalysisGraph';
|
| 8 |
-
import { Layers, Box, Code2, BookOpen, Cpu, Info, CheckCircle2, Target, HelpCircle } from 'lucide-react';
|
| 9 |
|
| 10 |
interface ComponentRelationship {
|
| 11 |
source: string;
|
|
@@ -36,7 +37,7 @@ export const AnalysisDashboard: React.FC<DashboardProps> = ({ repoParam }) => {
|
|
| 36 |
const [data, setData] = useState<AnalysisData | null>(null);
|
| 37 |
const [loading, setLoading] = useState(true);
|
| 38 |
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
| 39 |
-
const [activeTab, setActiveTab] = useState<'analysis' | 'graph' | 'reading_path' | 'impact_analysis' | 'issues'>('analysis');
|
| 40 |
|
| 41 |
// Impact Analysis states
|
| 42 |
const [impactData, setImpactData] = useState<any | null>(null);
|
|
@@ -81,6 +82,9 @@ export const AnalysisDashboard: React.FC<DashboardProps> = ({ repoParam }) => {
|
|
| 81 |
setLoading(false);
|
| 82 |
return;
|
| 83 |
}
|
|
|
|
|
|
|
|
|
|
| 84 |
const apiUrlFull = apiUrl(`/api/analysis/${owner}/${name}`);
|
| 85 |
console.log('Fetching analysis from', apiUrlFull);
|
| 86 |
fetch(apiUrlFull)
|
|
@@ -248,6 +252,17 @@ export const AnalysisDashboard: React.FC<DashboardProps> = ({ repoParam }) => {
|
|
| 248 |
<Cpu className="h-4 w-4" />
|
| 249 |
<span>ISSUE INTELLIGENCE</span>
|
| 250 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
</div>
|
| 252 |
|
| 253 |
{activeTab === 'analysis' && (
|
|
@@ -425,6 +440,12 @@ export const AnalysisDashboard: React.FC<DashboardProps> = ({ repoParam }) => {
|
|
| 425 |
</div>
|
| 426 |
)}
|
| 427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
{activeTab === 'graph' && (
|
| 429 |
<ArchitectureGraph repoName={repoName} />
|
| 430 |
)}
|
|
|
|
| 2 |
import { apiUrl } from '../../lib/api';
|
| 3 |
import FileTree from './FileTree';
|
| 4 |
import IssueMapper from './IssueMapper';
|
| 5 |
+
import ChatInterface from './ChatInterface';
|
| 6 |
import { ArchitectureGraph } from './ArchitectureGraph';
|
| 7 |
import { ReadingOrderTimeline } from './ReadingOrderTimeline';
|
| 8 |
import { ImpactAnalysisGraph } from './ImpactAnalysisGraph';
|
| 9 |
+
import { Layers, Box, Code2, BookOpen, Cpu, Info, CheckCircle2, Target, HelpCircle, MessageSquareCode } from 'lucide-react';
|
| 10 |
|
| 11 |
interface ComponentRelationship {
|
| 12 |
source: string;
|
|
|
|
| 37 |
const [data, setData] = useState<AnalysisData | null>(null);
|
| 38 |
const [loading, setLoading] = useState(true);
|
| 39 |
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
| 40 |
+
const [activeTab, setActiveTab] = useState<'analysis' | 'graph' | 'reading_path' | 'impact_analysis' | 'issues' | 'chat'>('analysis');
|
| 41 |
|
| 42 |
// Impact Analysis states
|
| 43 |
const [impactData, setImpactData] = useState<any | null>(null);
|
|
|
|
| 82 |
setLoading(false);
|
| 83 |
return;
|
| 84 |
}
|
| 85 |
+
if (typeof window !== 'undefined') {
|
| 86 |
+
localStorage.setItem('activeRepo', repoName);
|
| 87 |
+
}
|
| 88 |
const apiUrlFull = apiUrl(`/api/analysis/${owner}/${name}`);
|
| 89 |
console.log('Fetching analysis from', apiUrlFull);
|
| 90 |
fetch(apiUrlFull)
|
|
|
|
| 252 |
<Cpu className="h-4 w-4" />
|
| 253 |
<span>ISSUE INTELLIGENCE</span>
|
| 254 |
</button>
|
| 255 |
+
<button
|
| 256 |
+
onClick={() => setActiveTab('chat')}
|
| 257 |
+
className={`flex items-center gap-2 px-4 py-2 text-xs font-mono tracking-wider font-semibold border-b-2 transition-all ${
|
| 258 |
+
activeTab === 'chat'
|
| 259 |
+
? 'border-primary text-text bg-primary/5'
|
| 260 |
+
: 'border-transparent text-text-muted hover:text-text hover:bg-card/20'
|
| 261 |
+
}`}
|
| 262 |
+
>
|
| 263 |
+
<MessageSquareCode className="h-4 w-4" />
|
| 264 |
+
<span>CHAT</span>
|
| 265 |
+
</button>
|
| 266 |
</div>
|
| 267 |
|
| 268 |
{activeTab === 'analysis' && (
|
|
|
|
| 440 |
</div>
|
| 441 |
)}
|
| 442 |
|
| 443 |
+
{activeTab === 'chat' && (
|
| 444 |
+
<div className="min-h-[600px] flex flex-col">
|
| 445 |
+
<ChatInterface repoName={repoName} />
|
| 446 |
+
</div>
|
| 447 |
+
)}
|
| 448 |
+
|
| 449 |
{activeTab === 'graph' && (
|
| 450 |
<ArchitectureGraph repoName={repoName} />
|
| 451 |
)}
|
frontend/src/components/interactive/ChatInterface.tsx
CHANGED
|
@@ -1,47 +1,214 @@
|
|
| 1 |
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
import { apiUrl } from '../../lib/api';
|
| 3 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
interface Message {
|
| 6 |
id: string;
|
| 7 |
sender: 'user' | 'assistant';
|
| 8 |
text: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
interface ChatInterfaceProps {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
repoName: string;
|
| 13 |
}
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
export const ChatInterface: React.FC<ChatInterfaceProps> = ({ repoName }) => {
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
const [input, setInput] = useState('');
|
| 24 |
const [isStreaming, setIsStreaming] = useState(false);
|
|
|
|
|
|
|
| 25 |
const bottomRef = useRef<HTMLDivElement>(null);
|
| 26 |
|
| 27 |
const suggestedQuestions = [
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
];
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
useEffect(() => {
|
| 34 |
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 35 |
}, [messages]);
|
| 36 |
|
|
|
|
| 37 |
const triggerStreamResponse = async (userPrompt: string) => {
|
| 38 |
setIsStreaming(true);
|
| 39 |
-
const
|
| 40 |
-
|
| 41 |
-
// Add empty
|
| 42 |
-
setMessages(prev => [
|
| 43 |
...prev,
|
| 44 |
-
{ id:
|
| 45 |
]);
|
| 46 |
|
| 47 |
try {
|
|
@@ -49,16 +216,42 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({ repoName }) => {
|
|
| 49 |
method: 'POST',
|
| 50 |
headers: { 'Content-Type': 'application/json' },
|
| 51 |
body: JSON.stringify({
|
| 52 |
-
repo:
|
| 53 |
message: userPrompt,
|
| 54 |
-
history: messages.slice(1).map(m => ({
|
| 55 |
role: m.sender === 'user' ? 'user' : 'model',
|
| 56 |
-
parts: [m.text]
|
| 57 |
-
}))
|
| 58 |
-
})
|
| 59 |
});
|
| 60 |
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
const reader = response.body.getReader();
|
| 64 |
const decoder = new TextDecoder();
|
|
@@ -67,38 +260,71 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({ repoName }) => {
|
|
| 67 |
while (!finished) {
|
| 68 |
const { value, done } = await reader.read();
|
| 69 |
finished = done;
|
| 70 |
-
if (value)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
}
|
| 93 |
}
|
| 94 |
} catch (error) {
|
| 95 |
-
console.error(error);
|
| 96 |
-
setMessages(prev =>
|
| 97 |
-
prev.map(msg =>
|
| 98 |
-
msg.id ===
|
| 99 |
-
? {
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
);
|
| 103 |
} finally {
|
| 104 |
setIsStreaming(false);
|
|
@@ -108,62 +334,120 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({ repoName }) => {
|
|
| 108 |
const handleSend = async (textToSend: string) => {
|
| 109 |
if (!textToSend.trim() || isStreaming) return;
|
| 110 |
|
| 111 |
-
|
| 112 |
-
setMessages(prev => [
|
| 113 |
...prev,
|
| 114 |
-
{ id: Math.random().toString(), sender: 'user', text: textToSend }
|
| 115 |
]);
|
| 116 |
setInput('');
|
| 117 |
|
| 118 |
await triggerStreamResponse(textToSend);
|
| 119 |
};
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
return (
|
| 122 |
-
<div className="flex flex-col h-[550px] border border-border bg-card/10 rounded-lg overflow-hidden w-full
|
| 123 |
{/* Header */}
|
| 124 |
-
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-card/40">
|
| 125 |
<div className="flex items-center gap-2">
|
| 126 |
<MessageSquareCode className="h-4 w-4 text-primary" />
|
| 127 |
-
<span className="font-mono text-sm font-semibold text-text">
|
|
|
|
|
|
|
| 128 |
</div>
|
| 129 |
-
<span className="text-[10px] font-mono border border-primary/30 text-primary px-2 py-0.5 rounded bg-primary/5">
|
| 130 |
-
{
|
| 131 |
</span>
|
| 132 |
</div>
|
| 133 |
|
| 134 |
-
{/*
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
<
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
|
|
|
| 144 |
|
| 145 |
-
|
| 146 |
-
<div className=
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
: 'bg-card/30 border-border text-text opacity-95'
|
| 150 |
-
}`}>
|
| 151 |
-
<p className="whitespace-pre-wrap leading-relaxed">{msg.text}</p>
|
| 152 |
</div>
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
<RefreshCw className="h-3 w-3 animate-spin text-primary" />
|
| 158 |
-
<span>Agent is typing...</span>
|
| 159 |
-
</div>
|
| 160 |
-
)}
|
| 161 |
-
<div ref={bottomRef} />
|
| 162 |
-
</div>
|
| 163 |
|
| 164 |
-
{/* Suggested
|
| 165 |
-
{messages.length === 1 && (
|
| 166 |
-
<div className="px-4 py-2 bg-canvas/30 border-t border-border">
|
| 167 |
<p className="text-[10px] uppercase font-mono tracking-wider text-text-muted font-semibold mb-1.5 flex items-center gap-1">
|
| 168 |
<Sparkles className="h-3 w-3 text-primary" /> Suggested Questions
|
| 169 |
</p>
|
|
@@ -181,32 +465,38 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({ repoName }) => {
|
|
| 181 |
</div>
|
| 182 |
)}
|
| 183 |
|
| 184 |
-
{/* Input */}
|
| 185 |
-
|
| 186 |
-
<
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
<input
|
| 194 |
-
type="text"
|
| 195 |
-
value={input}
|
| 196 |
-
onChange={(e) => setInput(e.target.value)}
|
| 197 |
-
disabled={isStreaming}
|
| 198 |
-
placeholder={`Ask a question about ${repoName}...`}
|
| 199 |
-
className="flex-grow bg-canvas border border-border rounded px-3 py-2 text-sm focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary font-sans text-text placeholder:text-text-muted/50"
|
| 200 |
-
/>
|
| 201 |
-
<button
|
| 202 |
-
type="submit"
|
| 203 |
-
disabled={isStreaming || !input.trim()}
|
| 204 |
-
className="bg-primary hover:bg-primary-hover text-text font-medium px-3.5 py-2 rounded flex items-center gap-1.5 text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
| 205 |
>
|
| 206 |
-
<
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
);
|
| 212 |
};
|
|
|
|
| 1 |
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
import { apiUrl } from '../../lib/api';
|
| 3 |
+
import {
|
| 4 |
+
Send,
|
| 5 |
+
MessageSquareCode,
|
| 6 |
+
Sparkles,
|
| 7 |
+
Bot,
|
| 8 |
+
User,
|
| 9 |
+
RefreshCw,
|
| 10 |
+
FileCode,
|
| 11 |
+
AlertTriangle,
|
| 12 |
+
ChevronDown,
|
| 13 |
+
ChevronUp,
|
| 14 |
+
} from 'lucide-react';
|
| 15 |
+
|
| 16 |
+
// ---------------------------------------------------------------------------
|
| 17 |
+
// Types
|
| 18 |
+
// ---------------------------------------------------------------------------
|
| 19 |
|
| 20 |
interface Message {
|
| 21 |
id: string;
|
| 22 |
sender: 'user' | 'assistant';
|
| 23 |
text: string;
|
| 24 |
+
/** Sources attached to this assistant message (from the done event). */
|
| 25 |
+
sources?: string[];
|
| 26 |
+
/** 0-100 confidence score attached to this assistant message. */
|
| 27 |
+
confidence?: number;
|
| 28 |
+
/** True when the LLM was unavailable and we fell back to retrieval-only. */
|
| 29 |
+
fallbackMode?: boolean;
|
| 30 |
+
/** True when this is an inline error (no_repo_selected, etc.). */
|
| 31 |
+
isError?: boolean;
|
| 32 |
}
|
| 33 |
|
| 34 |
interface ChatInterfaceProps {
|
| 35 |
+
/**
|
| 36 |
+
* When non-empty (embedded in AnalysisDashboard), this value is used
|
| 37 |
+
* directly and all resolution fallbacks are skipped.
|
| 38 |
+
*
|
| 39 |
+
* When empty (standalone /chat page), the component resolves the repo
|
| 40 |
+
* via: URL params → localStorage → /api/repos/recent.
|
| 41 |
+
*/
|
| 42 |
repoName: string;
|
| 43 |
}
|
| 44 |
|
| 45 |
+
// ---------------------------------------------------------------------------
|
| 46 |
+
// Helpers
|
| 47 |
+
// ---------------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
/** Collapsible source citation block rendered under each assistant message. */
|
| 50 |
+
const SourcesPanel: React.FC<{
|
| 51 |
+
sources: string[];
|
| 52 |
+
confidence: number;
|
| 53 |
+
fallbackMode: boolean;
|
| 54 |
+
}> = ({ sources, confidence, fallbackMode }) => {
|
| 55 |
+
const [open, setOpen] = useState(false);
|
| 56 |
+
|
| 57 |
+
if (sources.length === 0) return null;
|
| 58 |
+
|
| 59 |
+
return (
|
| 60 |
+
<div className="mt-2 border border-border/60 rounded-md overflow-hidden text-xs font-mono">
|
| 61 |
+
{/* Toggle header */}
|
| 62 |
+
<button
|
| 63 |
+
onClick={() => setOpen((v) => !v)}
|
| 64 |
+
className="w-full flex items-center justify-between px-3 py-1.5 bg-canvas/40 hover:bg-canvas/60 transition-colors text-text-muted"
|
| 65 |
+
>
|
| 66 |
+
<span className="flex items-center gap-1.5">
|
| 67 |
+
<FileCode className="h-3 w-3 text-primary" />
|
| 68 |
+
<span>
|
| 69 |
+
{sources.length} source{sources.length !== 1 ? 's' : ''}
|
| 70 |
+
</span>
|
| 71 |
+
{fallbackMode && (
|
| 72 |
+
<span className="ml-2 flex items-center gap-1 text-amber-400">
|
| 73 |
+
<AlertTriangle className="h-3 w-3" />
|
| 74 |
+
<span>fallback mode</span>
|
| 75 |
+
</span>
|
| 76 |
+
)}
|
| 77 |
+
{!fallbackMode && confidence > 0 && (
|
| 78 |
+
<span className="ml-2 text-emerald-500">{confidence}% confidence</span>
|
| 79 |
+
)}
|
| 80 |
+
</span>
|
| 81 |
+
{open ? (
|
| 82 |
+
<ChevronUp className="h-3 w-3" />
|
| 83 |
+
) : (
|
| 84 |
+
<ChevronDown className="h-3 w-3" />
|
| 85 |
+
)}
|
| 86 |
+
</button>
|
| 87 |
+
|
| 88 |
+
{/* Source list */}
|
| 89 |
+
{open && (
|
| 90 |
+
<ul className="px-3 py-2 space-y-1 bg-canvas/20">
|
| 91 |
+
{sources.map((src) => (
|
| 92 |
+
<li key={src} className="text-text-muted truncate">
|
| 93 |
+
• {src}
|
| 94 |
+
</li>
|
| 95 |
+
))}
|
| 96 |
+
</ul>
|
| 97 |
+
)}
|
| 98 |
+
</div>
|
| 99 |
+
);
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
// ---------------------------------------------------------------------------
|
| 103 |
+
// Main component
|
| 104 |
+
// ---------------------------------------------------------------------------
|
| 105 |
+
|
| 106 |
export const ChatInterface: React.FC<ChatInterfaceProps> = ({ repoName }) => {
|
| 107 |
+
/**
|
| 108 |
+
* Repo resolution strategy:
|
| 109 |
+
*
|
| 110 |
+
* EMBEDDED MODE (repoName prop is non-empty — inside AnalysisDashboard)
|
| 111 |
+
* → use prop directly, skip all fallbacks.
|
| 112 |
+
*
|
| 113 |
+
* STANDALONE MODE (repoName prop is empty — /chat page)
|
| 114 |
+
* Priority 1: URL params ?owner=...&repo=...
|
| 115 |
+
* Priority 2: localStorage.activeRepo
|
| 116 |
+
* Priority 3: async GET /api/repos/recent → data[0].name
|
| 117 |
+
*/
|
| 118 |
+
const [activeRepo, setActiveRepo] = useState<string>(() => {
|
| 119 |
+
// Embedded mode: prop is the direct, authoritative value.
|
| 120 |
+
if (repoName) return repoName;
|
| 121 |
+
|
| 122 |
+
// Standalone mode fallbacks (synchronous part).
|
| 123 |
+
if (typeof window !== 'undefined') {
|
| 124 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 125 |
+
const owner = urlParams.get('owner');
|
| 126 |
+
const repo = urlParams.get('repo');
|
| 127 |
+
if (owner && repo) return `${owner}/${repo}`;
|
| 128 |
+
|
| 129 |
+
const stored = localStorage.getItem('activeRepo');
|
| 130 |
+
if (stored) return stored;
|
| 131 |
}
|
| 132 |
+
|
| 133 |
+
// Will be resolved async (Priority 3).
|
| 134 |
+
return '';
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
| 138 |
const [input, setInput] = useState('');
|
| 139 |
const [isStreaming, setIsStreaming] = useState(false);
|
| 140 |
+
/** Shown when the component is in standalone mode and has no repo at all. */
|
| 141 |
+
const [noRepoError, setNoRepoError] = useState(false);
|
| 142 |
const bottomRef = useRef<HTMLDivElement>(null);
|
| 143 |
|
| 144 |
const suggestedQuestions = [
|
| 145 |
+
'How are the agent schemas structured?',
|
| 146 |
+
'Where is the FastAPI router initialized?',
|
| 147 |
+
'Which files do I edit to add a new service?',
|
| 148 |
];
|
| 149 |
|
| 150 |
+
// ── Priority 3: async fallback — only runs in standalone mode when prop was empty ──
|
| 151 |
+
useEffect(() => {
|
| 152 |
+
// Skip if prop was provided (embedded) or already resolved.
|
| 153 |
+
if (repoName || activeRepo) return;
|
| 154 |
+
|
| 155 |
+
fetch(apiUrl('/api/repos/recent'))
|
| 156 |
+
.then((res) => res.json())
|
| 157 |
+
.then((data) => {
|
| 158 |
+
if (Array.isArray(data) && data.length > 0) {
|
| 159 |
+
const name: string = data[0].name;
|
| 160 |
+
setActiveRepo(name);
|
| 161 |
+
if (typeof window !== 'undefined') {
|
| 162 |
+
localStorage.setItem('activeRepo', name);
|
| 163 |
+
}
|
| 164 |
+
} else {
|
| 165 |
+
// Backend has no recent repos — show the "no repo" state.
|
| 166 |
+
setNoRepoError(true);
|
| 167 |
+
}
|
| 168 |
+
})
|
| 169 |
+
.catch((err) => {
|
| 170 |
+
console.error('Failed to fetch recent repos for chat', err);
|
| 171 |
+
setNoRepoError(true);
|
| 172 |
+
});
|
| 173 |
+
// intentionally only on mount
|
| 174 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 175 |
+
}, []);
|
| 176 |
+
|
| 177 |
+
// ── Sync prop changes into activeRepo (e.g. user switches repos in dashboard) ──
|
| 178 |
+
useEffect(() => {
|
| 179 |
+
if (repoName && repoName !== activeRepo) {
|
| 180 |
+
setActiveRepo(repoName);
|
| 181 |
+
}
|
| 182 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 183 |
+
}, [repoName]);
|
| 184 |
+
|
| 185 |
+
// ── Reset greeting whenever the active repo changes ──
|
| 186 |
+
useEffect(() => {
|
| 187 |
+
if (!activeRepo) return;
|
| 188 |
+
setNoRepoError(false);
|
| 189 |
+
setMessages([
|
| 190 |
+
{
|
| 191 |
+
id: 'init',
|
| 192 |
+
sender: 'assistant',
|
| 193 |
+
text: `Hello! I've indexed \`${activeRepo}\`. Ask me anything about its architecture, endpoints, or file structures.`,
|
| 194 |
+
},
|
| 195 |
+
]);
|
| 196 |
+
}, [activeRepo]);
|
| 197 |
+
|
| 198 |
+
// ── Auto-scroll ──
|
| 199 |
useEffect(() => {
|
| 200 |
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 201 |
}, [messages]);
|
| 202 |
|
| 203 |
+
// ── Stream a response from /api/chat ──
|
| 204 |
const triggerStreamResponse = async (userPrompt: string) => {
|
| 205 |
setIsStreaming(true);
|
| 206 |
+
const assistantId = Math.random().toString();
|
| 207 |
+
|
| 208 |
+
// Add empty placeholder for the streaming assistant message.
|
| 209 |
+
setMessages((prev) => [
|
| 210 |
...prev,
|
| 211 |
+
{ id: assistantId, sender: 'assistant', text: '' },
|
| 212 |
]);
|
| 213 |
|
| 214 |
try {
|
|
|
|
| 216 |
method: 'POST',
|
| 217 |
headers: { 'Content-Type': 'application/json' },
|
| 218 |
body: JSON.stringify({
|
| 219 |
+
repo: activeRepo,
|
| 220 |
message: userPrompt,
|
| 221 |
+
history: messages.slice(1).map((m) => ({
|
| 222 |
role: m.sender === 'user' ? 'user' : 'model',
|
| 223 |
+
parts: [m.text],
|
| 224 |
+
})),
|
| 225 |
+
}),
|
| 226 |
});
|
| 227 |
|
| 228 |
+
// Handle non-SSE errors (422 Pydantic validation, 500, etc.)
|
| 229 |
+
if (!response.ok) {
|
| 230 |
+
let detail = `Backend error ${response.status}`;
|
| 231 |
+
try {
|
| 232 |
+
const errBody = await response.json();
|
| 233 |
+
if (errBody?.detail) {
|
| 234 |
+
// Pydantic 422 wraps details in an array
|
| 235 |
+
if (Array.isArray(errBody.detail)) {
|
| 236 |
+
detail = errBody.detail.map((d: any) => d.msg).join('; ');
|
| 237 |
+
} else {
|
| 238 |
+
detail = String(errBody.detail);
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
} catch {
|
| 242 |
+
/* ignore parse error */
|
| 243 |
+
}
|
| 244 |
+
setMessages((prev) =>
|
| 245 |
+
prev.map((msg) =>
|
| 246 |
+
msg.id === assistantId
|
| 247 |
+
? { ...msg, text: detail, isError: true }
|
| 248 |
+
: msg,
|
| 249 |
+
),
|
| 250 |
+
);
|
| 251 |
+
return;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
if (!response.body) throw new Error('ReadableStream not available');
|
| 255 |
|
| 256 |
const reader = response.body.getReader();
|
| 257 |
const decoder = new TextDecoder();
|
|
|
|
| 260 |
while (!finished) {
|
| 261 |
const { value, done } = await reader.read();
|
| 262 |
finished = done;
|
| 263 |
+
if (!value) continue;
|
| 264 |
+
|
| 265 |
+
const raw = decoder.decode(value);
|
| 266 |
+
const lines = raw.split('\n');
|
| 267 |
+
|
| 268 |
+
for (const line of lines) {
|
| 269 |
+
if (!line.startsWith('data: ')) continue;
|
| 270 |
+
try {
|
| 271 |
+
const data = JSON.parse(line.slice(6));
|
| 272 |
+
|
| 273 |
+
// Inline SSE error from the backend (no_repo_selected, etc.)
|
| 274 |
+
if (data.error) {
|
| 275 |
+
setMessages((prev) =>
|
| 276 |
+
prev.map((msg) =>
|
| 277 |
+
msg.id === assistantId
|
| 278 |
+
? { ...msg, text: data.message ?? data.error, isError: true }
|
| 279 |
+
: msg,
|
| 280 |
+
),
|
| 281 |
+
);
|
| 282 |
+
continue;
|
| 283 |
}
|
| 284 |
+
|
| 285 |
+
// Streaming text token
|
| 286 |
+
if (data.text) {
|
| 287 |
+
setMessages((prev) =>
|
| 288 |
+
prev.map((msg) =>
|
| 289 |
+
msg.id === assistantId
|
| 290 |
+
? { ...msg, text: msg.text + data.text }
|
| 291 |
+
: msg,
|
| 292 |
+
),
|
| 293 |
+
);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// Terminal done event — attach sources, confidence, fallback flag
|
| 297 |
+
if (data.status === 'done') {
|
| 298 |
+
setMessages((prev) =>
|
| 299 |
+
prev.map((msg) =>
|
| 300 |
+
msg.id === assistantId
|
| 301 |
+
? {
|
| 302 |
+
...msg,
|
| 303 |
+
sources: data.sources ?? [],
|
| 304 |
+
confidence: data.confidence ?? 0,
|
| 305 |
+
fallbackMode: data.fallback_mode ?? false,
|
| 306 |
+
}
|
| 307 |
+
: msg,
|
| 308 |
+
),
|
| 309 |
+
);
|
| 310 |
+
}
|
| 311 |
+
} catch {
|
| 312 |
+
// Ignore malformed / partial SSE lines
|
| 313 |
}
|
| 314 |
}
|
| 315 |
}
|
| 316 |
} catch (error) {
|
| 317 |
+
console.error('Chat stream error:', error);
|
| 318 |
+
setMessages((prev) =>
|
| 319 |
+
prev.map((msg) =>
|
| 320 |
+
msg.id === assistantId
|
| 321 |
+
? {
|
| 322 |
+
...msg,
|
| 323 |
+
text: 'Could not reach the agent backend. Please verify that api.py is running on port 8001.',
|
| 324 |
+
isError: true,
|
| 325 |
+
}
|
| 326 |
+
: msg,
|
| 327 |
+
),
|
| 328 |
);
|
| 329 |
} finally {
|
| 330 |
setIsStreaming(false);
|
|
|
|
| 334 |
const handleSend = async (textToSend: string) => {
|
| 335 |
if (!textToSend.trim() || isStreaming) return;
|
| 336 |
|
| 337 |
+
setMessages((prev) => [
|
|
|
|
| 338 |
...prev,
|
| 339 |
+
{ id: Math.random().toString(), sender: 'user', text: textToSend },
|
| 340 |
]);
|
| 341 |
setInput('');
|
| 342 |
|
| 343 |
await triggerStreamResponse(textToSend);
|
| 344 |
};
|
| 345 |
|
| 346 |
+
// ---------------------------------------------------------------------------
|
| 347 |
+
// Render
|
| 348 |
+
// ---------------------------------------------------------------------------
|
| 349 |
+
|
| 350 |
return (
|
| 351 |
+
<div className="flex flex-col min-h-[550px] h-full border border-border bg-card/10 rounded-lg overflow-hidden w-full">
|
| 352 |
{/* Header */}
|
| 353 |
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-card/40 shrink-0">
|
| 354 |
<div className="flex items-center gap-2">
|
| 355 |
<MessageSquareCode className="h-4 w-4 text-primary" />
|
| 356 |
+
<span className="font-mono text-sm font-semibold text-text">
|
| 357 |
+
Repository Chat Assistant
|
| 358 |
+
</span>
|
| 359 |
</div>
|
| 360 |
+
<span className="text-[10px] font-mono border border-primary/30 text-primary px-2 py-0.5 rounded bg-primary/5 truncate max-w-[180px]">
|
| 361 |
+
{activeRepo || 'no repo selected'}
|
| 362 |
</span>
|
| 363 |
</div>
|
| 364 |
|
| 365 |
+
{/* No-repo state (standalone mode, no resolution succeeded) */}
|
| 366 |
+
{noRepoError && (
|
| 367 |
+
<div className="flex-grow flex flex-col items-center justify-center p-8 gap-3 text-center">
|
| 368 |
+
<AlertTriangle className="h-8 w-8 text-amber-400" />
|
| 369 |
+
<p className="text-sm font-mono text-text-muted">
|
| 370 |
+
No repository selected.
|
| 371 |
+
</p>
|
| 372 |
+
<p className="text-xs text-text-muted max-w-xs">
|
| 373 |
+
Analyse a repository first, then return here to chat with its
|
| 374 |
+
codebase.
|
| 375 |
+
</p>
|
| 376 |
+
</div>
|
| 377 |
+
)}
|
| 378 |
+
|
| 379 |
+
{/* Message thread */}
|
| 380 |
+
{!noRepoError && (
|
| 381 |
+
<div className="flex-grow overflow-y-auto p-4 space-y-4 font-sans text-sm">
|
| 382 |
+
{messages.map((msg) => (
|
| 383 |
+
<div
|
| 384 |
+
key={msg.id}
|
| 385 |
+
className={`flex gap-3 ${
|
| 386 |
+
msg.sender === 'user' ? 'ml-auto flex-row-reverse max-w-[85%]' : 'max-w-[92%]'
|
| 387 |
+
}`}
|
| 388 |
+
>
|
| 389 |
+
{/* Avatar */}
|
| 390 |
+
<div
|
| 391 |
+
className={`h-8 w-8 rounded-full flex items-center justify-center shrink-0 border ${
|
| 392 |
+
msg.sender === 'user'
|
| 393 |
+
? 'bg-primary/10 border-primary/30 text-primary'
|
| 394 |
+
: msg.isError
|
| 395 |
+
? 'bg-red-500/10 border-red-500/30 text-red-400'
|
| 396 |
+
: 'bg-card border-border text-text-muted'
|
| 397 |
+
}`}
|
| 398 |
+
>
|
| 399 |
+
{msg.sender === 'user' ? (
|
| 400 |
+
<User className="h-4 w-4" />
|
| 401 |
+
) : (
|
| 402 |
+
<Bot className="h-4 w-4" />
|
| 403 |
+
)}
|
| 404 |
+
</div>
|
| 405 |
+
|
| 406 |
+
{/* Bubble + sources */}
|
| 407 |
+
<div className="flex flex-col gap-1 min-w-0">
|
| 408 |
+
<div
|
| 409 |
+
className={`p-3 rounded-lg border ${
|
| 410 |
+
msg.sender === 'user'
|
| 411 |
+
? 'bg-primary/10 border-primary/20 text-text'
|
| 412 |
+
: msg.isError
|
| 413 |
+
? 'bg-red-500/5 border-red-500/20 text-red-400'
|
| 414 |
+
: msg.fallbackMode
|
| 415 |
+
? 'bg-amber-500/5 border-amber-500/20 text-text opacity-95'
|
| 416 |
+
: 'bg-card/30 border-border text-text opacity-95'
|
| 417 |
+
}`}
|
| 418 |
+
>
|
| 419 |
+
<p className="whitespace-pre-wrap leading-relaxed break-words">
|
| 420 |
+
{msg.text}
|
| 421 |
+
</p>
|
| 422 |
+
</div>
|
| 423 |
+
|
| 424 |
+
{/* Sources panel — only for assistant messages with sources */}
|
| 425 |
+
{msg.sender === 'assistant' &&
|
| 426 |
+
msg.sources &&
|
| 427 |
+
msg.sources.length > 0 && (
|
| 428 |
+
<SourcesPanel
|
| 429 |
+
sources={msg.sources}
|
| 430 |
+
confidence={msg.confidence ?? 0}
|
| 431 |
+
fallbackMode={msg.fallbackMode ?? false}
|
| 432 |
+
/>
|
| 433 |
+
)}
|
| 434 |
+
</div>
|
| 435 |
</div>
|
| 436 |
+
))}
|
| 437 |
|
| 438 |
+
{isStreaming && (
|
| 439 |
+
<div className="flex items-center gap-2 text-xs text-text-muted font-mono pl-11">
|
| 440 |
+
<RefreshCw className="h-3 w-3 animate-spin text-primary" />
|
| 441 |
+
<span>Agent is thinking...</span>
|
|
|
|
|
|
|
|
|
|
| 442 |
</div>
|
| 443 |
+
)}
|
| 444 |
+
<div ref={bottomRef} />
|
| 445 |
+
</div>
|
| 446 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
| 448 |
+
{/* Suggested questions — shown only after the greeting message */}
|
| 449 |
+
{!noRepoError && messages.length === 1 && (
|
| 450 |
+
<div className="px-4 py-2 bg-canvas/30 border-t border-border shrink-0">
|
| 451 |
<p className="text-[10px] uppercase font-mono tracking-wider text-text-muted font-semibold mb-1.5 flex items-center gap-1">
|
| 452 |
<Sparkles className="h-3 w-3 text-primary" /> Suggested Questions
|
| 453 |
</p>
|
|
|
|
| 465 |
</div>
|
| 466 |
)}
|
| 467 |
|
| 468 |
+
{/* Input bar */}
|
| 469 |
+
{!noRepoError && (
|
| 470 |
+
<div className="p-3 border-t border-border bg-card/25 shrink-0">
|
| 471 |
+
<form
|
| 472 |
+
onSubmit={(e) => {
|
| 473 |
+
e.preventDefault();
|
| 474 |
+
handleSend(input);
|
| 475 |
+
}}
|
| 476 |
+
className="flex gap-2"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
>
|
| 478 |
+
<input
|
| 479 |
+
type="text"
|
| 480 |
+
value={input}
|
| 481 |
+
onChange={(e) => setInput(e.target.value)}
|
| 482 |
+
disabled={isStreaming || !activeRepo}
|
| 483 |
+
placeholder={
|
| 484 |
+
activeRepo
|
| 485 |
+
? `Ask a question about ${activeRepo}...`
|
| 486 |
+
: 'Waiting for repository...'
|
| 487 |
+
}
|
| 488 |
+
className="flex-grow bg-canvas border border-border rounded px-3 py-2 text-sm focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary font-sans text-text placeholder:text-text-muted/50"
|
| 489 |
+
/>
|
| 490 |
+
<button
|
| 491 |
+
type="submit"
|
| 492 |
+
disabled={isStreaming || !input.trim() || !activeRepo}
|
| 493 |
+
className="bg-primary hover:bg-primary-hover text-text font-medium px-3.5 py-2 rounded flex items-center gap-1.5 text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
| 494 |
+
>
|
| 495 |
+
<Send className="h-4 w-4" />
|
| 496 |
+
</button>
|
| 497 |
+
</form>
|
| 498 |
+
</div>
|
| 499 |
+
)}
|
| 500 |
</div>
|
| 501 |
);
|
| 502 |
};
|
frontend/src/components/interactive/IssueMapper.tsx
CHANGED
|
@@ -25,7 +25,20 @@ interface IssueMapperProps {
|
|
| 25 |
}
|
| 26 |
|
| 27 |
export const IssueMapper: React.FC<IssueMapperProps> = ({ repoName }) => {
|
| 28 |
-
const [selectedRepo, setSelectedRepo] = useState(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
const [recentRepos, setRecentRepos] = useState<{ name: string }[]>([]);
|
| 30 |
const [title, setTitle] = useState('');
|
| 31 |
const [description, setDescription] = useState('');
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
export const IssueMapper: React.FC<IssueMapperProps> = ({ repoName }) => {
|
| 28 |
+
const [selectedRepo, setSelectedRepo] = useState(() => {
|
| 29 |
+
if (repoName) return repoName;
|
| 30 |
+
if (typeof window !== 'undefined') {
|
| 31 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 32 |
+
const owner = urlParams.get('owner');
|
| 33 |
+
const repo = urlParams.get('repo');
|
| 34 |
+
if (owner && repo) {
|
| 35 |
+
return `${owner}/${repo}`;
|
| 36 |
+
}
|
| 37 |
+
const stored = localStorage.getItem('activeRepo');
|
| 38 |
+
if (stored) return stored;
|
| 39 |
+
}
|
| 40 |
+
return '';
|
| 41 |
+
});
|
| 42 |
const [recentRepos, setRecentRepos] = useState<{ name: string }[]>([]);
|
| 43 |
const [title, setTitle] = useState('');
|
| 44 |
const [description, setDescription] = useState('');
|
frontend/src/components/interactive/RepoInput.tsx
CHANGED
|
@@ -50,7 +50,7 @@ export const RepoInput: React.FC = () => {
|
|
| 50 |
body: JSON.stringify({
|
| 51 |
url: repoUrl,
|
| 52 |
branch: 'main',
|
| 53 |
-
model: '
|
| 54 |
})
|
| 55 |
});
|
| 56 |
|
|
@@ -97,8 +97,11 @@ export const RepoInput: React.FC = () => {
|
|
| 97 |
if (repoPath) {
|
| 98 |
const parts = repoPath.split('/')
|
| 99 |
if (parts.length === 2) {
|
| 100 |
-
const [owner, repo] = parts
|
| 101 |
-
window
|
|
|
|
|
|
|
|
|
|
| 102 |
} else {
|
| 103 |
setErrorMessage('Invalid repo format received');
|
| 104 |
setIsAnalyzing(false);
|
|
@@ -178,10 +181,9 @@ export const RepoInput: React.FC = () => {
|
|
| 178 |
<div className="space-y-1">
|
| 179 |
<span className="font-bold uppercase tracking-wider text-[10px] block">Analysis Failed</span>
|
| 180 |
<p className="leading-relaxed">{errorMessage}</p>
|
| 181 |
-
{errorMessage.includes('quota') || errorMessage.includes('429') || errorMessage.includes('RESOURCE_EXHAUSTED') ? (
|
| 182 |
<p className="text-red-300/70 text-[10px] mt-1">
|
| 183 |
-
|
| 184 |
-
<a href="https://ai.dev/rate-limit" target="_blank" rel="noopener noreferrer" className="underline">ai.dev/rate-limit</a>.
|
| 185 |
</p>
|
| 186 |
) : null}
|
| 187 |
</div>
|
|
|
|
| 50 |
body: JSON.stringify({
|
| 51 |
url: repoUrl,
|
| 52 |
branch: 'main',
|
| 53 |
+
model: 'deepseek-ai/deepseek-v4-flash'
|
| 54 |
})
|
| 55 |
});
|
| 56 |
|
|
|
|
| 97 |
if (repoPath) {
|
| 98 |
const parts = repoPath.split('/')
|
| 99 |
if (parts.length === 2) {
|
| 100 |
+
const [owner, repo] = parts;
|
| 101 |
+
if (typeof window !== 'undefined') {
|
| 102 |
+
localStorage.setItem('activeRepo', repoPath);
|
| 103 |
+
}
|
| 104 |
+
window.location.href = `/analysis?owner=${owner}&repo=${repo}`;
|
| 105 |
} else {
|
| 106 |
setErrorMessage('Invalid repo format received');
|
| 107 |
setIsAnalyzing(false);
|
|
|
|
| 181 |
<div className="space-y-1">
|
| 182 |
<span className="font-bold uppercase tracking-wider text-[10px] block">Analysis Failed</span>
|
| 183 |
<p className="leading-relaxed">{errorMessage}</p>
|
| 184 |
+
{errorMessage.includes('quota') || errorMessage.includes('429') || errorMessage.includes('RESOURCE_EXHAUSTED') || errorMessage.includes('rate limit') ? (
|
| 185 |
<p className="text-red-300/70 text-[10px] mt-1">
|
| 186 |
+
AI provider rate limit reached. Please wait a moment before retrying.
|
|
|
|
| 187 |
</p>
|
| 188 |
) : null}
|
| 189 |
</div>
|
frontend/src/pages/chat.astro
CHANGED
|
@@ -19,7 +19,7 @@ import { MessageSquare } from 'lucide-react';
|
|
| 19 |
|
| 20 |
{/* Chat Interface react island */}
|
| 21 |
<div class="w-full flex justify-center flex-grow pt-2">
|
| 22 |
-
<ChatInterface repoName="
|
| 23 |
</div>
|
| 24 |
</div>
|
| 25 |
</Layout>
|
|
|
|
| 19 |
|
| 20 |
{/* Chat Interface react island */}
|
| 21 |
<div class="w-full flex justify-center flex-grow pt-2">
|
| 22 |
+
<ChatInterface repoName="" client:load />
|
| 23 |
</div>
|
| 24 |
</div>
|
| 25 |
</Layout>
|
services/architecture_service.py
CHANGED
|
@@ -111,6 +111,22 @@ class ArchitectureService:
|
|
| 111 |
)
|
| 112 |
logger.info("Parsed %d supported source files", len(parsed))
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
# ----------------------------------------------------------
|
| 115 |
# Step 2: Detect entry points
|
| 116 |
# ----------------------------------------------------------
|
|
|
|
| 111 |
)
|
| 112 |
logger.info("Parsed %d supported source files", len(parsed))
|
| 113 |
|
| 114 |
+
# Guard: if zero files were parsed, the graph will be empty and all
|
| 115 |
+
# downstream features (Reading Path, Impact Analysis, Architecture Graph)
|
| 116 |
+
# will fail. Log at ERROR level so it's immediately visible in server
|
| 117 |
+
# logs. We do NOT raise here — the empty graph is still saved so that
|
| 118 |
+
# the API endpoint can return a descriptive 404 rather than crashing.
|
| 119 |
+
if len(parsed) == 0:
|
| 120 |
+
logger.error(
|
| 121 |
+
"[PIPELINE:%s] architecture_service.build produced ZERO parsed files. "
|
| 122 |
+
"repo_path='%s' files_provided=%s. "
|
| 123 |
+
"The dependency graph will be empty. "
|
| 124 |
+
"Check CLONED_REPOS_PATH env var and that source files are reachable.",
|
| 125 |
+
repo_name,
|
| 126 |
+
repo_path or "(none)",
|
| 127 |
+
files is not None,
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
# ----------------------------------------------------------
|
| 131 |
# Step 2: Detect entry points
|
| 132 |
# ----------------------------------------------------------
|
services/chat_fallback.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chat Fallback Service.
|
| 2 |
+
|
| 3 |
+
Generates a retrieval-grounded answer from indexed code chunks when the LLM
|
| 4 |
+
provider is unavailable (429 / 5xx / timeout). No LLM call is made — the
|
| 5 |
+
retrieved chunks are formatted directly into a readable response.
|
| 6 |
+
|
| 7 |
+
Used exclusively by backend/api.py chat_streamer() when provider.stream()
|
| 8 |
+
raises after exhausting its retry budget.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from typing import List, Dict, Any
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def build_fallback_answer(chunks: List[Dict[str, Any]], question: str) -> str:
|
| 15 |
+
"""Build a plain-text fallback answer from retrieved chunks.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
chunks: List of Chroma chunk dicts — each has 'content' and
|
| 19 |
+
'metadata' keys (as returned by chroma_store.search_repository).
|
| 20 |
+
question: The original user question — echoed back for context.
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
A formatted string suitable for yielding directly as SSE text tokens.
|
| 24 |
+
Never contains raw HTTP errors, provider names, or exception details.
|
| 25 |
+
"""
|
| 26 |
+
if not chunks:
|
| 27 |
+
return (
|
| 28 |
+
"The AI provider is temporarily unavailable and no indexed content "
|
| 29 |
+
"was found for this query.\n\n"
|
| 30 |
+
"Please ensure the repository has been analysed and try again shortly."
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
header = (
|
| 34 |
+
"The AI provider is temporarily unavailable. "
|
| 35 |
+
"Here are the most relevant sections from the indexed codebase:\n\n"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
lines: List[str] = [header]
|
| 39 |
+
|
| 40 |
+
# Deduplicate by file path — show each file once with its best chunk
|
| 41 |
+
seen_paths: set = set()
|
| 42 |
+
included = 0
|
| 43 |
+
|
| 44 |
+
for chunk in chunks:
|
| 45 |
+
meta = chunk.get("metadata", {})
|
| 46 |
+
file_path = meta.get("file_path", "unknown")
|
| 47 |
+
content = chunk.get("content", "").strip()
|
| 48 |
+
|
| 49 |
+
if not content or file_path in seen_paths:
|
| 50 |
+
continue
|
| 51 |
+
|
| 52 |
+
seen_paths.add(file_path)
|
| 53 |
+
included += 1
|
| 54 |
+
|
| 55 |
+
# Truncate long chunks for readability — 600 chars is enough signal
|
| 56 |
+
preview = content[:600]
|
| 57 |
+
if len(content) > 600:
|
| 58 |
+
preview += "\n... [truncated]"
|
| 59 |
+
|
| 60 |
+
lines.append(f"📄 **{file_path}**\n```\n{preview}\n```\n")
|
| 61 |
+
|
| 62 |
+
# Cap at 4 files to keep the response focused
|
| 63 |
+
if included >= 4:
|
| 64 |
+
break
|
| 65 |
+
|
| 66 |
+
if included == 0:
|
| 67 |
+
return (
|
| 68 |
+
"The AI provider is temporarily unavailable and the retrieved chunks "
|
| 69 |
+
"contained no usable content for this query.\n\n"
|
| 70 |
+
"Please try again shortly."
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
lines.append(
|
| 74 |
+
"\n---\n"
|
| 75 |
+
"⚠️ *Fallback mode — AI synthesis unavailable. "
|
| 76 |
+
"Showing raw retrieved context only. "
|
| 77 |
+
"Retry your question once the provider recovers.*"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
return "".join(lines)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def is_provider_error(exc: Exception) -> bool:
|
| 84 |
+
"""Return True if the exception looks like an LLM provider failure.
|
| 85 |
+
|
| 86 |
+
Used to decide whether to trigger fallback mode vs. re-raise for
|
| 87 |
+
unexpected errors (e.g., embedding failures, Chroma errors).
|
| 88 |
+
"""
|
| 89 |
+
err_str = str(exc).lower()
|
| 90 |
+
provider_signals = [
|
| 91 |
+
"429",
|
| 92 |
+
"too many requests",
|
| 93 |
+
"rate limit",
|
| 94 |
+
"503",
|
| 95 |
+
"502",
|
| 96 |
+
"500",
|
| 97 |
+
"service unavailable",
|
| 98 |
+
"bad gateway",
|
| 99 |
+
"timeout",
|
| 100 |
+
"connect error",
|
| 101 |
+
"connection refused",
|
| 102 |
+
"nvidia",
|
| 103 |
+
"nim",
|
| 104 |
+
"deepseek",
|
| 105 |
+
"max retries exceeded",
|
| 106 |
+
"statuserror",
|
| 107 |
+
]
|
| 108 |
+
return any(signal in err_str for signal in provider_signals)
|
services/embedding_service.py
CHANGED
|
@@ -26,7 +26,6 @@ _MODEL_NAME = "BAAI/bge-small-en-v1.5"
|
|
| 26 |
def _get_model():
|
| 27 |
"""Return the cached SentenceTransformer model, loading it on first call."""
|
| 28 |
global _model
|
| 29 |
-
print("[EMBED] ENTER _get_model()")
|
| 30 |
if _model is not None:
|
| 31 |
return _model
|
| 32 |
|
|
@@ -36,11 +35,9 @@ def _get_model():
|
|
| 36 |
|
| 37 |
try:
|
| 38 |
from sentence_transformers import SentenceTransformer # type: ignore
|
| 39 |
-
print("[EMBED] Loading SentenceTransformer...")
|
| 40 |
logger.info("Loading BGE embedding model '%s' (first call)…", _MODEL_NAME)
|
| 41 |
t0 = time.perf_counter()
|
| 42 |
_model = SentenceTransformer(_MODEL_NAME)
|
| 43 |
-
print("[EMBED] SentenceTransformer loaded")
|
| 44 |
logger.info(
|
| 45 |
"BGE model loaded successfully. elapsed=%.2fs",
|
| 46 |
time.perf_counter() - t0,
|
|
@@ -97,11 +94,10 @@ class EmbeddingService:
|
|
| 97 |
"EmbeddingService: 'client' parameter is ignored — using local BGE model."
|
| 98 |
)
|
| 99 |
self.model_name = model_name
|
| 100 |
-
#
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
logger.warning("Could not pre-load BGE model at init time: %s", exc)
|
| 105 |
|
| 106 |
# ------------------------------------------------------------------
|
| 107 |
# Core embedding methods
|
|
|
|
| 26 |
def _get_model():
|
| 27 |
"""Return the cached SentenceTransformer model, loading it on first call."""
|
| 28 |
global _model
|
|
|
|
| 29 |
if _model is not None:
|
| 30 |
return _model
|
| 31 |
|
|
|
|
| 35 |
|
| 36 |
try:
|
| 37 |
from sentence_transformers import SentenceTransformer # type: ignore
|
|
|
|
| 38 |
logger.info("Loading BGE embedding model '%s' (first call)…", _MODEL_NAME)
|
| 39 |
t0 = time.perf_counter()
|
| 40 |
_model = SentenceTransformer(_MODEL_NAME)
|
|
|
|
| 41 |
logger.info(
|
| 42 |
"BGE model loaded successfully. elapsed=%.2fs",
|
| 43 |
time.perf_counter() - t0,
|
|
|
|
| 94 |
"EmbeddingService: 'client' parameter is ignored — using local BGE model."
|
| 95 |
)
|
| 96 |
self.model_name = model_name
|
| 97 |
+
# ponytail: lazy load — model loads on first generate_embedding(s) call.
|
| 98 |
+
# Eager load duplicated under uvicorn --reload (reloader parent + worker
|
| 99 |
+
# both instantiated EmbeddingService at import time). Singleton in
|
| 100 |
+
# _get_model() still guarantees one load per process.
|
|
|
|
| 101 |
|
| 102 |
# ------------------------------------------------------------------
|
| 103 |
# Core embedding methods
|
services/github_service.py
CHANGED
|
@@ -12,6 +12,8 @@ import logging
|
|
| 12 |
from typing import Dict, List, Any, Optional
|
| 13 |
import requests
|
| 14 |
|
|
|
|
|
|
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
| 17 |
|
|
@@ -80,8 +82,10 @@ class GitHubService:
|
|
| 80 |
Returns:
|
| 81 |
The local directory path.
|
| 82 |
"""
|
| 83 |
-
#
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
safe_name = repo_fullName.replace("/", "_")
|
| 86 |
return os.path.join(base_dir, safe_name)
|
| 87 |
|
|
|
|
| 12 |
from typing import Dict, List, Any, Optional
|
| 13 |
import requests
|
| 14 |
|
| 15 |
+
from services.storage_paths import get_cloned_repos_dir
|
| 16 |
+
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
| 19 |
|
|
|
|
| 82 |
Returns:
|
| 83 |
The local directory path.
|
| 84 |
"""
|
| 85 |
+
# Store clones OUTSIDE the project tree so uvicorn --reload (WatchFiles)
|
| 86 |
+
# does not treat clone activity as source-code changes. Configurable via
|
| 87 |
+
# CLONED_REPOS_PATH; defaults to ~/.repo_intelligence/cloned_repos.
|
| 88 |
+
base_dir = str(get_cloned_repos_dir())
|
| 89 |
safe_name = repo_fullName.replace("/", "_")
|
| 90 |
return os.path.join(base_dir, safe_name)
|
| 91 |
|
services/storage_paths.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Resolve writable storage locations outside the project tree.
|
| 2 |
+
|
| 3 |
+
Cloned repositories MUST live outside the project root, otherwise
|
| 4 |
+
`uvicorn --reload` (WatchFiles) treats every cloned file as a source-code
|
| 5 |
+
change and kills the in-flight analysis request.
|
| 6 |
+
|
| 7 |
+
Resolution order:
|
| 8 |
+
1. `CLONED_REPOS_PATH` env var (recommended, e.g. C:/repo_intelligence_storage/cloned_repos)
|
| 9 |
+
2. `~/.repo_intelligence/cloned_repos` (cross-platform fallback)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_cloned_repos_dir() -> Path:
|
| 19 |
+
"""Return the base directory for cloned repositories, creating it if needed.
|
| 20 |
+
|
| 21 |
+
Never returns a path inside the project tree — keeps WatchFiles quiet.
|
| 22 |
+
"""
|
| 23 |
+
raw = os.environ.get("CLONED_REPOS_PATH", "").strip()
|
| 24 |
+
base = Path(raw).expanduser() if raw else Path.home() / ".repo_intelligence" / "cloned_repos"
|
| 25 |
+
base.mkdir(parents=True, exist_ok=True)
|
| 26 |
+
return base
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
# ponytail: minimal self-check — proves the dir exists and lives outside cwd.
|
| 31 |
+
p = get_cloned_repos_dir()
|
| 32 |
+
assert p.exists() and p.is_dir(), f"clone dir not created: {p}"
|
| 33 |
+
assert Path.cwd() not in p.parents and p != Path.cwd(), f"clone dir inside project: {p}"
|
| 34 |
+
print(f"OK cloned_repos_dir={p}")
|