Spaces:
Sleeping
Sleeping
Commit Β·
944f820
0
Parent(s):
staging deployment completed
Browse files- .gitignore +21 -0
- Dockerfile +12 -0
- README.md +2 -0
- call_graph.json +117 -0
- config/__init__.py +0 -0
- config/config.py +7 -0
- inference/__init__.py +0 -0
- inference/inference.py +468 -0
- ingest/__init__.py +0 -0
- ingest/embed.py +529 -0
- ingest/parse_ast.py +413 -0
- ingest/walk_files.py +185 -0
- main.py +473 -0
- prompt/__init__.py +0 -0
- prompt/cross_module.py +424 -0
- prompt/macro_prompt.py +357 -0
- prompt/micro_prompt.py +330 -0
- render/__init__.py +0 -0
- render/render.py +0 -0
- requirements.txt +9 -0
- retrieve/__init__.py +0 -0
- retrieve/retrieve.py +489 -0
- store/__init__.py +0 -0
- store/call_graph.json +0 -0
- store/call_graph.py +577 -0
- store/vector_store.py +153 -0
- ui/index.html +121 -0
- ui/static/app.js +392 -0
- ui/static/style.css +587 -0
.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
.venv
|
| 11 |
+
|
| 12 |
+
# secrets
|
| 13 |
+
.env
|
| 14 |
+
|
| 15 |
+
# directories
|
| 16 |
+
design/
|
| 17 |
+
.idea/
|
| 18 |
+
|
| 19 |
+
# files
|
| 20 |
+
*.toml
|
| 21 |
+
.python-version
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.13-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# UCode - Understand Monolith Codebases with Ease
|
| 2 |
+
|
call_graph.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"AI Practice.api.read_root": {
|
| 3 |
+
"id": "AI Practice.api.read_root",
|
| 4 |
+
"name": "read_root",
|
| 5 |
+
"class_name": "",
|
| 6 |
+
"module": "AI Practice",
|
| 7 |
+
"file": "AI Practice/api.py",
|
| 8 |
+
"calls": [
|
| 9 |
+
"app.get"
|
| 10 |
+
],
|
| 11 |
+
"called_by": []
|
| 12 |
+
},
|
| 13 |
+
"AI Practice.api.get_todos": {
|
| 14 |
+
"id": "AI Practice.api.get_todos",
|
| 15 |
+
"name": "get_todos",
|
| 16 |
+
"class_name": "",
|
| 17 |
+
"module": "AI Practice",
|
| 18 |
+
"file": "AI Practice/api.py",
|
| 19 |
+
"calls": [
|
| 20 |
+
"app.get"
|
| 21 |
+
],
|
| 22 |
+
"called_by": []
|
| 23 |
+
},
|
| 24 |
+
"AI Practice.api.get_todo": {
|
| 25 |
+
"id": "AI Practice.api.get_todo",
|
| 26 |
+
"name": "get_todo",
|
| 27 |
+
"class_name": "",
|
| 28 |
+
"module": "AI Practice",
|
| 29 |
+
"file": "AI Practice/api.py",
|
| 30 |
+
"calls": [
|
| 31 |
+
"app.get",
|
| 32 |
+
"HTTPException"
|
| 33 |
+
],
|
| 34 |
+
"called_by": []
|
| 35 |
+
},
|
| 36 |
+
"AI Practice.api.create_todo": {
|
| 37 |
+
"id": "AI Practice.api.create_todo",
|
| 38 |
+
"name": "create_todo",
|
| 39 |
+
"class_name": "",
|
| 40 |
+
"module": "AI Practice",
|
| 41 |
+
"file": "AI Practice/api.py",
|
| 42 |
+
"calls": [
|
| 43 |
+
"app.post",
|
| 44 |
+
"todos.append",
|
| 45 |
+
"todo.dict"
|
| 46 |
+
],
|
| 47 |
+
"called_by": []
|
| 48 |
+
},
|
| 49 |
+
"AI Practice.api.update_todo": {
|
| 50 |
+
"id": "AI Practice.api.update_todo",
|
| 51 |
+
"name": "update_todo",
|
| 52 |
+
"class_name": "",
|
| 53 |
+
"module": "AI Practice",
|
| 54 |
+
"file": "AI Practice/api.py",
|
| 55 |
+
"calls": [
|
| 56 |
+
"app.put",
|
| 57 |
+
"enumerate",
|
| 58 |
+
"HTTPException",
|
| 59 |
+
"updated_todo.dict"
|
| 60 |
+
],
|
| 61 |
+
"called_by": []
|
| 62 |
+
},
|
| 63 |
+
"AI Practice.api.delete_todo": {
|
| 64 |
+
"id": "AI Practice.api.delete_todo",
|
| 65 |
+
"name": "delete_todo",
|
| 66 |
+
"class_name": "",
|
| 67 |
+
"module": "AI Practice",
|
| 68 |
+
"file": "AI Practice/api.py",
|
| 69 |
+
"calls": [
|
| 70 |
+
"app.delete",
|
| 71 |
+
"enumerate",
|
| 72 |
+
"HTTPException",
|
| 73 |
+
"todos.pop"
|
| 74 |
+
],
|
| 75 |
+
"called_by": []
|
| 76 |
+
},
|
| 77 |
+
"Chat Bot.app.home": {
|
| 78 |
+
"id": "Chat Bot.app.home",
|
| 79 |
+
"name": "home",
|
| 80 |
+
"class_name": "",
|
| 81 |
+
"module": "Chat Bot",
|
| 82 |
+
"file": "Chat Bot/app.py",
|
| 83 |
+
"calls": [
|
| 84 |
+
"app.route",
|
| 85 |
+
"render_template"
|
| 86 |
+
],
|
| 87 |
+
"called_by": []
|
| 88 |
+
},
|
| 89 |
+
"Chat Bot.app.chat": {
|
| 90 |
+
"id": "Chat Bot.app.chat",
|
| 91 |
+
"name": "chat",
|
| 92 |
+
"class_name": "",
|
| 93 |
+
"module": "Chat Bot",
|
| 94 |
+
"file": "Chat Bot/app.py",
|
| 95 |
+
"calls": [
|
| 96 |
+
"app.route",
|
| 97 |
+
"request.json.get",
|
| 98 |
+
"memory.append",
|
| 99 |
+
"client.chat.completions.create",
|
| 100 |
+
"jsonify",
|
| 101 |
+
"str"
|
| 102 |
+
],
|
| 103 |
+
"called_by": []
|
| 104 |
+
},
|
| 105 |
+
"IMCSBOT.app.generate": {
|
| 106 |
+
"id": "IMCSBOT.app.generate",
|
| 107 |
+
"name": "generate",
|
| 108 |
+
"class_name": "",
|
| 109 |
+
"module": "IMCSBOT",
|
| 110 |
+
"file": "IMCSBOT/app.py",
|
| 111 |
+
"calls": [
|
| 112 |
+
"app.post",
|
| 113 |
+
"generator"
|
| 114 |
+
],
|
| 115 |
+
"called_by": []
|
| 116 |
+
}
|
| 117 |
+
}
|
config/__init__.py
ADDED
|
File without changes
|
config/config.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
load_dotenv()
|
| 4 |
+
|
| 5 |
+
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
|
| 6 |
+
PINECONE_INDEX = "codebase-oracle"
|
| 7 |
+
EMBEDDING_DIM = 384
|
inference/__init__.py
ADDED
|
File without changes
|
inference/inference.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference.py
|
| 3 |
+
------------
|
| 4 |
+
Pure LLM inference layer for the Codebase Oracle system.
|
| 5 |
+
No HTTP server, no FastAPI β just context + query β response.
|
| 6 |
+
|
| 7 |
+
Responsibilities:
|
| 8 |
+
1. Accept query type + subtype + function/module name
|
| 9 |
+
2. Retrieve relevant context from vector store + call graph
|
| 10 |
+
3. Build correct prompt via prompts/ modules
|
| 11 |
+
4. Call OpenRouter API
|
| 12 |
+
5. Return clean markdown response string
|
| 13 |
+
|
| 14 |
+
This module is the single entry point that main.py (FastAPI) calls.
|
| 15 |
+
|
| 16 |
+
Depends on:
|
| 17 |
+
- retriever.py
|
| 18 |
+
- call_graph.py
|
| 19 |
+
- macro_prompts.py
|
| 20 |
+
- micro_prompts.py
|
| 21 |
+
- cross_module_prompts.py
|
| 22 |
+
- openai (OpenRouter is OpenAI-compatible)
|
| 23 |
+
- rich (logging only)
|
| 24 |
+
|
| 25 |
+
Install:
|
| 26 |
+
pip install openai rich
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
import os
|
| 30 |
+
from dataclasses import dataclass, field
|
| 31 |
+
from dotenv import load_dotenv
|
| 32 |
+
from openai import OpenAI
|
| 33 |
+
from rich.console import Console
|
| 34 |
+
|
| 35 |
+
load_dotenv() # loads OPENROUTER_API_KEY from .env file
|
| 36 |
+
|
| 37 |
+
from retrieve.retrieve import Retriever, QueryType
|
| 38 |
+
from store.call_graph import get_call_graph
|
| 39 |
+
from prompt.macro_prompt import (
|
| 40 |
+
get_macro_system_prompt,
|
| 41 |
+
build_macro_user_prompt,
|
| 42 |
+
)
|
| 43 |
+
from prompt.micro_prompt import (
|
| 44 |
+
get_micro_system_prompt,
|
| 45 |
+
build_micro_user_prompt,
|
| 46 |
+
build_micro_followup_prompt,
|
| 47 |
+
)
|
| 48 |
+
from prompt.cross_module import (
|
| 49 |
+
get_cross_module_system_prompt,
|
| 50 |
+
build_cross_module_user_prompt,
|
| 51 |
+
build_cross_module_followup_prompt,
|
| 52 |
+
build_call_graph_payload,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
console = Console()
|
| 56 |
+
|
| 57 |
+
# ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
|
| 59 |
+
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
| 60 |
+
DEFAULT_MODEL = "nvidia/nemotron-3-super-120b-a12b:free"
|
| 61 |
+
MAX_TOKENS = 4096
|
| 62 |
+
CONTEXT_MAX_CHARS = 6000 # max chars fed to LLM as retrieved context
|
| 63 |
+
N_RESULTS_DEFAULT = 5 # default number of chunks to retrieve
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ββ Request / Response Models βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
+
|
| 68 |
+
@dataclass
|
| 69 |
+
class InferenceRequest:
|
| 70 |
+
"""
|
| 71 |
+
Unified request model for all query types.
|
| 72 |
+
|
| 73 |
+
Fields:
|
| 74 |
+
query_type : "macro" | "micro" | "cross_module"
|
| 75 |
+
query : Developer's natural language question
|
| 76 |
+
subtype : macro subtype or "" for micro/cross_module
|
| 77 |
+
function_name : Target function/method name (micro + cross_module)
|
| 78 |
+
class_name : Target class name if method (optional)
|
| 79 |
+
module_name : Target module name (macro module_responsibility)
|
| 80 |
+
followup : True if this is a follow-up to a previous response
|
| 81 |
+
previous_response: Previous LLM response for follow-up context
|
| 82 |
+
"""
|
| 83 |
+
query_type: str
|
| 84 |
+
query: str
|
| 85 |
+
subtype: str = ""
|
| 86 |
+
function_name: str = ""
|
| 87 |
+
class_name: str = ""
|
| 88 |
+
module_name: str = ""
|
| 89 |
+
followup: bool = False
|
| 90 |
+
previous_response: str = ""
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@dataclass
|
| 94 |
+
class InferenceResponse:
|
| 95 |
+
"""
|
| 96 |
+
Unified response model returned to FastAPI / caller.
|
| 97 |
+
|
| 98 |
+
Fields:
|
| 99 |
+
success : True if inference succeeded
|
| 100 |
+
content : Markdown response string
|
| 101 |
+
error : Error message if success is False
|
| 102 |
+
metadata : Optional dict with retrieval stats
|
| 103 |
+
"""
|
| 104 |
+
success: bool
|
| 105 |
+
content: str = ""
|
| 106 |
+
error: str = ""
|
| 107 |
+
metadata: dict = field(default_factory=dict)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# ββ OpenRouter Client βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 111 |
+
|
| 112 |
+
def _get_client() -> OpenAI:
|
| 113 |
+
"""
|
| 114 |
+
Build and return an OpenAI-compatible client pointed at OpenRouter.
|
| 115 |
+
Reads OPENROUTER_API_KEY from .env file via load_dotenv.
|
| 116 |
+
|
| 117 |
+
Raises:
|
| 118 |
+
EnvironmentError: If OPENROUTER_API_KEY is not set.
|
| 119 |
+
"""
|
| 120 |
+
api_key = os.getenv("OPENROUTER_API_KEY")
|
| 121 |
+
if not api_key:
|
| 122 |
+
raise EnvironmentError(
|
| 123 |
+
"OPENROUTER_API_KEY environment variable is not set. "
|
| 124 |
+
"Add OPENROUTER_API_KEY=your_key to your .env file."
|
| 125 |
+
)
|
| 126 |
+
return OpenAI(
|
| 127 |
+
base_url=OPENROUTER_BASE_URL,
|
| 128 |
+
api_key=api_key,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _call_llm(system_prompt: str,
|
| 133 |
+
user_prompt: str,
|
| 134 |
+
model: str = DEFAULT_MODEL) -> str:
|
| 135 |
+
"""
|
| 136 |
+
Make a single LLM call via OpenRouter and return the response text.
|
| 137 |
+
|
| 138 |
+
Args:
|
| 139 |
+
system_prompt : System prompt string.
|
| 140 |
+
user_prompt : User prompt string.
|
| 141 |
+
model : OpenRouter model identifier.
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
Raw response text from LLM.
|
| 145 |
+
|
| 146 |
+
Raises:
|
| 147 |
+
Exception: On API errors, propagated to caller.
|
| 148 |
+
"""
|
| 149 |
+
client = _get_client()
|
| 150 |
+
|
| 151 |
+
response = client.chat.completions.create(
|
| 152 |
+
model=model,
|
| 153 |
+
max_tokens=MAX_TOKENS,
|
| 154 |
+
messages=[
|
| 155 |
+
{"role": "system", "content": system_prompt},
|
| 156 |
+
{"role": "user", "content": user_prompt},
|
| 157 |
+
],
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
return response.choices[0].message.content or ""
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ββ Inference Engine ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
|
| 165 |
+
class InferenceEngine:
|
| 166 |
+
"""
|
| 167 |
+
Core inference engine for the Codebase Oracle.
|
| 168 |
+
Retrieves context, builds prompts, calls LLM, returns response.
|
| 169 |
+
|
| 170 |
+
Single instance shared across all FastAPI requests.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
def __init__(self, model: str = DEFAULT_MODEL):
|
| 174 |
+
self.model = model
|
| 175 |
+
self.retriever = Retriever()
|
| 176 |
+
console.print(
|
| 177 |
+
f"[green]β[/green] InferenceEngine ready "
|
| 178 |
+
f"(model: [cyan]{model}[/cyan])\n"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# ββ Public Entry Point ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 182 |
+
|
| 183 |
+
def infer(self, request: InferenceRequest) -> InferenceResponse:
|
| 184 |
+
"""
|
| 185 |
+
Main entry point. Routes to the correct handler based on query_type.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
request: InferenceRequest with all query parameters.
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
InferenceResponse with markdown content or error.
|
| 192 |
+
"""
|
| 193 |
+
try:
|
| 194 |
+
if request.query_type == QueryType.MACRO:
|
| 195 |
+
return self._handle_macro(request)
|
| 196 |
+
|
| 197 |
+
elif request.query_type == QueryType.MICRO:
|
| 198 |
+
return self._handle_micro(request)
|
| 199 |
+
|
| 200 |
+
elif request.query_type == QueryType.CROSS_MODULE:
|
| 201 |
+
return self._handle_cross_module(request)
|
| 202 |
+
|
| 203 |
+
else:
|
| 204 |
+
return InferenceResponse(
|
| 205 |
+
success=False,
|
| 206 |
+
error=f"Unknown query_type: '{request.query_type}'. "
|
| 207 |
+
f"Use 'macro', 'micro', or 'cross_module'."
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
except EnvironmentError as e:
|
| 211 |
+
return InferenceResponse(success=False, error=str(e))
|
| 212 |
+
|
| 213 |
+
except Exception as e:
|
| 214 |
+
console.print(f"[red]β Inference error: {e}[/red]")
|
| 215 |
+
return InferenceResponse(
|
| 216 |
+
success=False,
|
| 217 |
+
error=f"Inference failed: {str(e)}"
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
# ββ Macro Handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
+
|
| 222 |
+
def _handle_macro(self, req: InferenceRequest) -> InferenceResponse:
|
| 223 |
+
"""Handle macro-level queries (architecture, module, data flow)."""
|
| 224 |
+
|
| 225 |
+
if not req.subtype:
|
| 226 |
+
return InferenceResponse(
|
| 227 |
+
success=False,
|
| 228 |
+
error="Macro queries require a subtype: "
|
| 229 |
+
"'overall_architecture', 'module_responsibility', "
|
| 230 |
+
"or 'data_flow'."
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Retrieve class-level chunks β macro uses class collection
|
| 234 |
+
chunks = self.retriever.retrieve(
|
| 235 |
+
query=req.query,
|
| 236 |
+
query_type=QueryType.MACRO,
|
| 237 |
+
n_results=N_RESULTS_DEFAULT,
|
| 238 |
+
filters={"module": {"$eq": req.module_name}}
|
| 239 |
+
if req.module_name and req.subtype == "module_responsibility"
|
| 240 |
+
else None,
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
context = self.retriever.build_context(chunks, CONTEXT_MAX_CHARS)
|
| 244 |
+
system = get_macro_system_prompt(req.subtype)
|
| 245 |
+
user = build_macro_user_prompt(
|
| 246 |
+
subtype=req.subtype,
|
| 247 |
+
context=context,
|
| 248 |
+
query=req.query,
|
| 249 |
+
module_name=req.module_name,
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
console.print(
|
| 253 |
+
f"[cyan]β Macro [{req.subtype}][/cyan] "
|
| 254 |
+
f"| {len(chunks)} chunks | {len(context)} chars"
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
content = _call_llm(system, user, self.model)
|
| 258 |
+
|
| 259 |
+
return InferenceResponse(
|
| 260 |
+
success=True,
|
| 261 |
+
content=content,
|
| 262 |
+
metadata={
|
| 263 |
+
"query_type": "macro",
|
| 264 |
+
"subtype": req.subtype,
|
| 265 |
+
"chunks_used": len(chunks),
|
| 266 |
+
"context_chars": len(context),
|
| 267 |
+
}
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
# ββ Micro Handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 271 |
+
|
| 272 |
+
def _handle_micro(self, req: InferenceRequest) -> InferenceResponse:
|
| 273 |
+
"""Handle micro-level queries (function definition, body, consideration)."""
|
| 274 |
+
|
| 275 |
+
if not req.function_name:
|
| 276 |
+
return InferenceResponse(
|
| 277 |
+
success=False,
|
| 278 |
+
error="Micro queries require a function_name."
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
# Follow-up path
|
| 282 |
+
if req.followup and req.previous_response:
|
| 283 |
+
complete_chunks = self.retriever.retrieve_complete_function(
|
| 284 |
+
req.function_name, req.class_name, n_results=1
|
| 285 |
+
)
|
| 286 |
+
context = self.retriever.build_context(complete_chunks, CONTEXT_MAX_CHARS)
|
| 287 |
+
system = get_micro_system_prompt(followup=True)
|
| 288 |
+
user = build_micro_followup_prompt(
|
| 289 |
+
previous_response=req.previous_response,
|
| 290 |
+
followup_query=req.query,
|
| 291 |
+
context=context,
|
| 292 |
+
)
|
| 293 |
+
else:
|
| 294 |
+
# Primary path β retrieve complete function + complete class context
|
| 295 |
+
complete_func_chunks = self.retriever.retrieve_complete_function(
|
| 296 |
+
req.function_name, req.class_name, n_results=1
|
| 297 |
+
)
|
| 298 |
+
complete_class_chunks = (
|
| 299 |
+
self.retriever.retrieve_complete_class(req.class_name, n_results=1)
|
| 300 |
+
if req.class_name else []
|
| 301 |
+
)
|
| 302 |
+
all_chunks = complete_func_chunks + complete_class_chunks
|
| 303 |
+
context = self.retriever.build_context(all_chunks, CONTEXT_MAX_CHARS)
|
| 304 |
+
system = get_micro_system_prompt(followup=False)
|
| 305 |
+
user = build_micro_user_prompt(
|
| 306 |
+
context=context,
|
| 307 |
+
query=req.query,
|
| 308 |
+
function_name=req.function_name,
|
| 309 |
+
class_name=req.class_name,
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
console.print(
|
| 313 |
+
f"[cyan]β Micro[/cyan] "
|
| 314 |
+
f"| fn: {req.function_name} "
|
| 315 |
+
f"| {len(context)} chars"
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
content = _call_llm(system, user, self.model)
|
| 319 |
+
|
| 320 |
+
return InferenceResponse(
|
| 321 |
+
success=True,
|
| 322 |
+
content=content,
|
| 323 |
+
metadata={
|
| 324 |
+
"query_type": "micro",
|
| 325 |
+
"function_name": req.function_name,
|
| 326 |
+
"class_name": req.class_name,
|
| 327 |
+
"context_chars": len(context),
|
| 328 |
+
"followup": req.followup,
|
| 329 |
+
}
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
# ββ Cross-Module Handler ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 333 |
+
|
| 334 |
+
def _handle_cross_module(self, req: InferenceRequest) -> InferenceResponse:
|
| 335 |
+
"""Handle cross-module queries (dependency + impact analysis)."""
|
| 336 |
+
|
| 337 |
+
if not req.function_name:
|
| 338 |
+
return InferenceResponse(
|
| 339 |
+
success=False,
|
| 340 |
+
error="Cross-module queries require a function_name."
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# Primary context β the target function
|
| 344 |
+
primary_chunks = self.retriever.retrieve_by_function(
|
| 345 |
+
req.function_name, req.class_name, n_results=2
|
| 346 |
+
)
|
| 347 |
+
primary_context = self.retriever.build_context(
|
| 348 |
+
primary_chunks, CONTEXT_MAX_CHARS // 2
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# Dependency context β callers and callees
|
| 352 |
+
dep_chunks = self.retriever.retrieve_dependencies(req.function_name)
|
| 353 |
+
dep_context = self.retriever.build_context(
|
| 354 |
+
dep_chunks, CONTEXT_MAX_CHARS // 2
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
# Call graph data
|
| 358 |
+
try:
|
| 359 |
+
graph = get_call_graph()
|
| 360 |
+
calls = graph.get_calls(req.function_name, req.class_name)
|
| 361 |
+
called_by = graph.get_called_by(req.function_name, req.class_name)
|
| 362 |
+
impact = graph.get_impact(req.function_name, req.class_name, depth=2)
|
| 363 |
+
except FileNotFoundError:
|
| 364 |
+
calls, called_by, impact = [], [], {}
|
| 365 |
+
console.print(
|
| 366 |
+
"[yellow]β call_graph.json not found β "
|
| 367 |
+
"proceeding without graph data.[/yellow]"
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
call_graph_payload = build_call_graph_payload(
|
| 371 |
+
function_name=req.function_name,
|
| 372 |
+
calls=calls,
|
| 373 |
+
called_by=called_by,
|
| 374 |
+
impact=impact,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
# Follow-up path
|
| 378 |
+
if req.followup and req.previous_response:
|
| 379 |
+
system = get_cross_module_system_prompt(followup=True)
|
| 380 |
+
user = build_cross_module_followup_prompt(
|
| 381 |
+
previous_response=req.previous_response,
|
| 382 |
+
followup_query=req.query,
|
| 383 |
+
primary_context=primary_context,
|
| 384 |
+
dependency_context=dep_context,
|
| 385 |
+
call_graph_data=call_graph_payload,
|
| 386 |
+
)
|
| 387 |
+
else:
|
| 388 |
+
system = get_cross_module_system_prompt(followup=False)
|
| 389 |
+
user = build_cross_module_user_prompt(
|
| 390 |
+
primary_context=primary_context,
|
| 391 |
+
dependency_context=dep_context,
|
| 392 |
+
call_graph_data=call_graph_payload,
|
| 393 |
+
query=req.query,
|
| 394 |
+
function_name=req.function_name,
|
| 395 |
+
class_name=req.class_name,
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
console.print(
|
| 399 |
+
f"[cyan]β Cross-Module[/cyan] "
|
| 400 |
+
f"| fn: {req.function_name} "
|
| 401 |
+
f"| deps: {len(dep_chunks)} "
|
| 402 |
+
f"| graph nodes: {len(calls) + len(called_by)}"
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
content = _call_llm(system, user, self.model)
|
| 406 |
+
|
| 407 |
+
return InferenceResponse(
|
| 408 |
+
success=True,
|
| 409 |
+
content=content,
|
| 410 |
+
metadata={
|
| 411 |
+
"query_type": "cross_module",
|
| 412 |
+
"function_name": req.function_name,
|
| 413 |
+
"calls": len(calls),
|
| 414 |
+
"called_by": len(called_by),
|
| 415 |
+
"dep_chunks": len(dep_chunks),
|
| 416 |
+
"followup": req.followup,
|
| 417 |
+
}
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
# ββ Singleton Access ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 422 |
+
|
| 423 |
+
_engine_instance: InferenceEngine | None = None
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def get_engine(model: str = DEFAULT_MODEL) -> InferenceEngine:
|
| 427 |
+
"""
|
| 428 |
+
Return a singleton InferenceEngine instance.
|
| 429 |
+
Creates it on first call, reuses on subsequent calls.
|
| 430 |
+
|
| 431 |
+
Args:
|
| 432 |
+
model: OpenRouter model identifier.
|
| 433 |
+
|
| 434 |
+
Returns:
|
| 435 |
+
Shared InferenceEngine instance.
|
| 436 |
+
"""
|
| 437 |
+
global _engine_instance
|
| 438 |
+
if _engine_instance is None:
|
| 439 |
+
_engine_instance = InferenceEngine(model)
|
| 440 |
+
return _engine_instance
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
# ββ Entry Point (manual test) βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 444 |
+
|
| 445 |
+
if __name__ == "__main__":
|
| 446 |
+
import sys
|
| 447 |
+
from rich.markdown import Markdown
|
| 448 |
+
|
| 449 |
+
console.rule("[bold cyan]Inference Engine β Manual Test[/bold cyan]")
|
| 450 |
+
|
| 451 |
+
engine = get_engine()
|
| 452 |
+
|
| 453 |
+
# Test micro query
|
| 454 |
+
console.rule("[cyan]Test β Micro Query[/cyan]")
|
| 455 |
+
req = InferenceRequest(
|
| 456 |
+
query_type="micro",
|
| 457 |
+
query="What does this function do and how do I use it?",
|
| 458 |
+
function_name=sys.argv[1] if len(sys.argv) > 1 else "parse_file",
|
| 459 |
+
class_name="",
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
resp = engine.infer(req)
|
| 463 |
+
|
| 464 |
+
if resp.success:
|
| 465 |
+
console.print(Markdown(resp.content))
|
| 466 |
+
console.print(f"\n[dim]Metadata: {resp.metadata}[/dim]")
|
| 467 |
+
else:
|
| 468 |
+
console.print(f"[red]β Error: {resp.error}[/red]")
|
ingest/__init__.py
ADDED
|
File without changes
|
ingest/embed.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
embedder.py
|
| 3 |
+
-----------
|
| 4 |
+
Converts parsed codebase (FileInfo objects) into hybrid chunks and
|
| 5 |
+
stores them in ChromaDB across two collections:
|
| 6 |
+
|
| 7 |
+
- class_chunks : one chunk per class (for macro / cross-module queries)
|
| 8 |
+
- function_chunks : one chunk per function/method (for micro queries)
|
| 9 |
+
|
| 10 |
+
Each chunk carries rich metadata so the retriever can filter precisely.
|
| 11 |
+
|
| 12 |
+
Depends on:
|
| 13 |
+
- ast_parser.parse_codebase() β list[FileInfo]
|
| 14 |
+
- chromadb
|
| 15 |
+
- sentence-transformers (local embedding, no API needed)
|
| 16 |
+
|
| 17 |
+
Install:
|
| 18 |
+
pip install chromadb sentence-transformers rich
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import json
|
| 22 |
+
import hashlib
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
|
| 25 |
+
from pinecone import Pinecone, ServerlessSpec
|
| 26 |
+
from sentence_transformers import SentenceTransformer
|
| 27 |
+
from rich.console import Console
|
| 28 |
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
|
| 29 |
+
from rich.panel import Panel
|
| 30 |
+
from rich.table import Table
|
| 31 |
+
from rich import box
|
| 32 |
+
from config.config import PINECONE_API_KEY, PINECONE_INDEX, EMBEDDING_DIM
|
| 33 |
+
from ingest.parse_ast import parse_codebase, FileInfo, ClassInfo, FunctionInfo
|
| 34 |
+
|
| 35 |
+
console = Console()
|
| 36 |
+
|
| 37 |
+
# ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
|
| 39 |
+
EMBEDDING_MODEL = "all-MiniLM-L6-v2" # fast, lightweight, good quality
|
| 40 |
+
CLASS_COLLECTION = "class_chunks"
|
| 41 |
+
FUNCTION_COLLECTION = "function_chunks"
|
| 42 |
+
COMPLETE_COLLECTION = "complete_chunks"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ββ Embedding Model βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
+
|
| 47 |
+
def load_embedding_model() -> SentenceTransformer:
|
| 48 |
+
"""Load the sentence transformer embedding model."""
|
| 49 |
+
with console.status("[bold cyan]Loading embedding model...[/bold cyan]"):
|
| 50 |
+
model = SentenceTransformer(EMBEDDING_MODEL)
|
| 51 |
+
console.print(f"[green]β[/green] Embedding model loaded: [cyan]{EMBEDDING_MODEL}[/cyan]")
|
| 52 |
+
return model
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# ββ ChromaDB Client βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 56 |
+
|
| 57 |
+
def get_pinecone_index():
|
| 58 |
+
"""Return a Pinecone index, creating it if it does not exist."""
|
| 59 |
+
pc = Pinecone(api_key=PINECONE_API_KEY)
|
| 60 |
+
existing = [i.name for i in pc.list_indexes()]
|
| 61 |
+
if PINECONE_INDEX not in existing:
|
| 62 |
+
pc.create_index(
|
| 63 |
+
name=PINECONE_INDEX,
|
| 64 |
+
dimension=EMBEDDING_DIM,
|
| 65 |
+
metric="cosine",
|
| 66 |
+
spec=ServerlessSpec(cloud="aws", region="us-east-1"),
|
| 67 |
+
)
|
| 68 |
+
return pc.Index(PINECONE_INDEX)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ββ Chunk Builders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
|
| 73 |
+
def _make_id(text: str) -> str:
|
| 74 |
+
"""Generate a stable unique ID from chunk text."""
|
| 75 |
+
return hashlib.md5(text.encode()).hexdigest()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def build_class_chunk(cls: ClassInfo, file_info: FileInfo) -> dict:
|
| 79 |
+
"""
|
| 80 |
+
Build a class-level chunk document.
|
| 81 |
+
Contains: class name, bases, docstring, all method signatures.
|
| 82 |
+
"""
|
| 83 |
+
method_signatures = []
|
| 84 |
+
for m in cls.methods:
|
| 85 |
+
params = ", ".join(
|
| 86 |
+
f"{p.name}: {p.annotation}" if p.annotation else p.name
|
| 87 |
+
for p in m.parameters
|
| 88 |
+
)
|
| 89 |
+
ret = f" -> {m.return_type}" if m.return_type else ""
|
| 90 |
+
method_signatures.append(f" def {m.name}({params}){ret}")
|
| 91 |
+
|
| 92 |
+
methods_block = "\n".join(method_signatures) if method_signatures else " # no methods"
|
| 93 |
+
bases_str = ", ".join(cls.bases) if cls.bases else "object"
|
| 94 |
+
docstring = cls.docstring or "No docstring provided."
|
| 95 |
+
|
| 96 |
+
text = (
|
| 97 |
+
f"Class: {cls.name}\n"
|
| 98 |
+
f"Inherits: {bases_str}\n"
|
| 99 |
+
f"Module: {file_info.module}\n"
|
| 100 |
+
f"File: {file_info.relative}\n"
|
| 101 |
+
f"Docstring: {docstring}\n"
|
| 102 |
+
f"Methods:\n{methods_block}"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
metadata = {
|
| 106 |
+
"type": "class",
|
| 107 |
+
"name": cls.name,
|
| 108 |
+
"module": file_info.module,
|
| 109 |
+
"file": file_info.relative,
|
| 110 |
+
"bases": json.dumps(cls.bases),
|
| 111 |
+
"methods": json.dumps([m.name for m in cls.methods]),
|
| 112 |
+
"lineno": cls.lineno,
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
return {"id": _make_id(text), "text": text, "metadata": metadata}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def build_function_chunk(func: FunctionInfo,
|
| 119 |
+
file_info: FileInfo,
|
| 120 |
+
class_name: str | None = None,
|
| 121 |
+
class_docstring: str | None = None) -> dict:
|
| 122 |
+
"""
|
| 123 |
+
Build a function/method-level chunk document.
|
| 124 |
+
Carries class context as metadata so micro queries stay grounded.
|
| 125 |
+
"""
|
| 126 |
+
params = ", ".join(
|
| 127 |
+
f"{p.name}: {p.annotation}" if p.annotation else p.name
|
| 128 |
+
for p in func.parameters
|
| 129 |
+
)
|
| 130 |
+
ret = f" -> {func.return_type}" if func.return_type else ""
|
| 131 |
+
signature = f"def {func.name}({params}){ret}"
|
| 132 |
+
|
| 133 |
+
docstring = func.docstring or "No docstring provided."
|
| 134 |
+
calls_str = ", ".join(func.calls[:15]) if func.calls else "none"
|
| 135 |
+
class_ctx = (
|
| 136 |
+
f"Class: {class_name}\nClass purpose: {class_docstring or 'N/A'}\n"
|
| 137 |
+
if class_name else "Scope: top-level function\n"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
text = (
|
| 141 |
+
f"{class_ctx}"
|
| 142 |
+
f"Function: {func.name}\n"
|
| 143 |
+
f"Module: {file_info.module}\n"
|
| 144 |
+
f"File: {file_info.relative}\n"
|
| 145 |
+
f"Signature: {signature}\n"
|
| 146 |
+
f"Docstring: {docstring}\n"
|
| 147 |
+
f"Calls: {calls_str}"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
metadata = {
|
| 151 |
+
"type": "function",
|
| 152 |
+
"name": func.name,
|
| 153 |
+
"module": file_info.module,
|
| 154 |
+
"file": file_info.relative,
|
| 155 |
+
"class_name": class_name or "",
|
| 156 |
+
"return_type": func.return_type or "",
|
| 157 |
+
"parameters": json.dumps([p.name for p in func.parameters]),
|
| 158 |
+
"calls": json.dumps(func.calls[:15]),
|
| 159 |
+
"is_method": str(func.is_method),
|
| 160 |
+
"lineno": func.lineno,
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
return {"id": _make_id(text), "text": text, "metadata": metadata}
|
| 164 |
+
|
| 165 |
+
def build_module_chunk(file_info: FileInfo) -> dict:
|
| 166 |
+
"""
|
| 167 |
+
Build a module-level chunk for files that contain no classes or functions.
|
| 168 |
+
Captures imports and docstring as the indexable content.
|
| 169 |
+
"""
|
| 170 |
+
imports_str = ", ".join(file_info.imports) if file_info.imports else "none"
|
| 171 |
+
docstring = file_info.docstring or "No module docstring."
|
| 172 |
+
|
| 173 |
+
text = (
|
| 174 |
+
f"Module: {file_info.module}\n"
|
| 175 |
+
f"File: {file_info.relative}\n"
|
| 176 |
+
f"Docstring: {docstring}\n"
|
| 177 |
+
f"Imports: {imports_str}\n"
|
| 178 |
+
f"Note: This file contains only module-level statements."
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
metadata = {
|
| 182 |
+
"type": "module",
|
| 183 |
+
"name": Path(file_info.relative).stem,
|
| 184 |
+
"module": file_info.module,
|
| 185 |
+
"file": file_info.relative,
|
| 186 |
+
"class_name": "",
|
| 187 |
+
"return_type": "",
|
| 188 |
+
"parameters": "[]",
|
| 189 |
+
"calls": "[]",
|
| 190 |
+
"is_method": "False",
|
| 191 |
+
"lineno": 0,
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
return {"id": _make_id(text), "text": text, "metadata": metadata}
|
| 195 |
+
|
| 196 |
+
def build_complete_function_chunk(func: FunctionInfo,
|
| 197 |
+
file_info: FileInfo,
|
| 198 |
+
class_name: str | None = None,
|
| 199 |
+
class_docstring: str | None = None) -> dict:
|
| 200 |
+
"""
|
| 201 |
+
Build a complete function chunk including full source code.
|
| 202 |
+
Used for edge case analysis and usage example generation.
|
| 203 |
+
"""
|
| 204 |
+
params = ", ".join(
|
| 205 |
+
f"{p.name}: {p.annotation}" if p.annotation else p.name
|
| 206 |
+
for p in func.parameters
|
| 207 |
+
)
|
| 208 |
+
ret = f" -> {func.return_type}" if func.return_type else ""
|
| 209 |
+
signature = f"def {func.name}({params}){ret}"
|
| 210 |
+
|
| 211 |
+
docstring = func.docstring or "No docstring provided."
|
| 212 |
+
calls_str = ", ".join(func.calls[:15]) if func.calls else "none"
|
| 213 |
+
class_ctx = (
|
| 214 |
+
f"Class: {class_name}\nClass purpose: {class_docstring or 'N/A'}\n"
|
| 215 |
+
if class_name else "Scope: top-level function\n"
|
| 216 |
+
)
|
| 217 |
+
source_block = func.source if func.source else "Source not available."
|
| 218 |
+
|
| 219 |
+
text = (
|
| 220 |
+
f"{class_ctx}"
|
| 221 |
+
f"Function: {func.name}\n"
|
| 222 |
+
f"Module: {file_info.module}\n"
|
| 223 |
+
f"File: {file_info.relative}\n"
|
| 224 |
+
f"Signature: {signature}\n"
|
| 225 |
+
f"Docstring: {docstring}\n"
|
| 226 |
+
f"Calls: {calls_str}\n"
|
| 227 |
+
f"Source Code:\n{source_block}"
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
metadata = {
|
| 231 |
+
"type": "complete_function",
|
| 232 |
+
"name": func.name,
|
| 233 |
+
"module": file_info.module,
|
| 234 |
+
"file": file_info.relative,
|
| 235 |
+
"class_name": class_name or "",
|
| 236 |
+
"return_type": func.return_type or "",
|
| 237 |
+
"parameters": json.dumps([p.name for p in func.parameters]),
|
| 238 |
+
"calls": json.dumps(func.calls[:15]),
|
| 239 |
+
"is_method": str(func.is_method),
|
| 240 |
+
"lineno": func.lineno,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return {"id": _make_id(text), "text": text, "metadata": metadata}
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def build_complete_class_chunk(cls: ClassInfo, file_info: FileInfo) -> dict:
|
| 247 |
+
"""
|
| 248 |
+
Build a complete class chunk including full source code.
|
| 249 |
+
Used for class-level deep queries.
|
| 250 |
+
"""
|
| 251 |
+
bases_str = ", ".join(cls.bases) if cls.bases else "object"
|
| 252 |
+
docstring = cls.docstring or "No docstring provided."
|
| 253 |
+
source_block = cls.source if cls.source else "Source not available."
|
| 254 |
+
|
| 255 |
+
text = (
|
| 256 |
+
f"Class: {cls.name}\n"
|
| 257 |
+
f"Inherits: {bases_str}\n"
|
| 258 |
+
f"Module: {file_info.module}\n"
|
| 259 |
+
f"File: {file_info.relative}\n"
|
| 260 |
+
f"Docstring: {docstring}\n"
|
| 261 |
+
f"Source Code:\n{source_block}"
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
metadata = {
|
| 265 |
+
"type": "complete_class",
|
| 266 |
+
"name": cls.name,
|
| 267 |
+
"module": file_info.module,
|
| 268 |
+
"file": file_info.relative,
|
| 269 |
+
"bases": json.dumps(cls.bases),
|
| 270 |
+
"methods": json.dumps([m.name for m in cls.methods]),
|
| 271 |
+
"lineno": cls.lineno,
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
return {"id": _make_id(text), "text": text, "metadata": metadata}
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def build_file_chunk(file_info: FileInfo) -> dict:
|
| 278 |
+
"""
|
| 279 |
+
Build a file-level chunk containing the entire source of a file.
|
| 280 |
+
Used for file-wide queries.
|
| 281 |
+
"""
|
| 282 |
+
try:
|
| 283 |
+
source_block = Path(file_info.path).read_text(encoding="utf-8", errors="ignore")
|
| 284 |
+
except Exception:
|
| 285 |
+
source_block = "Source not available."
|
| 286 |
+
|
| 287 |
+
docstring = file_info.docstring or "No module docstring."
|
| 288 |
+
imports_str = ", ".join(file_info.imports) if file_info.imports else "none"
|
| 289 |
+
|
| 290 |
+
text = (
|
| 291 |
+
f"File: {file_info.relative}\n"
|
| 292 |
+
f"Module: {file_info.module}\n"
|
| 293 |
+
f"Docstring: {docstring}\n"
|
| 294 |
+
f"Imports: {imports_str}\n"
|
| 295 |
+
f"Source Code:\n{source_block}"
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
metadata = {
|
| 299 |
+
"type": "file",
|
| 300 |
+
"name": Path(file_info.relative).stem,
|
| 301 |
+
"module": file_info.module,
|
| 302 |
+
"file": file_info.relative,
|
| 303 |
+
"class_name": "",
|
| 304 |
+
"return_type": "",
|
| 305 |
+
"parameters": "[]",
|
| 306 |
+
"calls": "[]",
|
| 307 |
+
"is_method": "False",
|
| 308 |
+
"lineno": 0,
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
return {"id": _make_id(text), "text": text, "metadata": metadata}
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
# ββ Embedding & Upserting βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 315 |
+
|
| 316 |
+
def _upsert_batch(index, chunks: list[dict], model: SentenceTransformer, namespace: str) -> None:
|
| 317 |
+
"""Embed and upsert a list of chunks into a Pinecone namespace."""
|
| 318 |
+
if not chunks:
|
| 319 |
+
return
|
| 320 |
+
|
| 321 |
+
texts = [c["text"] for c in chunks]
|
| 322 |
+
ids = [c["id"] for c in chunks]
|
| 323 |
+
metadatas = [c["metadata"] for c in chunks]
|
| 324 |
+
|
| 325 |
+
embeddings = model.encode(texts, show_progress_bar=False).tolist()
|
| 326 |
+
|
| 327 |
+
vectors = [
|
| 328 |
+
{"id": vid, "values": vec, "metadata": {**meta, "text": txt}}
|
| 329 |
+
for vid, vec, meta, txt in zip(ids, embeddings, metadatas, texts)
|
| 330 |
+
]
|
| 331 |
+
|
| 332 |
+
index.upsert(vectors=vectors, namespace=namespace)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
# ββ Main Embed Pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 336 |
+
|
| 337 |
+
def embed_codebase(root_path: str) -> None:
|
| 338 |
+
"""
|
| 339 |
+
Full pipeline:
|
| 340 |
+
1. Parse codebase via ast_parser
|
| 341 |
+
2. Build hybrid chunks (class + function level)
|
| 342 |
+
3. Embed with sentence-transformers
|
| 343 |
+
4. Store in ChromaDB (two collections)
|
| 344 |
+
|
| 345 |
+
Args:
|
| 346 |
+
root_path: Absolute path to the monolithic codebase root.
|
| 347 |
+
"""
|
| 348 |
+
console.rule("[bold cyan]Codebase Oracle β Embedder[/bold cyan]")
|
| 349 |
+
|
| 350 |
+
# Step 1 β Parse
|
| 351 |
+
console.print(f"\n[bold]π Root:[/bold] {root_path}\n")
|
| 352 |
+
parsed_files: list[FileInfo] = parse_codebase(root_path)
|
| 353 |
+
|
| 354 |
+
if not parsed_files:
|
| 355 |
+
console.print("[yellow]β No Python files parsed. Exiting.[/yellow]")
|
| 356 |
+
return
|
| 357 |
+
|
| 358 |
+
# Step 2 β Build chunks
|
| 359 |
+
class_chunks: list[dict] = []
|
| 360 |
+
function_chunks: list[dict] = []
|
| 361 |
+
|
| 362 |
+
for file_info in parsed_files:
|
| 363 |
+
# Class-level chunks
|
| 364 |
+
for cls in file_info.classes:
|
| 365 |
+
class_chunks.append(build_class_chunk(cls, file_info))
|
| 366 |
+
|
| 367 |
+
# Method-level chunks (carry class context)
|
| 368 |
+
for method in cls.methods:
|
| 369 |
+
function_chunks.append(build_function_chunk(
|
| 370 |
+
method, file_info,
|
| 371 |
+
class_name=cls.name,
|
| 372 |
+
class_docstring=cls.docstring,
|
| 373 |
+
))
|
| 374 |
+
|
| 375 |
+
# Top-level function chunks
|
| 376 |
+
for func in file_info.functions:
|
| 377 |
+
function_chunks.append(build_function_chunk(func, file_info))
|
| 378 |
+
|
| 379 |
+
# Module-level chunk for files with no classes and no functions
|
| 380 |
+
if not file_info.classes and not file_info.functions:
|
| 381 |
+
function_chunks.append(build_module_chunk(file_info))
|
| 382 |
+
|
| 383 |
+
complete_chunks: list[dict] = []
|
| 384 |
+
|
| 385 |
+
for file_info in parsed_files:
|
| 386 |
+
complete_chunks.append(build_file_chunk(file_info))
|
| 387 |
+
for cls in file_info.classes:
|
| 388 |
+
complete_chunks.append(build_complete_class_chunk(cls, file_info))
|
| 389 |
+
for method in cls.methods:
|
| 390 |
+
complete_chunks.append(build_complete_function_chunk(
|
| 391 |
+
method, file_info,
|
| 392 |
+
class_name=cls.name,
|
| 393 |
+
class_docstring=cls.docstring,
|
| 394 |
+
))
|
| 395 |
+
for func in file_info.functions:
|
| 396 |
+
complete_chunks.append(build_complete_function_chunk(func, file_info))
|
| 397 |
+
|
| 398 |
+
console.print(
|
| 399 |
+
f"[green]β[/green] Chunks built: "
|
| 400 |
+
f"[magenta]{len(class_chunks)}[/magenta] class chunks Β· "
|
| 401 |
+
f"[cyan]{len(function_chunks)}[/cyan] function chunks Β· "
|
| 402 |
+
f"[yellow]{len(complete_chunks)}[/yellow] complete chunks\n"
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# Step 3 β Load model
|
| 406 |
+
model = load_embedding_model()
|
| 407 |
+
|
| 408 |
+
# Step 4 β Pinecone
|
| 409 |
+
index = get_pinecone_index()
|
| 410 |
+
|
| 411 |
+
with Progress(
|
| 412 |
+
SpinnerColumn(),
|
| 413 |
+
TextColumn("[progress.description]{task.description}"),
|
| 414 |
+
BarColumn(),
|
| 415 |
+
TextColumn("{task.completed}/{task.total}"),
|
| 416 |
+
console=console,
|
| 417 |
+
) as progress:
|
| 418 |
+
|
| 419 |
+
# Embed class chunks in batches of 32
|
| 420 |
+
BATCH = 32
|
| 421 |
+
task1 = progress.add_task(
|
| 422 |
+
"[magenta]Embedding class chunks...", total=len(class_chunks)
|
| 423 |
+
)
|
| 424 |
+
for i in range(0, len(class_chunks), BATCH):
|
| 425 |
+
batch = class_chunks[i:i + BATCH]
|
| 426 |
+
_upsert_batch(index, batch, model, CLASS_COLLECTION)
|
| 427 |
+
progress.advance(task1, len(batch))
|
| 428 |
+
|
| 429 |
+
task2 = progress.add_task(
|
| 430 |
+
"[cyan]Embedding function chunks...", total=len(function_chunks)
|
| 431 |
+
)
|
| 432 |
+
for i in range(0, len(function_chunks), BATCH):
|
| 433 |
+
batch = function_chunks[i:i + BATCH]
|
| 434 |
+
_upsert_batch(index, batch, model, FUNCTION_COLLECTION)
|
| 435 |
+
progress.advance(task2, len(batch))
|
| 436 |
+
|
| 437 |
+
task3 = progress.add_task(
|
| 438 |
+
"[yellow]Embedding complete chunks...", total=len(complete_chunks)
|
| 439 |
+
)
|
| 440 |
+
for i in range(0, len(complete_chunks), BATCH):
|
| 441 |
+
batch = complete_chunks[i:i + BATCH]
|
| 442 |
+
_upsert_batch(index, batch, model, COMPLETE_COLLECTION)
|
| 443 |
+
progress.advance(task3, len(batch))
|
| 444 |
+
|
| 445 |
+
# Step 5 β Summary
|
| 446 |
+
_render_embed_summary(root_path, class_chunks, function_chunks, complete_chunks)
|
| 447 |
+
|
| 448 |
+
def _render_embed_summary(root_path: str,
|
| 449 |
+
class_chunks: list[dict],
|
| 450 |
+
function_chunks: list[dict],
|
| 451 |
+
complete_chunks: list[dict]) -> None:
|
| 452 |
+
"""Render a rich summary panel after embedding."""
|
| 453 |
+
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
| 454 |
+
table.add_column(style="dim")
|
| 455 |
+
table.add_column(style="bold white")
|
| 456 |
+
|
| 457 |
+
table.add_row("Codebase", root_path)
|
| 458 |
+
table.add_row("Embedding model", EMBEDDING_MODEL)
|
| 459 |
+
table.add_row("Class chunks", str(len(class_chunks)))
|
| 460 |
+
table.add_row("Function chunks", str(len(function_chunks)))
|
| 461 |
+
table.add_row("Complete chunks", str(len(complete_chunks)))
|
| 462 |
+
table.add_row("Total chunks", str(len(class_chunks) + len(function_chunks) + len(complete_chunks)))
|
| 463 |
+
table.add_row("Collections", f"{CLASS_COLLECTION}, {FUNCTION_COLLECTION}, {COMPLETE_COLLECTION}")
|
| 464 |
+
table.add_row("Status", "[bold green]β Indexing complete[/bold green]")
|
| 465 |
+
|
| 466 |
+
console.print(Panel(table, title="[bold cyan]Embedding Summary[/bold cyan]",
|
| 467 |
+
border_style="cyan"))
|
| 468 |
+
console.print("\n[bold green]β Codebase indexed. Ready for queries.[/bold green]\n")
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# ββ Query Helper (for retriever.py later) ββββββββββββββββββββββββββββββββββββ
|
| 472 |
+
|
| 473 |
+
def query_chunks(query: str,
|
| 474 |
+
collection_name: str,
|
| 475 |
+
model: SentenceTransformer,
|
| 476 |
+
n_results: int = 5,
|
| 477 |
+
filters: dict | None = None) -> list[dict]:
|
| 478 |
+
"""
|
| 479 |
+
Query a Pinecone namespace and return top-n matching chunks.
|
| 480 |
+
|
| 481 |
+
Args:
|
| 482 |
+
query: Natural language query string.
|
| 483 |
+
collection_name: Namespace β CLASS_COLLECTION, FUNCTION_COLLECTION, or COMPLETE_COLLECTION.
|
| 484 |
+
model: Loaded SentenceTransformer model.
|
| 485 |
+
n_results: Number of results to return.
|
| 486 |
+
filters: Optional Pinecone metadata filters.
|
| 487 |
+
|
| 488 |
+
Returns:
|
| 489 |
+
List of dicts with keys: text, metadata, distance.
|
| 490 |
+
"""
|
| 491 |
+
index = get_pinecone_index()
|
| 492 |
+
embedding = model.encode([query]).tolist()[0]
|
| 493 |
+
|
| 494 |
+
kwargs: dict = {
|
| 495 |
+
"vector": embedding,
|
| 496 |
+
"top_k": n_results,
|
| 497 |
+
"namespace": collection_name,
|
| 498 |
+
"include_metadata": True,
|
| 499 |
+
}
|
| 500 |
+
if filters:
|
| 501 |
+
kwargs["filter"] = filters
|
| 502 |
+
|
| 503 |
+
results = index.query(**kwargs)
|
| 504 |
+
|
| 505 |
+
output = []
|
| 506 |
+
for match in results["matches"]:
|
| 507 |
+
meta = dict(match["metadata"])
|
| 508 |
+
text = meta.pop("text", "")
|
| 509 |
+
output.append({
|
| 510 |
+
"text": text,
|
| 511 |
+
"metadata": meta,
|
| 512 |
+
"distance": 1 - match["score"],
|
| 513 |
+
})
|
| 514 |
+
|
| 515 |
+
return output
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# ββ Entry Point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 519 |
+
|
| 520 |
+
if __name__ == "__main__":
|
| 521 |
+
import sys
|
| 522 |
+
|
| 523 |
+
path = sys.argv[1] if len(sys.argv) > 1 else "."
|
| 524 |
+
|
| 525 |
+
try:
|
| 526 |
+
embed_codebase(path)
|
| 527 |
+
except (FileNotFoundError, NotADirectoryError) as e:
|
| 528 |
+
console.print(f"[red]β {e}[/red]")
|
| 529 |
+
sys.exit(1)
|
ingest/parse_ast.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ast_parser.py
|
| 3 |
+
-------------
|
| 4 |
+
Parses Python source files using the built-in `ast` module to extract
|
| 5 |
+
structured metadata: classes, functions, parameters, return types,
|
| 6 |
+
docstrings, and call relationships (for cross-module analysis).
|
| 7 |
+
|
| 8 |
+
Uses `rich` for all terminal output.
|
| 9 |
+
Depends on file_walker.walk_codebase() for file discovery.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import ast
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from dataclasses import dataclass, field
|
| 16 |
+
|
| 17 |
+
from rich.console import Console
|
| 18 |
+
from rich.tree import Tree
|
| 19 |
+
from rich.table import Table
|
| 20 |
+
from rich.panel import Panel
|
| 21 |
+
from rich.text import Text
|
| 22 |
+
from rich import box
|
| 23 |
+
|
| 24 |
+
# Import from file_walker (must be in same directory or on PYTHONPATH)
|
| 25 |
+
from ingest.walk_files import walk_codebase, group_by_module
|
| 26 |
+
|
| 27 |
+
console = Console()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ββ Data Models βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class ParameterInfo:
|
| 34 |
+
name: str
|
| 35 |
+
annotation: str | None = None
|
| 36 |
+
default: str | None = None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class FunctionInfo:
|
| 41 |
+
name: str
|
| 42 |
+
parameters: list[ParameterInfo] = field(default_factory=list)
|
| 43 |
+
return_type: str | None = None
|
| 44 |
+
docstring: str | None = None
|
| 45 |
+
calls: list[str] = field(default_factory=list) # functions this calls
|
| 46 |
+
lineno: int = 0
|
| 47 |
+
is_method: bool = False
|
| 48 |
+
source: str | None = None
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class ClassInfo:
|
| 53 |
+
name: str
|
| 54 |
+
bases: list[str] = field(default_factory=list)
|
| 55 |
+
docstring: str | None = None
|
| 56 |
+
methods: list[FunctionInfo] = field(default_factory=list)
|
| 57 |
+
lineno: int = 0
|
| 58 |
+
source: str | None = None
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class FileInfo:
|
| 63 |
+
path: str
|
| 64 |
+
relative: str
|
| 65 |
+
module: str
|
| 66 |
+
classes: list[ClassInfo] = field(default_factory=list)
|
| 67 |
+
functions: list[FunctionInfo] = field(default_factory=list) # top-level
|
| 68 |
+
imports: list[str] = field(default_factory=list)
|
| 69 |
+
docstring: str | None = None
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ββ AST Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 73 |
+
|
| 74 |
+
def _annotation_to_str(node) -> str | None:
|
| 75 |
+
"""Convert an AST annotation node to a readable string."""
|
| 76 |
+
if node is None:
|
| 77 |
+
return None
|
| 78 |
+
try:
|
| 79 |
+
return ast.unparse(node)
|
| 80 |
+
except Exception:
|
| 81 |
+
return None
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _extract_calls(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[str]:
|
| 85 |
+
"""Extract all function/method call names from a function body."""
|
| 86 |
+
calls = []
|
| 87 |
+
for node in ast.walk(func_node):
|
| 88 |
+
if isinstance(node, ast.Call):
|
| 89 |
+
if isinstance(node.func, ast.Name):
|
| 90 |
+
calls.append(node.func.id)
|
| 91 |
+
elif isinstance(node.func, ast.Attribute):
|
| 92 |
+
calls.append(f"{ast.unparse(node.func.value)}.{node.func.attr}")
|
| 93 |
+
return list(dict.fromkeys(calls)) # deduplicate, preserve order
|
| 94 |
+
|
| 95 |
+
def _parse_function(node: ast.FunctionDef | ast.AsyncFunctionDef,
|
| 96 |
+
is_method: bool = False,
|
| 97 |
+
raw_source: str | None = None) -> FunctionInfo:
|
| 98 |
+
"""Parse a function or method AST node into a FunctionInfo."""
|
| 99 |
+
params = []
|
| 100 |
+
|
| 101 |
+
args = node.args
|
| 102 |
+
all_args = args.posonlyargs + args.args + args.kwonlyargs
|
| 103 |
+
defaults_offset = len(all_args) - len(args.defaults)
|
| 104 |
+
|
| 105 |
+
for i, arg in enumerate(all_args):
|
| 106 |
+
if is_method and arg.arg == "self":
|
| 107 |
+
continue
|
| 108 |
+
default_val = None
|
| 109 |
+
default_idx = i - defaults_offset
|
| 110 |
+
if default_idx >= 0 and default_idx < len(args.defaults):
|
| 111 |
+
try:
|
| 112 |
+
default_val = ast.unparse(args.defaults[default_idx])
|
| 113 |
+
except Exception:
|
| 114 |
+
default_val = "..."
|
| 115 |
+
|
| 116 |
+
params.append(ParameterInfo(
|
| 117 |
+
name=arg.arg,
|
| 118 |
+
annotation=_annotation_to_str(arg.annotation),
|
| 119 |
+
default=default_val,
|
| 120 |
+
))
|
| 121 |
+
|
| 122 |
+
# *args
|
| 123 |
+
if args.vararg:
|
| 124 |
+
params.append(ParameterInfo(
|
| 125 |
+
name=f"*{args.vararg.arg}",
|
| 126 |
+
annotation=_annotation_to_str(args.vararg.annotation),
|
| 127 |
+
))
|
| 128 |
+
|
| 129 |
+
# **kwargs
|
| 130 |
+
if args.kwarg:
|
| 131 |
+
params.append(ParameterInfo(
|
| 132 |
+
name=f"**{args.kwarg.arg}",
|
| 133 |
+
annotation=_annotation_to_str(args.kwarg.annotation),
|
| 134 |
+
))
|
| 135 |
+
|
| 136 |
+
source_segment = None
|
| 137 |
+
if raw_source is not None:
|
| 138 |
+
source_segment = ast.get_source_segment(raw_source, node)
|
| 139 |
+
|
| 140 |
+
return FunctionInfo(
|
| 141 |
+
name=node.name,
|
| 142 |
+
parameters=params,
|
| 143 |
+
return_type=_annotation_to_str(node.returns),
|
| 144 |
+
docstring=ast.get_docstring(node),
|
| 145 |
+
calls=_extract_calls(node),
|
| 146 |
+
lineno=node.lineno,
|
| 147 |
+
is_method=is_method,
|
| 148 |
+
source=source_segment,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
def _parse_class(node: ast.ClassDef, raw_source: str | None = None) -> ClassInfo:
|
| 152 |
+
"""Parse a class AST node into a ClassInfo."""
|
| 153 |
+
bases = []
|
| 154 |
+
for base in node.bases:
|
| 155 |
+
try:
|
| 156 |
+
bases.append(ast.unparse(base))
|
| 157 |
+
except Exception:
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
methods = []
|
| 161 |
+
for item in node.body:
|
| 162 |
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 163 |
+
methods.append(_parse_function(item, is_method=True, raw_source=raw_source))
|
| 164 |
+
|
| 165 |
+
class_source = None
|
| 166 |
+
if raw_source is not None:
|
| 167 |
+
class_source = ast.get_source_segment(raw_source, node)
|
| 168 |
+
|
| 169 |
+
return ClassInfo(
|
| 170 |
+
name=node.name,
|
| 171 |
+
bases=bases,
|
| 172 |
+
docstring=ast.get_docstring(node),
|
| 173 |
+
methods=methods,
|
| 174 |
+
lineno=node.lineno,
|
| 175 |
+
source=class_source,
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def parse_file(file_meta: dict) -> FileInfo | None:
|
| 180 |
+
"""
|
| 181 |
+
Parse a single Python source file and return a FileInfo.
|
| 182 |
+
Returns None if the file cannot be parsed (syntax errors, encoding issues).
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
file_meta: A dict from walk_codebase() with keys: path, relative, module.
|
| 186 |
+
"""
|
| 187 |
+
if file_meta["extension"] != ".py":
|
| 188 |
+
return None # AST parser only supports Python for now
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
source = Path(file_meta["path"]).read_text(encoding="utf-8", errors="ignore")
|
| 192 |
+
tree = ast.parse(source)
|
| 193 |
+
except SyntaxError as e:
|
| 194 |
+
console.print(f"[yellow]β Syntax error in {file_meta['relative']}: {e}[/yellow]")
|
| 195 |
+
return None
|
| 196 |
+
except Exception as e:
|
| 197 |
+
console.print(f"[yellow]β Could not parse {file_meta['relative']}: {e}[/yellow]")
|
| 198 |
+
return None
|
| 199 |
+
|
| 200 |
+
info = FileInfo(
|
| 201 |
+
path=file_meta["path"],
|
| 202 |
+
relative=file_meta["relative"],
|
| 203 |
+
module=file_meta["module"],
|
| 204 |
+
docstring=ast.get_docstring(tree),
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Extract imports
|
| 208 |
+
for node in ast.walk(tree):
|
| 209 |
+
if isinstance(node, ast.Import):
|
| 210 |
+
for alias in node.names:
|
| 211 |
+
info.imports.append(alias.name)
|
| 212 |
+
elif isinstance(node, ast.ImportFrom):
|
| 213 |
+
mod = node.module or ""
|
| 214 |
+
for alias in node.names:
|
| 215 |
+
info.imports.append(f"{mod}.{alias.name}")
|
| 216 |
+
|
| 217 |
+
# Extract top-level classes and functions
|
| 218 |
+
for node in tree.body:
|
| 219 |
+
if isinstance(node, ast.ClassDef):
|
| 220 |
+
info.classes.append(_parse_class(node, raw_source=source))
|
| 221 |
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 222 |
+
info.functions.append(_parse_function(node, is_method=False, raw_source=source))
|
| 223 |
+
|
| 224 |
+
return info
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def parse_codebase(root_path: str) -> list[FileInfo]:
|
| 228 |
+
"""
|
| 229 |
+
Walk and parse an entire codebase, returning FileInfo for every
|
| 230 |
+
parseable Python file.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
root_path: Absolute path to the codebase root.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
List of FileInfo objects (one per parsed file).
|
| 237 |
+
"""
|
| 238 |
+
files = walk_codebase(root_path)
|
| 239 |
+
py_files = [f for f in files if f["extension"] == ".py"]
|
| 240 |
+
|
| 241 |
+
results = []
|
| 242 |
+
with console.status(f"[bold cyan]Parsing {len(py_files)} Python files...[/bold cyan]"):
|
| 243 |
+
for f in py_files:
|
| 244 |
+
info = parse_file(f)
|
| 245 |
+
if info:
|
| 246 |
+
results.append(info)
|
| 247 |
+
|
| 248 |
+
return results
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
# ββ Rich Renderers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 252 |
+
|
| 253 |
+
def render_file_tree(parsed_files: list[FileInfo]) -> None:
|
| 254 |
+
"""Render a rich tree of the parsed codebase structure."""
|
| 255 |
+
root_tree = Tree(
|
| 256 |
+
"[bold bright_cyan]π¦ Codebase Structure[/bold bright_cyan]",
|
| 257 |
+
guide_style="dim cyan",
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
# Group by module
|
| 261 |
+
modules: dict[str, list[FileInfo]] = {}
|
| 262 |
+
for f in parsed_files:
|
| 263 |
+
modules.setdefault(f.module, []).append(f)
|
| 264 |
+
|
| 265 |
+
for module_name, module_files in sorted(modules.items()):
|
| 266 |
+
module_branch = root_tree.add(
|
| 267 |
+
f"[bold yellow]π {module_name}[/bold yellow]"
|
| 268 |
+
)
|
| 269 |
+
for file_info in module_files:
|
| 270 |
+
file_label = f"[green]{Path(file_info.relative).name}[/green]"
|
| 271 |
+
file_branch = module_branch.add(file_label)
|
| 272 |
+
|
| 273 |
+
for cls in file_info.classes:
|
| 274 |
+
cls_branch = file_branch.add(
|
| 275 |
+
f"[bold magenta]π· {cls.name}[/bold magenta]"
|
| 276 |
+
)
|
| 277 |
+
for method in cls.methods:
|
| 278 |
+
cls_branch.add(f"[cyan] Ζ {method.name}()[/cyan]")
|
| 279 |
+
|
| 280 |
+
for func in file_info.functions:
|
| 281 |
+
file_branch.add(f"[blue] Ζ {func.name}()[/blue]")
|
| 282 |
+
|
| 283 |
+
console.print(root_tree)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def render_function(func: FunctionInfo, class_name: str | None = None) -> None:
|
| 287 |
+
"""Render a detailed view of a single function/method."""
|
| 288 |
+
title = f"{'Method' if func.is_method else 'Function'}: "
|
| 289 |
+
title += f"{class_name}.{func.name}" if class_name else func.name
|
| 290 |
+
|
| 291 |
+
# Signature table
|
| 292 |
+
param_table = Table(
|
| 293 |
+
"Parameter", "Type", "Default",
|
| 294 |
+
box=box.SIMPLE_HEAD,
|
| 295 |
+
style="dim",
|
| 296 |
+
header_style="bold cyan",
|
| 297 |
+
show_edge=False,
|
| 298 |
+
)
|
| 299 |
+
for p in func.parameters:
|
| 300 |
+
param_table.add_row(
|
| 301 |
+
p.name,
|
| 302 |
+
p.annotation or "[dim]any[/dim]",
|
| 303 |
+
p.default or "[dim]β[/dim]",
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
return_str = func.return_type or "[dim]None / untyped[/dim]"
|
| 307 |
+
doc_str = func.docstring or "[dim]No docstring.[/dim]"
|
| 308 |
+
calls_str = ", ".join(func.calls[:10]) if func.calls else "[dim]none[/dim]"
|
| 309 |
+
|
| 310 |
+
content = Text()
|
| 311 |
+
content.append("π Docstring\n", style="bold")
|
| 312 |
+
content.append(f"{doc_str}\n\n")
|
| 313 |
+
content.append("β© Return type\n", style="bold")
|
| 314 |
+
content.append(f"{return_str}\n\n")
|
| 315 |
+
content.append("π Calls\n", style="bold")
|
| 316 |
+
content.append(calls_str)
|
| 317 |
+
|
| 318 |
+
console.print(Panel(content, title=f"[bold white]{title}[/bold white]",
|
| 319 |
+
border_style="cyan"))
|
| 320 |
+
if func.parameters:
|
| 321 |
+
console.print(param_table)
|
| 322 |
+
else:
|
| 323 |
+
console.print(" [dim]No parameters.[/dim]\n")
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def render_class(cls: ClassInfo) -> None:
|
| 327 |
+
"""Render a detailed view of a class and all its methods."""
|
| 328 |
+
bases_str = ", ".join(cls.bases) if cls.bases else "object"
|
| 329 |
+
doc_str = cls.docstring or "[dim]No docstring.[/dim]"
|
| 330 |
+
|
| 331 |
+
header = Text()
|
| 332 |
+
header.append(f"class {cls.name}", style="bold magenta")
|
| 333 |
+
header.append(f"({bases_str})", style="dim")
|
| 334 |
+
|
| 335 |
+
console.print(Panel(
|
| 336 |
+
f"[bold]Docstring:[/bold]\n{doc_str}",
|
| 337 |
+
title=str(header),
|
| 338 |
+
border_style="magenta",
|
| 339 |
+
))
|
| 340 |
+
|
| 341 |
+
for method in cls.methods:
|
| 342 |
+
render_function(method, class_name=cls.name)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def render_summary(parsed_files: list[FileInfo]) -> None:
|
| 346 |
+
"""Render a high-level summary table of the parsed codebase."""
|
| 347 |
+
table = Table(
|
| 348 |
+
"Module", "File", "Classes", "Functions", "Imports",
|
| 349 |
+
box=box.ROUNDED,
|
| 350 |
+
header_style="bold bright_cyan",
|
| 351 |
+
border_style="cyan",
|
| 352 |
+
show_lines=True,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
for f in parsed_files:
|
| 356 |
+
table.add_row(
|
| 357 |
+
f.module,
|
| 358 |
+
Path(f.relative).name,
|
| 359 |
+
str(len(f.classes)),
|
| 360 |
+
str(len(f.functions)),
|
| 361 |
+
str(len(f.imports)),
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
total_classes = sum(len(f.classes) for f in parsed_files)
|
| 365 |
+
total_functions = sum(len(f.functions) for f in parsed_files)
|
| 366 |
+
total_methods = sum(len(c.methods) for f in parsed_files for c in f.classes)
|
| 367 |
+
|
| 368 |
+
console.print(table)
|
| 369 |
+
console.print(
|
| 370 |
+
f"\n[bold]Totals:[/bold] "
|
| 371 |
+
f"[cyan]{len(parsed_files)}[/cyan] files Β· "
|
| 372 |
+
f"[magenta]{total_classes}[/magenta] classes Β· "
|
| 373 |
+
f"[blue]{total_functions}[/blue] top-level functions Β· "
|
| 374 |
+
f"[green]{total_methods}[/green] methods\n"
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
# ββ Entry Point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 379 |
+
|
| 380 |
+
if __name__ == "__main__":
|
| 381 |
+
path = sys.argv[1] if len(sys.argv) > 1 else "."
|
| 382 |
+
|
| 383 |
+
console.print(f"\n[bold cyan]π Parsing codebase:[/bold cyan] {path}\n")
|
| 384 |
+
|
| 385 |
+
try:
|
| 386 |
+
parsed = parse_codebase(path)
|
| 387 |
+
except (FileNotFoundError, NotADirectoryError) as e:
|
| 388 |
+
console.print(f"[red]β Error: {e}[/red]")
|
| 389 |
+
sys.exit(1)
|
| 390 |
+
|
| 391 |
+
if not parsed:
|
| 392 |
+
console.print("[yellow]No Python files found or parsed.[/yellow]")
|
| 393 |
+
sys.exit(0)
|
| 394 |
+
|
| 395 |
+
console.rule("[bold cyan]Summary[/bold cyan]")
|
| 396 |
+
render_summary(parsed)
|
| 397 |
+
|
| 398 |
+
console.rule("[bold cyan]Codebase Tree[/bold cyan]")
|
| 399 |
+
render_file_tree(parsed)
|
| 400 |
+
|
| 401 |
+
# Demo: render first class found
|
| 402 |
+
for f in parsed:
|
| 403 |
+
if f.classes:
|
| 404 |
+
console.rule(f"[bold cyan]Sample Class β {f.classes[0].name}[/bold cyan]")
|
| 405 |
+
render_class(f.classes[0])
|
| 406 |
+
break
|
| 407 |
+
|
| 408 |
+
# Demo: render first top-level function found
|
| 409 |
+
for f in parsed:
|
| 410 |
+
if f.functions:
|
| 411 |
+
console.rule(f"[bold cyan]Sample Function β {f.functions[0].name}[/bold cyan]")
|
| 412 |
+
render_function(f.functions[0])
|
| 413 |
+
break
|
ingest/walk_files.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
file_walker.py
|
| 3 |
+
--------------
|
| 4 |
+
Recursively walks a monolithic codebase directory and collects
|
| 5 |
+
all relevant source code files, filtered by supported extensions.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Supported source file extensions
|
| 12 |
+
SUPPORTED_EXTENSIONS = {
|
| 13 |
+
".py", # Python
|
| 14 |
+
".java", # Java
|
| 15 |
+
".js", # JavaScript
|
| 16 |
+
".ts", # TypeScript
|
| 17 |
+
".cpp", # C++
|
| 18 |
+
".c", # C
|
| 19 |
+
".cs", # C#
|
| 20 |
+
".go", # Go
|
| 21 |
+
".rb", # Ruby
|
| 22 |
+
".php", # PHP
|
| 23 |
+
".rs", # Rust
|
| 24 |
+
".kt", # Kotlin
|
| 25 |
+
".swift", # Swift
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
# Directories to always skip
|
| 29 |
+
EXCLUDED_DIRS = {
|
| 30 |
+
".devcontainer"
|
| 31 |
+
"chroma_db",
|
| 32 |
+
".github",
|
| 33 |
+
".git",
|
| 34 |
+
"__pycache__",
|
| 35 |
+
".git",
|
| 36 |
+
".svn",
|
| 37 |
+
"node_modules",
|
| 38 |
+
"venv",
|
| 39 |
+
".venv",
|
| 40 |
+
"env",
|
| 41 |
+
".env",
|
| 42 |
+
"dist",
|
| 43 |
+
"build",
|
| 44 |
+
".idea",
|
| 45 |
+
".vscode",
|
| 46 |
+
"migrations",
|
| 47 |
+
".mypy_cache",
|
| 48 |
+
".pytest_cache",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def walk_codebase(root_path: str) -> list[dict]:
|
| 53 |
+
"""
|
| 54 |
+
Recursively walk the codebase from root_path and return
|
| 55 |
+
a list of file metadata dicts for all supported source files.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
root_path: Absolute path to the root of the codebase.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
List of dicts, each containing:
|
| 62 |
+
- path : absolute file path (str)
|
| 63 |
+
- relative : path relative to root (str)
|
| 64 |
+
- extension : file extension (str)
|
| 65 |
+
- size_bytes : file size in bytes (int)
|
| 66 |
+
- module : top-level module/package name (str)
|
| 67 |
+
"""
|
| 68 |
+
root = Path(root_path).resolve()
|
| 69 |
+
|
| 70 |
+
if not root.exists():
|
| 71 |
+
raise FileNotFoundError(f"Path does not exist: {root}")
|
| 72 |
+
|
| 73 |
+
if not root.is_dir():
|
| 74 |
+
raise NotADirectoryError(f"Path is not a directory: {root}")
|
| 75 |
+
|
| 76 |
+
collected = []
|
| 77 |
+
|
| 78 |
+
for dirpath, dirnames, filenames in os.walk(root):
|
| 79 |
+
# Prune excluded directories in-place so os.walk skips them
|
| 80 |
+
dirnames[:] = [
|
| 81 |
+
d for d in dirnames
|
| 82 |
+
if d not in EXCLUDED_DIRS and not d.startswith(".")
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
for filename in filenames:
|
| 86 |
+
ext = Path(filename).suffix.lower()
|
| 87 |
+
|
| 88 |
+
if ext not in SUPPORTED_EXTENSIONS:
|
| 89 |
+
continue
|
| 90 |
+
|
| 91 |
+
abs_path = Path(dirpath) / filename
|
| 92 |
+
relative = abs_path.relative_to(root)
|
| 93 |
+
|
| 94 |
+
# Derive top-level module name (first directory under root)
|
| 95 |
+
parts = relative.parts
|
| 96 |
+
module = parts[0] if len(parts) > 1 else "<root>"
|
| 97 |
+
|
| 98 |
+
collected.append({
|
| 99 |
+
"path": str(abs_path),
|
| 100 |
+
"relative": str(relative),
|
| 101 |
+
"extension": ext,
|
| 102 |
+
"size_bytes": abs_path.stat().st_size,
|
| 103 |
+
"module": module,
|
| 104 |
+
})
|
| 105 |
+
|
| 106 |
+
# Sort by relative path for deterministic ordering
|
| 107 |
+
collected.sort(key=lambda f: f["relative"])
|
| 108 |
+
|
| 109 |
+
return collected
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def group_by_module(files: list[dict]) -> dict[str, list[dict]]:
|
| 113 |
+
"""
|
| 114 |
+
Group a flat list of file dicts by their top-level module name.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
files: Output of walk_codebase().
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
Dict mapping module name β list of file dicts.
|
| 121 |
+
"""
|
| 122 |
+
grouped: dict[str, list[dict]] = {}
|
| 123 |
+
|
| 124 |
+
for f in files:
|
| 125 |
+
module = f["module"]
|
| 126 |
+
grouped.setdefault(module, []).append(f)
|
| 127 |
+
|
| 128 |
+
return grouped
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def summarize(files: list[dict]) -> dict:
|
| 132 |
+
"""
|
| 133 |
+
Return a quick summary of the walked codebase.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
files: Output of walk_codebase().
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Dict with total_files, total_size_kb, modules, extensions breakdown.
|
| 140 |
+
"""
|
| 141 |
+
from collections import Counter
|
| 142 |
+
|
| 143 |
+
ext_counts = Counter(f["extension"] for f in files)
|
| 144 |
+
module_counts = Counter(f["module"] for f in files)
|
| 145 |
+
total_size = sum(f["size_bytes"] for f in files)
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
"total_files": len(files),
|
| 149 |
+
"total_size_kb": round(total_size / 1024, 2),
|
| 150 |
+
"modules": dict(module_counts),
|
| 151 |
+
"extensions": dict(ext_counts),
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ββ Quick test when run directly βββββββββββββββββββββββββββββββββββββββββββββ
|
| 156 |
+
if __name__ == "__main__":
|
| 157 |
+
import sys
|
| 158 |
+
import json
|
| 159 |
+
|
| 160 |
+
path = sys.argv[1] if len(sys.argv) > 1 else "."
|
| 161 |
+
|
| 162 |
+
print(f"\nπ Walking codebase: {path}\n")
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
files = walk_codebase(path)
|
| 166 |
+
except (FileNotFoundError, NotADirectoryError) as e:
|
| 167 |
+
print(f"β Error: {e}")
|
| 168 |
+
sys.exit(1)
|
| 169 |
+
|
| 170 |
+
summary = summarize(files)
|
| 171 |
+
|
| 172 |
+
print(f"β
Found {summary['total_files']} source files "
|
| 173 |
+
f"({summary['total_size_kb']} KB total)\n")
|
| 174 |
+
|
| 175 |
+
print("π¦ By module:")
|
| 176 |
+
for module, count in summary["modules"].items():
|
| 177 |
+
print(f" {module:<30} {count} files")
|
| 178 |
+
|
| 179 |
+
print("\nπ€ By extension:")
|
| 180 |
+
for ext, count in summary["extensions"].items():
|
| 181 |
+
print(f" {ext:<10} {count} files")
|
| 182 |
+
|
| 183 |
+
print("\nπ All files:")
|
| 184 |
+
for f in files:
|
| 185 |
+
print(f" {f['relative']}")
|
main.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
main.py
|
| 3 |
+
-------
|
| 4 |
+
FastAPI backend for the Codebase Oracle system.
|
| 5 |
+
This is the HTTP layer β thin wrapper around inference.py.
|
| 6 |
+
|
| 7 |
+
Endpoints:
|
| 8 |
+
POST /index β ingest + embed a codebase from given path
|
| 9 |
+
POST /query β run a query (macro / micro / cross_module)
|
| 10 |
+
GET /status β check if a codebase is indexed and ready
|
| 11 |
+
GET /tree β return parsed codebase tree for UI sidebar
|
| 12 |
+
GET /health β simple health check
|
| 13 |
+
|
| 14 |
+
Run:
|
| 15 |
+
uvicorn main:app --reload --port 8000
|
| 16 |
+
|
| 17 |
+
Depends on:
|
| 18 |
+
- inference.py
|
| 19 |
+
- embedder.py
|
| 20 |
+
- call_graph.py
|
| 21 |
+
- ast_parser.py
|
| 22 |
+
- vector_store.py
|
| 23 |
+
- fastapi, uvicorn, pydantic, python-dotenv
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import os
|
| 27 |
+
from contextlib import asynccontextmanager
|
| 28 |
+
from dotenv import load_dotenv
|
| 29 |
+
|
| 30 |
+
import tempfile
|
| 31 |
+
import zipfile
|
| 32 |
+
import shutil
|
| 33 |
+
from fastapi import FastAPI, HTTPException, UploadFile, File
|
| 34 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 35 |
+
from fastapi.staticfiles import StaticFiles
|
| 36 |
+
from fastapi.responses import FileResponse
|
| 37 |
+
from pydantic import BaseModel, Field
|
| 38 |
+
from rich.console import Console
|
| 39 |
+
|
| 40 |
+
from inference.inference import get_engine, InferenceRequest
|
| 41 |
+
from ingest.embed import embed_codebase
|
| 42 |
+
from store.call_graph import build_and_save, get_call_graph, CALL_GRAPH_PATH
|
| 43 |
+
from ingest.parse_ast import parse_codebase
|
| 44 |
+
from store.vector_store import get_vector_store
|
| 45 |
+
|
| 46 |
+
load_dotenv()
|
| 47 |
+
|
| 48 |
+
console = Console()
|
| 49 |
+
|
| 50 |
+
# ββ App Lifespan ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
+
|
| 52 |
+
@asynccontextmanager
|
| 53 |
+
async def lifespan(app: FastAPI):
|
| 54 |
+
"""Initialize shared resources on startup."""
|
| 55 |
+
console.rule("[bold cyan]Codebase Oracle β Starting[/bold cyan]")
|
| 56 |
+
# Pre-warm the inference engine (loads embedding model once)
|
| 57 |
+
get_engine()
|
| 58 |
+
console.print("[green]β[/green] Server ready.\n")
|
| 59 |
+
yield
|
| 60 |
+
console.print("[dim]Server shutting down.[/dim]")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
|
| 65 |
+
app = FastAPI(
|
| 66 |
+
title="Codebase Oracle",
|
| 67 |
+
description="AI-powered monolithic codebase comprehension system.",
|
| 68 |
+
version="1.0.0",
|
| 69 |
+
lifespan=lifespan,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Allow UI (served from same origin or localhost dev)
|
| 73 |
+
app.add_middleware(
|
| 74 |
+
CORSMiddleware,
|
| 75 |
+
allow_origins=["*"],
|
| 76 |
+
allow_methods=["*"],
|
| 77 |
+
allow_headers=["*"],
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# Serve UI static files
|
| 81 |
+
UI_DIR = os.path.join(os.path.dirname(__file__), "ui")
|
| 82 |
+
if os.path.exists(UI_DIR):
|
| 83 |
+
app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui")
|
| 84 |
+
app.mount("/static", StaticFiles(directory=os.path.join(UI_DIR, "static")), name="static")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# ββ Pydantic Request / Response Models ββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
+
|
| 89 |
+
class IndexRequest(BaseModel):
|
| 90 |
+
"""Request body for POST /index"""
|
| 91 |
+
root_path: str = Field(
|
| 92 |
+
...,
|
| 93 |
+
description="Absolute path to the monolithic codebase root directory.",
|
| 94 |
+
example="/home/user/projects/my-django-app"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class QueryRequest(BaseModel):
|
| 99 |
+
"""Request body for POST /query"""
|
| 100 |
+
query_type: str = Field(
|
| 101 |
+
...,
|
| 102 |
+
description="One of: 'macro', 'micro', 'cross_module'",
|
| 103 |
+
example="micro"
|
| 104 |
+
)
|
| 105 |
+
query: str = Field(
|
| 106 |
+
...,
|
| 107 |
+
description="Natural language developer query.",
|
| 108 |
+
example="What does process_payment do and how do I use it?"
|
| 109 |
+
)
|
| 110 |
+
subtype: str = Field(
|
| 111 |
+
default="",
|
| 112 |
+
description="Macro subtype: 'overall_architecture' | 'module_responsibility' | 'data_flow'",
|
| 113 |
+
example="overall_architecture"
|
| 114 |
+
)
|
| 115 |
+
function_name: str = Field(
|
| 116 |
+
default="",
|
| 117 |
+
description="Target function/method name for micro and cross_module queries.",
|
| 118 |
+
example="process_payment"
|
| 119 |
+
)
|
| 120 |
+
class_name: str = Field(
|
| 121 |
+
default="",
|
| 122 |
+
description="Target class name if function is a method.",
|
| 123 |
+
example="PaymentProcessor"
|
| 124 |
+
)
|
| 125 |
+
module_name: str = Field(
|
| 126 |
+
default="",
|
| 127 |
+
description="Target module name for macro module_responsibility queries.",
|
| 128 |
+
example="payments"
|
| 129 |
+
)
|
| 130 |
+
followup: bool = Field(
|
| 131 |
+
default=False,
|
| 132 |
+
description="True if this is a follow-up to a previous response."
|
| 133 |
+
)
|
| 134 |
+
previous_response: str = Field(
|
| 135 |
+
default="",
|
| 136 |
+
description="Previous LLM response for follow-up context."
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class IndexResponse(BaseModel):
|
| 141 |
+
success: bool
|
| 142 |
+
message: str
|
| 143 |
+
class_chunks: int = 0
|
| 144 |
+
function_chunks: int = 0
|
| 145 |
+
total_chunks: int = 0
|
| 146 |
+
graph_nodes: int = 0
|
| 147 |
+
graph_edges: int = 0
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class QueryResponse(BaseModel):
|
| 151 |
+
success: bool
|
| 152 |
+
content: str
|
| 153 |
+
error: str = ""
|
| 154 |
+
metadata: dict = {}
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class StatusResponse(BaseModel):
|
| 158 |
+
indexed: bool
|
| 159 |
+
class_chunks: int
|
| 160 |
+
function_chunks: int
|
| 161 |
+
total_chunks: int
|
| 162 |
+
graph_loaded: bool
|
| 163 |
+
graph_nodes: int
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class TreeNode(BaseModel):
|
| 167 |
+
name: str
|
| 168 |
+
type: str # "module" | "file" | "class" | "function"
|
| 169 |
+
children: list["TreeNode"] = []
|
| 170 |
+
|
| 171 |
+
TreeNode.model_rebuild()
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class TreeResponse(BaseModel):
|
| 175 |
+
success: bool
|
| 176 |
+
tree: list[TreeNode] = []
|
| 177 |
+
error: str = ""
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# ββ Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
+
|
| 182 |
+
@app.post("/upload-index", response_model=IndexResponse)
|
| 183 |
+
async def upload_index(file: UploadFile = File(...)):
|
| 184 |
+
"""
|
| 185 |
+
Accept a ZIP file, extract it to a temp directory, and index it.
|
| 186 |
+
Allows deployment without requiring local filesystem access.
|
| 187 |
+
"""
|
| 188 |
+
if not file.filename.endswith(".zip"):
|
| 189 |
+
raise HTTPException(status_code=400, detail="Only .zip files are accepted.")
|
| 190 |
+
|
| 191 |
+
tmp_dir = tempfile.mkdtemp()
|
| 192 |
+
|
| 193 |
+
try:
|
| 194 |
+
zip_path = os.path.join(tmp_dir, file.filename)
|
| 195 |
+
with open(zip_path, "wb") as f:
|
| 196 |
+
shutil.copyfileobj(file.file, f)
|
| 197 |
+
|
| 198 |
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
| 199 |
+
zf.extractall(tmp_dir)
|
| 200 |
+
|
| 201 |
+
os.remove(zip_path)
|
| 202 |
+
|
| 203 |
+
# Find the extracted root β skip __MACOSX and similar artifacts
|
| 204 |
+
candidates = [
|
| 205 |
+
os.path.join(tmp_dir, d)
|
| 206 |
+
for d in os.listdir(tmp_dir)
|
| 207 |
+
if os.path.isdir(os.path.join(tmp_dir, d)) and not d.startswith("__")
|
| 208 |
+
]
|
| 209 |
+
root = candidates[0] if candidates else tmp_dir
|
| 210 |
+
|
| 211 |
+
console.rule(f"[bold cyan]Indexing ZIP: {file.filename}[/bold cyan]")
|
| 212 |
+
|
| 213 |
+
embed_codebase(root)
|
| 214 |
+
|
| 215 |
+
graph = build_and_save(root)
|
| 216 |
+
graph_stats = graph.stats()
|
| 217 |
+
|
| 218 |
+
store = get_vector_store()
|
| 219 |
+
vstats = store.stats()
|
| 220 |
+
|
| 221 |
+
console.print("[bold green]β ZIP Indexing complete.[/bold green]\n")
|
| 222 |
+
|
| 223 |
+
return IndexResponse(
|
| 224 |
+
success=True,
|
| 225 |
+
message=f"ZIP indexed successfully: {file.filename}",
|
| 226 |
+
class_chunks=vstats["class_chunks"],
|
| 227 |
+
function_chunks=vstats["function_chunks"],
|
| 228 |
+
total_chunks=vstats["total"],
|
| 229 |
+
graph_nodes=graph_stats["total_nodes"],
|
| 230 |
+
graph_edges=graph_stats["total_edges"],
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
except zipfile.BadZipFile:
|
| 234 |
+
raise HTTPException(status_code=400, detail="Invalid or corrupted ZIP file.")
|
| 235 |
+
|
| 236 |
+
except Exception as e:
|
| 237 |
+
console.print(f"[red]β ZIP indexing failed: {e}[/red]")
|
| 238 |
+
raise HTTPException(status_code=500, detail=f"ZIP indexing failed: {str(e)}")
|
| 239 |
+
|
| 240 |
+
finally:
|
| 241 |
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
| 242 |
+
|
| 243 |
+
@app.get("/health")
|
| 244 |
+
async def health():
|
| 245 |
+
"""Simple health check."""
|
| 246 |
+
return {"status": "ok", "service": "Codebase Oracle"}
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@app.get("/", response_class=FileResponse)
|
| 250 |
+
async def serve_ui():
|
| 251 |
+
"""Serve the UI index.html at root."""
|
| 252 |
+
ui_path = os.path.join(UI_DIR, "index.html")
|
| 253 |
+
if not os.path.exists(ui_path):
|
| 254 |
+
raise HTTPException(
|
| 255 |
+
status_code=404,
|
| 256 |
+
detail="UI not found. Place index.html in the ui/ directory."
|
| 257 |
+
)
|
| 258 |
+
return FileResponse(ui_path)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@app.post("/index", response_model=IndexResponse)
|
| 262 |
+
async def index_codebase(req: IndexRequest):
|
| 263 |
+
"""
|
| 264 |
+
Ingest, parse, embed, and index a monolithic codebase.
|
| 265 |
+
Builds both ChromaDB vector index and call_graph.json.
|
| 266 |
+
|
| 267 |
+
This is the first endpoint to call before any queries.
|
| 268 |
+
"""
|
| 269 |
+
root = req.root_path.strip()
|
| 270 |
+
|
| 271 |
+
if not os.path.exists(root):
|
| 272 |
+
raise HTTPException(
|
| 273 |
+
status_code=400,
|
| 274 |
+
detail=f"Path does not exist: {root}"
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
if not os.path.isdir(root):
|
| 278 |
+
raise HTTPException(
|
| 279 |
+
status_code=400,
|
| 280 |
+
detail=f"Path is not a directory: {root}"
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
try:
|
| 284 |
+
console.rule(f"[bold cyan]Indexing: {root}[/bold cyan]")
|
| 285 |
+
|
| 286 |
+
# Step 1 β Embed codebase into ChromaDB
|
| 287 |
+
embed_codebase(root)
|
| 288 |
+
|
| 289 |
+
# Step 2 β Build and save call graph
|
| 290 |
+
graph = build_and_save(root)
|
| 291 |
+
graph_stats = graph.stats()
|
| 292 |
+
|
| 293 |
+
# Step 3 β Fetch vector store stats
|
| 294 |
+
store = get_vector_store()
|
| 295 |
+
vstats = store.stats()
|
| 296 |
+
|
| 297 |
+
console.print("[bold green]β Indexing complete.[/bold green]\n")
|
| 298 |
+
|
| 299 |
+
return IndexResponse(
|
| 300 |
+
success=True,
|
| 301 |
+
message=f"Codebase indexed successfully: {root}",
|
| 302 |
+
class_chunks=vstats["class_chunks"],
|
| 303 |
+
function_chunks=vstats["function_chunks"],
|
| 304 |
+
total_chunks=vstats["total"],
|
| 305 |
+
graph_nodes=graph_stats["total_nodes"],
|
| 306 |
+
graph_edges=graph_stats["total_edges"],
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
except Exception as e:
|
| 310 |
+
console.print(f"[red]β Indexing failed: {e}[/red]")
|
| 311 |
+
raise HTTPException(status_code=500, detail=f"Indexing failed: {str(e)}")
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@app.post("/query", response_model=QueryResponse)
|
| 315 |
+
async def query(req: QueryRequest):
|
| 316 |
+
"""
|
| 317 |
+
Run a macro, micro, or cross-module query against the indexed codebase.
|
| 318 |
+
Returns a markdown-formatted response string.
|
| 319 |
+
"""
|
| 320 |
+
store = get_vector_store()
|
| 321 |
+
if not store.is_indexed():
|
| 322 |
+
raise HTTPException(
|
| 323 |
+
status_code=400,
|
| 324 |
+
detail="Codebase is not indexed yet. Call POST /index first."
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
engine = get_engine()
|
| 328 |
+
|
| 329 |
+
inference_req = InferenceRequest(
|
| 330 |
+
query_type=req.query_type,
|
| 331 |
+
query=req.query,
|
| 332 |
+
subtype=req.subtype,
|
| 333 |
+
function_name=req.function_name,
|
| 334 |
+
class_name=req.class_name,
|
| 335 |
+
module_name=req.module_name,
|
| 336 |
+
followup=req.followup,
|
| 337 |
+
previous_response=req.previous_response,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
resp = engine.infer(inference_req)
|
| 341 |
+
|
| 342 |
+
return QueryResponse(
|
| 343 |
+
success=resp.success,
|
| 344 |
+
content=resp.content,
|
| 345 |
+
error=resp.error,
|
| 346 |
+
metadata=resp.metadata,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
@app.get("/status", response_model=StatusResponse)
|
| 351 |
+
async def status():
|
| 352 |
+
"""
|
| 353 |
+
Check whether the codebase is indexed and the system is ready for queries.
|
| 354 |
+
"""
|
| 355 |
+
store = get_vector_store()
|
| 356 |
+
vstats = store.stats()
|
| 357 |
+
|
| 358 |
+
graph_loaded = False
|
| 359 |
+
graph_nodes = 0
|
| 360 |
+
|
| 361 |
+
if os.path.exists(CALL_GRAPH_PATH):
|
| 362 |
+
try:
|
| 363 |
+
graph = get_call_graph()
|
| 364 |
+
graph_loaded = graph.is_loaded()
|
| 365 |
+
graph_nodes = graph.stats()["total_nodes"]
|
| 366 |
+
except Exception:
|
| 367 |
+
pass
|
| 368 |
+
|
| 369 |
+
return StatusResponse(
|
| 370 |
+
indexed=store.is_indexed(),
|
| 371 |
+
class_chunks=vstats["class_chunks"],
|
| 372 |
+
function_chunks=vstats["function_chunks"],
|
| 373 |
+
total_chunks=vstats["total"],
|
| 374 |
+
graph_loaded=graph_loaded,
|
| 375 |
+
graph_nodes=graph_nodes,
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
@app.get("/tree", response_model=TreeResponse)
|
| 380 |
+
async def get_tree():
|
| 381 |
+
"""
|
| 382 |
+
Return the parsed codebase structure as a nested tree.
|
| 383 |
+
Used by the UI sidebar to render the codebase explorer.
|
| 384 |
+
"""
|
| 385 |
+
store = get_vector_store()
|
| 386 |
+
if not store.is_indexed():
|
| 387 |
+
return TreeResponse(
|
| 388 |
+
success=False,
|
| 389 |
+
error="Codebase not indexed yet. Call POST /index first."
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
try:
|
| 393 |
+
# Fetch both class and function chunks to reconstruct tree
|
| 394 |
+
class_results = store.get_all("class_chunks", limit=500)
|
| 395 |
+
func_results = store.get_all("function_chunks", limit=500)
|
| 396 |
+
|
| 397 |
+
# Group by module β file β classes/functions
|
| 398 |
+
modules: dict[str, dict[str, dict[str, set]]] = {}
|
| 399 |
+
|
| 400 |
+
# --- classes ---
|
| 401 |
+
for chunk in class_results:
|
| 402 |
+
mod = chunk.module
|
| 403 |
+
file = chunk.file
|
| 404 |
+
modules.setdefault(mod, {}).setdefault(file, {"classes": set(), "functions": set()})
|
| 405 |
+
modules[mod][file]["classes"].add(chunk.name)
|
| 406 |
+
|
| 407 |
+
# --- functions (top-level only) ---
|
| 408 |
+
for chunk in func_results:
|
| 409 |
+
if not chunk.class_name:
|
| 410 |
+
mod = chunk.module
|
| 411 |
+
file = chunk.file
|
| 412 |
+
modules.setdefault(mod, {}).setdefault(file, {"classes": set(), "functions": set()})
|
| 413 |
+
modules[mod][file]["functions"].add(chunk.name)
|
| 414 |
+
|
| 415 |
+
# Also fetch function chunks for top-level functions
|
| 416 |
+
func_results = store.get_all("function_chunks", limit=500)
|
| 417 |
+
func_by_file: dict[str, list[str]] = {}
|
| 418 |
+
for chunk in func_results:
|
| 419 |
+
if not chunk.class_name: # top-level only
|
| 420 |
+
func_by_file.setdefault(chunk.file, []).append(chunk.name)
|
| 421 |
+
|
| 422 |
+
# Build tree structure
|
| 423 |
+
|
| 424 |
+
all_files = set()
|
| 425 |
+
for files in modules.values():
|
| 426 |
+
for file_path in files:
|
| 427 |
+
all_files.add(file_path)
|
| 428 |
+
|
| 429 |
+
# Derive root directory name from common first path component
|
| 430 |
+
first_parts = [f.split("/")[0] for f in all_files if "/" in f]
|
| 431 |
+
root_name = first_parts[0] if first_parts else "codebase"
|
| 432 |
+
|
| 433 |
+
root_node = TreeNode(name=root_name, type="module")
|
| 434 |
+
|
| 435 |
+
for module_name, files in sorted(modules.items()):
|
| 436 |
+
module_node = TreeNode(name=module_name, type="module")
|
| 437 |
+
|
| 438 |
+
for file_path, content in sorted(files.items()):
|
| 439 |
+
file_node = TreeNode(
|
| 440 |
+
name=os.path.basename(file_path),
|
| 441 |
+
type="file"
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
for cls_name in sorted(content["classes"]):
|
| 445 |
+
file_node.children.append(
|
| 446 |
+
TreeNode(name=cls_name, type="class")
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
for fn_name in sorted(content["functions"]):
|
| 450 |
+
file_node.children.append(
|
| 451 |
+
TreeNode(name=fn_name, type="function")
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
module_node.children.append(file_node)
|
| 455 |
+
|
| 456 |
+
root_node.children.append(module_node)
|
| 457 |
+
|
| 458 |
+
return TreeResponse(success=True, tree=[root_node])
|
| 459 |
+
|
| 460 |
+
except Exception as e:
|
| 461 |
+
console.print(f"[red]β Tree build failed: {e}[/red]")
|
| 462 |
+
return TreeResponse(success=False, error=str(e))
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
# ββ Entry Point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 466 |
+
|
| 467 |
+
if __name__ == "__main__":
|
| 468 |
+
import uvicorn
|
| 469 |
+
uvicorn.run(
|
| 470 |
+
"main:app",
|
| 471 |
+
port=8000,
|
| 472 |
+
reload=True,
|
| 473 |
+
)
|
prompt/__init__.py
ADDED
|
File without changes
|
prompt/cross_module.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
cross_module_prompts.py
|
| 3 |
+
-----------------------
|
| 4 |
+
System prompts and prompt builders for cross-module codebase queries.
|
| 5 |
+
|
| 6 |
+
Cross-module queries reason across multiple modules simultaneously.
|
| 7 |
+
Two subtypes handled by ONE system prompt with internal classification:
|
| 8 |
+
|
| 9 |
+
1. dependency_analysis β what does function X depend on, what depends on it
|
| 10 |
+
2. impact_analysis β if function X changes, what breaks and where
|
| 11 |
+
|
| 12 |
+
The LLM receives:
|
| 13 |
+
- Primary function context (from vector_store / retriever)
|
| 14 |
+
- Dependency context (from call_graph β callers and callees)
|
| 15 |
+
- Call graph metadata (structured dependency relationships)
|
| 16 |
+
|
| 17 |
+
Depends on:
|
| 18 |
+
- rich (for __main__ preview only)
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# ββ Shared Rules ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
+
|
| 23 |
+
_SHARED_RULES = """
|
| 24 |
+
STRICT FORMATTING RULES:
|
| 25 |
+
- Respond ONLY in valid markdown.
|
| 26 |
+
- Use headers (##, ###) to separate sections clearly.
|
| 27 |
+
- Use triple backtick code blocks for flow diagrams and signatures.
|
| 28 |
+
- Use markdown tables for dependency and impact listings.
|
| 29 |
+
- Use bullet points for risk assessments and recommendations.
|
| 30 |
+
- Do NOT include conversational filler, preamble, or apologies.
|
| 31 |
+
- Do NOT say "Based on the context provided" or similar phrases.
|
| 32 |
+
- Be precise and technical. Assume the reader is a senior developer.
|
| 33 |
+
- Explicitly state cross-module boundaries using module names.
|
| 34 |
+
- If a dependency cannot be resolved from context, list it as unresolved.
|
| 35 |
+
- Always include a ## Limitations section for missing context.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
# ββ Subtype Classification (embedded in system prompt) ββββββββββββββββββββββββ
|
| 39 |
+
|
| 40 |
+
_SUBTYPE_DEFINITIONS = """
|
| 41 |
+
QUERY CLASSIFICATION β internally classify every query as ONE of:
|
| 42 |
+
|
| 43 |
+
TYPE D β Dependency Analysis
|
| 44 |
+
Triggered when developer asks:
|
| 45 |
+
- "What does X depend on?"
|
| 46 |
+
- "What calls X?"
|
| 47 |
+
- "What does X call?"
|
| 48 |
+
- "What are the dependencies of X?"
|
| 49 |
+
- "Who uses X?"
|
| 50 |
+
- "What modules interact with X?"
|
| 51 |
+
- "Show me the call chain for X"
|
| 52 |
+
|
| 53 |
+
TYPE E β Impact Analysis
|
| 54 |
+
Triggered when developer asks:
|
| 55 |
+
- "What breaks if X changes?"
|
| 56 |
+
- "How does changing X affect Y?"
|
| 57 |
+
- "What is the impact of modifying X?"
|
| 58 |
+
- "If I change X, what else do I need to update?"
|
| 59 |
+
- "What are the side effects of changing X across modules?"
|
| 60 |
+
- "How far does X's influence reach?"
|
| 61 |
+
|
| 62 |
+
If the query mentions both dependency and impact, classify as TYPE E
|
| 63 |
+
since impact analysis subsumes dependency analysis.
|
| 64 |
+
If ambiguous, default to TYPE D.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
# ββ Cross-Module System Prompt ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
|
| 69 |
+
CROSS_MODULE_SYSTEM = f"""
|
| 70 |
+
You are an expert software architect performing cross-module dependency
|
| 71 |
+
and impact analysis on a monolithic Python codebase.
|
| 72 |
+
|
| 73 |
+
Your task is to reason across multiple modules simultaneously,
|
| 74 |
+
tracing function dependencies and assessing the blast radius of changes.
|
| 75 |
+
|
| 76 |
+
You are strictly not allowed to do any task outside of this scope no matter the context.
|
| 77 |
+
If a function or code is not available in codebase, just return "The function/class might
|
| 78 |
+
not be indexed for explanation. Please check if it's available in codebase."
|
| 79 |
+
|
| 80 |
+
You will receive:
|
| 81 |
+
1. PRIMARY CONTEXT β the target function/class retrieved from the vector store
|
| 82 |
+
2. DEPENDENCY CONTEXT β functions this target calls and functions that call it
|
| 83 |
+
3. CALL GRAPH DATA β structured JSON of the dependency relationships
|
| 84 |
+
|
| 85 |
+
Use ALL three sources together. Prioritize CALL GRAPH DATA for structure,
|
| 86 |
+
PRIMARY and DEPENDENCY CONTEXT for semantic understanding.
|
| 87 |
+
|
| 88 |
+
{_SHARED_RULES}
|
| 89 |
+
|
| 90 |
+
{_SUBTYPE_DEFINITIONS}
|
| 91 |
+
|
| 92 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 93 |
+
RESPONSE SCHEMAS β use the schema matching your classified type:
|
| 94 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 95 |
+
|
| 96 |
+
βββ TYPE D β Dependency Analysis βββ
|
| 97 |
+
|
| 98 |
+
## Dependency Analysis: `<function_name>`
|
| 99 |
+
**Module:** `<module>` | **File:** `<file>` | **Class:** `<class or top-level>`
|
| 100 |
+
|
| 101 |
+
## Summary
|
| 102 |
+
2-3 sentences describing the function's role in the broader system
|
| 103 |
+
and its connectivity to other modules.
|
| 104 |
+
|
| 105 |
+
## Outgoing Dependencies (What it calls)
|
| 106 |
+
| Function | Class | Module | File | Relationship |
|
| 107 |
+
|----------|-------|--------|------|-------------|
|
| 108 |
+
| ... | ... | ... | ... | direct call |
|
| 109 |
+
|
| 110 |
+
## Incoming Dependencies (What calls it)
|
| 111 |
+
| Function | Class | Module | File | Relationship |
|
| 112 |
+
|----------|-------|--------|------|-------------|
|
| 113 |
+
| ... | ... | ... | ... | direct call |
|
| 114 |
+
|
| 115 |
+
## Call Chain
|
| 116 |
+
```
|
| 117 |
+
[caller_2] β [caller_1] β [<function_name>] β [callee_1] β [callee_2]
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## Cross-Module Boundaries
|
| 121 |
+
Bullet points identifying which module boundaries this function crosses,
|
| 122 |
+
and what data or control passes across those boundaries.
|
| 123 |
+
|
| 124 |
+
## Unresolved Dependencies
|
| 125 |
+
List any calls that could not be traced to a known function in the index.
|
| 126 |
+
|
| 127 |
+
## Limitations
|
| 128 |
+
Any gaps in the analysis due to missing context.
|
| 129 |
+
|
| 130 |
+
βββ TYPE E β Impact Analysis βββ
|
| 131 |
+
|
| 132 |
+
## Impact Analysis: `<function_name>`
|
| 133 |
+
**Module:** `<module>` | **File:** `<file>` | **Class:** `<class or top-level>`
|
| 134 |
+
|
| 135 |
+
## Proposed Change
|
| 136 |
+
Restate what the developer wants to change, clearly and precisely.
|
| 137 |
+
If no specific change is mentioned, analyze general modification impact.
|
| 138 |
+
|
| 139 |
+
## Direct Impact
|
| 140 |
+
What changes immediately inside this function and its direct callers.
|
| 141 |
+
|
| 142 |
+
## Ripple Effects
|
| 143 |
+
| Level | Affected Function | Class | Module | Impact Description | Risk |
|
| 144 |
+
|-------|-------------------|-------|--------|--------------------|------|
|
| 145 |
+
| 1 | ... | ... | ... | ... | Low/Medium/High |
|
| 146 |
+
| 2 | ... | ... | ... | ... | Low/Medium/High |
|
| 147 |
+
|
| 148 |
+
## Cross-Module Impact Map
|
| 149 |
+
```
|
| 150 |
+
[<function_name>] (changed)
|
| 151 |
+
β
|
| 152 |
+
[Level 1: direct callers] β <module_a>, <module_b>
|
| 153 |
+
β
|
| 154 |
+
[Level 2: indirect callers] β <module_c>
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
## Risk Assessment
|
| 158 |
+
- **Overall risk:** Low / Medium / High
|
| 159 |
+
- **Reason:** <why this risk level>
|
| 160 |
+
- **Most vulnerable function:** <which function is most at risk and why>
|
| 161 |
+
|
| 162 |
+
## Recommendations
|
| 163 |
+
- **Before changing:** <what to check or prepare>
|
| 164 |
+
- **After changing:** <what to test or update>
|
| 165 |
+
- **Safe change strategy:** <how to minimize blast radius>
|
| 166 |
+
|
| 167 |
+
## Unresolved Dependencies
|
| 168 |
+
List any functions in the impact chain that could not be fully traced.
|
| 169 |
+
|
| 170 |
+
## Limitations
|
| 171 |
+
Any gaps in the analysis due to missing context.
|
| 172 |
+
""".strip()
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ββ User Prompt Builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
+
|
| 177 |
+
def build_cross_module_user_prompt(primary_context: str,
|
| 178 |
+
dependency_context: str,
|
| 179 |
+
call_graph_data: dict,
|
| 180 |
+
query: str,
|
| 181 |
+
function_name: str,
|
| 182 |
+
class_name: str = "") -> str:
|
| 183 |
+
"""
|
| 184 |
+
Build the user prompt for a cross-module query.
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
primary_context: Retrieved context for the target function.
|
| 188 |
+
dependency_context: Retrieved context for its dependencies.
|
| 189 |
+
call_graph_data: Dict from CallGraph containing calls/called_by.
|
| 190 |
+
query: The developer's natural language query.
|
| 191 |
+
function_name: Name of the target function.
|
| 192 |
+
class_name: Optional class name if it's a method.
|
| 193 |
+
|
| 194 |
+
Returns:
|
| 195 |
+
Formatted user prompt string.
|
| 196 |
+
"""
|
| 197 |
+
import json
|
| 198 |
+
|
| 199 |
+
scope = (
|
| 200 |
+
f"Method `{class_name}.{function_name}`"
|
| 201 |
+
if class_name
|
| 202 |
+
else f"Function `{function_name}`"
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
# Serialize call graph data cleanly
|
| 206 |
+
call_graph_str = json.dumps(call_graph_data, indent=2)
|
| 207 |
+
|
| 208 |
+
return f"""
|
| 209 |
+
SUBJECT: {scope}
|
| 210 |
+
|
| 211 |
+
βββ PRIMARY CONTEXT βββββββββββββββββββββββββββββββββββββββββββ
|
| 212 |
+
{primary_context}
|
| 213 |
+
|
| 214 |
+
βββ DEPENDENCY CONTEXT ββββββββββββββββββββββββββββββββββββββββ
|
| 215 |
+
{dependency_context if dependency_context.strip() else "No dependency context retrieved."}
|
| 216 |
+
|
| 217 |
+
βββ CALL GRAPH DATA βββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
+
{call_graph_str}
|
| 219 |
+
|
| 220 |
+
βββ DEVELOPER QUERY βββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
+
{query}
|
| 222 |
+
|
| 223 |
+
Classify this query as Type D or Type E internally.
|
| 224 |
+
Then respond using the exact schema for the classified type.
|
| 225 |
+
Do not mention the type classification in your response.
|
| 226 |
+
Trace ALL cross-module boundaries explicitly using module names.
|
| 227 |
+
""".strip()
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
# ββ Follow-up Prompt Builder ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 231 |
+
|
| 232 |
+
CROSS_MODULE_FOLLOWUP_SYSTEM = """
|
| 233 |
+
You are an expert software architect continuing a cross-module dependency
|
| 234 |
+
or impact analysis discussion about a monolithic Python codebase.
|
| 235 |
+
|
| 236 |
+
The developer has a follow-up question after your previous analysis.
|
| 237 |
+
Use the same context, call graph, and your previous response to answer.
|
| 238 |
+
|
| 239 |
+
STRICT FORMATTING RULES:
|
| 240 |
+
- Respond ONLY in valid markdown.
|
| 241 |
+
- Be concise β the developer already has your full analysis.
|
| 242 |
+
- Focus only on what was asked in the follow-up.
|
| 243 |
+
- Do NOT repeat your previous full analysis.
|
| 244 |
+
- Always name modules explicitly when referencing cross-module relationships.
|
| 245 |
+
""".strip()
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def build_cross_module_followup_prompt(previous_response: str,
|
| 249 |
+
followup_query: str,
|
| 250 |
+
primary_context: str,
|
| 251 |
+
dependency_context: str,
|
| 252 |
+
call_graph_data: dict) -> str:
|
| 253 |
+
"""
|
| 254 |
+
Build a follow-up user prompt for a cross-module conversation.
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
previous_response: The LLM's previous cross-module response.
|
| 258 |
+
followup_query: The developer's follow-up question.
|
| 259 |
+
primary_context: Original primary context.
|
| 260 |
+
dependency_context: Original dependency context.
|
| 261 |
+
call_graph_data: Original call graph data.
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
Formatted follow-up user prompt string.
|
| 265 |
+
"""
|
| 266 |
+
import json
|
| 267 |
+
|
| 268 |
+
return f"""
|
| 269 |
+
βββ PRIMARY CONTEXT βββββββββββββββββββββββββββββββββββββββββββ
|
| 270 |
+
{primary_context}
|
| 271 |
+
|
| 272 |
+
βββ DEPENDENCY CONTEXT ββββββββββββββββββββββββββββββββββββββββ
|
| 273 |
+
{dependency_context if dependency_context.strip() else "No dependency context retrieved."}
|
| 274 |
+
|
| 275 |
+
βββ CALL GRAPH DATA βββββββββββββββββββββββββββββββββββββββββββ
|
| 276 |
+
{json.dumps(call_graph_data, indent=2)}
|
| 277 |
+
|
| 278 |
+
βββ YOUR PREVIOUS ANALYSIS ββββββββββββββββββββββββββββββββββββ
|
| 279 |
+
{previous_response}
|
| 280 |
+
|
| 281 |
+
βββ DEVELOPER FOLLOW-UP βββββββββββββββββββββββββββββββββββββββ
|
| 282 |
+
{followup_query}
|
| 283 |
+
|
| 284 |
+
Answer the follow-up using the context and your previous analysis.
|
| 285 |
+
Name all modules explicitly when referencing cross-module relationships.
|
| 286 |
+
""".strip()
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# ββ Call Graph Data Builder βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 290 |
+
|
| 291 |
+
def build_call_graph_payload(function_name: str,
|
| 292 |
+
calls: list,
|
| 293 |
+
called_by: list,
|
| 294 |
+
impact: dict) -> dict:
|
| 295 |
+
"""
|
| 296 |
+
Build a clean structured dict from CallGraph data to inject
|
| 297 |
+
into the cross-module prompt.
|
| 298 |
+
|
| 299 |
+
Args:
|
| 300 |
+
function_name: Target function name.
|
| 301 |
+
calls: List of CallNode objects (outgoing).
|
| 302 |
+
called_by: List of CallNode objects (incoming).
|
| 303 |
+
impact: Dict of impact levels from CallGraph.get_impact().
|
| 304 |
+
|
| 305 |
+
Returns:
|
| 306 |
+
Clean dict safe for JSON serialization.
|
| 307 |
+
"""
|
| 308 |
+
def node_to_dict(node) -> dict:
|
| 309 |
+
return {
|
| 310 |
+
"name": node.name,
|
| 311 |
+
"class_name": node.class_name,
|
| 312 |
+
"module": node.module,
|
| 313 |
+
"file": node.file,
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
impact_serialized = {}
|
| 317 |
+
for level, nodes in impact.items():
|
| 318 |
+
impact_serialized[level] = [node_to_dict(n) for n in nodes]
|
| 319 |
+
|
| 320 |
+
return {
|
| 321 |
+
"target_function": function_name,
|
| 322 |
+
"calls": [node_to_dict(n) for n in calls],
|
| 323 |
+
"called_by": [node_to_dict(n) for n in called_by],
|
| 324 |
+
"impact_chain": impact_serialized,
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ββ Prompt Accessor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 329 |
+
|
| 330 |
+
def get_cross_module_system_prompt(followup: bool = False) -> str:
|
| 331 |
+
"""
|
| 332 |
+
Return the cross-module system prompt.
|
| 333 |
+
|
| 334 |
+
Args:
|
| 335 |
+
followup: If True, returns the follow-up system prompt.
|
| 336 |
+
|
| 337 |
+
Returns:
|
| 338 |
+
System prompt string.
|
| 339 |
+
"""
|
| 340 |
+
return CROSS_MODULE_FOLLOWUP_SYSTEM if followup else CROSS_MODULE_SYSTEM
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
# ββ Entry Point (preview prompts) βββββββββββββββββββββββββββββββββββββββββββββ
|
| 344 |
+
|
| 345 |
+
if __name__ == "__main__":
|
| 346 |
+
from rich.console import Console
|
| 347 |
+
from rich.panel import Panel
|
| 348 |
+
from rich.rule import Rule
|
| 349 |
+
|
| 350 |
+
console = Console()
|
| 351 |
+
|
| 352 |
+
sample_primary = """
|
| 353 |
+
--- Chunk 1 (function: extract_function_types) ---
|
| 354 |
+
Scope: top-level function
|
| 355 |
+
Function: extract_function_types
|
| 356 |
+
Module: refurb
|
| 357 |
+
File: refurb/loader.py
|
| 358 |
+
Signature: def extract_function_types(func: Any) -> Generator[type[Node], None, None]
|
| 359 |
+
Docstring: No docstring provided.
|
| 360 |
+
Calls: list, callable, TypeError, signature, isinstance
|
| 361 |
+
""".strip()
|
| 362 |
+
|
| 363 |
+
sample_dependency = """
|
| 364 |
+
--- Chunk 1 (function: load_checks) ---
|
| 365 |
+
Scope: top-level function
|
| 366 |
+
Function: load_checks
|
| 367 |
+
Module: refurb
|
| 368 |
+
File: refurb/main.py
|
| 369 |
+
Signature: def load_checks(path: str) -> list[Check]
|
| 370 |
+
Calls: extract_function_types, import_module
|
| 371 |
+
|
| 372 |
+
--- Chunk 2 (function: run) ---
|
| 373 |
+
Class: Router
|
| 374 |
+
Module: refurb
|
| 375 |
+
File: refurb/main.py
|
| 376 |
+
Signature: def run(args: list[str]) -> int
|
| 377 |
+
Calls: load_checks, parse_args
|
| 378 |
+
""".strip()
|
| 379 |
+
|
| 380 |
+
sample_call_graph = build_call_graph_payload(
|
| 381 |
+
function_name="extract_function_types",
|
| 382 |
+
calls=[],
|
| 383 |
+
called_by=[],
|
| 384 |
+
impact={},
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
queries = [
|
| 388 |
+
("What does extract_function_types depend on and what depends on it?",
|
| 389 |
+
"Type D β Dependency Analysis"),
|
| 390 |
+
("What breaks if I change the return type of extract_function_types?",
|
| 391 |
+
"Type E β Impact Analysis"),
|
| 392 |
+
]
|
| 393 |
+
|
| 394 |
+
console.print(Rule("[bold cyan]Cross-Module β System Prompt[/bold cyan]"))
|
| 395 |
+
console.print(Panel(
|
| 396 |
+
CROSS_MODULE_SYSTEM,
|
| 397 |
+
border_style="cyan",
|
| 398 |
+
padding=(1, 2),
|
| 399 |
+
))
|
| 400 |
+
|
| 401 |
+
for query, label in queries:
|
| 402 |
+
console.print(Rule(f"[bold magenta]User Prompt β {label}[/bold magenta]"))
|
| 403 |
+
user_prompt = build_cross_module_user_prompt(
|
| 404 |
+
primary_context=sample_primary,
|
| 405 |
+
dependency_context=sample_dependency,
|
| 406 |
+
call_graph_data=sample_call_graph,
|
| 407 |
+
query=query,
|
| 408 |
+
function_name="extract_function_types",
|
| 409 |
+
)
|
| 410 |
+
console.print(Panel(
|
| 411 |
+
user_prompt,
|
| 412 |
+
border_style="magenta",
|
| 413 |
+
padding=(1, 2),
|
| 414 |
+
))
|
| 415 |
+
|
| 416 |
+
console.print(Rule("[bold yellow]Follow-up Prompt Preview[/bold yellow]"))
|
| 417 |
+
followup = build_cross_module_followup_prompt(
|
| 418 |
+
previous_response="## Dependency Analysis: `extract_function_types`\n...",
|
| 419 |
+
followup_query="Which of these callers is most critical to the system?",
|
| 420 |
+
primary_context=sample_primary,
|
| 421 |
+
dependency_context=sample_dependency,
|
| 422 |
+
call_graph_data=sample_call_graph,
|
| 423 |
+
)
|
| 424 |
+
console.print(Panel(followup, border_style="yellow", padding=(1, 2)))
|
prompt/macro_prompt.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
macro_prompts.py
|
| 3 |
+
----------------
|
| 4 |
+
System prompts and prompt builders for macro-level codebase queries.
|
| 5 |
+
|
| 6 |
+
Three macro subtypes:
|
| 7 |
+
1. overall_architecture β codebase tree + purpose summary
|
| 8 |
+
2. module_responsibility β module structure + class descriptions
|
| 9 |
+
3. data_flow β how data moves across modules
|
| 10 |
+
|
| 11 |
+
All responses are structured markdown, rendered via rich in terminal.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
# ββ Shared Instructions βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
|
| 16 |
+
_SHARED_RULES = """
|
| 17 |
+
STRICT FORMATTING RULES:
|
| 18 |
+
- Respond ONLY in valid markdown.
|
| 19 |
+
- Use headers (##, ###) to separate sections clearly.
|
| 20 |
+
- Use triple backtick code blocks for any tree structures.
|
| 21 |
+
- Use markdown tables where tabular data is appropriate.
|
| 22 |
+
- Use bullet points for lists of descriptions.
|
| 23 |
+
- Do NOT include any conversational filler, preamble, or apologies.
|
| 24 |
+
- Do NOT say "Based on the context provided" or similar phrases.
|
| 25 |
+
- Be precise and technical. Assume the reader is a developer.
|
| 26 |
+
- If information is missing or unclear from context, say so explicitly under a ## Limitations section.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
# ββ 1. Overall Architecture βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
|
| 31 |
+
OVERALL_ARCHITECTURE_SYSTEM = f"""
|
| 32 |
+
You are an expert software architect analyzing a monolithic Python codebase.
|
| 33 |
+
Your task is to provide a clear, structured overview of the entire codebase
|
| 34 |
+
architecture based on the context provided.
|
| 35 |
+
|
| 36 |
+
You are strictly not allowed to do
|
| 37 |
+
any task outside of this scope no matter the context. If a function or code is not
|
| 38 |
+
available in codebase, just return "The function/class might not be indexed for explanation.
|
| 39 |
+
Please check if it's available in codebase."
|
| 40 |
+
|
| 41 |
+
{_SHARED_RULES}
|
| 42 |
+
|
| 43 |
+
RESPONSE STRUCTURE β follow this EXACTLY:
|
| 44 |
+
|
| 45 |
+
## Codebase: <name>
|
| 46 |
+
|
| 47 |
+
## Purpose
|
| 48 |
+
A concise paragraph (3-5 sentences) explaining what this codebase does,
|
| 49 |
+
what problem it solves, and what its primary output or function is.
|
| 50 |
+
|
| 51 |
+
## Architecture Overview
|
| 52 |
+
```
|
| 53 |
+
<Render a text-based tree of the top-level modules and their submodules>
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Module Summary
|
| 57 |
+
| Module | Responsibility |
|
| 58 |
+
|--------|---------------|
|
| 59 |
+
| ... | ... |
|
| 60 |
+
|
| 61 |
+
## Key Design Patterns
|
| 62 |
+
Bullet points identifying any notable patterns (MVC, plugin system,
|
| 63 |
+
visitor pattern, pipeline, etc.) visible from the structure.
|
| 64 |
+
|
| 65 |
+
## Limitations
|
| 66 |
+
Any gaps in the analysis due to missing context.
|
| 67 |
+
""".strip()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def build_overall_architecture_prompt(context: str, query: str) -> str:
|
| 71 |
+
"""
|
| 72 |
+
Build the user prompt for an overall architecture query.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
context: Assembled chunk context from retriever.build_context().
|
| 76 |
+
query: The developer's original natural language query.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Formatted user prompt string.
|
| 80 |
+
"""
|
| 81 |
+
return f"""
|
| 82 |
+
CODEBASE CONTEXT:
|
| 83 |
+
{context}
|
| 84 |
+
|
| 85 |
+
DEVELOPER QUERY:
|
| 86 |
+
{query}
|
| 87 |
+
|
| 88 |
+
Analyze the codebase context above and produce the architecture overview
|
| 89 |
+
following your response structure exactly.
|
| 90 |
+
""".strip()
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ββ 2. Module Responsibility ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
|
| 95 |
+
MODULE_RESPONSIBILITY_SYSTEM = f"""
|
| 96 |
+
You are an expert software architect analyzing a specific module
|
| 97 |
+
within a monolithic Python codebase.
|
| 98 |
+
Your task is to explain what a given module does, its internal structure,
|
| 99 |
+
and the responsibility of each class within it.
|
| 100 |
+
|
| 101 |
+
{_SHARED_RULES}
|
| 102 |
+
|
| 103 |
+
RESPONSE STRUCTURE β follow this EXACTLY:
|
| 104 |
+
|
| 105 |
+
## Module: <module_name>
|
| 106 |
+
|
| 107 |
+
## Purpose
|
| 108 |
+
A concise paragraph (2-4 sentences) explaining what this module is
|
| 109 |
+
responsible for in the broader codebase.
|
| 110 |
+
|
| 111 |
+
## Internal Structure
|
| 112 |
+
```
|
| 113 |
+
<module_name>/
|
| 114 |
+
βββ <file.py>
|
| 115 |
+
β βββ ClassName
|
| 116 |
+
β β βββ method_one()
|
| 117 |
+
β β βββ method_two()
|
| 118 |
+
β βββ another_function()
|
| 119 |
+
βββ ...
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## Class Responsibilities
|
| 123 |
+
| Class | Responsibility |
|
| 124 |
+
|-------|---------------|
|
| 125 |
+
| ... | ... |
|
| 126 |
+
|
| 127 |
+
## Dependencies
|
| 128 |
+
Bullet points listing what external modules or packages this module
|
| 129 |
+
imports or depends on, based on the context.
|
| 130 |
+
|
| 131 |
+
## Limitations
|
| 132 |
+
Any gaps in the analysis due to missing context.
|
| 133 |
+
""".strip()
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def build_module_responsibility_prompt(context: str,
|
| 137 |
+
query: str,
|
| 138 |
+
module_name: str) -> str:
|
| 139 |
+
"""
|
| 140 |
+
Build the user prompt for a module responsibility query.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
context: Assembled chunk context from retriever.build_context().
|
| 144 |
+
query: The developer's original natural language query.
|
| 145 |
+
module_name: Name of the module being queried.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
Formatted user prompt string.
|
| 149 |
+
"""
|
| 150 |
+
return f"""
|
| 151 |
+
MODULE: {module_name}
|
| 152 |
+
|
| 153 |
+
CODEBASE CONTEXT:
|
| 154 |
+
{context}
|
| 155 |
+
|
| 156 |
+
DEVELOPER QUERY:
|
| 157 |
+
{query}
|
| 158 |
+
|
| 159 |
+
Analyze the module context above and explain the module's responsibility
|
| 160 |
+
and internal structure following your response structure exactly.
|
| 161 |
+
""".strip()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ββ 3. Data Flow ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 165 |
+
|
| 166 |
+
DATA_FLOW_SYSTEM = f"""
|
| 167 |
+
You are an expert software architect analyzing data flow across modules
|
| 168 |
+
in a monolithic Python codebase.
|
| 169 |
+
Your task is to trace and explain how data enters, transforms, and moves
|
| 170 |
+
through the system based on the context provided.
|
| 171 |
+
|
| 172 |
+
{_SHARED_RULES}
|
| 173 |
+
|
| 174 |
+
RESPONSE STRUCTURE β follow this EXACTLY:
|
| 175 |
+
|
| 176 |
+
## Data Flow: <topic or entry point>
|
| 177 |
+
|
| 178 |
+
## Overview
|
| 179 |
+
A concise paragraph (3-5 sentences) summarizing the high-level data
|
| 180 |
+
flow from input to output.
|
| 181 |
+
|
| 182 |
+
## Flow Diagram
|
| 183 |
+
```
|
| 184 |
+
[Input / Entry Point]
|
| 185 |
+
β
|
| 186 |
+
[Module A] β <what happens here>
|
| 187 |
+
β
|
| 188 |
+
[Module B] β <what transforms here>
|
| 189 |
+
β
|
| 190 |
+
[Output / Result]
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
## Step-by-Step Breakdown
|
| 194 |
+
### Step 1: <Entry Point>
|
| 195 |
+
Description of what initiates the flow and what data looks like at this stage.
|
| 196 |
+
|
| 197 |
+
### Step 2: <Module / Function>
|
| 198 |
+
Description of transformation or processing at this stage.
|
| 199 |
+
|
| 200 |
+
### Step N: <Final Output>
|
| 201 |
+
Description of what the data looks like at the end.
|
| 202 |
+
|
| 203 |
+
## Data Structures Involved
|
| 204 |
+
| Structure | Module | Role in Flow |
|
| 205 |
+
|-----------|--------|-------------|
|
| 206 |
+
| ... | ... | ... |
|
| 207 |
+
|
| 208 |
+
## Limitations
|
| 209 |
+
Any gaps in the analysis due to missing context or ambiguous flow.
|
| 210 |
+
""".strip()
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def build_data_flow_prompt(context: str, query: str) -> str:
|
| 214 |
+
"""
|
| 215 |
+
Build the user prompt for a data flow query.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
context: Assembled chunk context from retriever.build_context().
|
| 219 |
+
query: The developer's original natural language query.
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
Formatted user prompt string.
|
| 223 |
+
"""
|
| 224 |
+
return f"""
|
| 225 |
+
CODEBASE CONTEXT:
|
| 226 |
+
{context}
|
| 227 |
+
|
| 228 |
+
DEVELOPER QUERY:
|
| 229 |
+
{query}
|
| 230 |
+
|
| 231 |
+
Trace and explain the data flow described in the query using the
|
| 232 |
+
codebase context above. Follow your response structure exactly.
|
| 233 |
+
""".strip()
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# ββ Prompt Router βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
+
|
| 238 |
+
MACRO_SUBTYPES = {
|
| 239 |
+
"overall_architecture": "Overall Architecture",
|
| 240 |
+
"module_responsibility": "Module Responsibility",
|
| 241 |
+
"data_flow": "Data Flow",
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def get_macro_system_prompt(subtype: str) -> str:
|
| 246 |
+
"""
|
| 247 |
+
Return the correct system prompt for a given macro subtype.
|
| 248 |
+
|
| 249 |
+
Args:
|
| 250 |
+
subtype: One of 'overall_architecture', 'module_responsibility',
|
| 251 |
+
'data_flow'.
|
| 252 |
+
|
| 253 |
+
Returns:
|
| 254 |
+
System prompt string.
|
| 255 |
+
|
| 256 |
+
Raises:
|
| 257 |
+
ValueError: If subtype is not recognized.
|
| 258 |
+
"""
|
| 259 |
+
mapping = {
|
| 260 |
+
"overall_architecture": OVERALL_ARCHITECTURE_SYSTEM,
|
| 261 |
+
"module_responsibility": MODULE_RESPONSIBILITY_SYSTEM,
|
| 262 |
+
"data_flow": DATA_FLOW_SYSTEM,
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
if subtype not in mapping:
|
| 266 |
+
raise ValueError(
|
| 267 |
+
f"Unknown macro subtype: '{subtype}'. "
|
| 268 |
+
f"Choose from: {list(mapping.keys())}"
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
return mapping[subtype]
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def build_macro_user_prompt(subtype: str,
|
| 275 |
+
context: str,
|
| 276 |
+
query: str,
|
| 277 |
+
module_name: str = "") -> str:
|
| 278 |
+
"""
|
| 279 |
+
Build the correct user prompt for a given macro subtype.
|
| 280 |
+
|
| 281 |
+
Args:
|
| 282 |
+
subtype: One of the three macro subtypes.
|
| 283 |
+
context: Retrieved context string.
|
| 284 |
+
query: Developer's natural language query.
|
| 285 |
+
module_name: Required only for 'module_responsibility' subtype.
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
Formatted user prompt string.
|
| 289 |
+
"""
|
| 290 |
+
if subtype == "overall_architecture":
|
| 291 |
+
return build_overall_architecture_prompt(context, query)
|
| 292 |
+
|
| 293 |
+
elif subtype == "module_responsibility":
|
| 294 |
+
if not module_name:
|
| 295 |
+
raise ValueError(
|
| 296 |
+
"'module_name' is required for 'module_responsibility' subtype."
|
| 297 |
+
)
|
| 298 |
+
return build_module_responsibility_prompt(context, query, module_name)
|
| 299 |
+
|
| 300 |
+
elif subtype == "data_flow":
|
| 301 |
+
return build_data_flow_prompt(context, query)
|
| 302 |
+
|
| 303 |
+
raise ValueError(
|
| 304 |
+
f"Unknown macro subtype: '{subtype}'. "
|
| 305 |
+
f"Choose from: {list(MACRO_SUBTYPES.keys())}"
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# ββ Entry Point (preview prompts) βββββββββββββββββββββββββββββββββββββββββββββ
|
| 310 |
+
|
| 311 |
+
if __name__ == "__main__":
|
| 312 |
+
from rich.console import Console
|
| 313 |
+
from rich.panel import Panel
|
| 314 |
+
from rich.rule import Rule
|
| 315 |
+
|
| 316 |
+
console = Console()
|
| 317 |
+
|
| 318 |
+
sample_context = """
|
| 319 |
+
--- Chunk 1 (class: Router) ---
|
| 320 |
+
Class: Router
|
| 321 |
+
Module: refurb
|
| 322 |
+
File: refurb/main.py
|
| 323 |
+
Docstring: Main entry point router for the refurb linting tool.
|
| 324 |
+
Methods:
|
| 325 |
+
def run(args: list[str]) -> int
|
| 326 |
+
def load_checks(path: str) -> list[Check]
|
| 327 |
+
|
| 328 |
+
--- Chunk 2 (class: Loader) ---
|
| 329 |
+
Class: Loader
|
| 330 |
+
Module: refurb
|
| 331 |
+
File: refurb/loader.py
|
| 332 |
+
Docstring: Loads and validates check functions from plugin modules.
|
| 333 |
+
Methods:
|
| 334 |
+
def extract_function_types(func: Any) -> Generator
|
| 335 |
+
def load_module(path: str) -> ModuleType
|
| 336 |
+
""".strip()
|
| 337 |
+
|
| 338 |
+
for subtype, label in MACRO_SUBTYPES.items():
|
| 339 |
+
console.print(Rule(f"[bold cyan]{label} β System Prompt[/bold cyan]"))
|
| 340 |
+
console.print(Panel(
|
| 341 |
+
get_macro_system_prompt(subtype),
|
| 342 |
+
border_style="cyan",
|
| 343 |
+
padding=(1, 2),
|
| 344 |
+
))
|
| 345 |
+
|
| 346 |
+
console.print(Rule(f"[bold magenta]{label} β User Prompt Preview[/bold magenta]"))
|
| 347 |
+
user_prompt = build_macro_user_prompt(
|
| 348 |
+
subtype=subtype,
|
| 349 |
+
context=sample_context,
|
| 350 |
+
query=f"Explain the {label.lower()} of this codebase",
|
| 351 |
+
module_name="refurb" if subtype == "module_responsibility" else "",
|
| 352 |
+
)
|
| 353 |
+
console.print(Panel(
|
| 354 |
+
user_prompt,
|
| 355 |
+
border_style="magenta",
|
| 356 |
+
padding=(1, 2),
|
| 357 |
+
))
|
prompt/micro_prompt.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
micro_prompts.py
|
| 3 |
+
----------------
|
| 4 |
+
System prompts and prompt builders for micro-level codebase queries.
|
| 5 |
+
|
| 6 |
+
Micro queries operate at function/method level. The LLM classifies
|
| 7 |
+
the query internally into one of three subtypes:
|
| 8 |
+
|
| 9 |
+
1. function_definition β signature, purpose, parameters, usage example
|
| 10 |
+
2. function_body β internal logic, data flow, warnings
|
| 11 |
+
3. function_consideration β impact of proposed changes
|
| 12 |
+
|
| 13 |
+
Unlike macro prompts, there is ONE system prompt for micro queries.
|
| 14 |
+
The LLM classifies the subtype from the developer's natural language
|
| 15 |
+
query and responds in the appropriate schema automatically.
|
| 16 |
+
|
| 17 |
+
Depends on:
|
| 18 |
+
- rich (for __main__ preview only)
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# ββ Shared Rules ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
+
|
| 23 |
+
_SHARED_RULES = """
|
| 24 |
+
STRICT FORMATTING RULES:
|
| 25 |
+
- Respond ONLY in valid markdown.
|
| 26 |
+
- Use headers (##, ###) to separate sections clearly.
|
| 27 |
+
- Use triple backtick code blocks for ALL code snippets and signatures.
|
| 28 |
+
- Use markdown tables for parameter listings.
|
| 29 |
+
- Use bullet points for warnings, considerations, and impact lists.
|
| 30 |
+
- Do NOT include conversational filler, preamble, or apologies.
|
| 31 |
+
- Do NOT say "Based on the context provided" or similar phrases.
|
| 32 |
+
- Be precise and technical. Assume the reader is a developer.
|
| 33 |
+
- If information is unclear from context, say so under a ## Limitations section.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
# ββ Subtype Descriptions (used inside system prompt) βββββββββββββββββββββββββ
|
| 37 |
+
|
| 38 |
+
_SUBTYPE_DEFINITIONS = """
|
| 39 |
+
QUERY CLASSIFICATION β internally classify every incoming query as ONE of:
|
| 40 |
+
|
| 41 |
+
TYPE A β Function Definition
|
| 42 |
+
Triggered when developer asks:
|
| 43 |
+
- "What does X do?"
|
| 44 |
+
- "Explain X function"
|
| 45 |
+
- "What are the parameters of X?"
|
| 46 |
+
- "How do I use X?"
|
| 47 |
+
- "What does X return?"
|
| 48 |
+
|
| 49 |
+
TYPE B β Function Body
|
| 50 |
+
Triggered when developer asks:
|
| 51 |
+
- "How does X work internally?"
|
| 52 |
+
- "Walk me through X"
|
| 53 |
+
- "What is the logic inside X?"
|
| 54 |
+
- "Explain the flow inside X"
|
| 55 |
+
- "What happens step by step in X?"
|
| 56 |
+
|
| 57 |
+
TYPE C β Function Consideration
|
| 58 |
+
Triggered when developer asks:
|
| 59 |
+
- "What if I change X to Y?"
|
| 60 |
+
- "What happens if I modify this parameter?"
|
| 61 |
+
- "Is it safe to change X?"
|
| 62 |
+
- "What breaks if I alter X?"
|
| 63 |
+
- "What are the side effects of changing X?"
|
| 64 |
+
|
| 65 |
+
If the query is ambiguous, default to TYPE A.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
# ββ Micro System Prompt βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
|
| 70 |
+
MICRO_SYSTEM = f"""
|
| 71 |
+
You are an expert Python software engineer analyzing individual functions
|
| 72 |
+
and methods within a monolithic codebase.
|
| 73 |
+
|
| 74 |
+
Your task is to answer developer questions about specific functions or methods
|
| 75 |
+
at the deepest level of detail possible, based on the context provided.
|
| 76 |
+
|
| 77 |
+
You are strictly not allowed to do any task outside of this scope no matter the context.
|
| 78 |
+
If a function or code is not available in codebase, just return "The function/class might
|
| 79 |
+
not be indexed for explanation. Please check if it's available in codebase."
|
| 80 |
+
|
| 81 |
+
{_SHARED_RULES}
|
| 82 |
+
|
| 83 |
+
{_SUBTYPE_DEFINITIONS}
|
| 84 |
+
|
| 85 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
+
RESPONSE SCHEMAS β use the schema matching your classified type:
|
| 87 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
+
|
| 89 |
+
βββ TYPE A β Function Definition βββ
|
| 90 |
+
|
| 91 |
+
## Function: `<function_name>`
|
| 92 |
+
**Class:** `<ClassName>` | **Module:** `<module>` | **File:** `<file>`
|
| 93 |
+
|
| 94 |
+
## Signature
|
| 95 |
+
```python
|
| 96 |
+
def <function_name>(<params>) -> <return_type>:
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## Purpose
|
| 100 |
+
Concise paragraph (2-4 sentences) explaining what this function does
|
| 101 |
+
and why it exists.
|
| 102 |
+
|
| 103 |
+
## Parameters
|
| 104 |
+
| Parameter | Type | Default | Description |
|
| 105 |
+
|-----------|------|---------|-------------|
|
| 106 |
+
| ... | ... | ... | ... |
|
| 107 |
+
|
| 108 |
+
## Return Value
|
| 109 |
+
What the function returns, its type, and what it represents.
|
| 110 |
+
|
| 111 |
+
## Usage Example
|
| 112 |
+
```python
|
| 113 |
+
<concrete, realistic code snippet showing how to call this function>
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## Limitations
|
| 117 |
+
Any gaps or unclear aspects from the context.
|
| 118 |
+
|
| 119 |
+
βββ TYPE B β Function Body βββ
|
| 120 |
+
|
| 121 |
+
## Function: `<function_name>` β Internal Analysis
|
| 122 |
+
**Class:** `<ClassName>` | **Module:** `<module>` | **File:** `<file>`
|
| 123 |
+
|
| 124 |
+
## Internal Flow
|
| 125 |
+
Step-by-step breakdown of what happens inside the function:
|
| 126 |
+
|
| 127 |
+
### Step 1: <label>
|
| 128 |
+
Description of this step.
|
| 129 |
+
|
| 130 |
+
### Step N: <label>
|
| 131 |
+
Description of this step.
|
| 132 |
+
|
| 133 |
+
## Data Flow
|
| 134 |
+
```
|
| 135 |
+
input β <transformation> β <intermediate> β <transformation> β output
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
## Warnings & Edge Cases
|
| 139 |
+
- <potential issue or missing validation>
|
| 140 |
+
- <edge case not handled>
|
| 141 |
+
- <what happens with unexpected input>
|
| 142 |
+
|
| 143 |
+
## Limitations
|
| 144 |
+
Any gaps or unclear aspects from the context.
|
| 145 |
+
|
| 146 |
+
βββ TYPE C β Function Consideration βββ
|
| 147 |
+
|
| 148 |
+
## Impact Analysis: `<function_name>`
|
| 149 |
+
|
| 150 |
+
## Proposed Change
|
| 151 |
+
Restate the developer's proposed change clearly and precisely.
|
| 152 |
+
|
| 153 |
+
## Direct Effects
|
| 154 |
+
What changes immediately inside this function if the modification is made.
|
| 155 |
+
|
| 156 |
+
## Ripple Effects
|
| 157 |
+
| Affected Function | Module | Impact | Risk |
|
| 158 |
+
|-------------------|--------|--------|------|
|
| 159 |
+
| ... | ... | ... | Low/Medium/High |
|
| 160 |
+
|
| 161 |
+
## Recommendation
|
| 162 |
+
- **Safe to change:** <yes/no/conditional β explain>
|
| 163 |
+
- **Suggested approach:** <how to make the change safely>
|
| 164 |
+
- **Tests to update:** <what would need re-testing>
|
| 165 |
+
|
| 166 |
+
## Limitations
|
| 167 |
+
Any gaps or unclear aspects from the context.
|
| 168 |
+
""".strip()
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# ββ User Prompt Builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 172 |
+
|
| 173 |
+
def build_micro_user_prompt(context: str,
|
| 174 |
+
query: str,
|
| 175 |
+
function_name: str,
|
| 176 |
+
class_name: str = "") -> str:
|
| 177 |
+
"""
|
| 178 |
+
Build the user prompt for a micro-level query.
|
| 179 |
+
|
| 180 |
+
The system prompt handles subtype classification internally,
|
| 181 |
+
so the user prompt just needs to supply context + query clearly.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
context: Assembled chunk context from retriever.build_context().
|
| 185 |
+
query: The developer's natural language query.
|
| 186 |
+
function_name: Name of the function/method being queried.
|
| 187 |
+
class_name: Optional class name if it's a method.
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
Formatted user prompt string.
|
| 191 |
+
"""
|
| 192 |
+
scope = (
|
| 193 |
+
f"Method `{class_name}.{function_name}`"
|
| 194 |
+
if class_name
|
| 195 |
+
else f"Function `{function_name}`"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
return f"""
|
| 199 |
+
FUNCTION CONTEXT:
|
| 200 |
+
{context}
|
| 201 |
+
|
| 202 |
+
SUBJECT: {scope}
|
| 203 |
+
|
| 204 |
+
DEVELOPER QUERY:
|
| 205 |
+
{query}
|
| 206 |
+
|
| 207 |
+
Classify this query into Type A, B, or C internally.
|
| 208 |
+
Then respond using the exact schema for the classified type.
|
| 209 |
+
Do not mention the type classification in your response.
|
| 210 |
+
""".strip()
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
# ββ Follow-up Prompt Builder ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 214 |
+
|
| 215 |
+
MICRO_FOLLOWUP_SYSTEM = """
|
| 216 |
+
You are an expert Python software engineer continuing a technical discussion
|
| 217 |
+
about a specific function or method in a monolithic codebase.
|
| 218 |
+
|
| 219 |
+
The developer has a follow-up question after your previous analysis.
|
| 220 |
+
Use the same context and your previous response to answer precisely.
|
| 221 |
+
|
| 222 |
+
STRICT FORMATTING RULES:
|
| 223 |
+
- Respond ONLY in valid markdown.
|
| 224 |
+
- Be concise β the developer already has your full analysis.
|
| 225 |
+
- Focus only on what was asked in the follow-up.
|
| 226 |
+
- Do NOT repeat your previous full analysis.
|
| 227 |
+
""".strip()
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def build_micro_followup_prompt(previous_response: str,
|
| 231 |
+
followup_query: str,
|
| 232 |
+
context: str) -> str:
|
| 233 |
+
"""
|
| 234 |
+
Build a follow-up user prompt continuing a micro-level conversation.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
previous_response: The LLM's previous micro response.
|
| 238 |
+
followup_query: The developer's follow-up question.
|
| 239 |
+
context: Original retrieved context (still relevant).
|
| 240 |
+
|
| 241 |
+
Returns:
|
| 242 |
+
Formatted follow-up user prompt string.
|
| 243 |
+
"""
|
| 244 |
+
return f"""
|
| 245 |
+
ORIGINAL CONTEXT:
|
| 246 |
+
{context}
|
| 247 |
+
|
| 248 |
+
YOUR PREVIOUS ANALYSIS:
|
| 249 |
+
{previous_response}
|
| 250 |
+
|
| 251 |
+
DEVELOPER FOLLOW-UP:
|
| 252 |
+
{followup_query}
|
| 253 |
+
|
| 254 |
+
Answer the follow-up precisely using the context and your previous analysis.
|
| 255 |
+
""".strip()
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# ββ Prompt Accessor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 259 |
+
|
| 260 |
+
def get_micro_system_prompt(followup: bool = False) -> str:
|
| 261 |
+
"""
|
| 262 |
+
Return the micro system prompt.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
followup: If True, returns the follow-up system prompt.
|
| 266 |
+
|
| 267 |
+
Returns:
|
| 268 |
+
System prompt string.
|
| 269 |
+
"""
|
| 270 |
+
return MICRO_FOLLOWUP_SYSTEM if followup else MICRO_SYSTEM
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
# ββ Entry Point (preview prompts) βββββββββββββββββββββββββββββββββββββββββββββ
|
| 274 |
+
|
| 275 |
+
if __name__ == "__main__":
|
| 276 |
+
from rich.console import Console
|
| 277 |
+
from rich.panel import Panel
|
| 278 |
+
from rich.rule import Rule
|
| 279 |
+
|
| 280 |
+
console = Console()
|
| 281 |
+
|
| 282 |
+
sample_context = """
|
| 283 |
+
--- Chunk 1 (function: extract_function_types) ---
|
| 284 |
+
Scope: top-level function
|
| 285 |
+
Function: extract_function_types
|
| 286 |
+
Module: refurb
|
| 287 |
+
File: refurb/loader.py
|
| 288 |
+
Signature: def extract_function_types(func: Any) -> Generator[type[Node], None, None]
|
| 289 |
+
Docstring: No docstring provided.
|
| 290 |
+
Calls: list, callable, TypeError, signature(func).parameters.values,
|
| 291 |
+
len, type_error_with_line_info, isinstance, signature
|
| 292 |
+
""".strip()
|
| 293 |
+
|
| 294 |
+
queries = [
|
| 295 |
+
("What does extract_function_types do and how do I use it?",
|
| 296 |
+
"Type A β Definition"),
|
| 297 |
+
("Walk me through the internal logic of extract_function_types",
|
| 298 |
+
"Type B β Body"),
|
| 299 |
+
("What happens if I change the return type from Generator to list?",
|
| 300 |
+
"Type C β Consideration"),
|
| 301 |
+
]
|
| 302 |
+
|
| 303 |
+
console.print(Rule("[bold cyan]Micro β System Prompt[/bold cyan]"))
|
| 304 |
+
console.print(Panel(
|
| 305 |
+
MICRO_SYSTEM,
|
| 306 |
+
border_style="cyan",
|
| 307 |
+
padding=(1, 2),
|
| 308 |
+
))
|
| 309 |
+
|
| 310 |
+
for query, label in queries:
|
| 311 |
+
console.print(Rule(f"[bold magenta]User Prompt Preview β {label}[/bold magenta]"))
|
| 312 |
+
user_prompt = build_micro_user_prompt(
|
| 313 |
+
context=sample_context,
|
| 314 |
+
query=query,
|
| 315 |
+
function_name="extract_function_types",
|
| 316 |
+
class_name="",
|
| 317 |
+
)
|
| 318 |
+
console.print(Panel(
|
| 319 |
+
user_prompt,
|
| 320 |
+
border_style="magenta",
|
| 321 |
+
padding=(1, 2),
|
| 322 |
+
))
|
| 323 |
+
|
| 324 |
+
console.print(Rule("[bold yellow]Follow-up Prompt Preview[/bold yellow]"))
|
| 325 |
+
followup = build_micro_followup_prompt(
|
| 326 |
+
previous_response="## Function: `extract_function_types`\n...",
|
| 327 |
+
followup_query="Can this function handle async functions as input?",
|
| 328 |
+
context=sample_context,
|
| 329 |
+
)
|
| 330 |
+
console.print(Panel(followup, border_style="yellow", padding=(1, 2)))
|
render/__init__.py
ADDED
|
File without changes
|
render/render.py
ADDED
|
File without changes
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
networkx
|
| 4 |
+
python-dotenv
|
| 5 |
+
openai
|
| 6 |
+
rich
|
| 7 |
+
sentence-transformers
|
| 8 |
+
pinecone
|
| 9 |
+
python-multipart
|
retrieve/__init__.py
ADDED
|
File without changes
|
retrieve/retrieve.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
retriever.py
|
| 3 |
+
------------
|
| 4 |
+
Retrieves relevant chunks from ChromaDB based on query type.
|
| 5 |
+
|
| 6 |
+
Query type determines which collection to search:
|
| 7 |
+
- Macro / Cross-Module β class_chunks
|
| 8 |
+
- Micro β function_chunks
|
| 9 |
+
|
| 10 |
+
Provides filtered retrieval by module, class, or function name
|
| 11 |
+
for precise context fetching.
|
| 12 |
+
|
| 13 |
+
Depends on:
|
| 14 |
+
- embedder.py (query_chunks, get_chroma_client, load_embedding_model)
|
| 15 |
+
- rich (terminal output)
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from rich.console import Console
|
| 20 |
+
from rich.table import Table
|
| 21 |
+
from rich.panel import Panel
|
| 22 |
+
from rich.text import Text
|
| 23 |
+
from rich import box
|
| 24 |
+
|
| 25 |
+
from ingest.embed import (
|
| 26 |
+
query_chunks,
|
| 27 |
+
load_embedding_model,
|
| 28 |
+
CLASS_COLLECTION,
|
| 29 |
+
FUNCTION_COLLECTION,
|
| 30 |
+
COMPLETE_COLLECTION
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
console = Console()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# ββ Query Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
|
| 38 |
+
class QueryType:
|
| 39 |
+
MACRO = "macro"
|
| 40 |
+
MICRO = "micro"
|
| 41 |
+
CROSS_MODULE = "cross_module"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ββ Result Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
+
|
| 46 |
+
@dataclass
|
| 47 |
+
class RetrievedChunk:
|
| 48 |
+
text: str
|
| 49 |
+
metadata: dict
|
| 50 |
+
distance: float
|
| 51 |
+
collection: str
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def name(self) -> str:
|
| 55 |
+
return self.metadata.get("name", "unknown")
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def module(self) -> str:
|
| 59 |
+
return self.metadata.get("module", "unknown")
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def file(self) -> str:
|
| 63 |
+
return self.metadata.get("file", "unknown")
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def class_name(self) -> str:
|
| 67 |
+
return self.metadata.get("class_name", "")
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def chunk_type(self) -> str:
|
| 71 |
+
return self.metadata.get("type", "unknown")
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def relevance_score(self) -> float:
|
| 75 |
+
"""Convert distance to 0-1 relevance score (lower distance = higher relevance)."""
|
| 76 |
+
return round(1 / (1 + self.distance), 4)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ββ Core Retriever ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 80 |
+
|
| 81 |
+
class Retriever:
|
| 82 |
+
"""
|
| 83 |
+
Unified retriever for the Codebase Oracle system.
|
| 84 |
+
Maintains a single embedding model and ChromaDB client across queries.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def __init__(self):
|
| 88 |
+
self.model = load_embedding_model()
|
| 89 |
+
console.print("[green]β[/green] Retriever ready.\n")
|
| 90 |
+
|
| 91 |
+
# ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 92 |
+
|
| 93 |
+
def retrieve(self,
|
| 94 |
+
query: str,
|
| 95 |
+
query_type: str,
|
| 96 |
+
n_results: int = 5,
|
| 97 |
+
filters: dict | None = None) -> list[RetrievedChunk]:
|
| 98 |
+
"""
|
| 99 |
+
Main retrieval entry point. Routes to the correct collection
|
| 100 |
+
based on query_type.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
query: Natural language query string.
|
| 104 |
+
query_type: One of QueryType.MACRO / MICRO / CROSS_MODULE.
|
| 105 |
+
n_results: Number of chunks to retrieve.
|
| 106 |
+
filters: Optional metadata filters (e.g. filter by module).
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
List of RetrievedChunk objects sorted by relevance.
|
| 110 |
+
"""
|
| 111 |
+
collection = self._route_collection(query_type)
|
| 112 |
+
|
| 113 |
+
raw = query_chunks(
|
| 114 |
+
query=query,
|
| 115 |
+
collection_name=collection,
|
| 116 |
+
model=self.model,
|
| 117 |
+
n_results=n_results,
|
| 118 |
+
filters=filters,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
chunks = [
|
| 122 |
+
RetrievedChunk(
|
| 123 |
+
text=r["text"],
|
| 124 |
+
metadata=r["metadata"],
|
| 125 |
+
distance=r["distance"],
|
| 126 |
+
collection=collection,
|
| 127 |
+
)
|
| 128 |
+
for r in raw
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
return chunks
|
| 132 |
+
|
| 133 |
+
def retrieve_by_class(self,
|
| 134 |
+
class_name: str,
|
| 135 |
+
n_results: int = 1) -> list[RetrievedChunk]:
|
| 136 |
+
"""
|
| 137 |
+
Retrieve class-level chunk by exact class name.
|
| 138 |
+
Used by micro agent to ground function queries with class context.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
class_name: Exact class name string.
|
| 142 |
+
n_results: Usually 1 β we want the specific class.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
List of RetrievedChunk from class_chunks collection.
|
| 146 |
+
"""
|
| 147 |
+
return self.retrieve(
|
| 148 |
+
query=f"class {class_name}",
|
| 149 |
+
query_type=QueryType.MACRO,
|
| 150 |
+
n_results=n_results,
|
| 151 |
+
filters={"name": {"$eq": class_name}},
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
def retrieve_by_function(self,
|
| 155 |
+
function_name: str,
|
| 156 |
+
class_name: str | None = None,
|
| 157 |
+
n_results: int = 3) -> list[RetrievedChunk]:
|
| 158 |
+
"""
|
| 159 |
+
Retrieve function-level chunks by function name.
|
| 160 |
+
Optionally filter by class name for method disambiguation.
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
function_name: Exact function/method name.
|
| 164 |
+
class_name: Optional class name to narrow results.
|
| 165 |
+
n_results: Number of results.
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
List of RetrievedChunk from function_chunks collection.
|
| 169 |
+
"""
|
| 170 |
+
filters = {"name": {"$eq": function_name}}
|
| 171 |
+
if class_name:
|
| 172 |
+
filters = {
|
| 173 |
+
"$and": [
|
| 174 |
+
{"name": {"$eq": function_name}},
|
| 175 |
+
{"class_name": {"$eq": class_name}},
|
| 176 |
+
]
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
return self.retrieve(
|
| 180 |
+
query=f"function {function_name}",
|
| 181 |
+
query_type=QueryType.MICRO,
|
| 182 |
+
n_results=n_results,
|
| 183 |
+
filters=filters,
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
def retrieve_by_module(self,
|
| 187 |
+
module_name: str,
|
| 188 |
+
query: str,
|
| 189 |
+
query_type: str = QueryType.MACRO,
|
| 190 |
+
n_results: int = 5) -> list[RetrievedChunk]:
|
| 191 |
+
"""
|
| 192 |
+
Retrieve chunks scoped to a specific module.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
module_name: Top-level module/package name.
|
| 196 |
+
query: Natural language query within that module.
|
| 197 |
+
query_type: MACRO or MICRO.
|
| 198 |
+
n_results: Number of results.
|
| 199 |
+
|
| 200 |
+
Returns:
|
| 201 |
+
List of RetrievedChunk filtered to the given module.
|
| 202 |
+
"""
|
| 203 |
+
return self.retrieve(
|
| 204 |
+
query=query,
|
| 205 |
+
query_type=query_type,
|
| 206 |
+
n_results=n_results,
|
| 207 |
+
filters={"module": {"$eq": module_name}},
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
def retrieve_dependencies(self, function_name: str) -> list[RetrievedChunk]:
|
| 211 |
+
"""
|
| 212 |
+
Retrieve all functions that the given function calls.
|
| 213 |
+
Used by cross-module agent for dependency/impact analysis.
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
function_name: Name of the function to trace dependencies for.
|
| 217 |
+
|
| 218 |
+
Returns:
|
| 219 |
+
List of RetrievedChunk for each called function found in index.
|
| 220 |
+
"""
|
| 221 |
+
# First get the function itself to extract its call list
|
| 222 |
+
source_chunks = self.retrieve_by_function(function_name, n_results=1)
|
| 223 |
+
|
| 224 |
+
if not source_chunks:
|
| 225 |
+
console.print(f"[yellow]β Function '{function_name}' not found in index.[/yellow]")
|
| 226 |
+
return []
|
| 227 |
+
|
| 228 |
+
import json
|
| 229 |
+
calls = json.loads(source_chunks[0].metadata.get("calls", "[]"))
|
| 230 |
+
|
| 231 |
+
if not calls:
|
| 232 |
+
return []
|
| 233 |
+
|
| 234 |
+
# Retrieve each called function from the index
|
| 235 |
+
dep_chunks = []
|
| 236 |
+
seen = set()
|
| 237 |
+
for call in calls:
|
| 238 |
+
# Strip object prefix if present (e.g. "self.calculate" β "calculate")
|
| 239 |
+
name = call.split(".")[-1]
|
| 240 |
+
if name in seen:
|
| 241 |
+
continue
|
| 242 |
+
seen.add(name)
|
| 243 |
+
|
| 244 |
+
results = self.retrieve_by_function(name, n_results=1)
|
| 245 |
+
dep_chunks.extend(results)
|
| 246 |
+
|
| 247 |
+
return dep_chunks
|
| 248 |
+
|
| 249 |
+
def build_context(self, chunks: list[RetrievedChunk],
|
| 250 |
+
max_chars: int = 6000) -> str:
|
| 251 |
+
"""
|
| 252 |
+
Concatenate retrieved chunks into a single context string
|
| 253 |
+
for the LLM prompt. Truncates at max_chars to stay within
|
| 254 |
+
context window limits.
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
chunks: Retrieved chunks to concatenate.
|
| 258 |
+
max_chars: Maximum total character length of context.
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
A single string ready to inject into an LLM prompt.
|
| 262 |
+
"""
|
| 263 |
+
parts = []
|
| 264 |
+
total = 0
|
| 265 |
+
|
| 266 |
+
for i, chunk in enumerate(chunks, 1):
|
| 267 |
+
section = (
|
| 268 |
+
f"--- Chunk {i} ({chunk.chunk_type}: {chunk.name}) ---\n"
|
| 269 |
+
f"{chunk.text}\n"
|
| 270 |
+
)
|
| 271 |
+
if total + len(section) > max_chars:
|
| 272 |
+
parts.append("\n[Context truncated β token limit reached]")
|
| 273 |
+
break
|
| 274 |
+
parts.append(section)
|
| 275 |
+
total += len(section)
|
| 276 |
+
|
| 277 |
+
return "\n".join(parts)
|
| 278 |
+
|
| 279 |
+
def retrieve_complete_function(self,
|
| 280 |
+
function_name: str,
|
| 281 |
+
class_name: str | None = None,
|
| 282 |
+
n_results: int = 1) -> list[RetrievedChunk]:
|
| 283 |
+
"""
|
| 284 |
+
Retrieve complete function chunk including full source code.
|
| 285 |
+
Used for micro queries requiring edge case or usage analysis.
|
| 286 |
+
"""
|
| 287 |
+
filters = {
|
| 288 |
+
"$and": [
|
| 289 |
+
{"type": {"$eq": "complete_function"}},
|
| 290 |
+
{"name": {"$eq": function_name}},
|
| 291 |
+
]
|
| 292 |
+
}
|
| 293 |
+
if class_name:
|
| 294 |
+
filters = {
|
| 295 |
+
"$and": [
|
| 296 |
+
{"type": {"$eq": "complete_function"}},
|
| 297 |
+
{"name": {"$eq": function_name}},
|
| 298 |
+
{"class_name": {"$eq": class_name}},
|
| 299 |
+
]
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
raw = query_chunks(
|
| 303 |
+
query=f"function {function_name}",
|
| 304 |
+
collection_name=COMPLETE_COLLECTION,
|
| 305 |
+
model=self.model,
|
| 306 |
+
n_results=n_results,
|
| 307 |
+
filters=filters,
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
return [
|
| 311 |
+
RetrievedChunk(
|
| 312 |
+
text=r["text"],
|
| 313 |
+
metadata=r["metadata"],
|
| 314 |
+
distance=r["distance"],
|
| 315 |
+
collection=COMPLETE_COLLECTION,
|
| 316 |
+
)
|
| 317 |
+
for r in raw
|
| 318 |
+
]
|
| 319 |
+
|
| 320 |
+
def retrieve_complete_class(self,
|
| 321 |
+
class_name: str,
|
| 322 |
+
n_results: int = 1) -> list[RetrievedChunk]:
|
| 323 |
+
"""
|
| 324 |
+
Retrieve complete class chunk including full source code.
|
| 325 |
+
Used for class-level deep queries.
|
| 326 |
+
"""
|
| 327 |
+
filters = {
|
| 328 |
+
"$and": [
|
| 329 |
+
{"type": {"$eq": "complete_class"}},
|
| 330 |
+
{"name": {"$eq": class_name}},
|
| 331 |
+
]
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
raw = query_chunks(
|
| 335 |
+
query=f"class {class_name}",
|
| 336 |
+
collection_name=COMPLETE_COLLECTION,
|
| 337 |
+
model=self.model,
|
| 338 |
+
n_results=n_results,
|
| 339 |
+
filters=filters,
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
return [
|
| 343 |
+
RetrievedChunk(
|
| 344 |
+
text=r["text"],
|
| 345 |
+
metadata=r["metadata"],
|
| 346 |
+
distance=r["distance"],
|
| 347 |
+
collection=COMPLETE_COLLECTION,
|
| 348 |
+
)
|
| 349 |
+
for r in raw
|
| 350 |
+
]
|
| 351 |
+
|
| 352 |
+
def retrieve_file(self,
|
| 353 |
+
file_name: str,
|
| 354 |
+
n_results: int = 1) -> list[RetrievedChunk]:
|
| 355 |
+
"""
|
| 356 |
+
Retrieve complete file chunk.
|
| 357 |
+
Used for file-wide queries.
|
| 358 |
+
"""
|
| 359 |
+
filters = {
|
| 360 |
+
"$and": [
|
| 361 |
+
{"type": {"$eq": "file"}},
|
| 362 |
+
{"name": {"$eq": file_name}},
|
| 363 |
+
]
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
raw = query_chunks(
|
| 367 |
+
query=f"file {file_name}",
|
| 368 |
+
collection_name=COMPLETE_COLLECTION,
|
| 369 |
+
model=self.model,
|
| 370 |
+
n_results=n_results,
|
| 371 |
+
filters=filters,
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
return [
|
| 375 |
+
RetrievedChunk(
|
| 376 |
+
text=r["text"],
|
| 377 |
+
metadata=r["metadata"],
|
| 378 |
+
distance=r["distance"],
|
| 379 |
+
collection=COMPLETE_COLLECTION,
|
| 380 |
+
)
|
| 381 |
+
for r in raw
|
| 382 |
+
]
|
| 383 |
+
# ββ Internal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 384 |
+
|
| 385 |
+
def _route_collection(self, query_type: str) -> str:
|
| 386 |
+
"""Map query type to the appropriate ChromaDB collection."""
|
| 387 |
+
if query_type == QueryType.MICRO:
|
| 388 |
+
return FUNCTION_COLLECTION
|
| 389 |
+
return CLASS_COLLECTION # MACRO and CROSS_MODULE both use class chunks
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
# ββ Rich Renderers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 393 |
+
|
| 394 |
+
def render_chunks(chunks: list[RetrievedChunk], title: str = "Retrieved Chunks") -> None:
|
| 395 |
+
"""Render retrieved chunks as a rich table."""
|
| 396 |
+
if not chunks:
|
| 397 |
+
console.print("[yellow]No chunks retrieved.[/yellow]")
|
| 398 |
+
return
|
| 399 |
+
|
| 400 |
+
table = Table(
|
| 401 |
+
"Rank", "Type", "Name", "Class", "Module", "File", "Relevance",
|
| 402 |
+
box=box.ROUNDED,
|
| 403 |
+
header_style="bold bright_cyan",
|
| 404 |
+
border_style="cyan",
|
| 405 |
+
show_lines=True,
|
| 406 |
+
title=title,
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
for i, chunk in enumerate(chunks, 1):
|
| 410 |
+
score = chunk.relevance_score
|
| 411 |
+
score_style = (
|
| 412 |
+
"bold green" if score > 0.7
|
| 413 |
+
else "yellow" if score > 0.4
|
| 414 |
+
else "red"
|
| 415 |
+
)
|
| 416 |
+
table.add_row(
|
| 417 |
+
str(i),
|
| 418 |
+
chunk.chunk_type,
|
| 419 |
+
f"[bold]{chunk.name}[/bold]",
|
| 420 |
+
chunk.class_name or "[dim]β[/dim]",
|
| 421 |
+
chunk.module,
|
| 422 |
+
chunk.file,
|
| 423 |
+
f"[{score_style}]{score}[/{score_style}]",
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
console.print(table)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def render_chunk_detail(chunk: RetrievedChunk) -> None:
|
| 430 |
+
"""Render the full text of a single chunk in a panel."""
|
| 431 |
+
header = Text()
|
| 432 |
+
header.append(chunk.chunk_type.upper(), style="bold cyan")
|
| 433 |
+
header.append(f" {chunk.name}", style="bold white")
|
| 434 |
+
if chunk.class_name:
|
| 435 |
+
header.append(f" in {chunk.class_name}", style="dim")
|
| 436 |
+
|
| 437 |
+
console.print(Panel(
|
| 438 |
+
chunk.text,
|
| 439 |
+
title=str(header),
|
| 440 |
+
border_style="cyan",
|
| 441 |
+
padding=(1, 2),
|
| 442 |
+
))
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def render_context(context: str) -> None:
|
| 446 |
+
"""Render the assembled LLM context string."""
|
| 447 |
+
console.print(Panel(
|
| 448 |
+
context,
|
| 449 |
+
title="[bold cyan]Assembled LLM Context[/bold cyan]",
|
| 450 |
+
border_style="dim cyan",
|
| 451 |
+
padding=(1, 2),
|
| 452 |
+
))
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
# ββ Entry Point (manual test) βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 456 |
+
|
| 457 |
+
if __name__ == "__main__":
|
| 458 |
+
import sys
|
| 459 |
+
|
| 460 |
+
console.rule("[bold cyan]Retriever β Manual Test[/bold cyan]")
|
| 461 |
+
|
| 462 |
+
retriever = Retriever()
|
| 463 |
+
|
| 464 |
+
# Test 1 β Macro query
|
| 465 |
+
console.rule("[cyan]Test 1 β Macro Query[/cyan]")
|
| 466 |
+
query = "what classes handle file parsing and walking"
|
| 467 |
+
chunks = retriever.retrieve(query, QueryType.MACRO, n_results=3)
|
| 468 |
+
render_chunks(chunks, title=f'Macro: "{query}"')
|
| 469 |
+
|
| 470 |
+
# Test 2 β Micro query
|
| 471 |
+
console.rule("[cyan]Test 2 β Micro Query[/cyan]")
|
| 472 |
+
query = "function that parses a single file and extracts classes"
|
| 473 |
+
chunks = retriever.retrieve(query, QueryType.MICRO, n_results=3)
|
| 474 |
+
render_chunks(chunks, title=f'Micro: "{query}"')
|
| 475 |
+
if chunks:
|
| 476 |
+
render_chunk_detail(chunks[0])
|
| 477 |
+
|
| 478 |
+
# Test 3 β Cross module dependencies
|
| 479 |
+
console.rule("[cyan]Test 3 β Dependency Retrieval[/cyan]")
|
| 480 |
+
func_name = sys.argv[1] if len(sys.argv) > 1 else "parse_file"
|
| 481 |
+
dep_chunks = retriever.retrieve_dependencies(func_name)
|
| 482 |
+
render_chunks(dep_chunks, title=f'Dependencies of: {func_name}')
|
| 483 |
+
|
| 484 |
+
# Test 4 β Build context string
|
| 485 |
+
console.rule("[cyan]Test 4 β Context Assembly[/cyan]")
|
| 486 |
+
all_chunks = retriever.retrieve("class structure and responsibilities",
|
| 487 |
+
QueryType.MACRO, n_results=4)
|
| 488 |
+
context = retriever.build_context(all_chunks)
|
| 489 |
+
console.print(f"[green]β[/green] Context assembled: [cyan]{len(context)}[/cyan] chars")
|
store/__init__.py
ADDED
|
File without changes
|
store/call_graph.json
ADDED
|
File without changes
|
store/call_graph.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
call_graph.py
|
| 3 |
+
-------------
|
| 4 |
+
Builds a function-level call graph from parsed codebase (FileInfo objects)
|
| 5 |
+
and persists it as call_graph.json for cross-module dependency analysis.
|
| 6 |
+
|
| 7 |
+
Graph structure (stored in JSON):
|
| 8 |
+
{
|
| 9 |
+
"module.FileName.ClassName.function_name": {
|
| 10 |
+
"id": "module.FileName.ClassName.function_name",
|
| 11 |
+
"name": "function_name",
|
| 12 |
+
"class_name": "ClassName",
|
| 13 |
+
"module": "module",
|
| 14 |
+
"file": "relative/path/to/file.py",
|
| 15 |
+
"calls": ["other_func", "AnotherClass.method"],
|
| 16 |
+
"called_by": ["parent_func", "caller_func"]
|
| 17 |
+
},
|
| 18 |
+
...
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
Depends on:
|
| 22 |
+
- ast_parser.parse_codebase() β list[FileInfo]
|
| 23 |
+
- rich
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import json
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from dataclasses import dataclass, field
|
| 29 |
+
from collections import defaultdict
|
| 30 |
+
|
| 31 |
+
from rich.console import Console
|
| 32 |
+
from rich.tree import Tree
|
| 33 |
+
from rich.table import Table
|
| 34 |
+
from rich.panel import Panel
|
| 35 |
+
from rich.text import Text
|
| 36 |
+
from rich import box
|
| 37 |
+
|
| 38 |
+
from ingest.parse_ast import parse_codebase, FileInfo
|
| 39 |
+
|
| 40 |
+
console = Console()
|
| 41 |
+
|
| 42 |
+
# ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
|
| 44 |
+
CALL_GRAPH_PATH = "./call_graph.json"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ββ Node Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class CallNode:
|
| 51 |
+
"""Represents a single function/method in the call graph."""
|
| 52 |
+
id: str # unique fully-qualified ID
|
| 53 |
+
name: str # function name
|
| 54 |
+
class_name: str # class name or "" for top-level
|
| 55 |
+
module: str # top-level module/package
|
| 56 |
+
file: str # relative file path
|
| 57 |
+
calls: list[str] = field(default_factory=list) # what it calls
|
| 58 |
+
called_by: list[str] = field(default_factory=list) # what calls it
|
| 59 |
+
|
| 60 |
+
def to_dict(self) -> dict:
|
| 61 |
+
return {
|
| 62 |
+
"id": self.id,
|
| 63 |
+
"name": self.name,
|
| 64 |
+
"class_name": self.class_name,
|
| 65 |
+
"module": self.module,
|
| 66 |
+
"file": self.file,
|
| 67 |
+
"calls": self.calls,
|
| 68 |
+
"called_by": self.called_by,
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def from_dict(data: dict) -> "CallNode":
|
| 73 |
+
return CallNode(
|
| 74 |
+
id=data["id"],
|
| 75 |
+
name=data["name"],
|
| 76 |
+
class_name=data.get("class_name", ""),
|
| 77 |
+
module=data["module"],
|
| 78 |
+
file=data["file"],
|
| 79 |
+
calls=data.get("calls", []),
|
| 80 |
+
called_by=data.get("called_by", []),
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ββ CallGraph βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
+
|
| 86 |
+
class CallGraph:
|
| 87 |
+
"""
|
| 88 |
+
Builds, stores, queries, and persists the function call graph
|
| 89 |
+
for a monolithic codebase.
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(self):
|
| 93 |
+
# node_id β CallNode
|
| 94 |
+
self._graph: dict[str, CallNode] = {}
|
| 95 |
+
# name β list of node_ids (for fuzzy lookup by function name alone)
|
| 96 |
+
self._name_index: dict[str, list[str]] = defaultdict(list)
|
| 97 |
+
|
| 98 |
+
# ββ Build βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 99 |
+
|
| 100 |
+
def build(self, parsed_files: list[FileInfo]) -> None:
|
| 101 |
+
"""
|
| 102 |
+
Build the call graph from a list of parsed FileInfo objects.
|
| 103 |
+
Two passes:
|
| 104 |
+
1. Register all functions as nodes
|
| 105 |
+
2. Resolve call relationships and populate called_by
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
parsed_files: Output of ast_parser.parse_codebase().
|
| 109 |
+
"""
|
| 110 |
+
self._graph.clear()
|
| 111 |
+
self._name_index.clear()
|
| 112 |
+
|
| 113 |
+
# Pass 1 β register all nodes
|
| 114 |
+
for file_info in parsed_files:
|
| 115 |
+
# Top-level functions
|
| 116 |
+
for func in file_info.functions:
|
| 117 |
+
node = CallNode(
|
| 118 |
+
id=self._make_id(file_info, func.name, class_name=""),
|
| 119 |
+
name=func.name,
|
| 120 |
+
class_name="",
|
| 121 |
+
module=file_info.module,
|
| 122 |
+
file=file_info.relative,
|
| 123 |
+
calls=func.calls,
|
| 124 |
+
)
|
| 125 |
+
self._register(node)
|
| 126 |
+
|
| 127 |
+
# Class methods
|
| 128 |
+
for cls in file_info.classes:
|
| 129 |
+
for method in cls.methods:
|
| 130 |
+
node = CallNode(
|
| 131 |
+
id=self._make_id(file_info, method.name, class_name=cls.name),
|
| 132 |
+
name=method.name,
|
| 133 |
+
class_name=cls.name,
|
| 134 |
+
module=file_info.module,
|
| 135 |
+
file=file_info.relative,
|
| 136 |
+
calls=method.calls,
|
| 137 |
+
)
|
| 138 |
+
self._register(node)
|
| 139 |
+
|
| 140 |
+
# Pass 2 β resolve called_by relationships
|
| 141 |
+
for node in self._graph.values():
|
| 142 |
+
for raw_call in node.calls:
|
| 143 |
+
# Strip object prefix: "self.calculate" β "calculate"
|
| 144 |
+
callee_name = raw_call.split(".")[-1]
|
| 145 |
+
candidate_ids = self._name_index.get(callee_name, [])
|
| 146 |
+
for callee_id in candidate_ids:
|
| 147 |
+
callee = self._graph.get(callee_id)
|
| 148 |
+
if callee and node.id not in callee.called_by:
|
| 149 |
+
callee.called_by.append(node.id)
|
| 150 |
+
|
| 151 |
+
total_nodes = len(self._graph)
|
| 152 |
+
total_edges = sum(len(n.calls) for n in self._graph.values())
|
| 153 |
+
console.print(
|
| 154 |
+
f"[green]β[/green] Call graph built: "
|
| 155 |
+
f"[cyan]{total_nodes}[/cyan] nodes Β· "
|
| 156 |
+
f"[magenta]{total_edges}[/magenta] edges\n"
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
def _make_id(self, file_info: FileInfo,
|
| 160 |
+
func_name: str,
|
| 161 |
+
class_name: str) -> str:
|
| 162 |
+
"""Generate a unique fully-qualified node ID."""
|
| 163 |
+
stem = Path(file_info.relative).stem # filename without extension
|
| 164 |
+
parts = [file_info.module, stem]
|
| 165 |
+
if class_name:
|
| 166 |
+
parts.append(class_name)
|
| 167 |
+
parts.append(func_name)
|
| 168 |
+
return ".".join(parts)
|
| 169 |
+
|
| 170 |
+
def _register(self, node: CallNode) -> None:
|
| 171 |
+
"""Register a node in the graph and name index."""
|
| 172 |
+
self._graph[node.id] = node
|
| 173 |
+
self._name_index[node.name].append(node.id)
|
| 174 |
+
|
| 175 |
+
# ββ Persist βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
+
|
| 177 |
+
def save(self, path: str = CALL_GRAPH_PATH) -> None:
|
| 178 |
+
"""
|
| 179 |
+
Persist the call graph to a JSON file.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
path: File path to write call_graph.json.
|
| 183 |
+
"""
|
| 184 |
+
data = {node_id: node.to_dict() for node_id, node in self._graph.items()}
|
| 185 |
+
|
| 186 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 187 |
+
json.dump(data, f, indent=2)
|
| 188 |
+
|
| 189 |
+
size_kb = round(Path(path).stat().st_size / 1024, 2)
|
| 190 |
+
console.print(
|
| 191 |
+
f"[green]β[/green] Call graph saved to "
|
| 192 |
+
f"[cyan]{path}[/cyan] ({size_kb} KB)\n"
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
def load(self, path: str = CALL_GRAPH_PATH) -> None:
|
| 196 |
+
"""
|
| 197 |
+
Load a persisted call graph from JSON.
|
| 198 |
+
|
| 199 |
+
Args:
|
| 200 |
+
path: File path to read call_graph.json from.
|
| 201 |
+
|
| 202 |
+
Raises:
|
| 203 |
+
FileNotFoundError: If the file does not exist.
|
| 204 |
+
"""
|
| 205 |
+
graph_path = Path(path)
|
| 206 |
+
if not graph_path.exists():
|
| 207 |
+
raise FileNotFoundError(
|
| 208 |
+
f"Call graph not found at '{path}'. "
|
| 209 |
+
"Run build_and_save() first."
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 213 |
+
data = json.load(f)
|
| 214 |
+
|
| 215 |
+
self._graph.clear()
|
| 216 |
+
self._name_index.clear()
|
| 217 |
+
|
| 218 |
+
for node_id, node_data in data.items():
|
| 219 |
+
node = CallNode.from_dict(node_data)
|
| 220 |
+
self._graph[node_id] = node
|
| 221 |
+
self._name_index[node.name].append(node_id)
|
| 222 |
+
|
| 223 |
+
console.print(
|
| 224 |
+
f"[green]β[/green] Call graph loaded: "
|
| 225 |
+
f"[cyan]{len(self._graph)}[/cyan] nodes from "
|
| 226 |
+
f"[cyan]{path}[/cyan]\n"
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# ββ Query API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 230 |
+
|
| 231 |
+
def get_node(self, function_name: str,
|
| 232 |
+
class_name: str | None = None) -> list[CallNode]:
|
| 233 |
+
"""
|
| 234 |
+
Look up nodes by function name, optionally filtered by class.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
function_name: Name of the function/method to look up.
|
| 238 |
+
class_name: Optional class name to narrow results.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
List of matching CallNode objects.
|
| 242 |
+
"""
|
| 243 |
+
candidate_ids = self._name_index.get(function_name, [])
|
| 244 |
+
nodes = [self._graph[nid] for nid in candidate_ids if nid in self._graph]
|
| 245 |
+
|
| 246 |
+
if class_name:
|
| 247 |
+
nodes = [n for n in nodes if n.class_name == class_name]
|
| 248 |
+
|
| 249 |
+
return nodes
|
| 250 |
+
|
| 251 |
+
def get_calls(self, function_name: str,
|
| 252 |
+
class_name: str | None = None) -> list[CallNode]:
|
| 253 |
+
"""
|
| 254 |
+
Get all functions that the given function calls (outgoing edges).
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
function_name: Source function name.
|
| 258 |
+
class_name: Optional class scope.
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
List of CallNode objects this function calls.
|
| 262 |
+
"""
|
| 263 |
+
source_nodes = self.get_node(function_name, class_name)
|
| 264 |
+
if not source_nodes:
|
| 265 |
+
return []
|
| 266 |
+
|
| 267 |
+
results = []
|
| 268 |
+
seen = set()
|
| 269 |
+
for source in source_nodes:
|
| 270 |
+
for raw_call in source.calls:
|
| 271 |
+
callee_name = raw_call.split(".")[-1]
|
| 272 |
+
for callee_node in self.get_node(callee_name):
|
| 273 |
+
if callee_node.id not in seen:
|
| 274 |
+
seen.add(callee_node.id)
|
| 275 |
+
results.append(callee_node)
|
| 276 |
+
|
| 277 |
+
return results
|
| 278 |
+
|
| 279 |
+
def get_called_by(self, function_name: str,
|
| 280 |
+
class_name: str | None = None) -> list[CallNode]:
|
| 281 |
+
"""
|
| 282 |
+
Get all functions that call the given function (incoming edges).
|
| 283 |
+
|
| 284 |
+
Args:
|
| 285 |
+
function_name: Target function name.
|
| 286 |
+
class_name: Optional class scope.
|
| 287 |
+
|
| 288 |
+
Returns:
|
| 289 |
+
List of CallNode objects that call this function.
|
| 290 |
+
"""
|
| 291 |
+
target_nodes = self.get_node(function_name, class_name)
|
| 292 |
+
if not target_nodes:
|
| 293 |
+
return []
|
| 294 |
+
|
| 295 |
+
results = []
|
| 296 |
+
seen = set()
|
| 297 |
+
for target in target_nodes:
|
| 298 |
+
for caller_id in target.called_by:
|
| 299 |
+
if caller_id not in seen and caller_id in self._graph:
|
| 300 |
+
seen.add(caller_id)
|
| 301 |
+
results.append(self._graph[caller_id])
|
| 302 |
+
|
| 303 |
+
return results
|
| 304 |
+
|
| 305 |
+
def get_impact(self, function_name: str,
|
| 306 |
+
class_name: str | None = None,
|
| 307 |
+
depth: int = 2) -> dict[str, list[CallNode]]:
|
| 308 |
+
"""
|
| 309 |
+
Trace the impact of changing a function β who calls it,
|
| 310 |
+
who calls those callers, up to `depth` levels.
|
| 311 |
+
|
| 312 |
+
Args:
|
| 313 |
+
function_name: Function to analyze.
|
| 314 |
+
class_name: Optional class scope.
|
| 315 |
+
depth: How many levels up to trace (default 2).
|
| 316 |
+
|
| 317 |
+
Returns:
|
| 318 |
+
Dict mapping depth level string β list of affected CallNodes.
|
| 319 |
+
"""
|
| 320 |
+
impact: dict[str, list[CallNode]] = {}
|
| 321 |
+
current_level = self.get_node(function_name, class_name)
|
| 322 |
+
visited = {n.id for n in current_level}
|
| 323 |
+
|
| 324 |
+
for level in range(1, depth + 1):
|
| 325 |
+
next_level = []
|
| 326 |
+
for node in current_level:
|
| 327 |
+
for caller_id in node.called_by:
|
| 328 |
+
if caller_id not in visited and caller_id in self._graph:
|
| 329 |
+
visited.add(caller_id)
|
| 330 |
+
next_level.append(self._graph[caller_id])
|
| 331 |
+
if not next_level:
|
| 332 |
+
break
|
| 333 |
+
impact[f"level_{level}"] = next_level
|
| 334 |
+
current_level = next_level
|
| 335 |
+
|
| 336 |
+
return impact
|
| 337 |
+
|
| 338 |
+
def stats(self) -> dict:
|
| 339 |
+
"""Return summary statistics for the call graph."""
|
| 340 |
+
total_edges = sum(len(n.calls) for n in self._graph.values())
|
| 341 |
+
modules = list({n.module for n in self._graph.values()})
|
| 342 |
+
isolated = [n for n in self._graph.values()
|
| 343 |
+
if not n.calls and not n.called_by]
|
| 344 |
+
return {
|
| 345 |
+
"total_nodes": len(self._graph),
|
| 346 |
+
"total_edges": total_edges,
|
| 347 |
+
"modules": modules,
|
| 348 |
+
"isolated_nodes": len(isolated),
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
def is_loaded(self) -> bool:
|
| 352 |
+
"""Return True if graph has been built or loaded."""
|
| 353 |
+
return len(self._graph) > 0
|
| 354 |
+
|
| 355 |
+
# ββ Rich Renderers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 356 |
+
|
| 357 |
+
def render_stats(self) -> None:
|
| 358 |
+
"""Render call graph statistics as a rich panel."""
|
| 359 |
+
s = self.stats()
|
| 360 |
+
|
| 361 |
+
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
| 362 |
+
table.add_column(style="dim")
|
| 363 |
+
table.add_column(style="bold white")
|
| 364 |
+
|
| 365 |
+
table.add_row("Total nodes", str(s["total_nodes"]))
|
| 366 |
+
table.add_row("Total edges", str(s["total_edges"]))
|
| 367 |
+
table.add_row("Modules", ", ".join(s["modules"]))
|
| 368 |
+
table.add_row("Isolated nodes", str(s["isolated_nodes"]))
|
| 369 |
+
table.add_row(
|
| 370 |
+
"Status",
|
| 371 |
+
"[bold green]β Loaded[/bold green]"
|
| 372 |
+
if self.is_loaded()
|
| 373 |
+
else "[bold red]β Empty[/bold red]"
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
console.print(Panel(
|
| 377 |
+
table,
|
| 378 |
+
title="[bold cyan]Call Graph Stats[/bold cyan]",
|
| 379 |
+
border_style="cyan",
|
| 380 |
+
))
|
| 381 |
+
|
| 382 |
+
def render_node(self, node: CallNode) -> None:
|
| 383 |
+
"""Render a single node with its calls and callers."""
|
| 384 |
+
scope = f"{node.class_name}.{node.name}" if node.class_name else node.name
|
| 385 |
+
|
| 386 |
+
calls_str = "\n".join(f" β {c}" for c in node.calls) or " [dim]none[/dim]"
|
| 387 |
+
called_by_str = "\n".join(f" β {c}" for c in node.called_by) or " [dim]none[/dim]"
|
| 388 |
+
|
| 389 |
+
content = Text()
|
| 390 |
+
content.append("Module: ", style="dim")
|
| 391 |
+
content.append(f"{node.module}\n")
|
| 392 |
+
content.append("File: ", style="dim")
|
| 393 |
+
content.append(f"{node.file}\n\n")
|
| 394 |
+
content.append("Calls:\n", style="bold cyan")
|
| 395 |
+
content.append(calls_str + "\n\n")
|
| 396 |
+
content.append("Called by:\n", style="bold magenta")
|
| 397 |
+
content.append(called_by_str)
|
| 398 |
+
|
| 399 |
+
console.print(Panel(
|
| 400 |
+
content,
|
| 401 |
+
title=f"[bold white]{scope}[/bold white]",
|
| 402 |
+
border_style="cyan",
|
| 403 |
+
))
|
| 404 |
+
|
| 405 |
+
def render_impact(self, function_name: str,
|
| 406 |
+
impact: dict[str, list[CallNode]]) -> None:
|
| 407 |
+
"""Render the impact tree of a function change."""
|
| 408 |
+
root = Tree(
|
| 409 |
+
f"[bold yellow]β‘ Impact of changing:[/bold yellow] "
|
| 410 |
+
f"[bold white]{function_name}[/bold white]",
|
| 411 |
+
guide_style="dim yellow",
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
if not impact:
|
| 415 |
+
root.add("[dim]No callers found β isolated function.[/dim]")
|
| 416 |
+
else:
|
| 417 |
+
for level, nodes in impact.items():
|
| 418 |
+
level_num = level.split("_")[1]
|
| 419 |
+
level_branch = root.add(
|
| 420 |
+
f"[bold cyan]Level {level_num} β {len(nodes)} affected[/bold cyan]"
|
| 421 |
+
)
|
| 422 |
+
for node in nodes:
|
| 423 |
+
scope = (
|
| 424 |
+
f"{node.class_name}.{node.name}"
|
| 425 |
+
if node.class_name else node.name
|
| 426 |
+
)
|
| 427 |
+
level_branch.add(
|
| 428 |
+
f"[magenta]{scope}[/magenta] "
|
| 429 |
+
f"[dim]{node.file}[/dim]"
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
console.print(root)
|
| 433 |
+
|
| 434 |
+
def render_dependencies(self, function_name: str,
|
| 435 |
+
calls: list[CallNode],
|
| 436 |
+
called_by: list[CallNode]) -> None:
|
| 437 |
+
"""Render outgoing and incoming dependencies for a function."""
|
| 438 |
+
table = Table(
|
| 439 |
+
"Direction", "Function", "Class", "Module", "File",
|
| 440 |
+
box=box.ROUNDED,
|
| 441 |
+
header_style="bold bright_cyan",
|
| 442 |
+
border_style="cyan",
|
| 443 |
+
show_lines=True,
|
| 444 |
+
title=f"Dependencies: {function_name}",
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
for node in calls:
|
| 448 |
+
table.add_row(
|
| 449 |
+
"[cyan]β calls[/cyan]",
|
| 450 |
+
node.name,
|
| 451 |
+
node.class_name or "[dim]β[/dim]",
|
| 452 |
+
node.module,
|
| 453 |
+
node.file,
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
for node in called_by:
|
| 457 |
+
table.add_row(
|
| 458 |
+
"[magenta]β called by[/magenta]",
|
| 459 |
+
node.name,
|
| 460 |
+
node.class_name or "[dim]β[/dim]",
|
| 461 |
+
node.module,
|
| 462 |
+
node.file,
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
if not calls and not called_by:
|
| 466 |
+
console.print(f"[yellow]No dependencies found for '{function_name}'.[/yellow]")
|
| 467 |
+
else:
|
| 468 |
+
console.print(table)
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# ββ Convenience Pipeline ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 472 |
+
|
| 473 |
+
def build_and_save(root_path: str,
|
| 474 |
+
graph_path: str = CALL_GRAPH_PATH) -> CallGraph:
|
| 475 |
+
"""
|
| 476 |
+
Full pipeline: parse codebase β build graph β save to JSON.
|
| 477 |
+
|
| 478 |
+
Args:
|
| 479 |
+
root_path: Absolute path to the monolithic codebase root.
|
| 480 |
+
graph_path: Path to write call_graph.json.
|
| 481 |
+
|
| 482 |
+
Returns:
|
| 483 |
+
Built and saved CallGraph instance.
|
| 484 |
+
"""
|
| 485 |
+
console.rule("[bold cyan]Call Graph Builder[/bold cyan]")
|
| 486 |
+
console.print(f"[bold]π Root:[/bold] {root_path}\n")
|
| 487 |
+
|
| 488 |
+
parsed_files = parse_codebase(root_path)
|
| 489 |
+
|
| 490 |
+
graph = CallGraph()
|
| 491 |
+
graph.build(parsed_files)
|
| 492 |
+
graph.save(graph_path)
|
| 493 |
+
graph.render_stats()
|
| 494 |
+
|
| 495 |
+
return graph
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def load_graph(graph_path: str = CALL_GRAPH_PATH) -> CallGraph:
|
| 499 |
+
"""
|
| 500 |
+
Load a persisted call graph from JSON.
|
| 501 |
+
|
| 502 |
+
Args:
|
| 503 |
+
graph_path: Path to call_graph.json.
|
| 504 |
+
|
| 505 |
+
Returns:
|
| 506 |
+
Loaded CallGraph instance.
|
| 507 |
+
"""
|
| 508 |
+
graph = CallGraph()
|
| 509 |
+
graph.load(graph_path)
|
| 510 |
+
return graph
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
# ββ Singleton Access ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 514 |
+
|
| 515 |
+
_graph_instance: CallGraph | None = None
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
def get_call_graph(graph_path: str = CALL_GRAPH_PATH) -> CallGraph:
|
| 519 |
+
"""
|
| 520 |
+
Return a singleton CallGraph instance.
|
| 521 |
+
Loads from JSON on first call, reuses on subsequent calls.
|
| 522 |
+
|
| 523 |
+
Args:
|
| 524 |
+
graph_path: Path to call_graph.json.
|
| 525 |
+
|
| 526 |
+
Returns:
|
| 527 |
+
Shared CallGraph instance.
|
| 528 |
+
"""
|
| 529 |
+
global _graph_instance
|
| 530 |
+
if _graph_instance is None:
|
| 531 |
+
_graph_instance = load_graph(graph_path)
|
| 532 |
+
return _graph_instance
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
# ββ Entry Point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 536 |
+
|
| 537 |
+
if __name__ == "__main__":
|
| 538 |
+
import sys
|
| 539 |
+
|
| 540 |
+
if len(sys.argv) < 2:
|
| 541 |
+
console.print(
|
| 542 |
+
"[red]Usage:[/red] python call_graph.py <codebase_path> "
|
| 543 |
+
"[function_name]"
|
| 544 |
+
)
|
| 545 |
+
sys.exit(1)
|
| 546 |
+
|
| 547 |
+
root = sys.argv[1]
|
| 548 |
+
query = sys.argv[2] if len(sys.argv) > 2 else None
|
| 549 |
+
|
| 550 |
+
# Build and save
|
| 551 |
+
graph = build_and_save(root)
|
| 552 |
+
|
| 553 |
+
if query:
|
| 554 |
+
console.rule(f"[bold cyan]Query: {query}[/bold cyan]")
|
| 555 |
+
|
| 556 |
+
# Node detail
|
| 557 |
+
nodes = graph.get_node(query)
|
| 558 |
+
if not nodes:
|
| 559 |
+
console.print(f"[yellow]Function '{query}' not found in graph.[/yellow]")
|
| 560 |
+
else:
|
| 561 |
+
for node in nodes:
|
| 562 |
+
graph.render_node(node)
|
| 563 |
+
|
| 564 |
+
# Dependencies
|
| 565 |
+
calls = graph.get_calls(query)
|
| 566 |
+
called_by = graph.get_called_by(query)
|
| 567 |
+
graph.render_dependencies(query, calls, called_by)
|
| 568 |
+
|
| 569 |
+
# Impact analysis
|
| 570 |
+
impact = graph.get_impact(query, depth=2)
|
| 571 |
+
graph.render_impact(query, impact)
|
| 572 |
+
else:
|
| 573 |
+
# Show sample nodes
|
| 574 |
+
console.rule("[cyan]Sample Nodes[/cyan]")
|
| 575 |
+
sample = list(graph._graph.values())[:5]
|
| 576 |
+
for node in sample:
|
| 577 |
+
graph.render_node(node)
|
store/vector_store.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
vector_store.py
|
| 3 |
+
---------------
|
| 4 |
+
Pinecone-backed vector store interface for the Codebase Oracle system.
|
| 5 |
+
Reads from the same Pinecone index used by embed.py via namespaces.
|
| 6 |
+
|
| 7 |
+
Collections (as Pinecone namespaces):
|
| 8 |
+
- class_chunks : one chunk per class (macro / cross-module queries)
|
| 9 |
+
- function_chunks : one chunk per function/method (micro queries)
|
| 10 |
+
|
| 11 |
+
Depends on:
|
| 12 |
+
- pinecone
|
| 13 |
+
- ingest.embed (get_pinecone_index)
|
| 14 |
+
- rich
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from dataclasses import dataclass
|
| 18 |
+
from rich.console import Console
|
| 19 |
+
from rich.table import Table
|
| 20 |
+
from rich.panel import Panel
|
| 21 |
+
from rich.text import Text
|
| 22 |
+
from rich import box
|
| 23 |
+
|
| 24 |
+
from ingest.embed import get_pinecone_index, CLASS_COLLECTION, FUNCTION_COLLECTION
|
| 25 |
+
|
| 26 |
+
console = Console()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ββ Result Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class ChunkResult:
|
| 33 |
+
"""Represents a single retrieved chunk."""
|
| 34 |
+
id: str
|
| 35 |
+
text: str
|
| 36 |
+
metadata: dict
|
| 37 |
+
distance: float | None = None
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def name(self) -> str:
|
| 41 |
+
return self.metadata.get("name", "unknown")
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def module(self) -> str:
|
| 45 |
+
return self.metadata.get("module", "unknown")
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def file(self) -> str:
|
| 49 |
+
return self.metadata.get("file", "unknown")
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def chunk_type(self) -> str:
|
| 53 |
+
return self.metadata.get("type", "unknown")
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def class_name(self) -> str:
|
| 57 |
+
return self.metadata.get("class_name", "")
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
def relevance(self) -> float:
|
| 61 |
+
if self.distance is None:
|
| 62 |
+
return 0.0
|
| 63 |
+
return round(1 / (1 + self.distance), 4)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ββ VectorStore βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
+
|
| 68 |
+
class VectorStore:
|
| 69 |
+
"""
|
| 70 |
+
Pinecone-backed interface for stats and tree queries.
|
| 71 |
+
Reuses the same index as embed.py β no duplicate client.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
def __init__(self):
|
| 75 |
+
self._index = get_pinecone_index()
|
| 76 |
+
console.print("[green]β[/green] VectorStore ready (Pinecone)\n")
|
| 77 |
+
|
| 78 |
+
def _count(self, namespace: str) -> int:
|
| 79 |
+
"""Return approximate vector count in a namespace."""
|
| 80 |
+
stats = self._index.describe_index_stats()
|
| 81 |
+
return stats["namespaces"].get(namespace, {}).get("vector_count", 0)
|
| 82 |
+
|
| 83 |
+
def stats(self) -> dict:
|
| 84 |
+
class_count = self._count(CLASS_COLLECTION)
|
| 85 |
+
func_count = self._count(FUNCTION_COLLECTION)
|
| 86 |
+
return {
|
| 87 |
+
"class_chunks": class_count,
|
| 88 |
+
"function_chunks": func_count,
|
| 89 |
+
"total": class_count + func_count,
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
def is_indexed(self) -> bool:
|
| 93 |
+
s = self.stats()
|
| 94 |
+
return s["total"] > 0
|
| 95 |
+
|
| 96 |
+
def get_all(self, namespace: str, limit: int = 10) -> list[ChunkResult]:
|
| 97 |
+
"""
|
| 98 |
+
Fetch chunks from a namespace without a query vector.
|
| 99 |
+
Pinecone does not support scan β we use a zero vector as proxy.
|
| 100 |
+
"""
|
| 101 |
+
from config.config import EMBEDDING_DIM
|
| 102 |
+
zero_vector = [0.0] * EMBEDDING_DIM
|
| 103 |
+
|
| 104 |
+
results = self._index.query(
|
| 105 |
+
vector=zero_vector,
|
| 106 |
+
top_k=limit,
|
| 107 |
+
namespace=namespace,
|
| 108 |
+
include_metadata=True,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
output = []
|
| 112 |
+
for match in results["matches"]:
|
| 113 |
+
meta = dict(match["metadata"])
|
| 114 |
+
text = meta.pop("text", "")
|
| 115 |
+
output.append(ChunkResult(
|
| 116 |
+
id=match["id"],
|
| 117 |
+
text=text,
|
| 118 |
+
metadata=meta,
|
| 119 |
+
distance=1 - match["score"],
|
| 120 |
+
))
|
| 121 |
+
return output
|
| 122 |
+
|
| 123 |
+
def render_stats(self) -> None:
|
| 124 |
+
s = self.stats()
|
| 125 |
+
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
| 126 |
+
table.add_column(style="dim")
|
| 127 |
+
table.add_column(style="bold white")
|
| 128 |
+
table.add_row("Class chunks", str(s["class_chunks"]))
|
| 129 |
+
table.add_row("Function chunks", str(s["function_chunks"]))
|
| 130 |
+
table.add_row("Total chunks", str(s["total"]))
|
| 131 |
+
table.add_row(
|
| 132 |
+
"Status",
|
| 133 |
+
"[bold green]β Indexed[/bold green]"
|
| 134 |
+
if self.is_indexed()
|
| 135 |
+
else "[bold red]β Not indexed[/bold red]"
|
| 136 |
+
)
|
| 137 |
+
console.print(Panel(
|
| 138 |
+
table,
|
| 139 |
+
title="[bold cyan]VectorStore Stats[/bold cyan]",
|
| 140 |
+
border_style="cyan",
|
| 141 |
+
))
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# ββ Singleton βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 145 |
+
|
| 146 |
+
_store_instance: VectorStore | None = None
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def get_vector_store() -> VectorStore:
|
| 150 |
+
global _store_instance
|
| 151 |
+
if _store_instance is None:
|
| 152 |
+
_store_instance = VectorStore()
|
| 153 |
+
return _store_instance
|
ui/index.html
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Codebase Oracle</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet" />
|
| 10 |
+
<link rel="stylesheet" href="static/style.css" />
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
|
| 14 |
+
<!-- ββ Top Bar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 15 |
+
<header class="topbar">
|
| 16 |
+
<div class="topbar__brand">
|
| 17 |
+
<span class="topbar__icon">⬑</span>
|
| 18 |
+
<span class="topbar__title">Codebase Oracle</span>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div class="topbar__index">
|
| 22 |
+
<input
|
| 23 |
+
id="pathInput"
|
| 24 |
+
class="path-input"
|
| 25 |
+
type="text"
|
| 26 |
+
placeholder="/absolute/path/to/your/codebase"
|
| 27 |
+
autocomplete="off"
|
| 28 |
+
spellcheck="false"
|
| 29 |
+
/>
|
| 30 |
+
<label class="btn btn--secondary" for="zipInput" id="zipLabel">Upload ZIP</label>
|
| 31 |
+
<input id="zipInput" type="file" accept=".zip" style="display:none" />
|
| 32 |
+
<button id="indexBtn" class="btn btn--primary">
|
| 33 |
+
<span id="indexBtnText">Index</span>
|
| 34 |
+
<span id="indexSpinner" class="spinner hidden"></span>
|
| 35 |
+
</button>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div id="statusBadge" class="status-badge status-badge--idle">
|
| 39 |
+
<span class="status-badge__dot"></span>
|
| 40 |
+
<span id="statusText">Not indexed</span>
|
| 41 |
+
</div>
|
| 42 |
+
</header>
|
| 43 |
+
|
| 44 |
+
<!-- ββ Main Layout βββββββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 45 |
+
<div class="workspace">
|
| 46 |
+
|
| 47 |
+
<!-- Sidebar -->
|
| 48 |
+
<aside class="sidebar">
|
| 49 |
+
<div class="sidebar__header">
|
| 50 |
+
<span class="sidebar__label">Explorer</span>
|
| 51 |
+
<span id="chunkCount" class="sidebar__count"></span>
|
| 52 |
+
</div>
|
| 53 |
+
<div id="treeContainer" class="tree-container">
|
| 54 |
+
<div class="tree-placeholder">
|
| 55 |
+
<span class="tree-placeholder__icon">⬑</span>
|
| 56 |
+
<p>Index a codebase to explore its structure</p>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</aside>
|
| 60 |
+
|
| 61 |
+
<!-- Chat Area -->
|
| 62 |
+
<main class="chat">
|
| 63 |
+
|
| 64 |
+
<!-- Query Type Selector -->
|
| 65 |
+
<div class="query-controls">
|
| 66 |
+
<div class="query-type-group" id="queryTypeGroup">
|
| 67 |
+
<button class="query-type-btn active" data-type="micro">Micro</button>
|
| 68 |
+
<button class="query-type-btn" data-type="macro">Macro</button>
|
| 69 |
+
<button class="query-type-btn" data-type="cross_module">Cross-Module</button>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<!-- Macro subtype (shown only when macro selected) -->
|
| 73 |
+
<div id="macroSubtypes" class="macro-subtypes hidden">
|
| 74 |
+
<button class="subtype-btn active" data-subtype="overall_architecture">Architecture</button>
|
| 75 |
+
<button class="subtype-btn" data-subtype="module_responsibility">Module</button>
|
| 76 |
+
<button class="subtype-btn" data-subtype="data_flow">Data Flow</button>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<!-- Contextual inputs -->
|
| 80 |
+
<div id="contextInputs" class="context-inputs">
|
| 81 |
+
<input id="functionInput" class="context-field" type="text" placeholder="Function name" autocomplete="off" spellcheck="false" />
|
| 82 |
+
<input id="classInput" class="context-field" type="text" placeholder="Class name (optional)" autocomplete="off" spellcheck="false" />
|
| 83 |
+
<input id="moduleInput" class="context-field hidden" type="text" placeholder="Module name" autocomplete="off" spellcheck="false" />
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<!-- Messages -->
|
| 88 |
+
<div id="messages" class="messages">
|
| 89 |
+
<div class="message message--system">
|
| 90 |
+
<div class="message__content">
|
| 91 |
+
<p>Welcome to <strong>Codebase Oracle</strong>. Index a codebase using the path bar above, then ask anything about it.</p>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<!-- Input Bar -->
|
| 97 |
+
<div class="input-bar">
|
| 98 |
+
<textarea
|
| 99 |
+
id="queryInput"
|
| 100 |
+
class="query-textarea"
|
| 101 |
+
placeholder="Ask anything about the codebaseβ¦"
|
| 102 |
+
rows="1"
|
| 103 |
+
spellcheck="false"
|
| 104 |
+
></textarea>
|
| 105 |
+
<button id="sendBtn" class="btn btn--send" disabled>
|
| 106 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 107 |
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
| 108 |
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
| 109 |
+
</svg>
|
| 110 |
+
</button>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
</main>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
| 117 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
| 118 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
| 119 |
+
<script src="static/app.js"></script>
|
| 120 |
+
</body>
|
| 121 |
+
</html>
|
ui/static/app.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* app.js β Codebase Oracle frontend logic */
|
| 2 |
+
|
| 3 |
+
const API = 'http://localhost:8000';
|
| 4 |
+
|
| 5 |
+
// ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
const state = {
|
| 7 |
+
indexed: false,
|
| 8 |
+
querying: false,
|
| 9 |
+
queryType: 'micro',
|
| 10 |
+
macroSubtype: 'overall_architecture',
|
| 11 |
+
lastResponse: '',
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// ββ DOM Refs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
const $ = id => document.getElementById(id);
|
| 16 |
+
|
| 17 |
+
const dom = {
|
| 18 |
+
pathInput: $('pathInput'),
|
| 19 |
+
zipInput: $('zipInput'),
|
| 20 |
+
zipLabel: $('zipLabel'),
|
| 21 |
+
indexBtn: $('indexBtn'),
|
| 22 |
+
indexBtnText: $('indexBtnText'),
|
| 23 |
+
indexSpinner: $('indexSpinner'),
|
| 24 |
+
statusBadge: $('statusBadge'),
|
| 25 |
+
statusText: $('statusText'),
|
| 26 |
+
chunkCount: $('chunkCount'),
|
| 27 |
+
treeContainer: $('treeContainer'),
|
| 28 |
+
messages: $('messages'),
|
| 29 |
+
queryInput: $('queryInput'),
|
| 30 |
+
sendBtn: $('sendBtn'),
|
| 31 |
+
functionInput: $('functionInput'),
|
| 32 |
+
classInput: $('classInput'),
|
| 33 |
+
moduleInput: $('moduleInput'),
|
| 34 |
+
macroSubtypes: $('macroSubtypes'),
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
// ββ Marked config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
marked.setOptions({
|
| 39 |
+
breaks: true,
|
| 40 |
+
gfm: true,
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
marked.use({
|
| 44 |
+
renderer: (() => {
|
| 45 |
+
const r = new marked.Renderer();
|
| 46 |
+
r.code = (code, lang) => {
|
| 47 |
+
const highlighted = (lang && hljs.getLanguage(lang))
|
| 48 |
+
? hljs.highlight(code, { language: lang }).value
|
| 49 |
+
: hljs.highlightAuto(code).value;
|
| 50 |
+
return `<pre><code class="hljs language-${lang || ''}">${highlighted}</code></pre>`;
|
| 51 |
+
};
|
| 52 |
+
return r;
|
| 53 |
+
})(),
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
// ββ Status ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
+
function setStatus(type, text) {
|
| 58 |
+
dom.statusBadge.className = `status-badge status-badge--${type}`;
|
| 59 |
+
dom.statusText.textContent = text;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// ββ Index βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 63 |
+
dom.zipInput.addEventListener('change', () => {
|
| 64 |
+
if (dom.zipInput.files.length > 0) {
|
| 65 |
+
dom.zipLabel.textContent = dom.zipInput.files[0].name;
|
| 66 |
+
dom.pathInput.value = '';
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
dom.indexBtn.addEventListener('click', async () => {
|
| 71 |
+
const path = dom.pathInput.value.trim();
|
| 72 |
+
const zipFile = dom.zipInput.files[0];
|
| 73 |
+
|
| 74 |
+
if (!path && !zipFile) {
|
| 75 |
+
appendMessage('error', null, 'β Provide an absolute path or upload a ZIP file.');
|
| 76 |
+
return;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
dom.indexBtn.disabled = true;
|
| 80 |
+
dom.indexBtnText.classList.add('hidden');
|
| 81 |
+
dom.indexSpinner.classList.remove('hidden');
|
| 82 |
+
setStatus('indexing', 'Indexingβ¦');
|
| 83 |
+
|
| 84 |
+
try {
|
| 85 |
+
let res, data;
|
| 86 |
+
|
| 87 |
+
if (zipFile) {
|
| 88 |
+
const form = new FormData();
|
| 89 |
+
form.append('file', zipFile);
|
| 90 |
+
res = await fetch(`${API}/upload-index`, { method: 'POST', body: form });
|
| 91 |
+
data = await res.json();
|
| 92 |
+
} else {
|
| 93 |
+
res = await fetch(`${API}/index`, {
|
| 94 |
+
method: 'POST',
|
| 95 |
+
headers: { 'Content-Type': 'application/json' },
|
| 96 |
+
body: JSON.stringify({ root_path: path }),
|
| 97 |
+
});
|
| 98 |
+
data = await res.json();
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
if (!res.ok) throw new Error(data.detail || 'Indexing failed');
|
| 102 |
+
|
| 103 |
+
const label = zipFile ? zipFile.name : path;
|
| 104 |
+
state.indexed = true;
|
| 105 |
+
setStatus('ready', `Ready Β· ${data.total_chunks} chunks`);
|
| 106 |
+
dom.chunkCount.textContent = `${data.total_chunks} chunks`;
|
| 107 |
+
dom.sendBtn.disabled = false;
|
| 108 |
+
|
| 109 |
+
appendMessage('system', null,
|
| 110 |
+
`β Indexed **${label}**\n\n` +
|
| 111 |
+
`| Metric | Value |\n|--------|-------|\n` +
|
| 112 |
+
`| Class chunks | ${data.class_chunks} |\n` +
|
| 113 |
+
`| Function chunks | ${data.function_chunks} |\n` +
|
| 114 |
+
`| Total chunks | ${data.total_chunks} |\n` +
|
| 115 |
+
`| Graph nodes | ${data.graph_nodes} |\n` +
|
| 116 |
+
`| Graph edges | ${data.graph_edges} |`
|
| 117 |
+
);
|
| 118 |
+
|
| 119 |
+
loadTree();
|
| 120 |
+
|
| 121 |
+
} catch (err) {
|
| 122 |
+
setStatus('error', 'Error');
|
| 123 |
+
appendMessage('error', null, `β ${err.message}`);
|
| 124 |
+
} finally {
|
| 125 |
+
dom.indexBtn.disabled = false;
|
| 126 |
+
dom.indexBtnText.classList.remove('hidden');
|
| 127 |
+
dom.indexSpinner.classList.add('hidden');
|
| 128 |
+
dom.zipLabel.textContent = 'Upload ZIP';
|
| 129 |
+
dom.zipInput.value = '';
|
| 130 |
+
}
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
// ββ Tree ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
async function loadTree() {
|
| 135 |
+
try {
|
| 136 |
+
const res = await fetch(`${API}/tree`);
|
| 137 |
+
const data = await res.json();
|
| 138 |
+
|
| 139 |
+
if (!data.success || !data.tree.length) {
|
| 140 |
+
dom.treeContainer.innerHTML =
|
| 141 |
+
'<div class="tree-placeholder"><p>No structure found.</p></div>';
|
| 142 |
+
return;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
dom.treeContainer.innerHTML = '';
|
| 146 |
+
data.tree.forEach(node => {
|
| 147 |
+
dom.treeContainer.appendChild(buildTreeNode(node, 0));
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
} catch (err) {
|
| 151 |
+
console.error('Tree load error:', err);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
const ICONS = {
|
| 156 |
+
module: { icon: 'π¦', cls: 'icon-module' },
|
| 157 |
+
file: { icon: 'π', cls: 'icon-file' },
|
| 158 |
+
class: { icon: 'π·', cls: 'icon-class' },
|
| 159 |
+
function: { icon: 'Ζ', cls: 'icon-function' },
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
function buildTreeNode(node, depth) {
|
| 163 |
+
const wrap = document.createElement('div');
|
| 164 |
+
const hasKids = node.children && node.children.length > 0;
|
| 165 |
+
const info = ICONS[node.type] || ICONS.file;
|
| 166 |
+
|
| 167 |
+
wrap.className = 'tree-node';
|
| 168 |
+
wrap.dataset.depth = depth;
|
| 169 |
+
|
| 170 |
+
const row = document.createElement('div');
|
| 171 |
+
row.className = 'tree-node__row';
|
| 172 |
+
|
| 173 |
+
const toggle = document.createElement('span');
|
| 174 |
+
toggle.className = 'tree-node__toggle';
|
| 175 |
+
toggle.textContent = hasKids ? 'βΆ' : '';
|
| 176 |
+
|
| 177 |
+
const icon = document.createElement('span');
|
| 178 |
+
icon.className = `tree-node__icon ${info.cls}`;
|
| 179 |
+
icon.textContent = info.icon;
|
| 180 |
+
|
| 181 |
+
const label = document.createElement('span');
|
| 182 |
+
label.className = 'tree-node__name';
|
| 183 |
+
label.textContent = node.name;
|
| 184 |
+
|
| 185 |
+
row.append(toggle, icon, label);
|
| 186 |
+
wrap.appendChild(row);
|
| 187 |
+
|
| 188 |
+
if (hasKids) {
|
| 189 |
+
const children = document.createElement('div');
|
| 190 |
+
children.className = 'tree-node__children';
|
| 191 |
+
node.children.forEach(child => {
|
| 192 |
+
children.appendChild(buildTreeNode(child, depth + 1));
|
| 193 |
+
});
|
| 194 |
+
wrap.appendChild(children);
|
| 195 |
+
|
| 196 |
+
row.addEventListener('click', () => {
|
| 197 |
+
const open = children.classList.toggle('open');
|
| 198 |
+
toggle.classList.toggle('open', open);
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Click leaf β auto-fill inputs
|
| 203 |
+
if (node.type === 'function') {
|
| 204 |
+
row.addEventListener('click', () => {
|
| 205 |
+
dom.functionInput.value = node.name;
|
| 206 |
+
highlightRow(row);
|
| 207 |
+
});
|
| 208 |
+
}
|
| 209 |
+
if (node.type === 'class') {
|
| 210 |
+
row.addEventListener('click', () => {
|
| 211 |
+
dom.classInput.value = node.name;
|
| 212 |
+
highlightRow(row);
|
| 213 |
+
});
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
return wrap;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
function highlightRow(row) {
|
| 220 |
+
document.querySelectorAll('.tree-node__row.selected')
|
| 221 |
+
.forEach(r => r.classList.remove('selected'));
|
| 222 |
+
row.classList.add('selected');
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// ββ Query Type Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 226 |
+
document.querySelectorAll('.query-type-btn').forEach(btn => {
|
| 227 |
+
btn.addEventListener('click', () => {
|
| 228 |
+
document.querySelectorAll('.query-type-btn')
|
| 229 |
+
.forEach(b => b.classList.remove('active'));
|
| 230 |
+
btn.classList.add('active');
|
| 231 |
+
state.queryType = btn.dataset.type;
|
| 232 |
+
updateControls();
|
| 233 |
+
});
|
| 234 |
+
});
|
| 235 |
+
|
| 236 |
+
document.querySelectorAll('.subtype-btn').forEach(btn => {
|
| 237 |
+
btn.addEventListener('click', () => {
|
| 238 |
+
document.querySelectorAll('.subtype-btn')
|
| 239 |
+
.forEach(b => b.classList.remove('active'));
|
| 240 |
+
btn.classList.add('active');
|
| 241 |
+
state.macroSubtype = btn.dataset.subtype;
|
| 242 |
+
updateControls();
|
| 243 |
+
});
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
function updateControls() {
|
| 247 |
+
const isMacro = state.queryType === 'macro';
|
| 248 |
+
const needsModule =
|
| 249 |
+
isMacro && state.macroSubtype === 'module_responsibility';
|
| 250 |
+
|
| 251 |
+
dom.macroSubtypes.classList.toggle('hidden', !isMacro);
|
| 252 |
+
dom.functionInput.classList.toggle('hidden', isMacro);
|
| 253 |
+
dom.classInput.classList.toggle('hidden', isMacro);
|
| 254 |
+
dom.moduleInput.classList.toggle('hidden', !needsModule);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
// ββ Textarea auto-resize ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 258 |
+
dom.queryInput.addEventListener('input', () => {
|
| 259 |
+
dom.queryInput.style.height = 'auto';
|
| 260 |
+
dom.queryInput.style.height =
|
| 261 |
+
Math.min(dom.queryInput.scrollHeight, 160) + 'px';
|
| 262 |
+
});
|
| 263 |
+
|
| 264 |
+
dom.queryInput.addEventListener('keydown', e => {
|
| 265 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 266 |
+
e.preventDefault();
|
| 267 |
+
if (!dom.sendBtn.disabled) sendQuery();
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
|
| 271 |
+
dom.sendBtn.addEventListener('click', sendQuery);
|
| 272 |
+
|
| 273 |
+
// ββ Send Query ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 274 |
+
async function sendQuery() {
|
| 275 |
+
const query = dom.queryInput.value.trim();
|
| 276 |
+
if (!query || state.querying) return;
|
| 277 |
+
|
| 278 |
+
const badge = [
|
| 279 |
+
state.queryType,
|
| 280 |
+
state.queryType === 'macro'
|
| 281 |
+
? state.macroSubtype.replace(/_/g, ' ')
|
| 282 |
+
: dom.functionInput.value.trim(),
|
| 283 |
+
].filter(Boolean).join(' Β· ');
|
| 284 |
+
|
| 285 |
+
appendMessage('user', badge, query);
|
| 286 |
+
dom.queryInput.value = '';
|
| 287 |
+
dom.queryInput.style.height = 'auto';
|
| 288 |
+
|
| 289 |
+
const thinkId = appendThinking();
|
| 290 |
+
state.querying = true;
|
| 291 |
+
dom.sendBtn.disabled = true;
|
| 292 |
+
|
| 293 |
+
try {
|
| 294 |
+
const res = await fetch(`${API}/query`, {
|
| 295 |
+
method: 'POST',
|
| 296 |
+
headers: { 'Content-Type': 'application/json' },
|
| 297 |
+
body: JSON.stringify({
|
| 298 |
+
query_type: state.queryType,
|
| 299 |
+
query,
|
| 300 |
+
subtype: state.queryType === 'macro' ? state.macroSubtype : '',
|
| 301 |
+
function_name: dom.functionInput.value.trim(),
|
| 302 |
+
class_name: dom.classInput.value.trim(),
|
| 303 |
+
module_name: dom.moduleInput.value.trim(),
|
| 304 |
+
followup: !!state.lastResponse,
|
| 305 |
+
previous_response: state.lastResponse,
|
| 306 |
+
}),
|
| 307 |
+
});
|
| 308 |
+
|
| 309 |
+
const data = await res.json();
|
| 310 |
+
removeMessage(thinkId);
|
| 311 |
+
|
| 312 |
+
if (!res.ok) throw new Error(data.detail || 'Query failed');
|
| 313 |
+
if (!data.success) throw new Error(data.error || 'No result');
|
| 314 |
+
|
| 315 |
+
state.lastResponse = data.content;
|
| 316 |
+
appendMessage('assistant', buildMetaBadge(data.metadata), data.content);
|
| 317 |
+
|
| 318 |
+
} catch (err) {
|
| 319 |
+
removeMessage(thinkId);
|
| 320 |
+
appendMessage('error', null, `β ${err.message}`);
|
| 321 |
+
} finally {
|
| 322 |
+
state.querying = false;
|
| 323 |
+
dom.sendBtn.disabled = false;
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
// ββ Messages ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 328 |
+
let msgCounter = 0;
|
| 329 |
+
|
| 330 |
+
function appendMessage(role, badge, content) {
|
| 331 |
+
const id = `msg-${++msgCounter}`;
|
| 332 |
+
const div = document.createElement('div');
|
| 333 |
+
div.id = id;
|
| 334 |
+
div.className = `message message--${role}`;
|
| 335 |
+
|
| 336 |
+
const roleLabel = {
|
| 337 |
+
user: 'You', assistant: 'Oracle',
|
| 338 |
+
system: 'System', error: 'Error', thinking: 'Thinking',
|
| 339 |
+
}[role] || role;
|
| 340 |
+
|
| 341 |
+
let html = `<div class="message__meta">
|
| 342 |
+
<span class="message__role">${roleLabel}</span>
|
| 343 |
+
${badge ? `<span class="message__badge">${badge}</span>` : ''}
|
| 344 |
+
</div>
|
| 345 |
+
<div class="message__content">`;
|
| 346 |
+
|
| 347 |
+
html += role === 'thinking'
|
| 348 |
+
? `<span class="spinner"></span>${content}`
|
| 349 |
+
: marked.parse(content || '');
|
| 350 |
+
|
| 351 |
+
html += `</div>`;
|
| 352 |
+
div.innerHTML = html;
|
| 353 |
+
|
| 354 |
+
dom.messages.appendChild(div);
|
| 355 |
+
dom.messages.scrollTop = dom.messages.scrollHeight;
|
| 356 |
+
return id;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
function appendThinking() {
|
| 360 |
+
return appendMessage('thinking', null, 'Analyzing codebaseβ¦');
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
function removeMessage(id) {
|
| 364 |
+
document.getElementById(id)?.remove();
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
function buildMetaBadge(meta) {
|
| 368 |
+
if (!meta) return null;
|
| 369 |
+
return [
|
| 370 |
+
meta.query_type,
|
| 371 |
+
meta.function_name,
|
| 372 |
+
meta.chunks_used ? `${meta.chunks_used} chunks` : null,
|
| 373 |
+
meta.context_chars ? `${meta.context_chars} chars` : null,
|
| 374 |
+
].filter(Boolean).join(' Β· ');
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 378 |
+
(async () => {
|
| 379 |
+
try {
|
| 380 |
+
const res = await fetch(`${API}/status`);
|
| 381 |
+
const data = await res.json();
|
| 382 |
+
if (data.indexed) {
|
| 383 |
+
state.indexed = true;
|
| 384 |
+
setStatus('ready', `Ready Β· ${data.total_chunks} chunks`);
|
| 385 |
+
dom.chunkCount.textContent = `${data.total_chunks} chunks`;
|
| 386 |
+
dom.sendBtn.disabled = false;
|
| 387 |
+
loadTree();
|
| 388 |
+
}
|
| 389 |
+
} catch { /* server not up yet β stay idle */ }
|
| 390 |
+
|
| 391 |
+
updateControls();
|
| 392 |
+
})();
|
ui/static/style.css
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ββ Reset & Base ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 2 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 3 |
+
|
| 4 |
+
:root {
|
| 5 |
+
--bg-base: #0e0e10;
|
| 6 |
+
--bg-surface: #141416;
|
| 7 |
+
--bg-elevated: #1a1a1e;
|
| 8 |
+
--bg-hover: #202026;
|
| 9 |
+
--bg-active: #25252d;
|
| 10 |
+
--border: #2a2a32;
|
| 11 |
+
--border-focus: #4a90e8;
|
| 12 |
+
--text-primary: #d4d4d8;
|
| 13 |
+
--text-secondary: #71717a;
|
| 14 |
+
--text-muted: #3f3f46;
|
| 15 |
+
--text-bright: #f4f4f5;
|
| 16 |
+
--accent: #4a90e8;
|
| 17 |
+
--accent-dim: #1e3a5f;
|
| 18 |
+
--accent-glow: rgba(74, 144, 232, 0.15);
|
| 19 |
+
--green: #4ade80;
|
| 20 |
+
--yellow: #facc15;
|
| 21 |
+
--red: #f87171;
|
| 22 |
+
--purple: #c084fc;
|
| 23 |
+
--topbar-h: 60px;
|
| 24 |
+
--sidebar-w: 320px;
|
| 25 |
+
--radius: 6px;
|
| 26 |
+
--radius-lg: 10px;
|
| 27 |
+
--font-ui: 'Syne', sans-serif;
|
| 28 |
+
--font-mono: 'JetBrains Mono', monospace;
|
| 29 |
+
--transition: 150ms ease;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
html, body {
|
| 33 |
+
height: 100%;
|
| 34 |
+
overflow: hidden;
|
| 35 |
+
background: var(--bg-base);
|
| 36 |
+
color: var(--text-primary);
|
| 37 |
+
font-family: var(--font-ui);
|
| 38 |
+
font-size: 16px;
|
| 39 |
+
line-height: 1.6;
|
| 40 |
+
-webkit-font-smoothing: antialiased;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
::-webkit-scrollbar { width: 5px; }
|
| 44 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 45 |
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
| 46 |
+
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
| 47 |
+
|
| 48 |
+
/* ββ Topbar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 49 |
+
.topbar {
|
| 50 |
+
position: fixed;
|
| 51 |
+
top: 0; left: 0; right: 0;
|
| 52 |
+
height: var(--topbar-h);
|
| 53 |
+
background: var(--bg-surface);
|
| 54 |
+
border-bottom: 1px solid var(--border);
|
| 55 |
+
display: flex;
|
| 56 |
+
align-items: center;
|
| 57 |
+
gap: 16px;
|
| 58 |
+
padding: 0 20px;
|
| 59 |
+
z-index: 100;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.topbar__brand {
|
| 63 |
+
display: flex;
|
| 64 |
+
align-items: center;
|
| 65 |
+
gap: 8px;
|
| 66 |
+
flex-shrink: 0;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.topbar__icon {
|
| 70 |
+
font-size: 20px;
|
| 71 |
+
color: var(--accent);
|
| 72 |
+
animation: pulse 3s ease-in-out infinite;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
| 76 |
+
|
| 77 |
+
.topbar__title {
|
| 78 |
+
font-weight: 800;
|
| 79 |
+
font-size: 15px;
|
| 80 |
+
letter-spacing: 0.04em;
|
| 81 |
+
color: var(--text-bright);
|
| 82 |
+
white-space: nowrap;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.topbar__index {
|
| 86 |
+
flex: 1;
|
| 87 |
+
display: flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
gap: 8px;
|
| 90 |
+
max-width: 680px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.path-input {
|
| 94 |
+
flex: 1;
|
| 95 |
+
height: 34px;
|
| 96 |
+
background: var(--bg-base);
|
| 97 |
+
border: 1px solid var(--border);
|
| 98 |
+
border-radius: var(--radius);
|
| 99 |
+
color: var(--text-primary);
|
| 100 |
+
font-family: var(--font-mono);
|
| 101 |
+
font-size: 12px;
|
| 102 |
+
padding: 0 12px;
|
| 103 |
+
outline: none;
|
| 104 |
+
transition: border-color var(--transition), box-shadow var(--transition);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.path-input:focus {
|
| 108 |
+
border-color: var(--border-focus);
|
| 109 |
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.path-input::placeholder { color: var(--text-muted); }
|
| 113 |
+
|
| 114 |
+
/* ββ Buttons βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 115 |
+
.btn {
|
| 116 |
+
display: inline-flex;
|
| 117 |
+
align-items: center;
|
| 118 |
+
justify-content: center;
|
| 119 |
+
gap: 6px;
|
| 120 |
+
border: none;
|
| 121 |
+
border-radius: var(--radius);
|
| 122 |
+
cursor: pointer;
|
| 123 |
+
font-family: var(--font-ui);
|
| 124 |
+
font-weight: 600;
|
| 125 |
+
font-size: 13px;
|
| 126 |
+
transition: background var(--transition), transform var(--transition);
|
| 127 |
+
white-space: nowrap;
|
| 128 |
+
outline: none;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.btn:active:not(:disabled) { transform: scale(0.97); }
|
| 132 |
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 133 |
+
|
| 134 |
+
.btn--primary {
|
| 135 |
+
height: 34px;
|
| 136 |
+
padding: 0 18px;
|
| 137 |
+
background: var(--accent);
|
| 138 |
+
color: #fff;
|
| 139 |
+
}
|
| 140 |
+
.btn--primary:hover:not(:disabled) { background: #3b7dd8; }
|
| 141 |
+
|
| 142 |
+
.btn--send {
|
| 143 |
+
width: 40px; height: 40px;
|
| 144 |
+
background: var(--accent);
|
| 145 |
+
color: #fff;
|
| 146 |
+
border-radius: var(--radius);
|
| 147 |
+
flex-shrink: 0;
|
| 148 |
+
}
|
| 149 |
+
.btn--send:hover:not(:disabled) { background: #3b7dd8; }
|
| 150 |
+
|
| 151 |
+
/* ββ Status Badge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 152 |
+
.status-badge {
|
| 153 |
+
display: flex;
|
| 154 |
+
align-items: center;
|
| 155 |
+
gap: 6px;
|
| 156 |
+
padding: 4px 12px;
|
| 157 |
+
border-radius: 999px;
|
| 158 |
+
font-size: 11px;
|
| 159 |
+
font-weight: 600;
|
| 160 |
+
letter-spacing: 0.05em;
|
| 161 |
+
white-space: nowrap;
|
| 162 |
+
flex-shrink: 0;
|
| 163 |
+
border: 1px solid transparent;
|
| 164 |
+
font-family: var(--font-mono);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.status-badge__dot {
|
| 168 |
+
width: 6px; height: 6px;
|
| 169 |
+
border-radius: 50%;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.status-badge--idle { background: var(--bg-elevated); border-color: var(--border); color: var(--text-secondary); }
|
| 173 |
+
.status-badge--idle .status-badge__dot { background: var(--text-muted); }
|
| 174 |
+
|
| 175 |
+
.status-badge--indexing { background: rgba(250,204,21,.08); border-color: rgba(250,204,21,.3); color: var(--yellow); }
|
| 176 |
+
.status-badge--indexing .status-badge__dot { background: var(--yellow); animation: blink .8s ease-in-out infinite; }
|
| 177 |
+
|
| 178 |
+
.status-badge--ready { background: rgba(74,222,128,.08); border-color: rgba(74,222,128,.3); color: var(--green); }
|
| 179 |
+
.status-badge--ready .status-badge__dot { background: var(--green); }
|
| 180 |
+
|
| 181 |
+
.status-badge--error { background: rgba(248,113,113,.08); border-color: rgba(248,113,113,.3); color: var(--red); }
|
| 182 |
+
.status-badge--error .status-badge__dot { background: var(--red); }
|
| 183 |
+
|
| 184 |
+
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:.2; } }
|
| 185 |
+
|
| 186 |
+
/* ββ Workspace βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 187 |
+
.workspace {
|
| 188 |
+
display: flex;
|
| 189 |
+
position: fixed;
|
| 190 |
+
top: var(--topbar-h); left: 0; right: 0; bottom: 0;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/* ββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 194 |
+
.sidebar {
|
| 195 |
+
width: var(--sidebar-w);
|
| 196 |
+
flex-shrink: 0;
|
| 197 |
+
background: var(--bg-surface);
|
| 198 |
+
border-right: 1px solid var(--border);
|
| 199 |
+
display: flex;
|
| 200 |
+
flex-direction: column;
|
| 201 |
+
overflow: hidden;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.sidebar__header {
|
| 205 |
+
display: flex;
|
| 206 |
+
align-items: center;
|
| 207 |
+
justify-content: space-between;
|
| 208 |
+
padding: 12px 16px;
|
| 209 |
+
border-bottom: 1px solid var(--border);
|
| 210 |
+
flex-shrink: 0;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.sidebar__label {
|
| 214 |
+
font-size: 10px;
|
| 215 |
+
font-weight: 700;
|
| 216 |
+
letter-spacing: 0.12em;
|
| 217 |
+
text-transform: uppercase;
|
| 218 |
+
color: var(--text-secondary);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.sidebar__count {
|
| 222 |
+
font-family: var(--font-mono);
|
| 223 |
+
font-size: 10px;
|
| 224 |
+
color: var(--text-muted);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.tree-container {
|
| 228 |
+
flex: 1;
|
| 229 |
+
overflow-y: auto;
|
| 230 |
+
padding: 8px 0;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.tree-placeholder {
|
| 234 |
+
display: flex;
|
| 235 |
+
flex-direction: column;
|
| 236 |
+
align-items: center;
|
| 237 |
+
justify-content: center;
|
| 238 |
+
height: 100%;
|
| 239 |
+
gap: 12px;
|
| 240 |
+
padding: 40px 20px;
|
| 241 |
+
text-align: center;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.tree-placeholder__icon { font-size: 32px; color: var(--text-muted); opacity: .5; }
|
| 245 |
+
.tree-placeholder p { font-size: 12px; color: var(--text-muted); line-height: 1.6; }
|
| 246 |
+
|
| 247 |
+
/* ββ Tree Nodes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 248 |
+
.tree-node { user-select: none; }
|
| 249 |
+
|
| 250 |
+
.tree-node__row {
|
| 251 |
+
display: flex;
|
| 252 |
+
align-items: center;
|
| 253 |
+
gap: 5px;
|
| 254 |
+
padding: 3px 8px;
|
| 255 |
+
cursor: pointer;
|
| 256 |
+
border-radius: 4px;
|
| 257 |
+
margin: 0 6px;
|
| 258 |
+
transition: background var(--transition);
|
| 259 |
+
min-height: 24px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.tree-node__row:hover { background: var(--bg-hover); }
|
| 263 |
+
.tree-node__row.selected { background: var(--accent-dim); }
|
| 264 |
+
|
| 265 |
+
.tree-node__toggle {
|
| 266 |
+
width: 14px; text-align: center;
|
| 267 |
+
font-size: 9px;
|
| 268 |
+
color: var(--text-muted);
|
| 269 |
+
flex-shrink: 0;
|
| 270 |
+
transition: transform var(--transition);
|
| 271 |
+
}
|
| 272 |
+
.tree-node__toggle.open { transform: rotate(90deg); }
|
| 273 |
+
|
| 274 |
+
.tree-node__icon { font-size: 12px; flex-shrink: 0; }
|
| 275 |
+
.tree-node__name {
|
| 276 |
+
font-family: var(--font-mono);
|
| 277 |
+
font-size: 13px;
|
| 278 |
+
color: var(--text-primary);
|
| 279 |
+
overflow: hidden;
|
| 280 |
+
text-overflow: ellipsis;
|
| 281 |
+
white-space: nowrap;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.tree-node__children { display: none; }
|
| 285 |
+
.tree-node__children.open { display: block; }
|
| 286 |
+
|
| 287 |
+
.tree-node[data-depth="1"] .tree-node__row { padding-left: 20px; }
|
| 288 |
+
.tree-node[data-depth="2"] .tree-node__row { padding-left: 34px; }
|
| 289 |
+
.tree-node[data-depth="3"] .tree-node__row { padding-left: 48px; }
|
| 290 |
+
|
| 291 |
+
.icon-module { color: var(--yellow); }
|
| 292 |
+
.icon-file { color: #60a5fa; }
|
| 293 |
+
.icon-class { color: var(--purple); }
|
| 294 |
+
.icon-function { color: var(--green); }
|
| 295 |
+
|
| 296 |
+
/* ββ Chat ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 297 |
+
.chat {
|
| 298 |
+
flex: 1;
|
| 299 |
+
display: flex;
|
| 300 |
+
flex-direction: column;
|
| 301 |
+
overflow: hidden;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
/* ββ Query Controls ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 305 |
+
.query-controls {
|
| 306 |
+
display: flex;
|
| 307 |
+
flex-direction: column;
|
| 308 |
+
gap: 10px;
|
| 309 |
+
padding: 14px 20px;
|
| 310 |
+
border-bottom: 1px solid var(--border);
|
| 311 |
+
background: var(--bg-surface);
|
| 312 |
+
flex-shrink: 0;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.query-type-group, .macro-subtypes {
|
| 316 |
+
display: flex;
|
| 317 |
+
gap: 4px;
|
| 318 |
+
flex-wrap: wrap;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.query-type-btn, .subtype-btn {
|
| 322 |
+
padding: 7px 18px;
|
| 323 |
+
border-radius: 999px;
|
| 324 |
+
border: 1px solid var(--border);
|
| 325 |
+
background: transparent;
|
| 326 |
+
color: var(--text-secondary);
|
| 327 |
+
font-family: var(--font-ui);
|
| 328 |
+
font-size: 13px;
|
| 329 |
+
font-weight: 600;
|
| 330 |
+
cursor: pointer;
|
| 331 |
+
transition: all var(--transition);
|
| 332 |
+
letter-spacing: 0.02em;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.query-type-btn:hover, .subtype-btn:hover {
|
| 336 |
+
border-color: var(--accent);
|
| 337 |
+
color: var(--text-primary);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.query-type-btn.active, .subtype-btn.active {
|
| 341 |
+
background: var(--accent);
|
| 342 |
+
border-color: var(--accent);
|
| 343 |
+
color: #fff;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.context-inputs {
|
| 347 |
+
display: flex;
|
| 348 |
+
gap: 8px;
|
| 349 |
+
flex-wrap: wrap;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.context-field {
|
| 353 |
+
height: 30px;
|
| 354 |
+
background: var(--bg-base);
|
| 355 |
+
border: 1px solid var(--border);
|
| 356 |
+
border-radius: var(--radius);
|
| 357 |
+
color: var(--text-primary);
|
| 358 |
+
font-family: var(--font-mono);
|
| 359 |
+
font-size: 12px;
|
| 360 |
+
padding: 0 10px;
|
| 361 |
+
outline: none;
|
| 362 |
+
min-width: 160px;
|
| 363 |
+
transition: border-color var(--transition), box-shadow var(--transition);
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.context-field:focus {
|
| 367 |
+
border-color: var(--border-focus);
|
| 368 |
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.context-field::placeholder { color: var(--text-muted); }
|
| 372 |
+
|
| 373 |
+
/* ββ Messages ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 374 |
+
.messages {
|
| 375 |
+
flex: 1;
|
| 376 |
+
overflow-y: auto;
|
| 377 |
+
padding: 24px 20px;
|
| 378 |
+
display: flex;
|
| 379 |
+
flex-direction: column;
|
| 380 |
+
gap: 20px;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.message {
|
| 384 |
+
display: flex;
|
| 385 |
+
flex-direction: column;
|
| 386 |
+
gap: 6px;
|
| 387 |
+
animation: fadeSlideIn 200ms ease both;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
@keyframes fadeSlideIn {
|
| 391 |
+
from { opacity: 0; transform: translateY(8px); }
|
| 392 |
+
to { opacity: 1; transform: translateY(0); }
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.message__meta {
|
| 396 |
+
display: flex;
|
| 397 |
+
align-items: center;
|
| 398 |
+
gap: 8px;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.message__role {
|
| 402 |
+
font-size: 10px;
|
| 403 |
+
font-weight: 700;
|
| 404 |
+
letter-spacing: 0.1em;
|
| 405 |
+
text-transform: uppercase;
|
| 406 |
+
font-family: var(--font-mono);
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.message--user .message__role { color: var(--accent); }
|
| 410 |
+
.message--system .message__role { color: var(--text-muted); }
|
| 411 |
+
.message--assistant .message__role { color: var(--green); }
|
| 412 |
+
.message--error .message__role { color: var(--red); }
|
| 413 |
+
.message--thinking .message__role { color: var(--yellow); }
|
| 414 |
+
|
| 415 |
+
.message__badge {
|
| 416 |
+
font-size: 10px;
|
| 417 |
+
font-family: var(--font-mono);
|
| 418 |
+
padding: 2px 8px;
|
| 419 |
+
border-radius: 999px;
|
| 420 |
+
background: var(--bg-elevated);
|
| 421 |
+
color: var(--text-muted);
|
| 422 |
+
border: 1px solid var(--border);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.message__content {
|
| 426 |
+
background: var(--bg-elevated);
|
| 427 |
+
border: 1px solid var(--border);
|
| 428 |
+
border-radius: var(--radius-lg);
|
| 429 |
+
padding: 14px 18px;
|
| 430 |
+
line-height: 1.7;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.message--user .message__content {
|
| 434 |
+
background: var(--accent-dim);
|
| 435 |
+
border-color: rgba(74,144,232,.3);
|
| 436 |
+
align-self: flex-end;
|
| 437 |
+
max-width: 80%;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.message--error .message__content {
|
| 441 |
+
background: rgba(248,113,113,.06);
|
| 442 |
+
border-color: rgba(248,113,113,.25);
|
| 443 |
+
color: var(--red);
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.message--thinking .message__content {
|
| 447 |
+
color: var(--text-muted);
|
| 448 |
+
font-family: var(--font-mono);
|
| 449 |
+
font-size: 12px;
|
| 450 |
+
display: flex;
|
| 451 |
+
align-items: center;
|
| 452 |
+
gap: 10px;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
/* ββ Markdown ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 456 |
+
.message__content h1,
|
| 457 |
+
.message__content h2,
|
| 458 |
+
.message__content h3 {
|
| 459 |
+
font-family: var(--font-ui);
|
| 460 |
+
font-weight: 700;
|
| 461 |
+
color: var(--text-bright);
|
| 462 |
+
margin-top: 20px;
|
| 463 |
+
margin-bottom: 8px;
|
| 464 |
+
line-height: 1.3;
|
| 465 |
+
}
|
| 466 |
+
.message__content h1 { font-size: 18px; }
|
| 467 |
+
.message__content h2 { font-size: 15px; color: var(--accent); }
|
| 468 |
+
.message__content h3 { font-size: 13px; }
|
| 469 |
+
.message__content h1:first-child,
|
| 470 |
+
.message__content h2:first-child { margin-top: 0; }
|
| 471 |
+
|
| 472 |
+
.message__content p { margin-bottom: 10px; font-size: 15px; }
|
| 473 |
+
.message__content p:last-child { margin-bottom: 0; }
|
| 474 |
+
|
| 475 |
+
.message__content ul,
|
| 476 |
+
.message__content ol { padding-left: 20px; margin-bottom: 10px; }
|
| 477 |
+
.message__content li { margin-bottom: 4px; font-size: 15px; }
|
| 478 |
+
|
| 479 |
+
.message__content strong { color: var(--text-bright); font-weight: 700; }
|
| 480 |
+
.message__content em { color: var(--text-secondary); }
|
| 481 |
+
|
| 482 |
+
.message__content code {
|
| 483 |
+
font-family: var(--font-mono);
|
| 484 |
+
font-size: 12px;
|
| 485 |
+
background: var(--bg-active);
|
| 486 |
+
border: 1px solid var(--border);
|
| 487 |
+
border-radius: 4px;
|
| 488 |
+
padding: 1px 6px;
|
| 489 |
+
color: var(--purple);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.message__content pre {
|
| 493 |
+
background: #0d0d10 !important;
|
| 494 |
+
border: 1px solid var(--border);
|
| 495 |
+
border-radius: var(--radius);
|
| 496 |
+
padding: 14px 16px;
|
| 497 |
+
overflow-x: auto;
|
| 498 |
+
margin: 12px 0;
|
| 499 |
+
}
|
| 500 |
+
.message__content pre code {
|
| 501 |
+
background: none; border: none; padding: 0;
|
| 502 |
+
color: var(--text-primary); font-size: 12px;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
.message__content table {
|
| 506 |
+
width: 100%;
|
| 507 |
+
border-collapse: collapse;
|
| 508 |
+
margin: 12px 0;
|
| 509 |
+
font-size: 12.5px;
|
| 510 |
+
font-family: var(--font-mono);
|
| 511 |
+
}
|
| 512 |
+
.message__content th {
|
| 513 |
+
background: var(--bg-active);
|
| 514 |
+
color: var(--text-bright);
|
| 515 |
+
font-weight: 700;
|
| 516 |
+
padding: 8px 12px;
|
| 517 |
+
text-align: left;
|
| 518 |
+
border: 1px solid var(--border);
|
| 519 |
+
}
|
| 520 |
+
.message__content td {
|
| 521 |
+
padding: 7px 12px;
|
| 522 |
+
border: 1px solid var(--border);
|
| 523 |
+
}
|
| 524 |
+
.message__content tr:hover td { background: var(--bg-hover); }
|
| 525 |
+
|
| 526 |
+
.message__content blockquote {
|
| 527 |
+
border-left: 3px solid var(--accent);
|
| 528 |
+
padding-left: 14px;
|
| 529 |
+
margin: 10px 0;
|
| 530 |
+
color: var(--text-secondary);
|
| 531 |
+
font-style: italic;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.message__content hr {
|
| 535 |
+
border: none;
|
| 536 |
+
border-top: 1px solid var(--border);
|
| 537 |
+
margin: 16px 0;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
/* ββ Input Bar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 541 |
+
.input-bar {
|
| 542 |
+
display: flex;
|
| 543 |
+
align-items: flex-end;
|
| 544 |
+
gap: 10px;
|
| 545 |
+
padding: 14px 20px;
|
| 546 |
+
border-top: 1px solid var(--border);
|
| 547 |
+
background: var(--bg-surface);
|
| 548 |
+
flex-shrink: 0;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.query-textarea {
|
| 552 |
+
flex: 1;
|
| 553 |
+
background: var(--bg-base);
|
| 554 |
+
border: 1px solid var(--border);
|
| 555 |
+
border-radius: var(--radius);
|
| 556 |
+
color: var(--text-primary);
|
| 557 |
+
font-family: var(--font-mono);
|
| 558 |
+
font-size: 15px;
|
| 559 |
+
padding: 10px 14px;
|
| 560 |
+
resize: none;
|
| 561 |
+
outline: none;
|
| 562 |
+
min-height: 40px;
|
| 563 |
+
max-height: 160px;
|
| 564 |
+
overflow-y: auto;
|
| 565 |
+
line-height: 1.5;
|
| 566 |
+
transition: border-color var(--transition), box-shadow var(--transition);
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.query-textarea:focus {
|
| 570 |
+
border-color: var(--border-focus);
|
| 571 |
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.query-textarea::placeholder { color: var(--text-muted); }
|
| 575 |
+
|
| 576 |
+
/* ββ Spinner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 577 |
+
.spinner {
|
| 578 |
+
width: 14px; height: 14px;
|
| 579 |
+
border: 2px solid rgba(255,255,255,0.3);
|
| 580 |
+
border-top-color: #fff;
|
| 581 |
+
border-radius: 50%;
|
| 582 |
+
animation: spin .6s linear infinite;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 586 |
+
|
| 587 |
+
.hidden { display: none !important; }
|