project-brain-cli 1.0.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.
@@ -0,0 +1,108 @@
1
+ import json
2
+ from collections import Counter
3
+ from pathlib import Path
4
+
5
+ from project_brain.core.logger import log_error
6
+
7
+
8
+ def load_data(root: Path):
9
+ data_path = root / ".brain" / "data.json"
10
+ if not data_path.exists():
11
+ return None
12
+
13
+ try:
14
+ with data_path.open("r", encoding="utf-8") as f:
15
+ return json.load(f)
16
+ except Exception as e:
17
+ log_error(f"Function failed: {str(e)}")
18
+ return None
19
+
20
+
21
+ def compute_basic_stats(data: dict):
22
+ total_files = data.get("project", {}).get("total_files", 0)
23
+ total_functions = len(data.get("functions", []))
24
+ total_classes = len(data.get("classes", []))
25
+
26
+ return total_files, total_functions, total_classes
27
+
28
+
29
+ def get_top_files(functions: list, limit: int = 5):
30
+ counter = Counter()
31
+
32
+ for fn in functions:
33
+ file = fn.get("file")
34
+ if file:
35
+ counter[file] += 1
36
+
37
+ return counter.most_common(limit)
38
+
39
+
40
+ def get_key_classes(classes: list, limit: int = 5):
41
+ result = []
42
+ for cls in classes[:limit]:
43
+ result.append((cls.get("name"), cls.get("file")))
44
+ return result
45
+
46
+
47
+ def generate_overview(files: list):
48
+ names = " ".join([f.get("path", "").lower() for f in files])
49
+
50
+ parts = []
51
+
52
+ if "auth" in names:
53
+ parts.append("authentication")
54
+ if "db" in names or "database" in names:
55
+ parts.append("database layer")
56
+ if "api" in names or "routes" in names:
57
+ parts.append("API/backend")
58
+
59
+ if "cli" in names:
60
+ parts.append("CLI tool")
61
+
62
+ if not parts:
63
+ return "General purpose codebase with modular structure."
64
+
65
+ if len(parts) == 1:
66
+ return f"Project includes {parts[0]} functionality."
67
+
68
+ return f"Project includes {', '.join(parts[:-1])} and {parts[-1]}."
69
+
70
+
71
+ def format_summary(root: Path, data: dict):
72
+ total_files, total_functions, total_classes = compute_basic_stats(data)
73
+
74
+ top_files = get_top_files(data.get("functions", []))
75
+ key_classes = get_key_classes(data.get("classes", []))
76
+ overview = generate_overview(data.get("files", []))
77
+
78
+ lines = []
79
+
80
+ lines.append(f"Project: {root}")
81
+ lines.append("")
82
+ lines.append(f"Files: {total_files}")
83
+ lines.append(f"Functions: {total_functions}")
84
+ lines.append(f"Classes: {total_classes}")
85
+ lines.append("")
86
+ lines.append("Top Files:")
87
+ lines.append("")
88
+
89
+ if top_files:
90
+ for file, count in top_files:
91
+ lines.append(f"* {file} ({count} functions)")
92
+ else:
93
+ lines.append("* None")
94
+
95
+ lines.append("")
96
+ lines.append("Key Classes:")
97
+ lines.append("")
98
+
99
+ if key_classes:
100
+ for name, file in key_classes:
101
+ lines.append(f"* {name} ({file})")
102
+ else:
103
+ lines.append("* None")
104
+
105
+ lines.append("")
106
+ lines.append(f"Overview: {overview}")
107
+
108
+ return "\n".join(lines)
File without changes
@@ -0,0 +1,211 @@
1
+ import subprocess
2
+ import os
3
+ import requests
4
+
5
+
6
+ def call_ollama(model, prompt, include_models=False, timeout=60):
7
+ try:
8
+ proc = subprocess.run(
9
+ ["ollama", "run", model],
10
+ input=prompt,
11
+ text=True,
12
+ capture_output=True,
13
+ timeout = int(timeout) if timeout else 60
14
+ )
15
+
16
+ if proc.returncode != 0:
17
+ return _response(error=proc.stderr, status=500)
18
+
19
+ models = []
20
+ if include_models:
21
+ m = subprocess.run(["ollama", "list"], capture_output=True, text=True)
22
+ if m.returncode == 0:
23
+ models = m.stdout.splitlines()
24
+
25
+ return _response(proc.stdout.strip(), models, 200)
26
+
27
+ except subprocess.TimeoutExpired:
28
+ return _response(error="Ollama timeout", status=408)
29
+
30
+ except Exception as e:
31
+ return _response(error=str(e), status=500)
32
+
33
+
34
+ def call_openai(model, prompt, api_key, include_models=False, timeout=60):
35
+ if not api_key:
36
+ return _response(error="Missing API key", status=401)
37
+
38
+ try:
39
+ res = requests.post(
40
+ "https://api.openai.com/v1/responses",
41
+ headers={
42
+ "Authorization": f"Bearer {api_key}",
43
+ "Content-Type": "application/json"
44
+ },
45
+ json={"model": model, "input": prompt},
46
+ timeout = int(timeout) if timeout else 60
47
+ )
48
+
49
+ if res.status_code != 200:
50
+ try:
51
+ err = res.json()
52
+ except Exception:
53
+ err = res.text
54
+ return _response(
55
+ error=str(err),
56
+ status=res.status_code
57
+ )
58
+
59
+ data = res.json()
60
+ output = extract_openai_output(data)
61
+ if not output:
62
+ return _response(
63
+ error="Empty response from OpenAI",
64
+ status=502
65
+ )
66
+
67
+ models = []
68
+ if include_models:
69
+ m = requests.get(
70
+ "https://api.openai.com/v1/models",
71
+ headers={"Authorization": f"Bearer {api_key}"}
72
+ )
73
+ if m.status_code == 200:
74
+ models = [x.get("id") for x in m.json().get("data", [])]
75
+
76
+ return _response(output, models, res.status_code)
77
+
78
+ except requests.Timeout:
79
+ return _response(error="Request timeout", status=408)
80
+
81
+ except requests.ConnectionError:
82
+ return _response(error="Connection failed", status=503)
83
+
84
+ except Exception as e:
85
+ return _response(error=str(e), status=500)
86
+
87
+
88
+ def call_huggingface(model, prompt, api_key, include_models=False, timeout=60):
89
+ url = f"https://api-inference.huggingface.co/v1/models/{model}"
90
+
91
+ try:
92
+ res = requests.post(
93
+ url,
94
+ headers={"Authorization": f"Bearer {api_key}"},
95
+ json={"inputs": prompt},
96
+ timeout = int(timeout) if timeout else 60
97
+ )
98
+
99
+ if res.status_code != 200:
100
+ return _response(error=res.text, status=res.status_code)
101
+
102
+ data = res.json()
103
+ output = data[0].get("generated_text", "") if isinstance(data, list) else str(data)
104
+
105
+ models = []
106
+ if include_models:
107
+ m = requests.get("https://huggingface.co/api/models", timeout=timeout)
108
+ if m.status_code == 200:
109
+ models = [x.get("id") for x in m.json()[:10]]
110
+
111
+ return _response(output, models, res.status_code)
112
+
113
+ except requests.Timeout:
114
+ return _response(error="Request timeout", status=408)
115
+
116
+ except requests.ConnectionError:
117
+ return _response(error="Connection failed", status=503)
118
+
119
+ except Exception as e:
120
+ return _response(error=str(e), status=500)
121
+
122
+ def call_gemini(model, prompt, api_key, include_models=False, timeout=60):
123
+ if not api_key:
124
+ return _response(error="Missing API key", status=401)
125
+
126
+ try:
127
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
128
+
129
+ res = requests.post(
130
+ url,
131
+ json={"contents": [{"parts": [{"text": prompt}]}]},
132
+ timeout = int(timeout) if timeout else 60
133
+ )
134
+
135
+ if res.status_code != 200:
136
+ return _response(error=res.text, status=res.status_code)
137
+
138
+ data = res.json()
139
+ parts = data.get("candidates", [])[0].get("content", {}).get("parts", [])
140
+ output = "".join(p.get("text", "") for p in parts)
141
+
142
+ models = []
143
+ if include_models:
144
+ m = requests.get(
145
+ f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
146
+ )
147
+ if m.status_code == 200:
148
+ models = [x.get("name") for x in m.json().get("models", [])]
149
+
150
+ return _response(output.strip(), models, res.status_code)
151
+
152
+ except requests.Timeout:
153
+ return _response(error="Request timeout", status=408)
154
+
155
+ except requests.ConnectionError:
156
+ return _response(error="Connection failed", status=503)
157
+
158
+ except Exception as e:
159
+ return _response(error=str(e), status=500)
160
+
161
+
162
+ def call_llm(provider, model, prompt, api_key="", include_models=False, timeout=60):
163
+ if provider == "openai":
164
+ api_key = os.getenv("OPENAI_API_KEY", "")
165
+
166
+ elif provider == "gemini":
167
+ api_key = os.getenv("GEMINI_API_KEY", "")
168
+
169
+ elif provider == "huggingface":
170
+ api_key = os.getenv("HUGGINGFACE_API_KEY", "")
171
+
172
+ if not model and provider != "none":
173
+ return _response(error="Model not specified", status=400)
174
+
175
+ if provider == "openai":
176
+ return call_openai(model, prompt, api_key, include_models, timeout)
177
+
178
+ if provider == "huggingface":
179
+ return call_huggingface(model, prompt, api_key, include_models, timeout)
180
+
181
+ if provider == "gemini":
182
+ return call_gemini(model, prompt, api_key, include_models, timeout)
183
+
184
+ if provider == "ollama":
185
+ return call_ollama(model, prompt, include_models, timeout)
186
+
187
+ return _response(error="Unsupported provider", status=400)
188
+
189
+ def _response(output="", models=None, status=200, error=None):
190
+ return {
191
+ "output": output or "",
192
+ "models": models or [],
193
+ "status_code": status,
194
+ "error": error
195
+ }
196
+
197
+ def extract_openai_output(data):
198
+ try:
199
+ # ✅ Case 1: direct shortcut (new API)
200
+ if "output_text" in data:
201
+ return data["output_text"]
202
+ # ✅ Case 2: structured output
203
+ for item in data.get("output", []):
204
+ for c in item.get("content", []):
205
+ if c.get("type") == "output_text":
206
+ return c.get("text", "")
207
+ if "text" in c:
208
+ return c["text"]
209
+ except Exception:
210
+ pass
211
+ return ""
File without changes
@@ -0,0 +1,12 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+
5
+ def save_data(data: dict, root: Path):
6
+ brain_dir = root / ".brain"
7
+ brain_dir.mkdir(exist_ok=True)
8
+
9
+ data_path = brain_dir / "data.json"
10
+
11
+ with data_path.open("w", encoding="utf-8") as f:
12
+ json.dump(data, f, indent=2)