codebase-brain 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brain_cli.py +331 -0
- brain_parser/__init__.py +0 -0
- brain_parser/ast_parser.py +62 -0
- brain_parser/bug_detector.py +212 -0
- brain_parser/codebase_walker.py +101 -0
- brain_parser/file_watcher.py +81 -0
- brain_parser/graph_builder.py +190 -0
- brain_parser/llm_router.py +75 -0
- brain_parser/query_engine.py +127 -0
- brain_parser/root_cause.py +162 -0
- brain_parser/universal_parser.py +215 -0
- codebase_brain-0.1.0.dist-info/METADATA +13 -0
- codebase_brain-0.1.0.dist-info/RECORD +16 -0
- codebase_brain-0.1.0.dist-info/WHEEL +5 -0
- codebase_brain-0.1.0.dist-info/entry_points.txt +2 -0
- codebase_brain-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from brain_parser.universal_parser import parse_file
|
|
4
|
+
|
|
5
|
+
SKIP_FOLDERS = {
|
|
6
|
+
'lib', '.idea', '__pycache__',
|
|
7
|
+
'codebase_brain.egg-info', 'node_modules',
|
|
8
|
+
'.git', 'venv', '.env', 'tests', 'temp'
|
|
9
|
+
}
|
|
10
|
+
SKIP_FILES = {
|
|
11
|
+
'brain.json', 'brain_map.html'
|
|
12
|
+
}
|
|
13
|
+
def should_skip(path):
|
|
14
|
+
parts = path.replace('\\', '/').split('/')
|
|
15
|
+
for part in parts:
|
|
16
|
+
if part in SKIP_FOLDERS:
|
|
17
|
+
return True
|
|
18
|
+
return False
|
|
19
|
+
def walk_codebase(root_path):
|
|
20
|
+
# load existing brain to check hashes
|
|
21
|
+
existing_brain = load_brain() or {}
|
|
22
|
+
brain = {}
|
|
23
|
+
|
|
24
|
+
for folder, subfolders, files in os.walk(root_path):
|
|
25
|
+
subfolders[:] = [s for s in subfolders if s not in SKIP_FOLDERS]
|
|
26
|
+
|
|
27
|
+
for file in files:
|
|
28
|
+
if file in SKIP_FILES:
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
full_path = os.path.join(folder, file)
|
|
32
|
+
|
|
33
|
+
# read content to generate hash
|
|
34
|
+
try:
|
|
35
|
+
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
36
|
+
content = f.read()
|
|
37
|
+
except:
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
from brain_parser.universal_parser import get_file_hash
|
|
41
|
+
current_hash = get_file_hash(content)
|
|
42
|
+
|
|
43
|
+
# if file exists in brain and hash matches → skip re-parsing
|
|
44
|
+
if full_path in existing_brain:
|
|
45
|
+
if existing_brain[full_path].get('hash') == current_hash:
|
|
46
|
+
brain[full_path] = existing_brain[full_path]
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# file is new or changed → parse and summarize
|
|
50
|
+
result = parse_file(full_path)
|
|
51
|
+
if result is not None and result['language'] != 'unknown':
|
|
52
|
+
brain[full_path] = result
|
|
53
|
+
|
|
54
|
+
return brain
|
|
55
|
+
|
|
56
|
+
BRAIN_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'brain.json')
|
|
57
|
+
|
|
58
|
+
def save_brain(brain, output_path=BRAIN_PATH):
|
|
59
|
+
with open(output_path, "w") as f:
|
|
60
|
+
json.dump(brain, f, indent=4)
|
|
61
|
+
print(f"Brain saved to {output_path}")
|
|
62
|
+
|
|
63
|
+
def load_brain(input_path=BRAIN_PATH):
|
|
64
|
+
if not os.path.exists(input_path):
|
|
65
|
+
print("No brain found. Run walker first.")
|
|
66
|
+
return None
|
|
67
|
+
with open(input_path, "r") as f:
|
|
68
|
+
return json.load(f)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def summarize_high_risk_files(brain, risk):
|
|
72
|
+
from brain_parser.universal_parser import summarize_file
|
|
73
|
+
|
|
74
|
+
summarized = 0
|
|
75
|
+
for filepath, data in brain.items():
|
|
76
|
+
file_risk = risk.get(filepath, 'LOW')
|
|
77
|
+
content = data.get('content', '')
|
|
78
|
+
|
|
79
|
+
if file_risk == 'HIGH' and content:
|
|
80
|
+
print(f"Summarizing HIGH risk file: {filepath}")
|
|
81
|
+
data['summary'] = summarize_file(filepath, content, data.get('language', ''))
|
|
82
|
+
data.pop('content', None)
|
|
83
|
+
summarized += 1
|
|
84
|
+
else:
|
|
85
|
+
# low/medium risk - generate simple summary from structure
|
|
86
|
+
functions = [f['name'] for f in data.get('functions', [])]
|
|
87
|
+
classes = [c['name'] for c in data.get('classes', [])]
|
|
88
|
+
imports = [i['name'] for i in data.get('imports', [])]
|
|
89
|
+
data[
|
|
90
|
+
'summary'] = f"File with {len(functions)} functions: {', '.join(functions[:5])}. Classes: {', '.join(classes[:3])}. Imports: {', '.join(imports[:5])}"
|
|
91
|
+
data.pop('content', None)
|
|
92
|
+
|
|
93
|
+
print(f"Summarized {summarized} HIGH risk files with Groq. Rest used structure only.")
|
|
94
|
+
return brain
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
brain = walk_codebase(".")
|
|
98
|
+
save_brain(brain)
|
|
99
|
+
|
|
100
|
+
loaded = load_brain()
|
|
101
|
+
print(f"Brain loaded with {len(loaded)} files")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from watchdog.observers import Observer
|
|
3
|
+
from watchdog.events import FileSystemEventHandler
|
|
4
|
+
from brain_parser.universal_parser import parse_file
|
|
5
|
+
from brain_parser.codebase_walker import walk_codebase, save_brain, load_brain
|
|
6
|
+
|
|
7
|
+
SKIP_WATCH = {'.git', '.idea', '__pycache__', 'node_modules', 'lib'}
|
|
8
|
+
|
|
9
|
+
class BrainEventHandler(FileSystemEventHandler):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.brain = load_brain() or {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def should_skip(self,path):
|
|
15
|
+
# skip git, ide, temp files
|
|
16
|
+
if path.endswith('~'):
|
|
17
|
+
return True
|
|
18
|
+
for skip in SKIP_WATCH:
|
|
19
|
+
if skip in path.replace('\\', '/'):
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def on_modified(self, event):
|
|
24
|
+
if event.is_directory:
|
|
25
|
+
return
|
|
26
|
+
if self.should_skip(event.src_path):
|
|
27
|
+
return
|
|
28
|
+
# skip brain output files
|
|
29
|
+
|
|
30
|
+
if event.src_path.endswith('brain.json') or event.src_path.endswith('brain_map.html'):
|
|
31
|
+
return
|
|
32
|
+
print(f"Modified: {event.src_path}")
|
|
33
|
+
result = parse_file(event.src_path)
|
|
34
|
+
if result and result['language'] != 'unknown':
|
|
35
|
+
self.brain[event.src_path] = result
|
|
36
|
+
save_brain(self.brain)
|
|
37
|
+
|
|
38
|
+
def on_created(self, event):
|
|
39
|
+
if self.should_skip(event.src_path):
|
|
40
|
+
return
|
|
41
|
+
if not event.is_directory:
|
|
42
|
+
print(f"New file: {event.src_path}")
|
|
43
|
+
result = parse_file(event.src_path)
|
|
44
|
+
if result and result['language'] != 'unknown':
|
|
45
|
+
self.brain[event.src_path] = result
|
|
46
|
+
save_brain(self.brain)
|
|
47
|
+
|
|
48
|
+
def on_deleted(self, event):
|
|
49
|
+
if self.should_skip(event.src_path):
|
|
50
|
+
return
|
|
51
|
+
if not event.is_directory:
|
|
52
|
+
print(f"Deleted: {event.src_path}")
|
|
53
|
+
if event.src_path in self.brain:
|
|
54
|
+
del self.brain[event.src_path]
|
|
55
|
+
save_brain(self.brain)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def start_watching(path="."):
|
|
59
|
+
brain = load_brain() or {}
|
|
60
|
+
if not brain:
|
|
61
|
+
print("No brain found. Building first...")
|
|
62
|
+
brain = walk_codebase(path)
|
|
63
|
+
save_brain(brain)
|
|
64
|
+
|
|
65
|
+
event_handler = BrainEventHandler()
|
|
66
|
+
observer = Observer()
|
|
67
|
+
observer.schedule(event_handler, path, recursive=True)
|
|
68
|
+
observer.start()
|
|
69
|
+
print(f"Brain is watching: {path}")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
while True:
|
|
73
|
+
time.sleep(1)
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
observer.stop()
|
|
76
|
+
print("Brain stopped.")
|
|
77
|
+
observer.join()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
start_watching(".")
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from brain_parser.codebase_walker import load_brain
|
|
4
|
+
from pyvis.network import Network
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_graph(brain):
|
|
8
|
+
G = nx.DiGraph()
|
|
9
|
+
|
|
10
|
+
for filepath, data in brain.items():
|
|
11
|
+
G.add_node(filepath)
|
|
12
|
+
|
|
13
|
+
for imp in data.get('imports', []):
|
|
14
|
+
raw = imp.get('name', '')
|
|
15
|
+
|
|
16
|
+
# extract module from "from x import y" or "import x"
|
|
17
|
+
if raw.startswith('from '):
|
|
18
|
+
module = raw.split('from ')[1].split(' import')[0].strip()
|
|
19
|
+
elif raw.startswith('import '):
|
|
20
|
+
module = raw.replace('import ', '').split(' as ')[0].strip()
|
|
21
|
+
else:
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
# convert module path to file path format
|
|
25
|
+
# tests.circular_test.file_b → tests/circular_test/file_b
|
|
26
|
+
module_as_path = module.replace('.', '/').lower()
|
|
27
|
+
|
|
28
|
+
for other_file in brain.keys():
|
|
29
|
+
other_normalized = other_file.replace('\\', '/').lower()
|
|
30
|
+
# match whole filename not partial
|
|
31
|
+
filename = other_normalized.split('/')[-1].replace('.py', '').replace('.js', '').replace('.java',
|
|
32
|
+
'').replace(
|
|
33
|
+
'.ts', '')
|
|
34
|
+
if module_as_path == filename or other_normalized.endswith(module_as_path + '.py'):
|
|
35
|
+
G.add_edge(filepath, other_file)
|
|
36
|
+
|
|
37
|
+
return G
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def visualize_graph(G):
|
|
41
|
+
risk = calculate_risk(G)
|
|
42
|
+
|
|
43
|
+
color_map = {
|
|
44
|
+
"HIGH": "red",
|
|
45
|
+
"MEDIUM": "orange",
|
|
46
|
+
"LOW": "lightgreen"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
node_colors = [color_map[risk[node]] for node in G.nodes()]
|
|
50
|
+
|
|
51
|
+
plt.figure(figsize=(10, 7))
|
|
52
|
+
pos = nx.spring_layout(G)
|
|
53
|
+
|
|
54
|
+
nx.draw(G, pos,
|
|
55
|
+
with_labels=True,
|
|
56
|
+
node_color=node_colors,
|
|
57
|
+
node_size=2000,
|
|
58
|
+
font_size=8,
|
|
59
|
+
arrows=True,
|
|
60
|
+
edge_color='gray',
|
|
61
|
+
font_weight='bold'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
plt.title("Live Codebase Brain - Risk Map")
|
|
65
|
+
plt.savefig("brain_graph.png")
|
|
66
|
+
plt.show()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def calculate_risk(G):
|
|
70
|
+
risk = {}
|
|
71
|
+
|
|
72
|
+
for node in G.nodes():
|
|
73
|
+
in_degree = G.in_degree(node)
|
|
74
|
+
|
|
75
|
+
if in_degree >= 2:
|
|
76
|
+
risk[node] = "HIGH"
|
|
77
|
+
elif in_degree == 1:
|
|
78
|
+
risk[node] = "MEDIUM"
|
|
79
|
+
else:
|
|
80
|
+
risk[node] = "LOW"
|
|
81
|
+
|
|
82
|
+
return risk
|
|
83
|
+
|
|
84
|
+
def visualize_interactive(G):
|
|
85
|
+
risk = calculate_risk(G)\
|
|
86
|
+
|
|
87
|
+
color_map = {
|
|
88
|
+
"HIGH": "#ff4444",
|
|
89
|
+
"MEDIUM": "#ffa500",
|
|
90
|
+
"LOW": "#44cc44"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
net = Network(
|
|
94
|
+
height="750px",
|
|
95
|
+
width="100%",
|
|
96
|
+
bgcolor="#1a1a2e",
|
|
97
|
+
font_color="white",
|
|
98
|
+
directed=True
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
for node in G.nodes():
|
|
102
|
+
risk_level = risk[node]
|
|
103
|
+
color = color_map[risk_level]
|
|
104
|
+
label = node.split("\\")[-1].split("/")[-1]
|
|
105
|
+
|
|
106
|
+
net.add_node(
|
|
107
|
+
node,
|
|
108
|
+
label=label,
|
|
109
|
+
color=color,
|
|
110
|
+
size=20 + (G.in_degree(node) * 3),
|
|
111
|
+
title=f"File: {label}\nRisk: {risk_level}\nConnections: {G.in_degree(node)}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
for edge in G.edges():
|
|
115
|
+
net.add_edge(edge[0], edge[1])
|
|
116
|
+
|
|
117
|
+
net.set_options("""
|
|
118
|
+
{
|
|
119
|
+
"nodes": {
|
|
120
|
+
"borderWidth": 2,
|
|
121
|
+
"shadow": true
|
|
122
|
+
},
|
|
123
|
+
"edges": {
|
|
124
|
+
"smooth": {
|
|
125
|
+
"type": "dynamic"
|
|
126
|
+
},
|
|
127
|
+
"shadow": true
|
|
128
|
+
},
|
|
129
|
+
"interaction": {
|
|
130
|
+
"hover": true,
|
|
131
|
+
"navigationButtons": true,
|
|
132
|
+
"hideEdgesOnDrag": true
|
|
133
|
+
},
|
|
134
|
+
"physics": {
|
|
135
|
+
"stabilization": true,
|
|
136
|
+
"barnesHut": {
|
|
137
|
+
"gravitationalConstant": -8000,
|
|
138
|
+
"springLength": 200,
|
|
139
|
+
"springConstant": 0.02
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
""")
|
|
144
|
+
net.save_graph("brain_map.html")
|
|
145
|
+
print(f"Nodes in graph: {len(G.nodes())}")
|
|
146
|
+
print(f"Edges in graph: {len(G.edges())}")
|
|
147
|
+
import webbrowser
|
|
148
|
+
webbrowser.open("brain_map.html")
|
|
149
|
+
print("Interactive graph saved to brain_map.html")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_impact(G, filepath):
|
|
153
|
+
import networkx as nx
|
|
154
|
+
import os
|
|
155
|
+
|
|
156
|
+
# Normalize incoming path
|
|
157
|
+
target_norm = os.path.abspath(filepath).replace('\\', '/')
|
|
158
|
+
target_rel = filepath.replace('\\', '/').lstrip('./')
|
|
159
|
+
|
|
160
|
+
# Find matching node — handles both absolute and relative keys
|
|
161
|
+
matched = None
|
|
162
|
+
|
|
163
|
+
for node in G.nodes():
|
|
164
|
+
node_norm = os.path.abspath(node).replace('\\', '/')
|
|
165
|
+
if node_norm == target_norm:
|
|
166
|
+
matched = node
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
if not matched:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
direct = list(G.predecessors(matched))
|
|
173
|
+
all_affected = nx.ancestors(G, matched)
|
|
174
|
+
indirect = [f for f in all_affected if f not in direct]
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
'target': matched,
|
|
178
|
+
'direct': direct,
|
|
179
|
+
'indirect': indirect,
|
|
180
|
+
'total_affected': len(direct) + len(indirect)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
brain = load_brain()
|
|
185
|
+
G = build_graph(brain)
|
|
186
|
+
visualize_interactive(G)
|
|
187
|
+
|
|
188
|
+
print("\nRisk Report:")
|
|
189
|
+
for node, level in calculate_risk(G).items():
|
|
190
|
+
print(f" {node.split(chr(92))[-1]}: {level}")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
|
|
4
|
+
load_dotenv()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def call_llm(prompt, max_tokens=500):
|
|
8
|
+
provider = os.getenv("LLM_PROVIDER", "groq").lower()
|
|
9
|
+
api_key = os.getenv("LLM_API_KEY") or os.getenv("GROQ_API_KEY")
|
|
10
|
+
model = os.getenv("LLM_MODEL") or os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
|
|
11
|
+
|
|
12
|
+
if provider == "groq":
|
|
13
|
+
return _call_groq(api_key, model, prompt, max_tokens)
|
|
14
|
+
elif provider == "openai":
|
|
15
|
+
return _call_openai(api_key, model, prompt, max_tokens)
|
|
16
|
+
elif provider == "anthropic":
|
|
17
|
+
return _call_anthropic(api_key, model, prompt, max_tokens)
|
|
18
|
+
elif provider == "google":
|
|
19
|
+
return _call_google(api_key, model, prompt, max_tokens)
|
|
20
|
+
elif provider == "ollama":
|
|
21
|
+
return _call_ollama(model, prompt, max_tokens)
|
|
22
|
+
else:
|
|
23
|
+
return _call_groq(api_key, model, prompt, max_tokens)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _call_groq(api_key, model, prompt, max_tokens):
|
|
27
|
+
from groq import Groq
|
|
28
|
+
client = Groq(api_key=api_key)
|
|
29
|
+
response = client.chat.completions.create(
|
|
30
|
+
model=model,
|
|
31
|
+
messages=[{"role": "user", "content": prompt}],
|
|
32
|
+
max_tokens=max_tokens,
|
|
33
|
+
temperature=0
|
|
34
|
+
)
|
|
35
|
+
return response.choices[0].message.content
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _call_openai(api_key, model, prompt, max_tokens):
|
|
39
|
+
from openai import OpenAI
|
|
40
|
+
client = OpenAI(api_key=api_key)
|
|
41
|
+
response = client.chat.completions.create(
|
|
42
|
+
model=model,
|
|
43
|
+
messages=[{"role": "user", "content": prompt}],
|
|
44
|
+
max_tokens=max_tokens,
|
|
45
|
+
temperature=0
|
|
46
|
+
)
|
|
47
|
+
return response.choices[0].message.content
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _call_anthropic(api_key, model, prompt, max_tokens):
|
|
51
|
+
import anthropic
|
|
52
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
53
|
+
response = client.messages.create(
|
|
54
|
+
model=model,
|
|
55
|
+
max_tokens=max_tokens,
|
|
56
|
+
messages=[{"role": "user", "content": prompt}]
|
|
57
|
+
)
|
|
58
|
+
return response.content[0].text
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _call_google(api_key, model, prompt, max_tokens):
|
|
62
|
+
import google.generativeai as genai
|
|
63
|
+
genai.configure(api_key=api_key)
|
|
64
|
+
gemini = genai.GenerativeModel(model)
|
|
65
|
+
response = gemini.generate_content(prompt)
|
|
66
|
+
return response.text
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _call_ollama(model, prompt, max_tokens):
|
|
70
|
+
import requests
|
|
71
|
+
response = requests.post(
|
|
72
|
+
"http://localhost:11434/api/generate",
|
|
73
|
+
json={"model": model, "prompt": prompt, "stream": False}
|
|
74
|
+
)
|
|
75
|
+
return response.json().get("response", "")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from brain_parser.codebase_walker import load_brain
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_client():
|
|
8
|
+
from brain_parser.llm_router import call_llm
|
|
9
|
+
# returns a wrapper so existing code doesn't break
|
|
10
|
+
load_dotenv()
|
|
11
|
+
model = os.getenv("LLM_MODEL") or os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
|
|
12
|
+
return None, model
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def find_relevant_context(brain, question):
|
|
16
|
+
from brain_parser.graph_builder import build_graph, calculate_risk
|
|
17
|
+
|
|
18
|
+
# build graph and get risk scores
|
|
19
|
+
G = build_graph(brain)
|
|
20
|
+
risk = calculate_risk(G)
|
|
21
|
+
|
|
22
|
+
scores = {}
|
|
23
|
+
question_lower = question.lower()
|
|
24
|
+
question_words = set(question_lower.split())
|
|
25
|
+
|
|
26
|
+
for filepath, data in brain.items():
|
|
27
|
+
score = 0
|
|
28
|
+
|
|
29
|
+
filename = filepath.lower()
|
|
30
|
+
for word in question_words:
|
|
31
|
+
if word in filename:
|
|
32
|
+
score += 3
|
|
33
|
+
|
|
34
|
+
for func in data.get('functions', []):
|
|
35
|
+
if func['name'].lower() in question_lower:
|
|
36
|
+
score += 3
|
|
37
|
+
|
|
38
|
+
for cls in data.get('classes', []):
|
|
39
|
+
if cls['name'].lower() in question_lower:
|
|
40
|
+
score += 3
|
|
41
|
+
|
|
42
|
+
for imp in data.get('imports', []):
|
|
43
|
+
name = imp.get('name', '').lower()
|
|
44
|
+
if any(word in name for word in question_words):
|
|
45
|
+
score += 2
|
|
46
|
+
|
|
47
|
+
summary = data.get('summary', '').lower()
|
|
48
|
+
for word in question_words:
|
|
49
|
+
if word in summary:
|
|
50
|
+
score += 1
|
|
51
|
+
|
|
52
|
+
# boost score for HIGH risk files when question is about risk
|
|
53
|
+
if risk.get(filepath) == 'HIGH':
|
|
54
|
+
score += 2
|
|
55
|
+
|
|
56
|
+
scores[filepath] = score
|
|
57
|
+
|
|
58
|
+
sorted_files = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
|
59
|
+
|
|
60
|
+
# build context with risk scores included
|
|
61
|
+
top_5 = {}
|
|
62
|
+
for f, s in sorted_files[:5]:
|
|
63
|
+
if f in brain:
|
|
64
|
+
file_data = dict(brain[f])
|
|
65
|
+
file_data['risk'] = risk.get(f, 'LOW')
|
|
66
|
+
file_data['dependents'] = len(list(G.predecessors(f)))
|
|
67
|
+
file_data.pop('content', None) # strip raw content — too many tokens
|
|
68
|
+
top_5[f] = file_data
|
|
69
|
+
|
|
70
|
+
if not top_5:
|
|
71
|
+
for f, s in sorted_files[:5]:
|
|
72
|
+
if f in brain:
|
|
73
|
+
file_data = dict(brain[f])
|
|
74
|
+
file_data['risk'] = risk.get(f, 'LOW')
|
|
75
|
+
file_data['dependents'] = len(list(G.predecessors(f)))
|
|
76
|
+
file_data.pop('content', None)
|
|
77
|
+
top_5[f] = file_data
|
|
78
|
+
|
|
79
|
+
return top_5
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def ask_brain(question):
|
|
83
|
+
from brain_parser.llm_router import call_llm
|
|
84
|
+
|
|
85
|
+
brain = load_brain()
|
|
86
|
+
if not brain:
|
|
87
|
+
return "Brain is empty. Run walker first."
|
|
88
|
+
|
|
89
|
+
context = find_relevant_context(brain, question)
|
|
90
|
+
if len(context) > 5:
|
|
91
|
+
context = dict(list(context.items())[:5])
|
|
92
|
+
context_str = json.dumps(context, indent=2)
|
|
93
|
+
|
|
94
|
+
prompt = f"""You are an intelligent codebase brain.
|
|
95
|
+
You have deep knowledge of this codebase structure.
|
|
96
|
+
Here is what you know:
|
|
97
|
+
|
|
98
|
+
{context_str}
|
|
99
|
+
|
|
100
|
+
Answer this question precisely and clearly:
|
|
101
|
+
{question}"""
|
|
102
|
+
|
|
103
|
+
return call_llm(prompt, max_tokens=500)
|
|
104
|
+
|
|
105
|
+
def calculate_risk(G):
|
|
106
|
+
risk = {}
|
|
107
|
+
|
|
108
|
+
for node in G.nodes():
|
|
109
|
+
in_degree = G.in_degree(node)
|
|
110
|
+
|
|
111
|
+
if in_degree >= 2:
|
|
112
|
+
risk[node] = "HIGH"
|
|
113
|
+
elif in_degree == 1:
|
|
114
|
+
risk[node] = "MEDIUM"
|
|
115
|
+
else:
|
|
116
|
+
risk[node] = "LOW"
|
|
117
|
+
|
|
118
|
+
return risk
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
while True:
|
|
123
|
+
question = input("\nAsk your brain: ")
|
|
124
|
+
if question == "exit":
|
|
125
|
+
break
|
|
126
|
+
answer = ask_brain(question)
|
|
127
|
+
print(f"\nBrain: {answer}")
|