codebase-brain 0.3.2__py3-none-any.whl → 0.3.4__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.
@@ -103,4 +103,4 @@ if __name__ == "__main__":
103
103
  save_brain(brain)
104
104
 
105
105
  loaded = load_brain()
106
- print(f"Brain loaded with {len(loaded)} files")
106
+ print(f"Brain loaded with {len(loaded)} files")
@@ -8,11 +8,9 @@ def get_config():
8
8
  if os.path.exists(local_env):
9
9
  load_dotenv(local_env, override=True)
10
10
 
11
- # check if local .env actually had Brain's keys
12
11
  has_key = os.getenv("LLM_API_KEY") or os.getenv("GROQ_API_KEY")
13
12
 
14
13
  if not has_key:
15
- # local .env didn't have Brain's config — fall back to global
16
14
  global_config = os.path.expanduser("~/.codebase-brain/config.env")
17
15
  if os.path.exists(global_config):
18
16
  load_dotenv(global_config, override=True)
@@ -20,7 +18,7 @@ def get_config():
20
18
  return {
21
19
  'provider': os.getenv("LLM_PROVIDER", "groq").lower(),
22
20
  'api_key': os.getenv("LLM_API_KEY") or os.getenv("GROQ_API_KEY"),
23
- 'model': os.getenv("LLM_MODEL") or os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
21
+ 'model': os.getenv("LLM_MODEL") or os.getenv("GROQ_MODEL", "openai/gpt-oss-120b")
24
22
  }
25
23
 
26
24
 
@@ -44,53 +42,103 @@ def call_llm(prompt, max_tokens=500):
44
42
  return _call_groq(api_key, model, prompt, max_tokens)
45
43
 
46
44
 
45
+ def _try_models(model, fallback_list, call_fn):
46
+ """Generic fallback runner — tries the configured model first, then known-stable alternatives."""
47
+ models_to_try = [model] + [m for m in fallback_list if m != model]
48
+ seen = set()
49
+ models_to_try = [m for m in models_to_try if not (m in seen or seen.add(m))]
50
+
51
+ last_error = None
52
+ for try_model in models_to_try:
53
+ try:
54
+ return call_fn(try_model)
55
+ except Exception as e:
56
+ last_error = e
57
+ continue
58
+
59
+ return f"Could not get AI response — all models unavailable: {str(last_error)}"
60
+
61
+
47
62
  def _call_groq(api_key, model, prompt, max_tokens):
48
63
  from groq import Groq
49
64
  client = Groq(api_key=api_key)
50
- response = client.chat.completions.create(
51
- model=model,
52
- messages=[{"role": "user", "content": prompt}],
53
- max_tokens=max_tokens,
54
- temperature=0
55
- )
56
- return response.choices[0].message.content
65
+
66
+ fallback_list = ["openai/gpt-oss-120b", "llama-3.1-8b-instant", "qwen3.6-27b"]
67
+
68
+ def call_fn(m):
69
+ response = client.chat.completions.create(
70
+ model=m,
71
+ messages=[{"role": "user", "content": prompt}],
72
+ max_tokens=max_tokens,
73
+ temperature=0
74
+ )
75
+ return response.choices[0].message.content
76
+
77
+ return _try_models(model, fallback_list, call_fn)
57
78
 
58
79
 
59
80
  def _call_openai(api_key, model, prompt, max_tokens):
60
81
  from openai import OpenAI
61
82
  client = OpenAI(api_key=api_key)
62
- response = client.chat.completions.create(
63
- model=model,
64
- messages=[{"role": "user", "content": prompt}],
65
- max_tokens=max_tokens,
66
- temperature=0
67
- )
68
- return response.choices[0].message.content
83
+
84
+ fallback_list = ["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"]
85
+
86
+ def call_fn(m):
87
+ response = client.chat.completions.create(
88
+ model=m,
89
+ messages=[{"role": "user", "content": prompt}],
90
+ max_tokens=max_tokens,
91
+ temperature=0
92
+ )
93
+ return response.choices[0].message.content
94
+
95
+ return _try_models(model, fallback_list, call_fn)
69
96
 
70
97
 
71
98
  def _call_anthropic(api_key, model, prompt, max_tokens):
72
99
  import anthropic
73
100
  client = anthropic.Anthropic(api_key=api_key)
74
- response = client.messages.create(
75
- model=model,
76
- max_tokens=max_tokens,
77
- messages=[{"role": "user", "content": prompt}]
78
- )
79
- return response.content[0].text
101
+
102
+ fallback_list = ["claude-3-5-haiku-20241022", "claude-3-5-sonnet-20241022", "claude-3-haiku-20240307"]
103
+
104
+ def call_fn(m):
105
+ response = client.messages.create(
106
+ model=m,
107
+ max_tokens=max_tokens,
108
+ messages=[{"role": "user", "content": prompt}]
109
+ )
110
+ return response.content[0].text
111
+
112
+ return _try_models(model, fallback_list, call_fn)
80
113
 
81
114
 
82
115
  def _call_google(api_key, model, prompt, max_tokens):
83
116
  import google.generativeai as genai
84
117
  genai.configure(api_key=api_key)
85
- gemini = genai.GenerativeModel(model)
86
- response = gemini.generate_content(prompt)
87
- return response.text
118
+
119
+ fallback_list = ["gemini-1.5-flash", "gemini-1.5-pro", "gemini-2.0-flash"]
120
+
121
+ def call_fn(m):
122
+ gemini = genai.GenerativeModel(m)
123
+ response = gemini.generate_content(prompt)
124
+ return response.text
125
+
126
+ return _try_models(model, fallback_list, call_fn)
88
127
 
89
128
 
90
129
  def _call_ollama(model, prompt, max_tokens):
91
130
  import requests
92
- response = requests.post(
93
- "http://localhost:11434/api/generate",
94
- json={"model": model, "prompt": prompt, "stream": False}
95
- )
96
- return response.json().get("response", "")
131
+
132
+ fallback_list = ["llama3.2", "codellama", "deepseek-coder"]
133
+
134
+ def call_fn(m):
135
+ response = requests.post(
136
+ "http://localhost:11434/api/generate",
137
+ json={"model": m, "prompt": prompt, "stream": False}
138
+ )
139
+ result = response.json().get("response", "")
140
+ if not result:
141
+ raise ValueError(f"Empty response from Ollama model {m}")
142
+ return result
143
+
144
+ return _try_models(model, fallback_list, call_fn)
@@ -37,21 +37,47 @@ def extract_frames(stacktrace):
37
37
 
38
38
 
39
39
  def match_frame_to_graph(frame_file, G):
40
- """Match a short filename from stacktrace to full path in graph."""
41
- frame_norm = frame_file.replace('\\', '/')
42
- candidates = []
40
+ """Match any path format from stacktrace to full path in graph — universal matching."""
41
+
42
+ def normalize(path):
43
+ p = path.replace('\\', '/').lower()
44
+ if len(p) > 1 and p[1] == ':':
45
+ p = p[2:]
46
+ return p.lstrip('/')
47
+
48
+ frame_norm = normalize(frame_file)
49
+ frame_parts = frame_norm.split('/')
43
50
 
51
+ candidates = []
44
52
  for node in G.nodes():
45
- node_norm = node.replace('\\', '/')
46
- # exact suffix match — auth/middleware.js matches ./src/auth/middleware.js
47
- if node_norm.endswith(frame_norm):
53
+ node_norm = normalize(node)
54
+ node_parts = node_norm.split('/')
55
+
56
+ min_len = min(len(frame_parts), len(node_parts))
57
+ if min_len == 0:
58
+ continue
59
+
60
+ if frame_parts[-min_len:] == node_parts[-min_len:]:
61
+ candidates.append(node)
62
+ continue
63
+
64
+ if frame_parts[-1] == node_parts[-1]:
48
65
  candidates.append(node)
49
66
 
50
67
  if not candidates:
51
68
  return None
52
- # prefer shortest path — most specific match
53
- return min(candidates, key=len)
54
69
 
70
+ def match_score(node):
71
+ node_parts = normalize(node).split('/')
72
+ score = 0
73
+ for a, b in zip(reversed(frame_parts), reversed(node_parts)):
74
+ if a == b:
75
+ score += 1
76
+ else:
77
+ break
78
+ return score
79
+
80
+ return max(candidates, key=match_score)
55
81
 
56
82
  def find_root_cause(stacktrace):
57
83
  """Main function — paste stacktrace, get root cause analysis."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-brain
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: An AI brain that lives inside your codebase
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: watchdog
@@ -2,15 +2,15 @@ brain_cli.py,sha256=uYpVxWpM7_fVM-Z4JSgWj0fqU9TGc4Zhc9uu6ritbZA,14576
2
2
  brain_parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  brain_parser/ast_parser.py,sha256=3Vy1XXyInjgA2xn6IBYv4K1juEdtFwAo58upqukCHXI,1799
4
4
  brain_parser/bug_detector.py,sha256=IbEpyXfatL0r1cjaIZ7LXzYPg7z0molyRjg2ULu4828,7653
5
- brain_parser/codebase_walker.py,sha256=rWAHtHWurpZ7HhHAXLSuySMn07TIfg20LrRJ8wyCxKQ,3714
5
+ brain_parser/codebase_walker.py,sha256=VqzHcVerp-CEnSsGvHf_13RjT1YBne9iMeAc8kHVLuY,3716
6
6
  brain_parser/file_watcher.py,sha256=cXsXxrSPvVhSzVmbGsLu5u-jd98VUHvTs8-W0QkhHf4,2582
7
7
  brain_parser/graph_builder.py,sha256=gOXII_SwEs_sJfVFBk81ftedT3fxYN2gz1_aopbpTUU,8741
8
- brain_parser/llm_router.py,sha256=wJP1piBEFqeIMDrHUcGbVpLsJP-aGbn7rKLFYSCcJmc,3224
8
+ brain_parser/llm_router.py,sha256=D0RD71xVNqy9PCPuDUsSCYmlSaWtczDkERUnLslHGQ8,4733
9
9
  brain_parser/query_engine.py,sha256=Lunb_-9z1Lk2VYOE7f_-jF8X2AQtby1tPFXgFUjqnpA,3671
10
- brain_parser/root_cause.py,sha256=jvRB9OzFREU0oguGhfoU8wV2mCJ7A85y2BohybzIQ-0,5326
10
+ brain_parser/root_cause.py,sha256=bgBVjP6rlDlcOUrjM5mUi4Fa-ERQvrhgFRqbMfgWZpw,5974
11
11
  brain_parser/universal_parser.py,sha256=47StXj-GPGJb4g0WzM5VYgM3tIc1tOzwEa8mcL_q9AY,6694
12
- codebase_brain-0.3.2.dist-info/METADATA,sha256=7S_PQcUGheuzOMBkWV3ITr9y5HXzcmOm1kJg4Un4Ds0,531
13
- codebase_brain-0.3.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
14
- codebase_brain-0.3.2.dist-info/entry_points.txt,sha256=O9yHP4zy_uoVPxmewGbdNjNksmxRYUv34MQJDRXXodc,41
15
- codebase_brain-0.3.2.dist-info/top_level.txt,sha256=RmSvznU63-vmm5zLSKAlYYsFzQ8X3RyPrB-m0q0uQfI,23
16
- codebase_brain-0.3.2.dist-info/RECORD,,
12
+ codebase_brain-0.3.4.dist-info/METADATA,sha256=UrO6il1XfJ_OeEw8uoO6qM7h2XIOC9jmZsdcG8lKkAM,531
13
+ codebase_brain-0.3.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
+ codebase_brain-0.3.4.dist-info/entry_points.txt,sha256=O9yHP4zy_uoVPxmewGbdNjNksmxRYUv34MQJDRXXodc,41
15
+ codebase_brain-0.3.4.dist-info/top_level.txt,sha256=RmSvznU63-vmm5zLSKAlYYsFzQ8X3RyPrB-m0q0uQfI,23
16
+ codebase_brain-0.3.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5