G-Madhuri commited on
Commit
7e2f74d
·
1 Parent(s): 3b44559

deploy to hf space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +9 -0
  2. .gitignore +0 -0
  3. Dockerfile +32 -0
  4. README.md +216 -5
  5. backend/.env +1 -0
  6. backend/Dockerfile +21 -0
  7. backend/__pycache__/main.cpython-312.pyc +0 -0
  8. backend/__pycache__/test_agent_flow.cpython-312.pyc +0 -0
  9. backend/__pycache__/test_rag_pipeline.cpython-312.pyc +0 -0
  10. backend/adapters/__init__.py +3 -0
  11. backend/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
  12. backend/adapters/__pycache__/adk_adapter.cpython-312.pyc +0 -0
  13. backend/adapters/__pycache__/agent_adapter.cpython-312.pyc +0 -0
  14. backend/adapters/__pycache__/planner_adapter.cpython-312.pyc +0 -0
  15. backend/adapters/adk_adapter.py +36 -0
  16. backend/adapters/agent_adapter.py +26 -0
  17. backend/adapters/planner_adapter.py +38 -0
  18. backend/agents/__init__.py +2 -0
  19. backend/agents/api_agent.py +33 -0
  20. backend/agents/architecture_agent.py +33 -0
  21. backend/agents/base_agent.py +31 -0
  22. backend/agents/dependency_agent.py +33 -0
  23. backend/agents/llm_client.py +40 -0
  24. backend/agents/onboarding_agent.py +33 -0
  25. backend/agents/orchestrator.py +351 -0
  26. backend/agents/planner_agent.py +83 -0
  27. backend/agents/quality_agent.py +33 -0
  28. backend/agents/response_synthesizer.py +63 -0
  29. backend/agents/security_agent.py +33 -0
  30. backend/main.py +561 -0
  31. backend/memory/__init__.py +7 -0
  32. backend/memory/__pycache__/__init__.cpython-312.pyc +0 -0
  33. backend/memory/__pycache__/conversation_manager.cpython-312.pyc +0 -0
  34. backend/memory/__pycache__/embedding_service.cpython-312.pyc +0 -0
  35. backend/memory/__pycache__/knowledge_index.cpython-312.pyc +0 -0
  36. backend/memory/__pycache__/memory_cache.cpython-312.pyc +0 -0
  37. backend/memory/__pycache__/retriever.cpython-312.pyc +0 -0
  38. backend/memory/__pycache__/session_manager.cpython-312.pyc +0 -0
  39. backend/memory/__pycache__/vector_store.cpython-312.pyc +0 -0
  40. backend/memory/conversation_manager.py +80 -0
  41. backend/memory/embedding_service.py +46 -0
  42. backend/memory/knowledge_index.py +185 -0
  43. backend/memory/memory_cache.py +42 -0
  44. backend/memory/retriever.py +43 -0
  45. backend/memory/session_manager.py +65 -0
  46. backend/memory/vector_store.py +95 -0
  47. backend/requirements.txt +7 -0
  48. backend/services/__init__.py +1 -0
  49. backend/services/__pycache__/__init__.cpython-312.pyc +0 -0
  50. backend/services/__pycache__/graphBuilder.cpython-312.pyc +0 -0
.env.example ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Gemini API key (required for repository analysis, chat, and semantic search)
2
+ GEMINI_API_KEY=your_gemini_api_key_here
3
+
4
+ # Optional: comma-separated CORS origins (default: *)
5
+ # CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
6
+
7
+ # Frontend (local dev) — leave empty to use Vite proxy
8
+ # VITE_API_URL=
9
+ # VITE_API_PROXY=http://localhost:8000
.gitignore ADDED
Binary file (262 Bytes). View file
 
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Stage 1: Build Frontend ---
2
+ FROM node:20-alpine AS frontend-builder
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package*.json ./
5
+ RUN npm install
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
+
9
+ # --- Stage 2: Serve Backend & Frontend ---
10
+ FROM python:3.11-slim
11
+ RUN apt-get update && apt-get install -y --no-install-recommends \
12
+ git \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ WORKDIR /app
16
+
17
+ # Copy backend requirements and install
18
+ COPY backend/requirements.txt ./backend/
19
+ RUN pip install --no-cache-dir -r backend/requirements.txt
20
+
21
+ # Copy backend source
22
+ COPY backend/ ./backend/
23
+
24
+ # Copy compiled frontend build
25
+ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
26
+
27
+ # Expose port (Hugging Face Spaces requires port 7860)
28
+ EXPOSE 7860
29
+
30
+ # Run FastAPI from the backend directory so imports resolve locally
31
+ WORKDIR /app/backend
32
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,221 @@
1
  ---
2
- title: Software Engineer Agent
3
- emoji: 🐠
4
- colorFrom: pink
5
- colorTo: yellow
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Repository Intelligence Layer
3
+ emoji: 🔍
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # Repository Intelligence Layer
13
+
14
+ AI-powered repository analysis platform that clones or uploads codebases, generates structured intelligence artifacts with **Gemini 2.5 Flash**, and provides an interactive dashboard with graph visualization, semantic search, and multi-agent chat.
15
+
16
+ ![Dashboard Screenshot](./docs/screenshots/dashboard.png)
17
+ <!-- Replace with actual screenshot after first run -->
18
+
19
+ ## Features
20
+
21
+ - **GitHub cloning** — public and private repos (PAT authentication)
22
+ - **ZIP upload** — drag-and-drop repository archives
23
+ - **Repository scanner** — file tree, manifest parsing, static profiling
24
+ - **Graph builder** — static import/dependency graph + LLM architecture graph
25
+ - **Gemini analysis** — markdown report, profile, summary, and graph JSON
26
+ - **Interactive dashboard** — report, summary, profile, graph viewer, repo tree
27
+ - **AI Assistant** — multi-agent orchestration with RAG context
28
+ - **Knowledge Explorer** — ChromaDB semantic search and conversation history
29
+ - **Download endpoints** — export all intelligence artifacts
30
+ - **Persistent memory** — artifacts saved to disk; ChromaDB vector index
31
+
32
+ ## Architecture
33
+
34
+ ```mermaid
35
+ flowchart LR
36
+ subgraph Input
37
+ URL[GitHub URL + PAT]
38
+ ZIP[ZIP Upload]
39
+ end
40
+ subgraph Pipeline
41
+ Scan[Repository Scanner]
42
+ Profile[Repository Profiler]
43
+ Graph[Graph Builder]
44
+ LLM[Gemini Analyzer]
45
+ Mem[Memory Layer]
46
+ Chroma[ChromaDB Index]
47
+ end
48
+ subgraph Frontend
49
+ Tree[Repo Tree]
50
+ Dash[Dashboard]
51
+ GraphV[Graph Viewer]
52
+ Chat[AI Assistant]
53
+ end
54
+ URL --> Scan
55
+ ZIP --> Scan
56
+ Scan --> Profile
57
+ Profile --> Graph
58
+ Graph --> LLM
59
+ LLM --> Mem
60
+ LLM --> Chroma
61
+ Mem --> Dash
62
+ Scan --> Tree
63
+ Mem --> GraphV
64
+ Chroma --> Chat
65
+ ```
66
+
67
+ ## Project Structure
68
+
69
+ ```
70
+ ├── backend/ # FastAPI application
71
+ │ ├── main.py # API routes
72
+ │ ├── services/ # Scanner, profiler, graph builder, LLM, memory
73
+ │ ├── agents/ # Multi-agent orchestration
74
+ │ ├── memory/ # ChromaDB, RAG, conversations
75
+ │ └── tools/ # MCP-ready tool registry
76
+ ├── frontend/ # React + Vite dashboard
77
+ ├── Dockerfile # Unified build for Hugging Face Spaces (port 7860)
78
+ └── docker-compose.yml # Local split-stack development
79
+ ```
80
+
81
+ ## Installation
82
+
83
+ ### Prerequisites
84
+
85
+ - Python 3.11+
86
+ - Node.js 20+
87
+ - Git (for repository cloning)
88
+ - Gemini API key from [Google AI Studio](https://aistudio.google.com/)
89
+
90
+ ### Local Setup
91
+
92
+ 1. **Clone the repository**
93
+
94
+ ```bash
95
+ git clone <your-repo-url>
96
+ cd "Software Engineer Agent"
97
+ ```
98
+
99
+ 2. **Configure environment**
100
+
101
+ ```bash
102
+ cp .env.example backend/.env
103
+ # Edit backend/.env and set GEMINI_API_KEY
104
+ ```
105
+
106
+ 3. **Install backend dependencies**
107
+
108
+ ```bash
109
+ cd backend
110
+ pip install -r requirements.txt
111
+ ```
112
+
113
+ 4. **Install frontend dependencies**
114
+
115
+ ```bash
116
+ cd ../frontend
117
+ npm install
118
+ ```
119
+
120
+ 5. **Run locally (two terminals)**
121
+
122
+ Terminal 1 — Backend:
123
+ ```bash
124
+ cd backend
125
+ uvicorn main:app --reload --host 127.0.0.1 --port 8000
126
+ ```
127
+
128
+ Terminal 2 — Frontend:
129
+ ```bash
130
+ cd frontend
131
+ npm run dev
132
+ ```
133
+
134
+ Open **http://localhost:5173** — the Vite dev server proxies `/api` to the backend.
135
+
136
+ ## Environment Variables
137
+
138
+ | Variable | Required | Description |
139
+ |----------|----------|-------------|
140
+ | `GEMINI_API_KEY` | Yes | Google Gemini API key for analysis, chat, and embeddings |
141
+ | `CORS_ORIGINS` | No | Comma-separated allowed origins (default: `*`) |
142
+ | `VITE_API_URL` | No | Frontend API base URL (empty = same origin / Vite proxy) |
143
+ | `VITE_API_PROXY` | No | Vite dev proxy target (default: `http://localhost:8000`) |
144
+
145
+ ## API Endpoints
146
+
147
+ | Method | Path | Description |
148
+ |--------|------|-------------|
149
+ | `GET` | `/api/health` | Health check |
150
+ | `POST` | `/api/analyze-url` | Clone and analyze a GitHub repository |
151
+ | `POST` | `/api/analyze-zip` | Upload and analyze a ZIP archive |
152
+ | `GET` | `/api/download/{repo_id}/{type}` | Download artifact (`profile`, `graph`, `summary`, `report`) |
153
+ | `POST` | `/api/chat` | Multi-agent chat with RAG |
154
+ | `POST` | `/api/search` | Semantic search over indexed knowledge |
155
+ | `GET` | `/api/memory?repo_id=` | Vector index statistics |
156
+ | `GET` | `/api/conversations?repo_id=` | List chat sessions |
157
+ | `GET` | `/api/conversations/{session_id}` | Session message history |
158
+ | `GET` | `/api/tools` | Tool catalog |
159
+
160
+ ## Docker
161
+
162
+ ### Unified (Hugging Face / production)
163
+
164
+ ```bash
165
+ docker build -t repo-intelligence .
166
+ docker run -p 7860:7860 -e GEMINI_API_KEY=your_key repo-intelligence
167
+ ```
168
+
169
+ Open **http://localhost:7860**
170
+
171
+ ### Split stack (development)
172
+
173
+ ```bash
174
+ export GEMINI_API_KEY=your_key
175
+ docker compose up --build
176
+ ```
177
+
178
+ - Frontend: **http://localhost:5173**
179
+ - Backend: **http://localhost:8000**
180
+
181
+ ## Hugging Face Spaces Deployment
182
+
183
+ This project is ready for **free deployment** on [Hugging Face Spaces](https://huggingface.co/spaces) using the Docker SDK.
184
+
185
+ 1. Create a new Space → select **Docker** as the SDK
186
+ 2. Push this repository (or connect GitHub)
187
+ 3. Ensure the root `Dockerfile` is used (builds frontend + serves backend on port **7860**)
188
+ 4. Add a Space secret: `GEMINI_API_KEY` = your Gemini API key
189
+ 5. Wait for the build to complete
190
+
191
+ The Space will serve both the React dashboard and FastAPI backend from a single container.
192
+
193
+ ### HF Space Settings
194
+
195
+ - **SDK:** Docker
196
+ - **App port:** 7860
197
+ - **Secrets:** `GEMINI_API_KEY`
198
+
199
+ ## Generated Artifacts
200
+
201
+ After analysis, the platform produces:
202
+
203
+ | File | Description |
204
+ |------|-------------|
205
+ | `repository_report.md` | Full markdown intelligence report |
206
+ | `repository_profile.json` | Languages, frameworks, APIs, modules, auth |
207
+ | `repository_summary.json` | Elevator pitch, features, workflows, risks |
208
+ | `repository_graph.json` | Architecture nodes, edges, flows, concepts |
209
+
210
+ Artifacts are stored in `backend/storage/repos/{repo_id}/` and available via the dashboard download buttons.
211
+
212
+ ## Security
213
+
214
+ - ZIP extraction includes path-traversal protection
215
+ - GitHub PAT tokens are redacted from error messages
216
+ - Temporary clone/extract workspaces are cleaned up after analysis
217
+ - Private repos require a valid GitHub Personal Access Token
218
+
219
+ ## License
220
+
221
+ MIT
backend/.env ADDED
@@ -0,0 +1 @@
 
 
1
+ GEMINI_API_KEY=AQ.Ab8RN6JCtHvyMAUiC4ldysHtPKp2vzTXWHjWFeke1frUha3BAA
backend/Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install system dependencies (Git is required for cloning repos)
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ git \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+
10
+ # Copy requirements and install
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy backend code
15
+ COPY . .
16
+
17
+ # Expose port
18
+ EXPOSE 8000
19
+
20
+ # Start FastAPI application
21
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/__pycache__/main.cpython-312.pyc ADDED
Binary file (25.3 kB). View file
 
backend/__pycache__/test_agent_flow.cpython-312.pyc ADDED
Binary file (5.34 kB). View file
 
backend/__pycache__/test_rag_pipeline.cpython-312.pyc ADDED
Binary file (9.62 kB). View file
 
backend/adapters/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from adapters.adk_adapter import GoogleADKPlatformAdapter
2
+ from adapters.agent_adapter import ADKAgentAdapter
3
+ from adapters.planner_adapter import ADKPlannerAdapter
backend/adapters/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (327 Bytes). View file
 
backend/adapters/__pycache__/adk_adapter.cpython-312.pyc ADDED
Binary file (1.96 kB). View file
 
backend/adapters/__pycache__/agent_adapter.cpython-312.pyc ADDED
Binary file (1.55 kB). View file
 
backend/adapters/__pycache__/planner_adapter.cpython-312.pyc ADDED
Binary file (2.02 kB). View file
 
backend/adapters/adk_adapter.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from adapters.agent_adapter import ADKAgentAdapter
3
+ from adapters.planner_adapter import ADKPlannerAdapter
4
+
5
+ class GoogleADKPlatformAdapter:
6
+ """
7
+ High-level adapter class representing a Google ADK Application context.
8
+ Integrates the multi-agent layers and provides entry points for task execution.
9
+ """
10
+ def __init__(self, orchestrator: Any):
11
+ self.orchestrator = orchestrator
12
+ self.planner_adapter = ADKPlannerAdapter(orchestrator.planner)
13
+ self.agent_adapters = {
14
+ name: ADKAgentAdapter(agent)
15
+ for name, agent in orchestrator.agents.items()
16
+ }
17
+
18
+ async def dispatch_query(
19
+ self,
20
+ profile: Dict[str, Any],
21
+ graph: Dict[str, Any],
22
+ summary: Dict[str, Any],
23
+ report: str,
24
+ query: str
25
+ ) -> Dict[str, Any]:
26
+ """
27
+ Executes queries by forwarding them to the underlying orchestrator,
28
+ representing a standard ADK execution cycle.
29
+ """
30
+ return await self.orchestrator.execute(
31
+ profile=profile,
32
+ graph=graph,
33
+ summary=summary,
34
+ report=report,
35
+ query=query
36
+ )
backend/adapters/agent_adapter.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from agents.base_agent import BaseAgent
3
+
4
+ class ADKAgentAdapter:
5
+ """
6
+ Adapter wrapping existing BaseAgent specialized agents to conform to Google ADK
7
+ agent invocation schemas.
8
+ """
9
+ def __init__(self, agent: BaseAgent):
10
+ self.agent = agent
11
+ self.name = agent.__class__.__name__
12
+
13
+ async def execute_task(self, context: Dict[str, Any], query: str) -> Dict[str, Any]:
14
+ profile = context.get("profile", {})
15
+ graph = context.get("graph", {})
16
+ summary = context.get("summary", {})
17
+ report = context.get("report", "")
18
+
19
+ # Call the underlying agent
20
+ return await self.agent.run(
21
+ profile=profile,
22
+ graph=graph,
23
+ summary=summary,
24
+ report=report,
25
+ query=query
26
+ )
backend/adapters/planner_adapter.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from typing import Dict, Any
3
+ from agents.planner_agent import PlannerAgent
4
+
5
+ class ADKPlannerAdapter:
6
+ """
7
+ Adapter wrapping the PlannerAgent to match ADK planner expectations.
8
+ """
9
+ def __init__(self, planner: PlannerAgent):
10
+ self.planner = planner
11
+
12
+ async def create_plan(self, context: Dict[str, Any], query: str) -> Dict[str, Any]:
13
+ profile = context.get("profile", {})
14
+ graph = context.get("graph", {})
15
+ summary = context.get("summary", {})
16
+ report = context.get("report", "")
17
+
18
+ # Call planner
19
+ plan = await self.planner.run(
20
+ profile=profile,
21
+ graph=graph,
22
+ summary=summary,
23
+ report=report,
24
+ query=query
25
+ )
26
+ return {
27
+ "plan_id": f"plan_{int(time.time())}",
28
+ "steps": [
29
+ {
30
+ "stage": idx + 1,
31
+ "agents": stage,
32
+ "parameters": {"query": query}
33
+ }
34
+ for idx, stage in enumerate(plan.get("execution_order", []))
35
+ ],
36
+ "reasoning": plan.get("reasoning", ""),
37
+ "selected_agents": plan.get("selected_agents", [])
38
+ }
backend/agents/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from agents.llm_client import LLMClient, GeminiLLMClient
2
+ from agents.orchestrator import AgentOrchestrator
backend/agents/api_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any
3
+ from agents.base_agent import BaseAgent, AgentResponseSchema
4
+
5
+ class ApiAgent(BaseAgent):
6
+ async def run(
7
+ self,
8
+ profile: Dict[str, Any],
9
+ graph: Dict[str, Any],
10
+ summary: Dict[str, Any],
11
+ report: str,
12
+ query: str
13
+ ) -> Dict[str, Any]:
14
+ prompt = f"""
15
+ You are the API Agent. Your responsibility is to analyze the API design of the repository, including HTTP methods, routes/endpoints, payload structures, external API dependencies, and data exchange/request-response flow.
16
+
17
+ Here is the repository context:
18
+ 1. Profile:
19
+ {json.dumps(profile, indent=2)}
20
+ 2. Graph Structure:
21
+ {json.dumps(graph, indent=2)}
22
+ 3. Summary:
23
+ {json.dumps(summary, indent=2)}
24
+ 4. Intelligence Report:
25
+ {report}
26
+
27
+ User Query:
28
+ {query}
29
+
30
+ Perform a rigorous analysis of the endpoints and API flow to answer this query. Your citations must specify specific routes or lines where endpoints are declared or configured.
31
+ Return your structured answer matching the AgentResponseSchema.
32
+ """
33
+ return await self._call_llm_json(prompt, AgentResponseSchema)
backend/agents/architecture_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any
3
+ from agents.base_agent import BaseAgent, AgentResponseSchema
4
+
5
+ class ArchitectureAgent(BaseAgent):
6
+ async def run(
7
+ self,
8
+ profile: Dict[str, Any],
9
+ graph: Dict[str, Any],
10
+ summary: Dict[str, Any],
11
+ report: str,
12
+ query: str
13
+ ) -> Dict[str, Any]:
14
+ prompt = f"""
15
+ You are the Architecture Agent. Your responsibility is to analyze the codebase structure, architectural design patterns, component relationships, data flow, business workflows, and entry points to answer the user query.
16
+
17
+ Here is the repository context:
18
+ 1. Profile:
19
+ {json.dumps(profile, indent=2)}
20
+ 2. Graph Structure:
21
+ {json.dumps(graph, indent=2)}
22
+ 3. Summary:
23
+ {json.dumps(summary, indent=2)}
24
+ 4. Intelligence Report:
25
+ {report}
26
+
27
+ User Query:
28
+ {query}
29
+
30
+ Perform a rigorous architectural analysis to answer this query. Your citations must specify files, code blocks, or components (e.g. "backend/main.py:L10-L40", "Graph Node: App.jsx").
31
+ Return your structured answer matching the AgentResponseSchema.
32
+ """
33
+ return await self._call_llm_json(prompt, AgentResponseSchema)
backend/agents/base_agent.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any, List
3
+ import asyncio
4
+ from pydantic import BaseModel, Field
5
+ from agents.llm_client import LLMClient
6
+
7
+ class AgentResponseSchema(BaseModel):
8
+ agent: str = Field(description="Name of the agent, e.g., SecurityAgent")
9
+ confidence: float = Field(description="Confidence score between 0.0 and 1.0 based on relevance and sufficiency of data")
10
+ answer: str = Field(description="Detailed answer or analysis regarding the user query")
11
+ citations: List[str] = Field(description="Source files, line numbers, or endpoints cited as reference")
12
+ reasoning: List[str] = Field(description="Step-by-step reasoning steps the agent took")
13
+
14
+ class BaseAgent(ABC):
15
+ def __init__(self, llm_client: LLMClient):
16
+ self.llm_client = llm_client
17
+
18
+ @abstractmethod
19
+ async def run(
20
+ self,
21
+ profile: Dict[str, Any],
22
+ graph: Dict[str, Any],
23
+ summary: Dict[str, Any],
24
+ report: str,
25
+ query: str
26
+ ) -> Dict[str, Any]:
27
+ """Runs the agent's analysis."""
28
+ pass
29
+
30
+ async def _call_llm_json(self, prompt: str, schema: Any, temperature: float = 0.2) -> Dict[str, Any]:
31
+ return await asyncio.to_thread(self.llm_client.generate_json, prompt, schema, temperature)
backend/agents/dependency_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any
3
+ from agents.base_agent import BaseAgent, AgentResponseSchema
4
+
5
+ class DependencyAgent(BaseAgent):
6
+ async def run(
7
+ self,
8
+ profile: Dict[str, Any],
9
+ graph: Dict[str, Any],
10
+ summary: Dict[str, Any],
11
+ report: str,
12
+ query: str
13
+ ) -> Dict[str, Any]:
14
+ prompt = f"""
15
+ You are the Dependency Agent. Your responsibility is to analyze libraries, package configurations (e.g. package.json, requirements.txt), framework selections, database integrations, cloud stack, and deployment environment setups.
16
+
17
+ Here is the repository context:
18
+ 1. Profile:
19
+ {json.dumps(profile, indent=2)}
20
+ 2. Graph Structure:
21
+ {json.dumps(graph, indent=2)}
22
+ 3. Summary:
23
+ {json.dumps(summary, indent=2)}
24
+ 4. Intelligence Report:
25
+ {report}
26
+
27
+ User Query:
28
+ {query}
29
+
30
+ Perform a rigorous analysis of project dependencies and frameworks to answer this query. Your citations must reference the package manifest files or configurations (e.g., "requirements.txt:L3", "docker-compose.yml:L5").
31
+ Return your structured answer matching the AgentResponseSchema.
32
+ """
33
+ return await self._call_llm_json(prompt, AgentResponseSchema)
backend/agents/llm_client.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any, Type
3
+ from pydantic import BaseModel
4
+
5
+ class LLMClient(ABC):
6
+ @abstractmethod
7
+ def generate_json(
8
+ self,
9
+ prompt: str,
10
+ response_schema: Type[BaseModel],
11
+ temperature: float = 0.2
12
+ ) -> Dict[str, Any]:
13
+ """Generates a structured JSON response matching the given response_schema."""
14
+ pass
15
+
16
+ class GeminiLLMClient(LLMClient):
17
+ def __init__(self, api_key: str):
18
+ from google import genai
19
+ self._client = genai.Client(api_key=api_key)
20
+
21
+ def generate_json(
22
+ self,
23
+ prompt: str,
24
+ response_schema: Type[BaseModel],
25
+ temperature: float = 0.2
26
+ ) -> Dict[str, Any]:
27
+ import json
28
+
29
+ response = self._client.models.generate_content(
30
+ model='gemini-2.5-flash',
31
+ contents=prompt,
32
+ config={
33
+ 'response_mime_type': 'application/json',
34
+ 'response_schema': response_schema,
35
+ 'temperature': temperature
36
+ }
37
+ )
38
+ if not response.text:
39
+ raise ValueError("Gemini returned empty response text.")
40
+ return json.loads(response.text)
backend/agents/onboarding_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any
3
+ from agents.base_agent import BaseAgent, AgentResponseSchema
4
+
5
+ class OnboardingAgent(BaseAgent):
6
+ async def run(
7
+ self,
8
+ profile: Dict[str, Any],
9
+ graph: Dict[str, Any],
10
+ summary: Dict[str, Any],
11
+ report: str,
12
+ query: str
13
+ ) -> Dict[str, Any]:
14
+ prompt = f"""
15
+ You are the Onboarding Agent. Your responsibility is to guide new developers in understanding the project execution flow, configuring local developer environments, locating entry point scripts, outlining recommended learning paths, and listing the key folders/files to read first.
16
+
17
+ Here is the repository context:
18
+ 1. Profile:
19
+ {json.dumps(profile, indent=2)}
20
+ 2. Graph Structure:
21
+ {json.dumps(graph, indent=2)}
22
+ 3. Summary:
23
+ {json.dumps(summary, indent=2)}
24
+ 4. Intelligence Report:
25
+ {report}
26
+
27
+ User Query:
28
+ {query}
29
+
30
+ Perform a rigorous developer-focused onboarding walkthrough to answer this query. Your citations must point out documentation, entry files, or configuration variables that speed up developer setup.
31
+ Return your structured answer matching the AgentResponseSchema.
32
+ """
33
+ return await self._call_llm_json(prompt, AgentResponseSchema)
backend/agents/orchestrator.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import asyncio
3
+ import logging
4
+ import json
5
+ from typing import Dict, Any, List, Optional
6
+ from agents.llm_client import LLMClient
7
+ from agents.planner_agent import PlannerAgent
8
+ from agents.architecture_agent import ArchitectureAgent
9
+ from agents.security_agent import SecurityAgent
10
+ from agents.api_agent import ApiAgent
11
+ from agents.dependency_agent import DependencyAgent
12
+ from agents.quality_agent import QualityAgent
13
+ from agents.onboarding_agent import OnboardingAgent
14
+ from agents.response_synthesizer import ResponseSynthesizer
15
+ from tools.tool_registry import tool_registry, setup_default_registry
16
+ from memory.memory_cache import memory_cache
17
+ from memory.conversation_manager import conversation_manager
18
+
19
+ logger = logging.getLogger("orchestrator")
20
+
21
+ class AgentOrchestrator:
22
+ def __init__(self, llm_client: LLMClient):
23
+ self.llm_client = llm_client
24
+ self.planner = PlannerAgent(llm_client)
25
+ self.synthesizer = ResponseSynthesizer(llm_client)
26
+
27
+ # Register specialized agents
28
+ self.agents = {
29
+ "ArchitectureAgent": ArchitectureAgent(llm_client),
30
+ "SecurityAgent": SecurityAgent(llm_client),
31
+ "ApiAgent": ApiAgent(llm_client),
32
+ "DependencyAgent": DependencyAgent(llm_client),
33
+ "QualityAgent": QualityAgent(llm_client),
34
+ "OnboardingAgent": OnboardingAgent(llm_client)
35
+ }
36
+
37
+ async def execute(
38
+ self,
39
+ profile: Dict[str, Any],
40
+ graph: Dict[str, Any],
41
+ summary: Dict[str, Any],
42
+ report: str,
43
+ query: str,
44
+ repo_id: str,
45
+ session_id: Optional[str] = None,
46
+ vector_store: Optional[Any] = None,
47
+ retriever: Optional[Any] = None
48
+ ) -> Dict[str, Any]:
49
+ timeline = []
50
+ agents_used = []
51
+ start_total = time.time()
52
+
53
+ # Initialize tools registry if empty and vector dependencies are available
54
+ if not tool_registry.list_tools() and vector_store and retriever:
55
+ setup_default_registry(vector_store, retriever)
56
+
57
+ # 1. Run Planner Agent to decide execution steps
58
+ logger.info(f"Running PlannerAgent for query: '{query}'")
59
+ planner_start = time.time()
60
+ try:
61
+ planner_res = await self.planner.run(profile, graph, summary, report, query)
62
+ planner_latency = time.time() - planner_start
63
+ logger.info(f"PlannerAgent completed in {planner_latency:.2f}s. Plan: {planner_res}")
64
+ timeline.append({
65
+ "agent": "PlannerAgent",
66
+ "execution_time_ms": int(planner_latency * 1000),
67
+ "status": "success",
68
+ "message": f"Pipeline: memory={planner_res.get('retrieve_memory')}, search={planner_res.get('run_semantic_search')}, tools={planner_res.get('invoke_tools')}, agents={planner_res.get('selected_agents')}"
69
+ })
70
+ except Exception as e:
71
+ planner_latency = time.time() - planner_start
72
+ logger.error(f"PlannerAgent failed: {e}")
73
+ timeline.append({
74
+ "agent": "PlannerAgent",
75
+ "execution_time_ms": int(planner_latency * 1000),
76
+ "status": "failure",
77
+ "message": f"Planner failed: {str(e)}"
78
+ })
79
+ # Fallback plan: enable all pipeline elements and run all agents
80
+ planner_res = {
81
+ "retrieve_memory": True,
82
+ "run_semantic_search": True,
83
+ "invoke_tools": [],
84
+ "run_agents": True,
85
+ "selected_agents": list(self.agents.keys()),
86
+ "execution_order": [list(self.agents.keys())],
87
+ "synthesize_final_answer": True,
88
+ "reasoning": "Fallback plan due to planner failure."
89
+ }
90
+
91
+ retrieve_memory = planner_res.get("retrieve_memory", True)
92
+ run_semantic_search = planner_res.get("run_semantic_search", True)
93
+ invoke_tools = planner_res.get("invoke_tools", [])
94
+ run_agents = planner_res.get("run_agents", True)
95
+ selected_agents = planner_res.get("selected_agents", [])
96
+ execution_order = planner_res.get("execution_order", [])
97
+ synthesize_final_answer = planner_res.get("synthesize_final_answer", True)
98
+
99
+ # 2. Retrieve Memory if requested
100
+ memory_context = ""
101
+ if retrieve_memory and session_id:
102
+ logger.info(f"Retrieving conversation memory for session {session_id}")
103
+ mem_start = time.time()
104
+ session = conversation_manager.get_session(session_id)
105
+ if session and session.history:
106
+ past_turns = []
107
+ for msg in session.history[:-1]: # Exclude current question which was already added
108
+ past_turns.append(f"{msg.role.capitalize()}: {msg.content}")
109
+ memory_context = "\n".join(past_turns)
110
+ timeline.append({
111
+ "agent": "MemoryRetrieval",
112
+ "execution_time_ms": int((time.time() - mem_start) * 1000),
113
+ "status": "success",
114
+ "message": "Retrieved past turns" if memory_context else "No past turns found"
115
+ })
116
+
117
+ # 3. Run Semantic Search if requested
118
+ search_results = []
119
+ if run_semantic_search and retriever:
120
+ logger.info("Running semantic search retrieval")
121
+ search_start = time.time()
122
+ try:
123
+ search_results = retriever.retrieve(repo_id=repo_id, query=query, top_k=5)
124
+ timeline.append({
125
+ "agent": "SemanticSearch",
126
+ "execution_time_ms": int((time.time() - search_start) * 1000),
127
+ "status": "success",
128
+ "message": f"Found {len(search_results)} relevant chunks"
129
+ })
130
+ except Exception as e:
131
+ logger.error(f"Semantic search failed: {e}")
132
+ timeline.append({
133
+ "agent": "SemanticSearch",
134
+ "execution_time_ms": int((time.time() - search_start) * 1000),
135
+ "status": "failure",
136
+ "message": str(e)
137
+ })
138
+
139
+ # 4. Invoke Tools (with caching) if requested
140
+ tool_outputs = {}
141
+ if invoke_tools:
142
+ logger.info(f"Invoking tools: {invoke_tools}")
143
+ tool_start = time.time()
144
+ for tool_name in invoke_tools:
145
+ # Check memory cache first
146
+ cache_key = f"tool:{repo_id}:{tool_name}:{query}"
147
+ cached_res = memory_cache.get(cache_key)
148
+ if cached_res is not None:
149
+ logger.info(f"Cache hit for tool '{tool_name}'")
150
+ tool_outputs[tool_name] = cached_res
151
+ else:
152
+ logger.info(f"Cache miss for tool '{tool_name}'. Executing...")
153
+ res = tool_registry.execute_tool(tool_name, repo_id=repo_id, query=query)
154
+ memory_cache.set(cache_key, res, ttl=300) # Cache for 5 mins
155
+ tool_outputs[tool_name] = res
156
+ timeline.append({
157
+ "agent": "ToolInvocation",
158
+ "execution_time_ms": int((time.time() - tool_start) * 1000),
159
+ "status": "success",
160
+ "message": f"Executed {len(tool_outputs)} tools"
161
+ })
162
+
163
+ # 5. Run Specialized Agents if requested
164
+ agent_responses = []
165
+ if run_agents and selected_agents:
166
+ # Construct augmented query containing retrieved context and tool outputs
167
+ augmented_context_parts = []
168
+ if memory_context:
169
+ augmented_context_parts.append(f"--- Conversation History ---\n{memory_context}")
170
+ if search_results:
171
+ search_text = "\n\n".join(
172
+ f"[Relevant Code Chunk | similarity={r['similarity']:.2f}]\n{r['content']}"
173
+ for r in search_results
174
+ )
175
+ augmented_context_parts.append(f"--- Codebase Semantic Search Results ---\n{search_text}")
176
+ if tool_outputs:
177
+ tools_text = json.dumps(tool_outputs, indent=2)
178
+ augmented_context_parts.append(f"--- Direct Tool Outputs ---\n{tools_text}")
179
+
180
+ augmented_query = query
181
+ if augmented_context_parts:
182
+ augmented_query = f"{query}\n\n" + "\n\n".join(augmented_context_parts)
183
+
184
+ for stage_idx, stage in enumerate(execution_order):
185
+ tasks = []
186
+ agent_names = []
187
+
188
+ for agent_name in stage:
189
+ if agent_name in self.agents and agent_name in selected_agents:
190
+ agent_names.append(agent_name)
191
+ tasks.append(self._run_agent_with_retry(agent_name, profile, graph, summary, report, augmented_query))
192
+
193
+ if not tasks:
194
+ continue
195
+
196
+ logger.info(f"Executing Stage {stage_idx + 1} with agents in parallel: {agent_names}")
197
+ stage_results = await asyncio.gather(*tasks, return_exceptions=True)
198
+
199
+ for agent_name, result in zip(agent_names, stage_results):
200
+ agents_used.append(agent_name)
201
+
202
+ if isinstance(result, Exception):
203
+ logger.error(f"Agent {agent_name} failed execution: {result}")
204
+ timeline.append({
205
+ "agent": agent_name,
206
+ "execution_time_ms": 0,
207
+ "status": "failure",
208
+ "message": str(result),
209
+ "confidence": 0.0
210
+ })
211
+ else:
212
+ agent_responses.append(result["response"])
213
+ timeline.append({
214
+ "agent": agent_name,
215
+ "execution_time_ms": result["latency_ms"],
216
+ "status": "success",
217
+ "confidence": result["response"].get("confidence", 0.0),
218
+ "answer": result["response"].get("answer", "")
219
+ })
220
+
221
+ # 6. Run Response Synthesizer if requested
222
+ synth_res = {}
223
+ if synthesize_final_answer:
224
+ logger.info("Running ResponseSynthesizer...")
225
+ synth_start = time.time()
226
+ try:
227
+ synth_res = await self.synthesizer.synthesize(
228
+ query=query,
229
+ agent_responses=agent_responses,
230
+ memory_context=memory_context,
231
+ search_results=search_results,
232
+ tool_outputs=tool_outputs
233
+ )
234
+ synth_latency = time.time() - synth_start
235
+ logger.info(f"ResponseSynthesizer completed in {synth_latency:.2f}s")
236
+ timeline.append({
237
+ "agent": "ResponseSynthesizer",
238
+ "execution_time_ms": int(synth_latency * 1000),
239
+ "status": "success"
240
+ })
241
+ except Exception as e:
242
+ synth_latency = time.time() - synth_start
243
+ logger.error(f"ResponseSynthesizer failed: {e}")
244
+ timeline.append({
245
+ "agent": "ResponseSynthesizer",
246
+ "execution_time_ms": int(synth_latency * 1000),
247
+ "status": "failure",
248
+ "message": str(e)
249
+ })
250
+ # Fallback synthesis
251
+ fallback_answer = "\n\n".join([f"### {r.get('agent')}\n{r.get('answer')}" for r in agent_responses])
252
+ synth_res = {
253
+ "summary": "Fallback summary compiled from individual agents.",
254
+ "detailed_explanation": fallback_answer,
255
+ "agent_contributions": [f"{r.get('agent')} (direct contribution)" for r in agent_responses],
256
+ "confidence_score": 0.5
257
+ }
258
+ else:
259
+ # Synthesis bypassed, compile direct report from tools/search
260
+ logger.info("Bypassing ResponseSynthesizer per Planner decision")
261
+ direct_parts = []
262
+ if tool_outputs:
263
+ direct_parts.append("### Direct Tool Outputs")
264
+ for t_name, val in tool_outputs.items():
265
+ direct_parts.append(f"**{t_name}**:\n```json\n{json.dumps(val, indent=2)}\n```")
266
+ if search_results:
267
+ direct_parts.append("### Codebase Search Results")
268
+ for r in search_results:
269
+ direct_parts.append(f"- **{r['metadata'].get('path', 'unknown')}** (similarity={r['similarity']:.2f}):\n{r['content']}")
270
+
271
+ detailed_explanation = "\n\n".join(direct_parts) if direct_parts else "No tools or search results were requested, and LLM synthesis was bypassed."
272
+ synth_res = {
273
+ "summary": "Direct result compiled from tools/search.",
274
+ "detailed_explanation": detailed_explanation,
275
+ "agent_contributions": ["Direct tool output execution."],
276
+ "confidence_score": 1.0
277
+ }
278
+ timeline.append({
279
+ "agent": "ResponseSynthesizer",
280
+ "execution_time_ms": 0,
281
+ "status": "success",
282
+ "message": "Bypassed synthesizer"
283
+ })
284
+
285
+ total_time_ms = int((time.time() - start_total) * 1000)
286
+
287
+ # Merge citations from all agent responses and search results
288
+ references = []
289
+ for r in agent_responses:
290
+ references.extend(r.get("citations", []))
291
+ for r in search_results:
292
+ path = r["metadata"].get("path")
293
+ if path and path not in references:
294
+ references.append(path)
295
+
296
+ unique_references = []
297
+ for ref in references:
298
+ if ref not in unique_references:
299
+ unique_references.append(ref)
300
+
301
+ answer = synth_res.get("detailed_explanation", "") or ""
302
+ if not answer:
303
+ logger.warning("Synthesized response was empty. Falling back to agent answers or summary.")
304
+ if agent_responses:
305
+ fallback_answer = "\n\n".join(
306
+ f"### {r.get('agent', 'UnnamedAgent')}\n{r.get('answer', '') or 'No answer generated.'}"
307
+ for r in agent_responses
308
+ ).strip()
309
+ answer = fallback_answer or synth_res.get("summary", "")
310
+ else:
311
+ answer = synth_res.get("summary", "No answer could be generated from the agents.")
312
+
313
+ return {
314
+ "answer": answer,
315
+ "summary": synth_res.get("summary", ""),
316
+ "agents_used": agents_used,
317
+ "confidence": synth_res.get("confidence_score", 0.0),
318
+ "references": unique_references,
319
+ "agent_contributions": synth_res.get("agent_contributions", []),
320
+ "planner_decision": planner_res,
321
+ "timeline": timeline,
322
+ "total_time_ms": total_time_ms,
323
+ "retrieved_context": search_results
324
+ }
325
+
326
+ async def _run_agent_with_retry(
327
+ self,
328
+ agent_name: str,
329
+ profile: Dict[str, Any],
330
+ graph: Dict[str, Any],
331
+ summary: Dict[str, Any],
332
+ report: str,
333
+ query: str,
334
+ retries: int = 2
335
+ ) -> Dict[str, Any]:
336
+ agent = self.agents[agent_name]
337
+
338
+ for attempt in range(retries + 1):
339
+ start = time.time()
340
+ try:
341
+ response = await agent.run(profile, graph, summary, report, query)
342
+ latency_ms = int((time.time() - start) * 1000)
343
+ return {
344
+ "response": response,
345
+ "latency_ms": latency_ms
346
+ }
347
+ except Exception as e:
348
+ logger.warning(f"Agent {agent_name} attempt {attempt + 1} failed: {e}")
349
+ if attempt == retries:
350
+ raise e
351
+ await asyncio.sleep(0.5)
backend/agents/planner_agent.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any, List
3
+ from pydantic import BaseModel, Field
4
+ from agents.base_agent import BaseAgent
5
+
6
+ class PlannerDecision(BaseModel):
7
+ retrieve_memory: bool = Field(
8
+ description="Whether to retrieve past conversation history or context for the current query."
9
+ )
10
+ run_semantic_search: bool = Field(
11
+ description="Whether to query the vector database for matching chunks from the codebase."
12
+ )
13
+ invoke_tools: List[str] = Field(
14
+ description="List of tool names to invoke, from: ['repository_search', 'graph_query', 'dependency_lookup', 'file_reader', 'architecture_lookup', 'api_lookup']."
15
+ )
16
+ run_agents: bool = Field(
17
+ description="Whether specialized agents should be executed."
18
+ )
19
+ selected_agents: List[str] = Field(
20
+ description="List of agent names selected to run if run_agents is true, from: ['ArchitectureAgent', 'SecurityAgent', 'ApiAgent', 'DependencyAgent', 'QualityAgent', 'OnboardingAgent']."
21
+ )
22
+ execution_order: List[List[str]] = Field(
23
+ description="Execution order for agents (e.g., [['SecurityAgent', 'ApiAgent'], ['ArchitectureAgent']]) if run_agents is true."
24
+ )
25
+ synthesize_final_answer: bool = Field(
26
+ description="Whether the response synthesizer should combine everything into the final answer. Set to True unless a direct tool/search result is sufficient."
27
+ )
28
+ reasoning: str = Field(
29
+ description="Explanation of why these pipeline decisions and agent executions were chosen."
30
+ )
31
+
32
+ class PlannerAgent(BaseAgent):
33
+ async def run(
34
+ self,
35
+ profile: Dict[str, Any],
36
+ graph: Dict[str, Any],
37
+ summary: Dict[str, Any],
38
+ report: str,
39
+ query: str
40
+ ) -> Dict[str, Any]:
41
+ prompt = f"""
42
+ You are the Agent Orchestration Planner. Your task is to analyze the user's query and decide the best execution pipeline to resolve it.
43
+
44
+ The execution pipeline consists of:
45
+ 1. retrieve_memory: Fetching past chat history/turns.
46
+ 2. run_semantic_search: Finding relevant code snippets via vector embeddings.
47
+ 3. invoke_tools: Executing direct lookup/search tools.
48
+ 4. run_agents: Launching specialized agents in parallel or sequential stages.
49
+ 5. synthesize_final_answer: Combining all info into a clean final markdown explanation.
50
+
51
+ Available Tools:
52
+ - repository_search: Semantic search over indexed repo chunks.
53
+ - graph_query: Query dependency graph, business flows, entry points.
54
+ - dependency_lookup: Look up project languages, frameworks, packages.
55
+ - file_reader: Retrieve specific source code file content.
56
+ - architecture_lookup: Query high-level architecture pattern and major folders.
57
+ - api_lookup: Look up HTTP endpoints, authentication details.
58
+
59
+ Available Agents:
60
+ 1. ArchitectureAgent: Focuses on architectural patterns, component interaction, data flow, component responsibilities, and business flows.
61
+ 2. SecurityAgent: Focuses on authentication, authorization, API keys, secrets, security risks, vulnerability findings, and unsafe practices.
62
+ 3. ApiAgent: Focuses on endpoints, routes, HTTP methods, request/response models, and external APIs.
63
+ 4. DependencyAgent: Focuses on libraries, frameworks, cloud stack, dependencies, and infrastructure setup.
64
+ 5. QualityAgent: Focuses on complexity, maintainability, dead code, refactoring suggestions, and testing hints.
65
+ 6. OnboardingAgent: Focuses on developer onboarding walkthrough, where to start reading, and execution setup.
66
+
67
+ Repository Profile:
68
+ {json.dumps(profile, indent=2)}
69
+
70
+ Repository Summary:
71
+ {json.dumps(summary, indent=2)}
72
+
73
+ User Query:
74
+ {query}
75
+
76
+ Determine:
77
+ 1. Which pipeline components should run (retrieve_memory, run_semantic_search, invoke_tools, run_agents, synthesize_final_answer).
78
+ 2. Which agents and tools are needed and their execution plan.
79
+ 3. The reasoning behind your plan.
80
+
81
+ Return your decision in structured JSON format matching the schema.
82
+ """
83
+ return await self._call_llm_json(prompt, PlannerDecision, temperature=0.1)
backend/agents/quality_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any
3
+ from agents.base_agent import BaseAgent, AgentResponseSchema
4
+
5
+ class QualityAgent(BaseAgent):
6
+ async def run(
7
+ self,
8
+ profile: Dict[str, Any],
9
+ graph: Dict[str, Any],
10
+ summary: Dict[str, Any],
11
+ report: str,
12
+ query: str
13
+ ) -> Dict[str, Any]:
14
+ prompt = f"""
15
+ You are the Quality Agent. Your responsibility is to analyze the codebase for code quality, maintainability, architectural complexity, indicators of dead code, large/bloated modules, potential code smells, and testing/coverage strategies.
16
+
17
+ Here is the repository context:
18
+ 1. Profile:
19
+ {json.dumps(profile, indent=2)}
20
+ 2. Graph Structure:
21
+ {json.dumps(graph, indent=2)}
22
+ 3. Summary:
23
+ {json.dumps(summary, indent=2)}
24
+ 4. Intelligence Report:
25
+ {report}
26
+
27
+ User Query:
28
+ {query}
29
+
30
+ Perform a rigorous analysis of the code quality and maintainability indicators to answer this query. Your citations must specify modules or files containing smells, complex blocks, or missing test configurations.
31
+ Return your structured answer matching the AgentResponseSchema.
32
+ """
33
+ return await self._call_llm_json(prompt, AgentResponseSchema)
backend/agents/response_synthesizer.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any, List, Optional
3
+ from pydantic import BaseModel, Field
4
+ from agents.llm_client import LLMClient
5
+
6
+ class SynthesizedResponse(BaseModel):
7
+ summary: str = Field(description="A concise summary of all findings across agents")
8
+ detailed_explanation: str = Field(description="A comprehensive, detailed markdown explanation combining all insights, resolving duplicates, and directly answering the user query")
9
+ agent_contributions: List[str] = Field(description="List of strings explaining what each agent contributed (e.g. 'SecurityAgent: identified lack of rate limiting on the login route.')")
10
+ confidence_score: float = Field(description="A combined confidence score representing the quality and consensus of the generated answer (0.0 to 1.0)")
11
+
12
+ class ResponseSynthesizer:
13
+ def __init__(self, llm_client: LLMClient):
14
+ self.llm_client = llm_client
15
+
16
+ async def synthesize(
17
+ self,
18
+ query: str,
19
+ agent_responses: List[Dict[str, Any]],
20
+ memory_context: Optional[str] = None,
21
+ search_results: Optional[List[Dict[str, Any]]] = None,
22
+ tool_outputs: Optional[Dict[str, Any]] = None
23
+ ) -> Dict[str, Any]:
24
+ import asyncio
25
+ responses_str = json.dumps(agent_responses, indent=2)
26
+
27
+ context_parts = []
28
+ if memory_context:
29
+ context_parts.append(f"--- Past Conversation turns ---\n{memory_context}")
30
+ if search_results:
31
+ search_text = "\n\n".join(
32
+ f"[Search similarity={r['similarity']:.2f}]\n{r['content']}"
33
+ for r in search_results
34
+ )
35
+ context_parts.append(f"--- Semantic Search Context ---\n{search_text}")
36
+ if tool_outputs:
37
+ tools_text = json.dumps(tool_outputs, indent=2)
38
+ context_parts.append(f"--- Direct Tool Outputs ---\n{tools_text}")
39
+
40
+ extra_context = "\n\n".join(context_parts)
41
+
42
+ prompt = f"""
43
+ You are the Principal Systems Integrator and Response Synthesizer.
44
+ Your goal is to digest all specialized agent reports, retrieve context, tool outputs, and compile them into a single, cohesive, premium response answering the user's query.
45
+
46
+ User Query:
47
+ {query}
48
+
49
+ {extra_context}
50
+
51
+ Specialized Agent Responses:
52
+ {responses_str}
53
+
54
+ Tasks:
55
+ 1. Synthesize a single coherent, authoritative detailed explanation in markdown.
56
+ 2. Merge duplicate findings.
57
+ 3. Call out specific contributions or viewpoints of the agents/tools.
58
+ 4. Calculate an overall confidence score based on individual agent confidence levels, tool relevance, and consensus.
59
+ 5. Create a concise summary.
60
+
61
+ Return the result matching the schema in structured JSON format.
62
+ """
63
+ return await asyncio.to_thread(self.llm_client.generate_json, prompt, SynthesizedResponse, 0.2)
backend/agents/security_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any
3
+ from agents.base_agent import BaseAgent, AgentResponseSchema
4
+
5
+ class SecurityAgent(BaseAgent):
6
+ async def run(
7
+ self,
8
+ profile: Dict[str, Any],
9
+ graph: Dict[str, Any],
10
+ summary: Dict[str, Any],
11
+ report: str,
12
+ query: str
13
+ ) -> Dict[str, Any]:
14
+ prompt = f"""
15
+ You are the Security Agent. Your responsibility is to analyze the codebase for authentication methods, authorization mechanisms, handling of secrets/API keys, safety vulnerabilities, unsafe coding practices, and package dependencies that pose risks.
16
+
17
+ Here is the repository context:
18
+ 1. Profile:
19
+ {json.dumps(profile, indent=2)}
20
+ 2. Graph Structure:
21
+ {json.dumps(graph, indent=2)}
22
+ 3. Summary:
23
+ {json.dumps(summary, indent=2)}
24
+ 4. Intelligence Report:
25
+ {report}
26
+
27
+ User Query:
28
+ {query}
29
+
30
+ Perform a rigorous security analysis to answer this query. Your citations must specify files, lines, or configurations where security measures are implemented, missing, or potentially vulnerable.
31
+ Return your structured answer matching the AgentResponseSchema.
32
+ """
33
+ return await self._call_llm_json(prompt, AgentResponseSchema)
backend/main.py ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import shutil
4
+ import tempfile
5
+ import uuid
6
+ import logging
7
+ from typing import Optional
8
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Header, status
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import FileResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from starlette.background import BackgroundTask
13
+ from pydantic import BaseModel
14
+
15
+ # Load .env variables manually if the file is present
16
+ env_path = os.path.join(os.path.dirname(__file__), ".env")
17
+ if os.path.exists(env_path):
18
+ with open(env_path, "r", encoding="utf-8") as f:
19
+ for line in f:
20
+ line = line.strip()
21
+ if line and not line.startswith("#") and "=" in line:
22
+ key, val = line.split("=", 1)
23
+ os.environ[key.strip()] = val.strip()
24
+
25
+ # Import services
26
+ from services.repositoryScanner import (
27
+ check_repository_privacy,
28
+ clone_repository,
29
+ extract_zip,
30
+ scan_directory,
31
+ handle_remove_readonly
32
+ )
33
+ from services.repositoryProfiler import profile_repository
34
+ from services.graphBuilder import build_initial_graph
35
+ from services.llmAnalyzer import analyze_repository
36
+ from services.repositoryMemory import memory_service
37
+
38
+ # Phase 3: Memory, RAG, Tools
39
+ from memory.embedding_service import EmbeddingService
40
+ from memory.vector_store import VectorStore
41
+ from memory.knowledge_index import KnowledgeIndexBuilder
42
+ from memory.retriever import KnowledgeRetriever
43
+ from memory.conversation_manager import conversation_manager
44
+ from memory.session_manager import session_manager
45
+ from memory.memory_cache import memory_cache
46
+ # Singleton memory infrastructure (initialised lazily per-request)
47
+ _vector_store: VectorStore = None
48
+
49
+ def get_vector_store() -> VectorStore:
50
+ global _vector_store
51
+ if _vector_store is None:
52
+ _vector_store = VectorStore()
53
+ return _vector_store
54
+
55
+ # Setup logging
56
+ logging.basicConfig(level=logging.INFO)
57
+ logger = logging.getLogger("main")
58
+
59
+ app = FastAPI(
60
+ title="Repository Intelligence API",
61
+ description="Foundational Memory and Intelligence Layer for Multi-Agent AI Software Engineering",
62
+ version="1.0.0"
63
+ )
64
+
65
+ # Enable CORS for frontend integration (credentials disabled when using wildcard origins)
66
+ _cors_origins = [o.strip() for o in os.environ.get("CORS_ORIGINS", "*").split(",") if o.strip()]
67
+ app.add_middleware(
68
+ CORSMiddleware,
69
+ allow_origins=_cors_origins,
70
+ allow_credentials="*" not in _cors_origins,
71
+ allow_methods=["*"],
72
+ allow_headers=["*"],
73
+ )
74
+
75
+ # Request schema for analyzing git repository
76
+ class AnalyzeUrlRequest(BaseModel):
77
+ url: str
78
+ token: Optional[str] = None
79
+
80
+ @app.get("/api/health")
81
+ def health_check():
82
+ return {"status": "healthy"}
83
+
84
+ @app.post("/api/analyze-url")
85
+ async def analyze_git_url(
86
+ request: AnalyzeUrlRequest,
87
+ x_gemini_key: Optional[str] = Header(None)
88
+ ):
89
+ """
90
+ Clones a GitHub repository, validates access permissions, processes the pipeline,
91
+ calls Gemini 2.5 Flash, and returns structured intelligence outputs.
92
+ """
93
+ repo_url = request.url
94
+ token = request.token
95
+ gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY")
96
+
97
+ if not gemini_key:
98
+ raise HTTPException(
99
+ status_code=status.HTTP_400_BAD_REQUEST,
100
+ detail="Gemini API Key is missing. Please provide it in the headers (x-gemini-key) or configure it on the server."
101
+ )
102
+
103
+ # 1. Validate repository privacy and access
104
+ logger.info(f"Validating access to repository: {repo_url}")
105
+ privacy_info = await check_repository_privacy(repo_url, token)
106
+
107
+ if privacy_info["status"] in ["private_requires_auth", "private_denied"]:
108
+ raise HTTPException(
109
+ status_code=status.HTTP_401_UNAUTHORIZED,
110
+ detail=privacy_info["message"]
111
+ )
112
+ elif privacy_info["status"] == "invalid":
113
+ raise HTTPException(
114
+ status_code=status.HTTP_400_BAD_REQUEST,
115
+ detail=privacy_info["message"]
116
+ )
117
+ elif privacy_info["status"] == "error":
118
+ raise HTTPException(
119
+ status_code=status.HTTP_502_BAD_GATEWAY,
120
+ detail=privacy_info["message"]
121
+ )
122
+
123
+ owner_repo = privacy_info["owner_repo"] or "cloned_repo"
124
+ repo_id = str(uuid.uuid4())
125
+
126
+ # Create a temporary directory in a secure, OS-agnostic manner
127
+ temp_dir = tempfile.mkdtemp(prefix="repo_intel_")
128
+
129
+ try:
130
+ # 2. Clone the repository
131
+ logger.info(f"Cloning repository into temporary directory: {temp_dir}")
132
+ clone_repository(repo_url, temp_dir, token)
133
+
134
+ # 3. Scan the repository file tree and text files
135
+ logger.info("Scanning directory structure...")
136
+ scan_results = scan_directory(temp_dir)
137
+
138
+ # 4. Generate static profile and basic relationship graph
139
+ logger.info("Generating static profiles...")
140
+ static_profile = profile_repository(scan_results["files"])
141
+ static_graph = build_initial_graph(scan_results["files"])
142
+ static_profile["static_graph"] = static_graph
143
+
144
+ # 5. Call LLM for deep reasoning and structured outputs
145
+ logger.info("Triggering Gemini 2.5 Flash intelligence analysis...")
146
+ analysis_result = await analyze_repository(
147
+ repo_name=owner_repo,
148
+ tree_structure=scan_results["tree"],
149
+ static_profile=static_profile,
150
+ flat_files=scan_results["files"],
151
+ api_key=gemini_key
152
+ )
153
+
154
+ # 6. Save outputs inside the repositoryMemory service
155
+ logger.info("Storing generated artifacts in memory layer...")
156
+ stored = memory_service.store(
157
+ repo_id=repo_id,
158
+ profile=analysis_result["profile"],
159
+ graph=analysis_result["graph"],
160
+ summary=analysis_result["summary"],
161
+ report_markdown=analysis_result["report"]
162
+ )
163
+
164
+ # 7. Phase 3: Build semantic vector index asynchronously
165
+ gemini_key_for_embed = gemini_key
166
+ try:
167
+ logger.info("Building semantic knowledge index (ChromaDB)...")
168
+ import asyncio
169
+ embedder = EmbeddingService(api_key=gemini_key_for_embed)
170
+ vs = get_vector_store()
171
+ indexer = KnowledgeIndexBuilder(embedder, vs)
172
+ await asyncio.to_thread(
173
+ indexer.build_index,
174
+ repo_id,
175
+ analysis_result["profile"],
176
+ analysis_result["summary"],
177
+ analysis_result["graph"],
178
+ analysis_result["report"],
179
+ scan_results["files"]
180
+ )
181
+ logger.info(f"Knowledge index built for repo {repo_id}.")
182
+ except Exception as idx_e:
183
+ logger.warning(f"Knowledge index build failed (non-fatal): {idx_e}")
184
+
185
+ return {
186
+ "success": True,
187
+ "repo_id": repo_id,
188
+ "project_name": owner_repo,
189
+ "tree": scan_results["tree"],
190
+ "data": stored
191
+ }
192
+
193
+ except Exception as e:
194
+ logger.error(f"Error during repository analysis: {str(e)}")
195
+ raise HTTPException(
196
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
197
+ detail=f"Analysis failed: {str(e)}"
198
+ )
199
+ finally:
200
+ # Clean up temporary directory (safe rmtree for Windows/Linux read-only files)
201
+ logger.info(f"Cleaning up temporary workspace directory: {temp_dir}")
202
+ shutil.rmtree(temp_dir, onerror=handle_remove_readonly)
203
+
204
+ @app.post("/api/analyze-zip")
205
+ async def analyze_uploaded_zip(
206
+ file: UploadFile = File(...),
207
+ x_gemini_key: Optional[str] = Header(None)
208
+ ):
209
+ """
210
+ Extracts an uploaded repository ZIP file, runs structural scanning,
211
+ generates static/dynamic profile schemas, and runs the LLM analysis.
212
+ """
213
+ gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY")
214
+
215
+ if not gemini_key:
216
+ raise HTTPException(
217
+ status_code=status.HTTP_400_BAD_REQUEST,
218
+ detail="Gemini API Key is missing. Please provide it in the headers (x-gemini-key) or configure it on the server."
219
+ )
220
+
221
+ if not file.filename.endswith(".zip"):
222
+ raise HTTPException(
223
+ status_code=status.HTTP_400_BAD_REQUEST,
224
+ detail="Invalid file format. Only ZIP archives are supported."
225
+ )
226
+
227
+ repo_id = str(uuid.uuid4())
228
+ project_name = file.filename[:-4] # Strip .zip
229
+
230
+ # Set up temporary directory and paths
231
+ temp_dir = tempfile.mkdtemp(prefix="zip_intel_")
232
+ fd, zip_path = tempfile.mkstemp(suffix=".zip")
233
+
234
+ try:
235
+ # Save ZIP upload chunk by chunk
236
+ with os.fdopen(fd, 'wb') as tmp_zip:
237
+ shutil.copyfileobj(file.file, tmp_zip)
238
+
239
+ # 1. Extract ZIP securely with path traversal protection
240
+ logger.info(f"Extracting zip archive: {file.filename}")
241
+ extract_zip(zip_path, temp_dir)
242
+
243
+ # 2. Scan directory
244
+ logger.info("Scanning unzipped directory structure...")
245
+ scan_results = scan_directory(temp_dir)
246
+
247
+ # 3. Generate static profiles
248
+ logger.info("Generating static profiles...")
249
+ static_profile = profile_repository(scan_results["files"])
250
+ static_graph = build_initial_graph(scan_results["files"])
251
+ static_profile["static_graph"] = static_graph
252
+
253
+ # 4. Trigger Gemini analysis
254
+ logger.info("Analyzing unzipped codebase with Gemini 2.5 Flash...")
255
+ analysis_result = await analyze_repository(
256
+ repo_name=project_name,
257
+ tree_structure=scan_results["tree"],
258
+ static_profile=static_profile,
259
+ flat_files=scan_results["files"],
260
+ api_key=gemini_key
261
+ )
262
+
263
+ # 5. Store generated artifacts
264
+ logger.info("Storing artifacts in memory service...")
265
+ stored = memory_service.store(
266
+ repo_id=repo_id,
267
+ profile=analysis_result["profile"],
268
+ graph=analysis_result["graph"],
269
+ summary=analysis_result["summary"],
270
+ report_markdown=analysis_result["report"]
271
+ )
272
+
273
+ # 6. Phase 3: Build semantic vector index
274
+ try:
275
+ logger.info("Building semantic knowledge index for ZIP repo...")
276
+ import asyncio
277
+ embedder = EmbeddingService(api_key=gemini_key)
278
+ vs = get_vector_store()
279
+ indexer = KnowledgeIndexBuilder(embedder, vs)
280
+ await asyncio.to_thread(
281
+ indexer.build_index,
282
+ repo_id,
283
+ analysis_result["profile"],
284
+ analysis_result["summary"],
285
+ analysis_result["graph"],
286
+ analysis_result["report"],
287
+ scan_results["files"]
288
+ )
289
+ logger.info(f"Knowledge index built for ZIP repo {repo_id}.")
290
+ except Exception as idx_e:
291
+ logger.warning(f"Knowledge index build failed (non-fatal): {idx_e}")
292
+
293
+ return {
294
+ "success": True,
295
+ "repo_id": repo_id,
296
+ "project_name": project_name,
297
+ "tree": scan_results["tree"],
298
+ "data": stored
299
+ }
300
+
301
+ except Exception as e:
302
+ logger.error(f"Error processing uploaded zip file: {str(e)}")
303
+ raise HTTPException(
304
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
305
+ detail=f"Analysis failed: {str(e)}"
306
+ )
307
+ finally:
308
+ # Cleanup
309
+ logger.info(f"Cleaning up temporary workspace files...")
310
+ if os.path.exists(zip_path):
311
+ os.remove(zip_path)
312
+ shutil.rmtree(temp_dir, onerror=handle_remove_readonly)
313
+
314
+ def _cleanup_temp_dir(path: str) -> None:
315
+ if os.path.exists(path):
316
+ shutil.rmtree(path, ignore_errors=True)
317
+
318
+
319
+ @app.get("/api/download/{repo_id}/{artifact_type}")
320
+ async def download_intelligence_artifact(repo_id: str, artifact_type: str):
321
+ """
322
+ Downloads specific intelligence output as files (profile.json, graph.json, summary.json, report.md)
323
+ """
324
+ data = memory_service.retrieve(repo_id)
325
+ if not data:
326
+ raise HTTPException(
327
+ status_code=status.HTTP_404_NOT_FOUND,
328
+ detail="Repository intelligence data not found or has expired."
329
+ )
330
+
331
+ artifact_map = {
332
+ "profile": ("repository_profile.json", "application/json", lambda d: json.dumps(d["profile"], indent=2)),
333
+ "graph": ("repository_graph.json", "application/json", lambda d: json.dumps(d["graph"], indent=2)),
334
+ "summary": ("repository_summary.json", "application/json", lambda d: json.dumps(d["summary"], indent=2)),
335
+ "report": ("repository_report.md", "text/markdown", lambda d: d["report"]),
336
+ }
337
+
338
+ if artifact_type not in artifact_map:
339
+ raise HTTPException(
340
+ status_code=status.HTTP_400_BAD_REQUEST,
341
+ detail=f"Invalid artifact type: {artifact_type}"
342
+ )
343
+
344
+ filename, media_type, content_fn = artifact_map[artifact_type]
345
+ temp_dir = tempfile.mkdtemp()
346
+ file_path = os.path.join(temp_dir, filename)
347
+
348
+ try:
349
+ with open(file_path, "w", encoding="utf-8") as f:
350
+ f.write(content_fn(data))
351
+ return FileResponse(
352
+ file_path,
353
+ media_type=media_type,
354
+ filename=filename,
355
+ background=BackgroundTask(_cleanup_temp_dir, temp_dir),
356
+ )
357
+ except Exception as e:
358
+ _cleanup_temp_dir(temp_dir)
359
+ logger.error(f"Error compiling download file: {str(e)}")
360
+ raise HTTPException(
361
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
362
+ detail="Failed to generate download file."
363
+ )
364
+
365
+ class ChatRequest(BaseModel):
366
+ repo_id: str
367
+ question: str
368
+ session_id: Optional[str] = None
369
+
370
+ @app.post("/api/chat")
371
+ async def chat_with_repo(
372
+ request: ChatRequest,
373
+ x_gemini_key: Optional[str] = Header(None)
374
+ ):
375
+ """
376
+ Phase 2+3: Executes the multi-agent orchestration pipeline with RAG context
377
+ retrieval and conversation memory.
378
+ """
379
+ import time
380
+ import asyncio
381
+ from agents.llm_client import GeminiLLMClient
382
+ from agents.orchestrator import AgentOrchestrator
383
+
384
+ gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY")
385
+ if not gemini_key:
386
+ raise HTTPException(
387
+ status_code=status.HTTP_400_BAD_REQUEST,
388
+ detail="Gemini API Key is missing."
389
+ )
390
+
391
+ repo_data = memory_service.retrieve(request.repo_id)
392
+ if not repo_data:
393
+ raise HTTPException(
394
+ status_code=status.HTTP_404_NOT_FOUND,
395
+ detail="Repository intelligence data not found or has expired."
396
+ )
397
+
398
+ session_id = request.session_id or str(uuid.uuid4())
399
+
400
+ # Add user message to memory first
401
+ conversation_manager.add_message(
402
+ session_id=session_id,
403
+ repo_id=request.repo_id,
404
+ role="user",
405
+ content=request.question
406
+ )
407
+
408
+ try:
409
+ llm_client = GeminiLLMClient(api_key=gemini_key)
410
+ orchestrator = AgentOrchestrator(llm_client)
411
+
412
+ embedder = EmbeddingService(api_key=gemini_key)
413
+ retriever = KnowledgeRetriever(embedder, get_vector_store())
414
+
415
+ logger.info(f"Orchestrating agents for repo {request.repo_id}: '{request.question}'")
416
+
417
+ # Run pipeline
418
+ result = await orchestrator.execute(
419
+ profile=repo_data["profile"],
420
+ graph=repo_data["graph"],
421
+ summary=repo_data["summary"],
422
+ report=repo_data["report"],
423
+ query=request.question,
424
+ repo_id=request.repo_id,
425
+ session_id=session_id,
426
+ vector_store=get_vector_store(),
427
+ retriever=retriever
428
+ )
429
+
430
+ # Attach session and RAG details
431
+ result["session_id"] = session_id
432
+
433
+ # Populate retrieved context on user message
434
+ session = conversation_manager.get_session(session_id)
435
+ if session and session.history:
436
+ session.history[-1].retrieved_context = result.get("retrieved_context", [])
437
+
438
+ # Store assistant response in conversation memory
439
+ conversation_manager.add_message(
440
+ session_id=session_id,
441
+ repo_id=request.repo_id,
442
+ role="assistant",
443
+ content=result.get("answer", ""),
444
+ agent_decisions={
445
+ "agents_used": result.get("agents_used", []),
446
+ "confidence": result.get("confidence", 0.0),
447
+ "planner_decision": result.get("planner_decision", {})
448
+ }
449
+ )
450
+
451
+ # Persist conversation sessions to disk
452
+ session_manager.save_all()
453
+
454
+ return result
455
+
456
+ except Exception as e:
457
+ logger.error(f"Chat orchestration error: {str(e)}")
458
+ raise HTTPException(
459
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
460
+ detail=f"Chat execution failed: {str(e)}"
461
+ )
462
+
463
+
464
+ class SearchRequest(BaseModel):
465
+ repo_id: str
466
+ query: str
467
+ top_k: int = 5
468
+ category: Optional[str] = None
469
+
470
+ @app.post("/api/search")
471
+ async def semantic_search(
472
+ request: SearchRequest,
473
+ x_gemini_key: Optional[str] = Header(None)
474
+ ):
475
+ """Phase 3: Semantic search against the repository knowledge vector index."""
476
+ import time
477
+ gemini_key = x_gemini_key or os.environ.get("GEMINI_API_KEY")
478
+ if not gemini_key:
479
+ raise HTTPException(status_code=400, detail="Gemini API Key required for semantic search.")
480
+
481
+ repo_data = memory_service.retrieve(request.repo_id)
482
+ if not repo_data:
483
+ raise HTTPException(status_code=404, detail="Repository data not found.")
484
+
485
+ try:
486
+ start = time.time()
487
+ embedder = EmbeddingService(api_key=gemini_key)
488
+ retriever = KnowledgeRetriever(embedder, get_vector_store())
489
+ results = retriever.retrieve(
490
+ repo_id=request.repo_id,
491
+ query=request.query,
492
+ top_k=request.top_k,
493
+ category=request.category
494
+ )
495
+ latency_ms = int((time.time() - start) * 1000)
496
+ return {
497
+ "query": request.query,
498
+ "results": results,
499
+ "result_count": len(results),
500
+ "latency_ms": latency_ms
501
+ }
502
+ except Exception as e:
503
+ logger.error(f"Semantic search error: {e}")
504
+ raise HTTPException(status_code=500, detail=f"Search failed: {str(e)}")
505
+
506
+
507
+ @app.get("/api/memory")
508
+ async def get_memory_info(repo_id: str):
509
+ """Phase 3: Returns vector index stats for a repository."""
510
+ try:
511
+ vs = get_vector_store()
512
+ count = vs.count_documents(repo_id)
513
+ return {
514
+ "repo_id": repo_id,
515
+ "indexed_chunks": count,
516
+ "storage_path": vs.storage_path
517
+ }
518
+ except Exception as e:
519
+ raise HTTPException(status_code=500, detail=str(e))
520
+
521
+
522
+ @app.get("/api/conversations")
523
+ async def list_conversations(repo_id: str):
524
+ """Phase 3: Returns conversation sessions for a repository."""
525
+ sessions = conversation_manager.list_sessions_for_repo(repo_id)
526
+ return {"repo_id": repo_id, "sessions": sessions}
527
+
528
+
529
+ @app.get("/api/conversations/{session_id}")
530
+ async def get_conversation(session_id: str):
531
+ """Returns full message history for a conversation session."""
532
+ history = session_manager.get_session_history(session_id)
533
+ if history is None:
534
+ raise HTTPException(
535
+ status_code=status.HTTP_404_NOT_FOUND,
536
+ detail="Conversation session not found."
537
+ )
538
+ return {"session_id": session_id, "history": history}
539
+
540
+
541
+ @app.get("/api/tools")
542
+ async def list_tools():
543
+ """Phase 3: Returns the registered tool catalog (MCP-ready)."""
544
+ tools = [
545
+ {"name": "repository_search", "description": "Semantic search over indexed repo chunks."},
546
+ {"name": "graph_query", "description": "Query architecture graph, entry points, and flows."},
547
+ {"name": "dependency_lookup", "description": "Lookup packages, frameworks, and databases."},
548
+ {"name": "file_reader", "description": "Retrieve specific source file content."},
549
+ {"name": "architecture_lookup", "description": "Query architecture pattern and key modules."},
550
+ {"name": "api_lookup", "description": "Lookup HTTP routes and authentication methods."}
551
+ ]
552
+ return {"tools": tools, "count": len(tools)}
553
+
554
+ # Serve static frontend build if present
555
+ frontend_dist = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "frontend", "dist"))
556
+ if os.path.exists(frontend_dist):
557
+ logger.info(f"Serving static frontend files from: {frontend_dist}")
558
+ app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="static")
559
+ else:
560
+ logger.warning(f"Frontend dist folder not found at {frontend_dist}. Running in API-only mode.")
561
+
backend/memory/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from memory.embedding_service import EmbeddingService
2
+ from memory.vector_store import VectorStore
3
+ from memory.knowledge_index import KnowledgeIndexBuilder
4
+ from memory.retriever import KnowledgeRetriever
5
+ from memory.conversation_manager import conversation_manager
6
+ from memory.session_manager import session_manager
7
+ from memory.memory_cache import memory_cache
backend/memory/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (569 Bytes). View file
 
backend/memory/__pycache__/conversation_manager.cpython-312.pyc ADDED
Binary file (4.59 kB). View file
 
backend/memory/__pycache__/embedding_service.cpython-312.pyc ADDED
Binary file (3.14 kB). View file
 
backend/memory/__pycache__/knowledge_index.cpython-312.pyc ADDED
Binary file (7.5 kB). View file
 
backend/memory/__pycache__/memory_cache.cpython-312.pyc ADDED
Binary file (2.59 kB). View file
 
backend/memory/__pycache__/retriever.cpython-312.pyc ADDED
Binary file (2.12 kB). View file
 
backend/memory/__pycache__/session_manager.cpython-312.pyc ADDED
Binary file (4.83 kB). View file
 
backend/memory/__pycache__/vector_store.cpython-312.pyc ADDED
Binary file (5.44 kB). View file
 
backend/memory/conversation_manager.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from typing import List, Dict, Any, Optional
3
+ from pydantic import BaseModel, Field
4
+
5
+ class MessageRecord(BaseModel):
6
+ role: str # 'user' | 'assistant'
7
+ content: str
8
+ timestamp: float = Field(default_factory=time.time)
9
+ retrieved_context: Optional[List[Dict[str, Any]]] = None
10
+ agent_decisions: Optional[Dict[str, Any]] = None
11
+
12
+ class ConversationSession(BaseModel):
13
+ session_id: str
14
+ repo_id: str
15
+ history: List[MessageRecord] = Field(default_factory=list)
16
+ summary: str = ""
17
+
18
+ class ConversationManager:
19
+ """
20
+ Manages multi-session conversation tracking, caching history in-memory.
21
+ Persists context, questions, answers, and internal timeline decisions.
22
+ """
23
+ def __init__(self):
24
+ self._sessions: Dict[str, ConversationSession] = {}
25
+
26
+ def get_or_create_session(self, session_id: str, repo_id: str) -> ConversationSession:
27
+ if session_id not in self._sessions:
28
+ self._sessions[session_id] = ConversationSession(session_id=session_id, repo_id=repo_id)
29
+ return self._sessions[session_id]
30
+
31
+ def add_message(
32
+ self,
33
+ session_id: str,
34
+ repo_id: str,
35
+ role: str,
36
+ content: str,
37
+ retrieved_context: Optional[List[Dict[str, Any]]] = None,
38
+ agent_decisions: Optional[Dict[str, Any]] = None
39
+ ) -> MessageRecord:
40
+ session = self.get_or_create_session(session_id, repo_id)
41
+ record = MessageRecord(
42
+ role=role,
43
+ content=content,
44
+ timestamp=time.time(),
45
+ retrieved_context=retrieved_context,
46
+ agent_decisions=agent_decisions
47
+ )
48
+ session.history.append(record)
49
+
50
+ # Keep summary updated with the last user prompt summary or simple description
51
+ if role == "user" and not session.summary:
52
+ # First query acts as session title/summary
53
+ session.summary = content[:40] + ("..." if len(content) > 40 else "")
54
+
55
+ return record
56
+
57
+ def update_summary(self, session_id: str, summary: str):
58
+ if session_id in self._sessions:
59
+ self._sessions[session_id].summary = summary
60
+
61
+ def get_session(self, session_id: str) -> Optional[ConversationSession]:
62
+ return self._sessions.get(session_id)
63
+
64
+ def list_sessions_for_repo(self, repo_id: str) -> List[Dict[str, Any]]:
65
+ return [
66
+ {
67
+ "session_id": s.session_id,
68
+ "repo_id": s.repo_id,
69
+ "summary": s.summary,
70
+ "message_count": len(s.history),
71
+ "last_updated": s.history[-1].timestamp if s.history else time.time()
72
+ }
73
+ for s in self._sessions.values() if s.repo_id == repo_id
74
+ ]
75
+
76
+ def clear_sessions(self):
77
+ self._sessions.clear()
78
+
79
+ # Global conversation manager singleton
80
+ conversation_manager = ConversationManager()
backend/memory/embedding_service.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from typing import List, Optional
4
+
5
+ logger = logging.getLogger("embedding_service")
6
+
7
+ class EmbeddingService:
8
+ def __init__(self, api_key: Optional[str] = None):
9
+ self.api_key = api_key or os.environ.get("GEMINI_API_KEY")
10
+ if not self.api_key:
11
+ raise ValueError("Gemini API key is required to generate embeddings.")
12
+ from google import genai
13
+ self.client = genai.Client(api_key=self.api_key)
14
+
15
+ def embed_text(self, text: str) -> List[float]:
16
+ """Generates embedding for a single text string."""
17
+ try:
18
+ response = self.client.models.embed_content(
19
+ model='text-embedding-004',
20
+ contents=text
21
+ )
22
+ if response.embeddings and len(response.embeddings) > 0:
23
+ return response.embeddings[0].values
24
+ raise ValueError("No embeddings returned from Gemini API.")
25
+ except Exception as e:
26
+ logger.error(f"Error generating embedding: {e}")
27
+ raise e
28
+
29
+ def embed_texts(self, texts: List[str]) -> List[List[float]]:
30
+ """Generates embeddings for a batch list of text strings."""
31
+ if not texts:
32
+ return []
33
+ try:
34
+ # Check length to prevent massive batch issues; text-embedding-004 supports bulk
35
+ response = self.client.models.embed_content(
36
+ model='text-embedding-004',
37
+ contents=texts
38
+ )
39
+ if response.embeddings and len(response.embeddings) == len(texts):
40
+ return [e.values for e in response.embeddings]
41
+ elif response.embeddings:
42
+ return [e.values for e in response.embeddings]
43
+ raise ValueError("No embeddings returned from batch API call.")
44
+ except Exception as e:
45
+ logger.error(f"Error generating batch embeddings: {e}")
46
+ raise e
backend/memory/knowledge_index.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ from typing import List, Dict, Any, Tuple
4
+ from memory.embedding_service import EmbeddingService
5
+ from memory.vector_store import VectorStore
6
+
7
+ logger = logging.getLogger("knowledge_index")
8
+
9
+ class KnowledgeIndexBuilder:
10
+ def __init__(self, embedding_service: EmbeddingService, vector_store: VectorStore):
11
+ self.embedder = embedding_service
12
+ self.store = vector_store
13
+
14
+ def chunk_text(self, text: str, chunk_size: int = 1000, overlap: int = 100) -> List[str]:
15
+ """Simple sliding window text splitter."""
16
+ if not text:
17
+ return []
18
+ chunks = []
19
+ start = 0
20
+ text_len = len(text)
21
+
22
+ while start < text_len:
23
+ end = min(start + chunk_size, text_len)
24
+ chunks.append(text[start:end])
25
+ start += chunk_size - overlap
26
+
27
+ # Prevent infinite loop if overlap >= chunk_size
28
+ if chunk_size - overlap <= 0:
29
+ break
30
+
31
+ return chunks
32
+
33
+ def build_index(
34
+ self,
35
+ repo_id: str,
36
+ profile: Dict[str, Any],
37
+ summary: Dict[str, Any],
38
+ graph: Dict[str, Any],
39
+ report: str,
40
+ flat_files: List[Dict[str, Any]]
41
+ ):
42
+ """Builds and indexes a repository's code, structure, and profile metadata into ChromaDB."""
43
+ logger.info(f"Starting index build for repository: {repo_id}")
44
+
45
+ # Clear existing collection if any
46
+ self.store.delete_collection(repo_id)
47
+
48
+ documents: List[str] = []
49
+ metadatas: List[Dict[str, Any]] = []
50
+ ids: List[str] = []
51
+
52
+ # Helper to generate unique IDs
53
+ def add_chunk(content: str, metadata: Dict[str, Any], prefix: str):
54
+ chunk_id = f"{prefix}_{len(documents)}"
55
+ documents.append(content)
56
+ metadatas.append(metadata)
57
+ ids.append(chunk_id)
58
+
59
+ # 1. Index the Repository Intelligence Report (Markdown)
60
+ report_chunks = self.chunk_text(report, chunk_size=1000, overlap=100)
61
+ for i, chunk in enumerate(report_chunks):
62
+ add_chunk(
63
+ content=chunk,
64
+ metadata={"category": "report", "chunk_index": i},
65
+ prefix="report"
66
+ )
67
+
68
+ # 2. Index the Summary details
69
+ elevator_pitch = summary.get("elevator_pitch", "")
70
+ if elevator_pitch:
71
+ add_chunk(
72
+ content=f"Project Elevator Pitch:\n{elevator_pitch}",
73
+ metadata={"category": "summary", "subcategory": "elevator_pitch"},
74
+ prefix="summary_pitch"
75
+ )
76
+
77
+ for feat in summary.get("core_features", []):
78
+ add_chunk(
79
+ content=f"Core Feature: {feat}",
80
+ metadata={"category": "summary", "subcategory": "core_feature"},
81
+ prefix="summary_feat"
82
+ )
83
+
84
+ for start_point in summary.get("developer_start_points", []):
85
+ add_chunk(
86
+ content=f"Developer Starting Point File: {start_point}",
87
+ metadata={"category": "summary", "subcategory": "developer_start_point"},
88
+ prefix="summary_start"
89
+ )
90
+
91
+ # 3. Index Profile Metadata (Architecture, APIs, Auth, Dependencies)
92
+ arch_pattern = profile.get("architecture_pattern", "")
93
+ if arch_pattern:
94
+ add_chunk(
95
+ content=f"Architecture Pattern: {arch_pattern}",
96
+ metadata={"category": "architecture", "pattern": arch_pattern},
97
+ prefix="profile_arch"
98
+ )
99
+
100
+ for auth_method in profile.get("authentication_methods", []):
101
+ add_chunk(
102
+ content=f"Authentication / Security Method: {auth_method}",
103
+ metadata={"category": "authentication", "method": auth_method},
104
+ prefix="profile_auth"
105
+ )
106
+
107
+ for endpoint in profile.get("api_endpoints", []):
108
+ add_chunk(
109
+ content=f"API Endpoint / Route: {endpoint}",
110
+ metadata={"category": "api", "endpoint": endpoint},
111
+ prefix="profile_api"
112
+ )
113
+
114
+ for dep in profile.get("dependencies", []):
115
+ add_chunk(
116
+ content=f"Package Dependency: {dep}",
117
+ metadata={"category": "dependency", "dependency": dep},
118
+ prefix="profile_dep"
119
+ )
120
+
121
+ # 4. Index Business Flows & Concepts from Graph
122
+ for flow in graph.get("business_flows", []):
123
+ flow_name = flow.get("flow_name", "")
124
+ flow_desc = flow.get("description", "")
125
+ flow_steps = ", ".join(flow.get("steps", []))
126
+ add_chunk(
127
+ content=f"Business Flow: {flow_name}\nDescription: {flow_desc}\nSteps: {flow_steps}",
128
+ metadata={"category": "business_flow", "flow_name": flow_name},
129
+ prefix="graph_flow"
130
+ )
131
+
132
+ for concept in graph.get("concepts", []):
133
+ concept_name = concept.get("name", "")
134
+ concept_desc = concept.get("description", "")
135
+ concept_files = ", ".join(concept.get("files", []))
136
+ add_chunk(
137
+ content=f"Core Concept: {concept_name}\nDescription: {concept_desc}\nFiles: {concept_files}",
138
+ metadata={"category": "concept", "concept_name": concept_name},
139
+ prefix="graph_concept"
140
+ )
141
+
142
+ # 5. Index Source Code Files Content
143
+ for f in flat_files:
144
+ file_path = f.get("path", "")
145
+ content = f.get("content", "")
146
+ file_size = f.get("size", 0)
147
+
148
+ # Skip massive files to avoid cluttering vector space
149
+ if file_size > 100 * 1024 or not content:
150
+ continue
151
+
152
+ file_chunks = self.chunk_text(content, chunk_size=1000, overlap=100)
153
+ for j, chunk in enumerate(file_chunks):
154
+ # Include path info inside the document text to maintain retrieval association
155
+ chunk_content = f"File: {file_path} (Chunk {j+1}/{len(file_chunks)})\n\n{chunk}"
156
+ add_chunk(
157
+ content=chunk_content,
158
+ metadata={"category": "file", "path": file_path, "chunk_index": j},
159
+ prefix="file_chunk"
160
+ )
161
+
162
+ if not documents:
163
+ logger.info("No documents found to index.")
164
+ return
165
+
166
+ # 6. Generate Embeddings in Batches of 50 to respect API rate limits and connection pooling
167
+ batch_size = 50
168
+ logger.info(f"Generating embeddings for {len(documents)} document chunks in batches of {batch_size}...")
169
+
170
+ all_embeddings: List[List[float]] = []
171
+ for start_idx in range(0, len(documents), batch_size):
172
+ end_idx = min(start_idx + batch_size, len(documents))
173
+ batch_docs = documents[start_idx:end_idx]
174
+ batch_embeddings = self.embedder.embed_texts(batch_docs)
175
+ all_embeddings.extend(batch_embeddings)
176
+
177
+ # 7. Write to Vector Store
178
+ self.store.add_documents(
179
+ repo_id=repo_id,
180
+ documents=documents,
181
+ metadatas=metadatas,
182
+ ids=ids,
183
+ embeddings=all_embeddings
184
+ )
185
+ logger.info(f"Vector database indexing complete. Indexed {len(documents)} chunks.")
backend/memory/memory_cache.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from typing import Dict, Any, Optional
3
+
4
+ class CacheEntry:
5
+ def __init__(self, value: Any, ttl: Optional[float] = None):
6
+ self.value = value
7
+ self.expires_at = time.time() + ttl if ttl is not None else None
8
+
9
+ def is_expired(self) -> bool:
10
+ if self.expires_at is None:
11
+ return False
12
+ return time.time() > self.expires_at
13
+
14
+ class MemoryCache:
15
+ """
16
+ In-memory cache with TTL support for accelerating AI agent executions
17
+ and caching heavy search/graph query results.
18
+ """
19
+ def __init__(self):
20
+ self._cache: Dict[str, CacheEntry] = {}
21
+
22
+ def get(self, key: str) -> Optional[Any]:
23
+ entry = self._cache.get(key)
24
+ if not entry:
25
+ return None
26
+ if entry.is_expired():
27
+ del self._cache[key]
28
+ return None
29
+ return entry.value
30
+
31
+ def set(self, key: str, value: Any, ttl: Optional[float] = None):
32
+ self._cache[key] = CacheEntry(value, ttl)
33
+
34
+ def delete(self, key: str):
35
+ if key in self._cache:
36
+ del self._cache[key]
37
+
38
+ def clear(self):
39
+ self._cache.clear()
40
+
41
+ # Global memory cache singleton
42
+ memory_cache = MemoryCache()
backend/memory/retriever.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import List, Dict, Any, Optional
3
+ from memory.embedding_service import EmbeddingService
4
+ from memory.vector_store import VectorStore
5
+
6
+ logger = logging.getLogger("retriever")
7
+
8
+ class KnowledgeRetriever:
9
+ def __init__(self, embedding_service: EmbeddingService, vector_store: VectorStore):
10
+ self.embedder = embedding_service
11
+ self.store = vector_store
12
+
13
+ def retrieve(
14
+ self,
15
+ repo_id: str,
16
+ query: str,
17
+ top_k: int = 5,
18
+ category: Optional[str] = None
19
+ ) -> List[Dict[str, Any]]:
20
+ """
21
+ Runs semantic search on ChromaDB for the repo using the query.
22
+ Supports filtering by metadata 'category'.
23
+ """
24
+ logger.info(f"Retrieving context for repo {repo_id}, query: '{query}' (category filter: {category}, top_k: {top_k})")
25
+
26
+ try:
27
+ # Generate query vector using embedder
28
+ query_embedding = self.embedder.embed_text(query)
29
+
30
+ # Setup filter
31
+ where_filter = None
32
+ if category:
33
+ where_filter = {"category": category}
34
+
35
+ return self.store.query_documents(
36
+ repo_id=repo_id,
37
+ query_embedding=query_embedding,
38
+ top_k=top_k,
39
+ where_filter=where_filter
40
+ )
41
+ except Exception as e:
42
+ logger.error(f"Failed to retrieve vector search results: {e}")
43
+ return []
backend/memory/session_manager.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ from typing import Dict, Any, List, Optional
5
+ from pydantic import BaseModel
6
+ from memory.conversation_manager import conversation_manager, ConversationSession, MessageRecord
7
+
8
+ logger = logging.getLogger("session_manager")
9
+
10
+ class SessionManager:
11
+ """
12
+ Manages persistence and high-level metadata for multiple repository sessions.
13
+ Saves and loads conversation history and repository state to/from disk.
14
+ """
15
+ def __init__(self, storage_dir: str = None):
16
+ if storage_dir is None:
17
+ storage_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "storage")
18
+ self.storage_dir = storage_dir
19
+ os.makedirs(self.storage_dir, exist_ok=True)
20
+ self.conversations_file = os.path.join(self.storage_dir, "conversations.json")
21
+ self.load_all()
22
+
23
+ def load_all(self):
24
+ if os.path.exists(self.conversations_file):
25
+ try:
26
+ with open(self.conversations_file, "r", encoding="utf-8") as f:
27
+ data = json.load(f)
28
+ for session_id, s_data in data.items():
29
+ session = ConversationSession(
30
+ session_id=session_id,
31
+ repo_id=s_data.get("repo_id"),
32
+ summary=s_data.get("summary", ""),
33
+ history=[
34
+ MessageRecord(**msg) for msg in s_data.get("history", [])
35
+ ]
36
+ )
37
+ conversation_manager._sessions[session_id] = session
38
+ logger.info(f"Loaded conversations from {self.conversations_file}")
39
+ except Exception as e:
40
+ logger.error(f"Error loading conversations: {e}")
41
+
42
+ def save_all(self):
43
+ data = {}
44
+ for session_id, session in conversation_manager._sessions.items():
45
+ data[session_id] = {
46
+ "session_id": session.session_id,
47
+ "repo_id": session.repo_id,
48
+ "summary": session.summary,
49
+ "history": [msg.model_dump() for msg in session.history]
50
+ }
51
+ try:
52
+ with open(self.conversations_file, "w", encoding="utf-8") as f:
53
+ json.dump(data, f, indent=2)
54
+ logger.info(f"Saved conversations to {self.conversations_file}")
55
+ except Exception as e:
56
+ logger.error(f"Error saving conversations: {e}")
57
+
58
+ def get_session_history(self, session_id: str) -> Optional[List[Dict[str, Any]]]:
59
+ session = conversation_manager.get_session(session_id)
60
+ if not session:
61
+ return None
62
+ return [msg.model_dump() for msg in session.history]
63
+
64
+ # Global session manager singleton
65
+ session_manager = SessionManager()
backend/memory/vector_store.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from typing import Dict, Any, List, Optional
4
+ import chromadb
5
+
6
+ logger = logging.getLogger("vector_store")
7
+
8
+ class VectorStore:
9
+ def __init__(self, storage_path: Optional[str] = None):
10
+ self.storage_path = storage_path or os.environ.get("CHROMA_DB_PATH", "./chroma_db")
11
+ os.makedirs(self.storage_path, exist_ok=True)
12
+ # Initialize persistent ChromaDB client
13
+ self.client = chromadb.PersistentClient(path=self.storage_path)
14
+
15
+ def get_collection(self, repo_id: str):
16
+ # Convert UUID repo_id to a valid Chroma collection name (alphanumeric and underscores)
17
+ coll_name = f"repo_{repo_id.replace('-', '_')}"
18
+ return self.client.get_or_create_collection(
19
+ name=coll_name,
20
+ metadata={"hnsw:space": "cosine"}
21
+ )
22
+
23
+ def add_documents(
24
+ self,
25
+ repo_id: str,
26
+ documents: List[str],
27
+ metadatas: List[Dict[str, Any]],
28
+ ids: List[str],
29
+ embeddings: List[List[float]]
30
+ ):
31
+ """Adds documents with their corresponding embeddings and metadata."""
32
+ if not documents:
33
+ return
34
+ collection = self.get_collection(repo_id)
35
+ collection.add(
36
+ documents=documents,
37
+ metadatas=metadatas,
38
+ ids=ids,
39
+ embeddings=embeddings
40
+ )
41
+ logger.info(f"Added {len(documents)} chunks to vector store collection for repo {repo_id}")
42
+
43
+ def query_documents(
44
+ self,
45
+ repo_id: str,
46
+ query_embedding: List[float],
47
+ top_k: int = 5,
48
+ where_filter: Optional[Dict[str, Any]] = None
49
+ ) -> List[Dict[str, Any]]:
50
+ """Queries ChromaDB using the query vector, returning matching chunks with similarity scores."""
51
+ collection = self.get_collection(repo_id)
52
+
53
+ # Query ChromaDB
54
+ results = collection.query(
55
+ query_embeddings=[query_embedding],
56
+ n_results=top_k,
57
+ where=where_filter
58
+ )
59
+
60
+ formatted = []
61
+ if results and "documents" in results and len(results["documents"]) > 0:
62
+ docs = results["documents"][0]
63
+ metas = results["metadatas"][0] if results.get("metadatas") else [{} for _ in range(len(docs))]
64
+ ids = results["ids"][0] if results.get("ids") else [str(i) for i in range(len(docs))]
65
+ distances = results["distances"][0] if results.get("distances") else [0.0 for _ in range(len(docs))]
66
+
67
+ for doc, meta, doc_id, dist in zip(docs, metas, ids, distances):
68
+ # Cosine distance to similarity: 1 - distance
69
+ similarity = 1.0 - dist
70
+ formatted.append({
71
+ "id": doc_id,
72
+ "content": doc,
73
+ "metadata": meta,
74
+ "similarity": round(similarity, 4),
75
+ "distance": round(dist, 4)
76
+ })
77
+
78
+ # Sort by similarity descending
79
+ formatted.sort(key=lambda x: x["similarity"], reverse=True)
80
+ return formatted
81
+
82
+ def delete_collection(self, repo_id: str):
83
+ coll_name = f"repo_{repo_id.replace('-', '_')}"
84
+ try:
85
+ self.client.delete_collection(name=coll_name)
86
+ logger.info(f"Deleted vector store collection: {coll_name}")
87
+ except Exception as e:
88
+ logger.warning(f"Could not delete collection {coll_name}: {e}")
89
+
90
+ def count_documents(self, repo_id: str) -> int:
91
+ try:
92
+ collection = self.get_collection(repo_id)
93
+ return collection.count()
94
+ except Exception:
95
+ return 0
backend/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.6
2
+ uvicorn==0.34.0
3
+ pydantic==2.10.4
4
+ google-genai==1.14.0
5
+ python-multipart==0.0.20
6
+ httpx==0.28.1
7
+ chromadb==1.5.9
backend/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Services package
backend/services/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (125 Bytes). View file
 
backend/services/__pycache__/graphBuilder.cpython-312.pyc ADDED
Binary file (4.44 kB). View file