coreinsight-cli 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.
- coreinsight/Dockerfile.cpp-sandbox +2 -0
- coreinsight/Dockerfile.python-sandbox +3 -0
- coreinsight/__init__.py +0 -0
- coreinsight/analyzer.py +374 -0
- coreinsight/config.py +51 -0
- coreinsight/hardware.py +45 -0
- coreinsight/indexer.py +97 -0
- coreinsight/main.py +366 -0
- coreinsight/parser.py +95 -0
- coreinsight/prompts.py +50 -0
- coreinsight/sandbox.py +566 -0
- coreinsight/scanner.py +103 -0
- coreinsight_cli-0.1.0.dist-info/METADATA +115 -0
- coreinsight_cli-0.1.0.dist-info/RECORD +18 -0
- coreinsight_cli-0.1.0.dist-info/WHEEL +5 -0
- coreinsight_cli-0.1.0.dist-info/entry_points.txt +2 -0
- coreinsight_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- coreinsight_cli-0.1.0.dist-info/top_level.txt +1 -0
coreinsight/__init__.py
ADDED
|
File without changes
|
coreinsight/analyzer.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from langchain_core.output_parsers import JsonOutputParser
|
|
7
|
+
from langchain_core.prompts import PromptTemplate
|
|
8
|
+
from langchain_core.exceptions import OutputParserException
|
|
9
|
+
|
|
10
|
+
from langchain_ollama import ChatOllama
|
|
11
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
12
|
+
from langchain_openai import ChatOpenAI
|
|
13
|
+
from langchain_anthropic import ChatAnthropic
|
|
14
|
+
|
|
15
|
+
from coreinsight.prompts import SYSTEM_PROMPT, ANALYSIS_TEMPLATE
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Bottleneck(BaseModel):
|
|
21
|
+
line: int = Field(description="The approximate line number of the issue in the original code")
|
|
22
|
+
severity: str = Field(description="Critical, High, Medium, Low")
|
|
23
|
+
message: str = Field(description="Specific hardware or algorithmic bottleneck at this line")
|
|
24
|
+
suggestion: str = Field(description="How to fix this specific line")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuditResult(BaseModel):
|
|
28
|
+
severity: str = Field(description="Overall severity: Critical, High, Medium, Low")
|
|
29
|
+
issue: str = Field(description="Brief overall description of the bottleneck")
|
|
30
|
+
reasoning: str = Field(description="Step-by-step hardware-level reasoning for the proposed changes")
|
|
31
|
+
suggestion: str = Field(description="Overall specific fix strategy")
|
|
32
|
+
bottlenecks: List[Bottleneck] = Field(description="List of specific line-level bottlenecks", default_factory=list)
|
|
33
|
+
optimized_code: Optional[str] = Field(description="The entirely rewritten optimized code, ready to drop in", default=None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_HARNESS_TEMPLATE = """
|
|
37
|
+
You are a strict QA engineer writing a standalone asymptotic scaling benchmark script in {language}.
|
|
38
|
+
|
|
39
|
+
ORIGINAL FUNCTION (Name: {func_name}):
|
|
40
|
+
{original}
|
|
41
|
+
|
|
42
|
+
OPTIMIZED FUNCTION:
|
|
43
|
+
{optimized}
|
|
44
|
+
|
|
45
|
+
GLOBAL DEPENDENCIES (Helper functions/structs required to run the code):
|
|
46
|
+
{context}
|
|
47
|
+
|
|
48
|
+
Write the complete executable script (e.g., `int main()` or `if __name__ == "__main__":`) that:
|
|
49
|
+
1. Includes necessary imports/headers.
|
|
50
|
+
2. Includes ALL required helper functions or structs from GLOBAL DEPENDENCIES so the script is fully standalone.
|
|
51
|
+
3. Defines BOTH the original and optimized functions exactly as provided above.
|
|
52
|
+
4. Tests multiple data sizes (e.g., N=10, 100, 1000, 5000).
|
|
53
|
+
5. Target Hardware: {hardware_target}. The largest N MUST cross cache boundaries but MUST NOT exceed 20% of available RAM to prevent OOM crashes.
|
|
54
|
+
6. Initializes realistic dummy data for each size N.
|
|
55
|
+
7. Times execution of original vs optimized using high-resolution timers.
|
|
56
|
+
|
|
57
|
+
CRITICAL TIMING:
|
|
58
|
+
- Python: use `time.perf_counter()`. C++: use `std::chrono::high_resolution_clock`.
|
|
59
|
+
- Clamp: `orig_time = max(end - start, 1e-9)` to prevent zero-division.
|
|
60
|
+
- Speedup: `speedup = orig_time / opt_time`.
|
|
61
|
+
|
|
62
|
+
ISOLATION RULES (CRITICAL):
|
|
63
|
+
- This runs in an empty Docker container. NO local files exist.
|
|
64
|
+
- DO NOT use local imports. Define everything inline.
|
|
65
|
+
- DO NOT rename the original function — call it exactly `{func_name}`.
|
|
66
|
+
|
|
67
|
+
OUTPUT FORMAT (CRITICAL):
|
|
68
|
+
Print ONLY this exact CSV to stdout, no other text:
|
|
69
|
+
N,Original_Time,Optimized_Time,Speedup
|
|
70
|
+
10,0.002,0.001,2.00
|
|
71
|
+
|
|
72
|
+
[PYTHON ONLY]: Also import matplotlib, plot results, and save as `benchmark_plot.png`.
|
|
73
|
+
|
|
74
|
+
FORMATTING RULE: Wrap your ENTIRE script in a single markdown code block. No text before or after.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
_FIX_TEMPLATE = """
|
|
78
|
+
You are an expert {language} developer. Your previous benchmark script FAILED in an isolated sandbox.
|
|
79
|
+
|
|
80
|
+
ORIGINAL FUNCTION (Name: {func_name}):
|
|
81
|
+
{original}
|
|
82
|
+
|
|
83
|
+
GLOBAL DEPENDENCIES:
|
|
84
|
+
{context}
|
|
85
|
+
|
|
86
|
+
YOUR FAILED SCRIPT:
|
|
87
|
+
{bad_harness}
|
|
88
|
+
|
|
89
|
+
EXECUTION ERROR LOGS:
|
|
90
|
+
{error_logs}
|
|
91
|
+
|
|
92
|
+
ISOLATION CONSTRAINTS (CRITICAL):
|
|
93
|
+
- Empty Docker container. No local files. NO local imports.
|
|
94
|
+
- Define `{func_name}` and all GLOBAL DEPENDENCIES inline.
|
|
95
|
+
|
|
96
|
+
FIX INSTRUCTIONS:
|
|
97
|
+
1. Diagnose the failure from the error logs above.
|
|
98
|
+
2. Fix imports, NameErrors, type mismatches, infinite loops, or OOM issues.
|
|
99
|
+
3. Maintain the CSV stdout format exactly: N,Original_Time,Optimized_Time,Speedup
|
|
100
|
+
4. Use high-resolution timers and clamp with `max(t, 1e-9)`.
|
|
101
|
+
5. [PYTHON ONLY]: Save benchmark plot as `benchmark_plot.png`.
|
|
102
|
+
|
|
103
|
+
FORMATTING RULE: Wrap your ENTIRE fixed script in a single markdown code block. No text before or after.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
_TEST_CASES_TEMPLATE = """
|
|
107
|
+
You are a QA engineer writing correctness test cases for a function.
|
|
108
|
+
|
|
109
|
+
FUNCTION NAME: {func_name}
|
|
110
|
+
LANGUAGE: {language}
|
|
111
|
+
|
|
112
|
+
FUNCTION SIGNATURE AND BODY:
|
|
113
|
+
{original}
|
|
114
|
+
|
|
115
|
+
GLOBAL DEPENDENCIES (helper functions / structs this function relies on):
|
|
116
|
+
{context}
|
|
117
|
+
|
|
118
|
+
Your task: generate {num_cases} diverse test cases that call `{func_name}` with different
|
|
119
|
+
arguments. The cases must cover:
|
|
120
|
+
- Small inputs (N ~ 10)
|
|
121
|
+
- Medium inputs (N ~ 100-500)
|
|
122
|
+
- Edge cases: empty collections, single-element, all-zeros, negative values (where applicable)
|
|
123
|
+
- Boundary conditions specific to this function's logic
|
|
124
|
+
|
|
125
|
+
OUTPUT FORMAT — respond with ONLY a valid JSON array, nothing else. No markdown fences,
|
|
126
|
+
no explanation. Each element must be a JSON object with exactly two keys:
|
|
127
|
+
"args" : a JSON array of positional arguments (use only JSON-serialisable types:
|
|
128
|
+
numbers, strings, booleans, arrays, objects — NO numpy, NO bytes)
|
|
129
|
+
"kwargs": a JSON object of keyword arguments (may be empty {{}})
|
|
130
|
+
|
|
131
|
+
Example (do NOT copy this — generate cases specific to {func_name}):
|
|
132
|
+
[
|
|
133
|
+
{{"args": [[1, 2, 3]], "kwargs": {{}}}},
|
|
134
|
+
{{"args": [[]], "kwargs": {{}}}},
|
|
135
|
+
{{"args": [[9, -1, 4, 0, 7]], "kwargs": {{"reverse": true}}}}
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
CONSTRAINTS:
|
|
139
|
+
- All values must be plain JSON types — no numpy arrays, no custom objects.
|
|
140
|
+
- If the function operates on a matrix, represent it as a list-of-lists.
|
|
141
|
+
- If the function takes a size integer N, generate concrete data of that size inline.
|
|
142
|
+
- Do NOT include function calls or expressions — only literal values.
|
|
143
|
+
- Produce exactly {num_cases} test cases.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AnalyzerAgent:
|
|
148
|
+
def __init__(self, provider="ollama", model_name="llama3.2", api_keys=None):
|
|
149
|
+
self.parser = JsonOutputParser(pydantic_object=AuditResult)
|
|
150
|
+
self.provider = provider
|
|
151
|
+
api_keys = api_keys or {}
|
|
152
|
+
|
|
153
|
+
if provider == "openai":
|
|
154
|
+
if not api_keys.get("openai"):
|
|
155
|
+
raise ValueError("OpenAI API Key required.")
|
|
156
|
+
self.base_llm = ChatOpenAI(
|
|
157
|
+
model=model_name,
|
|
158
|
+
api_key=api_keys["openai"],
|
|
159
|
+
temperature=0.1,
|
|
160
|
+
model_kwargs={"response_format": {"type": "json_object"}},
|
|
161
|
+
)
|
|
162
|
+
self.json_llm = self.base_llm
|
|
163
|
+
|
|
164
|
+
elif provider == "local_server":
|
|
165
|
+
base_url = api_keys.get("local_url", "http://localhost:1234/v1")
|
|
166
|
+
self.base_llm = ChatOpenAI(
|
|
167
|
+
model=model_name,
|
|
168
|
+
api_key="not-needed",
|
|
169
|
+
base_url=base_url,
|
|
170
|
+
temperature=0.1,
|
|
171
|
+
model_kwargs={"response_format": {"type": "json_object"}},
|
|
172
|
+
)
|
|
173
|
+
self.json_llm = self.base_llm
|
|
174
|
+
|
|
175
|
+
elif provider == "anthropic":
|
|
176
|
+
if not api_keys.get("anthropic"):
|
|
177
|
+
raise ValueError("Anthropic API Key required.")
|
|
178
|
+
self.base_llm = ChatAnthropic(
|
|
179
|
+
model=model_name,
|
|
180
|
+
api_key=api_keys["anthropic"],
|
|
181
|
+
temperature=0.1,
|
|
182
|
+
)
|
|
183
|
+
# Anthropic doesn't support response_format; JSON is enforced via prompt only
|
|
184
|
+
self.json_llm = self.base_llm
|
|
185
|
+
|
|
186
|
+
elif provider == "google":
|
|
187
|
+
if not api_keys.get("google"):
|
|
188
|
+
raise ValueError("Google Gemini API Key required.")
|
|
189
|
+
self.base_llm = ChatGoogleGenerativeAI(
|
|
190
|
+
model=model_name,
|
|
191
|
+
google_api_key=api_keys["google"],
|
|
192
|
+
temperature=0.1,
|
|
193
|
+
convert_system_message_to_human=True,
|
|
194
|
+
)
|
|
195
|
+
self.json_llm = self.base_llm
|
|
196
|
+
|
|
197
|
+
else: # Ollama default
|
|
198
|
+
self.base_llm = ChatOllama(
|
|
199
|
+
model=model_name,
|
|
200
|
+
temperature=0.1,
|
|
201
|
+
num_predict=4096,
|
|
202
|
+
num_ctx=8192,
|
|
203
|
+
)
|
|
204
|
+
self.json_llm = self.base_llm.bind(format="json")
|
|
205
|
+
|
|
206
|
+
self.prompt = PromptTemplate(
|
|
207
|
+
template=ANALYSIS_TEMPLATE + "\n\n{format_instructions}",
|
|
208
|
+
input_variables=["language", "code_content", "context", "hardware_target"],
|
|
209
|
+
partial_variables={
|
|
210
|
+
"system_prompt": SYSTEM_PROMPT,
|
|
211
|
+
"format_instructions": self.parser.get_format_instructions(),
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
self.chain = self.prompt | self.json_llm | self.parser
|
|
215
|
+
|
|
216
|
+
def analyze(self, code: str, language: str, context: str = "", hardware_target: str = "Generic CPU"):
|
|
217
|
+
try:
|
|
218
|
+
return self.chain.invoke({
|
|
219
|
+
"language": language,
|
|
220
|
+
"code_content": code,
|
|
221
|
+
"context": context,
|
|
222
|
+
"hardware_target": hardware_target,
|
|
223
|
+
})
|
|
224
|
+
except OutputParserException:
|
|
225
|
+
return {
|
|
226
|
+
"severity": "Error",
|
|
227
|
+
"issue": "AI Output Parsing Failed",
|
|
228
|
+
"reasoning": "The model failed to return valid JSON.",
|
|
229
|
+
"suggestion": "Try running the analysis again or use a larger parameter model.",
|
|
230
|
+
"bottlenecks": [],
|
|
231
|
+
"optimized_code": None,
|
|
232
|
+
}
|
|
233
|
+
except Exception as e:
|
|
234
|
+
return {
|
|
235
|
+
"severity": "Error",
|
|
236
|
+
"issue": str(e),
|
|
237
|
+
"reasoning": "System error during analysis pipeline.",
|
|
238
|
+
"suggestion": "Check LLM API keys and connectivity.",
|
|
239
|
+
"bottlenecks": [],
|
|
240
|
+
"optimized_code": None,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def _extract_executable_code(self, response_text: str) -> str:
|
|
244
|
+
"""Extract the first/longest fenced code block from model output."""
|
|
245
|
+
blocks = re.findall(r"```[a-zA-Z+#]*\s*\n(.*?)```", response_text, re.DOTALL)
|
|
246
|
+
if blocks:
|
|
247
|
+
return max(blocks, key=len).strip()
|
|
248
|
+
|
|
249
|
+
# Fallback: strip fence markers line by line to avoid corrupting code
|
|
250
|
+
lines = response_text.strip().split("\n")
|
|
251
|
+
lines = [l for l in lines if not re.match(r"^```", l)]
|
|
252
|
+
while lines and lines[0].lower().startswith(("here is", "sure", "certainly", "output:")) \
|
|
253
|
+
and not lines[0].strip().startswith(("#", "//")):
|
|
254
|
+
lines.pop(0)
|
|
255
|
+
return "\n".join(lines).strip()
|
|
256
|
+
|
|
257
|
+
def _invoke_code_chain(self, template: str, variables: dict, language: str) -> str:
|
|
258
|
+
"""Shared invocation + extraction logic for harness and fix chains."""
|
|
259
|
+
chain = PromptTemplate.from_template(template) | self.base_llm
|
|
260
|
+
result = chain.invoke(variables)
|
|
261
|
+
raw = result.content if hasattr(result, "content") else str(result)
|
|
262
|
+
# Handle Anthropic returning a list of content blocks
|
|
263
|
+
if isinstance(raw, list):
|
|
264
|
+
raw = "\n".join(
|
|
265
|
+
item["text"] if isinstance(item, dict) and "text" in item else str(item)
|
|
266
|
+
for item in raw
|
|
267
|
+
)
|
|
268
|
+
return self._extract_executable_code(raw)
|
|
269
|
+
|
|
270
|
+
def generate_harness(
|
|
271
|
+
self,
|
|
272
|
+
func_name: str,
|
|
273
|
+
original_code: str,
|
|
274
|
+
optimized_code: str,
|
|
275
|
+
language: str,
|
|
276
|
+
context: str = "",
|
|
277
|
+
hardware_target: str = "Generic CPU",
|
|
278
|
+
) -> str:
|
|
279
|
+
try:
|
|
280
|
+
return self._invoke_code_chain(
|
|
281
|
+
_HARNESS_TEMPLATE,
|
|
282
|
+
{
|
|
283
|
+
"language": language,
|
|
284
|
+
"func_name": func_name,
|
|
285
|
+
"original": original_code,
|
|
286
|
+
"optimized": optimized_code,
|
|
287
|
+
"context": context,
|
|
288
|
+
"hardware_target": hardware_target,
|
|
289
|
+
},
|
|
290
|
+
language,
|
|
291
|
+
)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
is_python = language.lower() == "python"
|
|
294
|
+
opener = "#" if is_python else "//"
|
|
295
|
+
entry = 'if __name__ == "__main__":' if is_python else "int main() {"
|
|
296
|
+
stub = " pass" if is_python else " return 1;\n}"
|
|
297
|
+
return f"{opener} Failed to generate harness: {e}\n{entry}\n{stub}"
|
|
298
|
+
|
|
299
|
+
def fix_harness(
|
|
300
|
+
self,
|
|
301
|
+
func_name: str,
|
|
302
|
+
original_code: str,
|
|
303
|
+
bad_harness: str,
|
|
304
|
+
error_logs: str,
|
|
305
|
+
language: str,
|
|
306
|
+
context: str = "",
|
|
307
|
+
) -> str:
|
|
308
|
+
try:
|
|
309
|
+
return self._invoke_code_chain(
|
|
310
|
+
_FIX_TEMPLATE,
|
|
311
|
+
{
|
|
312
|
+
"language": language,
|
|
313
|
+
"func_name": func_name,
|
|
314
|
+
"original": original_code,
|
|
315
|
+
"bad_harness": bad_harness,
|
|
316
|
+
"error_logs": error_logs,
|
|
317
|
+
"context": context,
|
|
318
|
+
},
|
|
319
|
+
language,
|
|
320
|
+
)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
is_python = language.lower() == "python"
|
|
323
|
+
opener = "#" if is_python else "//"
|
|
324
|
+
return f"{opener} Failed to fix harness: {e}"
|
|
325
|
+
|
|
326
|
+
def generate_test_cases(
|
|
327
|
+
self,
|
|
328
|
+
func_name: str,
|
|
329
|
+
original_code: str,
|
|
330
|
+
language: str,
|
|
331
|
+
context: str = "",
|
|
332
|
+
num_cases: int = 8,
|
|
333
|
+
) -> list:
|
|
334
|
+
"""
|
|
335
|
+
Ask the LLM to generate diverse, JSON-serialisable test cases for
|
|
336
|
+
`func_name` so the sandbox can run correctness verification.
|
|
337
|
+
|
|
338
|
+
Returns a list of {"args": [...], "kwargs": {...}} dicts, or an
|
|
339
|
+
empty list if generation or parsing fails (sandbox skips gracefully).
|
|
340
|
+
"""
|
|
341
|
+
import json
|
|
342
|
+
|
|
343
|
+
chain = PromptTemplate.from_template(_TEST_CASES_TEMPLATE) | self.base_llm
|
|
344
|
+
try:
|
|
345
|
+
result = chain.invoke({
|
|
346
|
+
"func_name": func_name,
|
|
347
|
+
"language": language,
|
|
348
|
+
"original": original_code,
|
|
349
|
+
"context": context or "None",
|
|
350
|
+
"num_cases": num_cases,
|
|
351
|
+
})
|
|
352
|
+
raw = result.content if hasattr(result, "content") else str(result)
|
|
353
|
+
if isinstance(raw, list):
|
|
354
|
+
raw = "\n".join(
|
|
355
|
+
item["text"] if isinstance(item, dict) and "text" in item else str(item)
|
|
356
|
+
for item in raw
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Strip markdown fences if the model wrapped anyway
|
|
360
|
+
raw = re.sub(r"```[a-zA-Z]*\s*", "", raw).strip()
|
|
361
|
+
|
|
362
|
+
cases = json.loads(raw)
|
|
363
|
+
|
|
364
|
+
# Validate structure — drop malformed entries silently
|
|
365
|
+
return [
|
|
366
|
+
case for case in cases
|
|
367
|
+
if isinstance(case, dict)
|
|
368
|
+
and isinstance(case.get("args"), list)
|
|
369
|
+
and isinstance(case.get("kwargs"), dict)
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.warning(f"generate_test_cases failed for '{func_name}': {e}")
|
|
374
|
+
return []
|
coreinsight/config.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.prompt import Prompt, Confirm
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
CONFIG_FILE = Path.home() / ".coreinsight" / "config.json"
|
|
8
|
+
|
|
9
|
+
def load_config():
|
|
10
|
+
if not CONFIG_FILE.exists():
|
|
11
|
+
return {"provider": "ollama", "model_name": "llama3.2", "api_keys": {}}
|
|
12
|
+
with open(CONFIG_FILE, "r") as f:
|
|
13
|
+
return json.load(f)
|
|
14
|
+
|
|
15
|
+
def save_config(config_data):
|
|
16
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
with open(CONFIG_FILE, "w") as f:
|
|
18
|
+
json.dump(config_data, f, indent=4)
|
|
19
|
+
|
|
20
|
+
def run_configure():
|
|
21
|
+
"""Interactive CLI to set up models and API keys."""
|
|
22
|
+
console.print("[bold cyan]⚙️ CoreInsight Configuration[/bold cyan]")
|
|
23
|
+
|
|
24
|
+
config = load_config()
|
|
25
|
+
|
|
26
|
+
provider = Prompt.ask(
|
|
27
|
+
"Which AI provider do you want to use?",
|
|
28
|
+
choices=["ollama", "local_server", "openai", "anthropic", "google"],
|
|
29
|
+
default=config.get("provider", "ollama")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
config["provider"] = provider
|
|
33
|
+
|
|
34
|
+
if provider == "ollama":
|
|
35
|
+
config["model_name"] = Prompt.ask("Ollama model name", default=config.get("model_name", "llama3.2"))
|
|
36
|
+
elif provider == "local_server":
|
|
37
|
+
config["model_name"] = Prompt.ask("Local model name (optional)", default=config.get("model_name", "local-model"))
|
|
38
|
+
config["api_keys"]["local_url"] = Prompt.ask("Local Server Base URL", default="http://localhost:1234/v1")
|
|
39
|
+
elif provider == "openai":
|
|
40
|
+
config["model_name"] = Prompt.ask("OpenAI model name", default="gpt-4o")
|
|
41
|
+
config["api_keys"]["openai"] = Prompt.ask("OpenAI API Key (hidden)", password=True)
|
|
42
|
+
elif provider == "anthropic":
|
|
43
|
+
config["model_name"] = Prompt.ask("Claude model name", default="claude-3-5-sonnet-latest")
|
|
44
|
+
config["api_keys"]["anthropic"] = Prompt.ask("Anthropic API Key (hidden)", password=True)
|
|
45
|
+
elif provider == "google":
|
|
46
|
+
config["model_name"] = Prompt.ask("Gemini model name", default="gemini-1.5-pro")
|
|
47
|
+
config["api_keys"]["google"] = Prompt.ask("Google Gemini API Key (hidden)", password=True)
|
|
48
|
+
|
|
49
|
+
save_config(config)
|
|
50
|
+
console.print("\n[bold green]✅ Configuration saved successfully![/bold green]")
|
|
51
|
+
console.print(f"CoreInsight is now using [bold]{provider}[/bold] ({config['model_name']}).")
|
coreinsight/hardware.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import psutil
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
class HardwareDetector:
|
|
6
|
+
@staticmethod
|
|
7
|
+
def get_system_specs() -> dict:
|
|
8
|
+
"""Dynamically detects the host system's CPU, RAM, and GPU."""
|
|
9
|
+
specs = {
|
|
10
|
+
"os": platform.system(),
|
|
11
|
+
"cpu_cores": psutil.cpu_count(logical=False),
|
|
12
|
+
"cpu_threads": psutil.cpu_count(logical=True),
|
|
13
|
+
"ram_gb": round(psutil.virtual_memory().total / (1024**3), 2),
|
|
14
|
+
"gpu": "None detected",
|
|
15
|
+
"vram_gb": 0.0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Attempt to detect NVIDIA GPU and VRAM
|
|
19
|
+
try:
|
|
20
|
+
smi_output = subprocess.check_output(
|
|
21
|
+
["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
|
|
22
|
+
encoding="utf-8",
|
|
23
|
+
stderr=subprocess.DEVNULL
|
|
24
|
+
).strip()
|
|
25
|
+
|
|
26
|
+
if smi_output:
|
|
27
|
+
# E.g., "NVIDIA GeForce RTX 4090, 24564 MiB"
|
|
28
|
+
parts = smi_output.split(',')
|
|
29
|
+
specs["gpu"] = parts[0].strip()
|
|
30
|
+
specs["vram_gb"] = round(int(parts[1].replace('MiB', '').strip()) / 1024, 2)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass # No NVIDIA GPU found or drivers not installed
|
|
33
|
+
|
|
34
|
+
return specs
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def format_for_llm(specs: dict) -> str:
|
|
38
|
+
"""Formats the dictionary into a readable block for the LLM prompt."""
|
|
39
|
+
return (
|
|
40
|
+
f"OS: {specs['os']} | "
|
|
41
|
+
f"CPU: {specs['cpu_cores']} Cores ({specs['cpu_threads']} Threads) | "
|
|
42
|
+
f"System RAM: {specs['ram_gb']} GB | "
|
|
43
|
+
f"GPU: {specs['gpu']} | "
|
|
44
|
+
f"GPU VRAM: {specs['vram_gb']} GB"
|
|
45
|
+
)
|
coreinsight/indexer.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.progress import track
|
|
5
|
+
import chromadb
|
|
6
|
+
from chromadb.utils import embedding_functions
|
|
7
|
+
|
|
8
|
+
from coreinsight.parser import CodeParser
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
class RepoIndexer:
|
|
13
|
+
def __init__(self, repo_path: str):
|
|
14
|
+
self.repo_path = Path(repo_path)
|
|
15
|
+
self.db_path = self.repo_path / ".coreinsight_db"
|
|
16
|
+
self.parser = CodeParser()
|
|
17
|
+
|
|
18
|
+
# Initialize local ChromaDB
|
|
19
|
+
self.chroma_client = chromadb.PersistentClient(path=str(self.db_path))
|
|
20
|
+
|
|
21
|
+
# Use a lightweight, fast local embedding model
|
|
22
|
+
self.embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
|
|
23
|
+
model_name="all-MiniLM-L6-v2"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
self.collection = self.chroma_client.get_or_create_collection(
|
|
27
|
+
name="codebase_context",
|
|
28
|
+
embedding_function=self.embedding_fn
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def index_repository(self):
|
|
32
|
+
"""Scans the repo and embeds all code blocks into the vector DB."""
|
|
33
|
+
valid_extensions = {".py", ".cpp", ".cc", ".h", ".hpp", ".cu", ".cuh"}
|
|
34
|
+
files_to_index = []
|
|
35
|
+
|
|
36
|
+
for root, _, files in os.walk(self.repo_path):
|
|
37
|
+
if ".coreinsight_db" in root or ".git" in root or "venv" in root:
|
|
38
|
+
continue
|
|
39
|
+
for file in files:
|
|
40
|
+
path = Path(root) / file
|
|
41
|
+
if path.suffix in valid_extensions:
|
|
42
|
+
files_to_index.append(path)
|
|
43
|
+
|
|
44
|
+
if not files_to_index:
|
|
45
|
+
console.print("[yellow]No source files found to index.[/yellow]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
console.print(f"[cyan]Found {len(files_to_index)} files. Building vector index...[/cyan]")
|
|
49
|
+
|
|
50
|
+
documents = []
|
|
51
|
+
metadatas = []
|
|
52
|
+
ids = []
|
|
53
|
+
|
|
54
|
+
for filepath in track(files_to_index, description="Parsing and Embedding..."):
|
|
55
|
+
try:
|
|
56
|
+
content = filepath.read_bytes()
|
|
57
|
+
# Use your existing AST parser to chunk the file!
|
|
58
|
+
functions = self.parser.parse_file(str(filepath), content)
|
|
59
|
+
|
|
60
|
+
for idx, func in enumerate(functions):
|
|
61
|
+
documents.append(func['code'])
|
|
62
|
+
metadatas.append({
|
|
63
|
+
"file": str(filepath.relative_to(self.repo_path)),
|
|
64
|
+
"name": func['name'],
|
|
65
|
+
"language": func['language']
|
|
66
|
+
})
|
|
67
|
+
ids.append(f"{filepath.name}_{func['name']}_{idx}")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
console.print(f"[dim red]Skipped {filepath.name}: {e}[/dim red]")
|
|
70
|
+
|
|
71
|
+
if documents:
|
|
72
|
+
# Batch add to ChromaDB
|
|
73
|
+
self.collection.add(
|
|
74
|
+
documents=documents,
|
|
75
|
+
metadatas=metadatas,
|
|
76
|
+
ids=ids
|
|
77
|
+
)
|
|
78
|
+
console.print(f"[bold green]✅ Successfully indexed {len(documents)} code chunks into local Vector DB![/bold green]")
|
|
79
|
+
|
|
80
|
+
def get_context_for_code(self, code_snippet: str, n_results: int = 3) -> str:
|
|
81
|
+
"""Retrieves related code (like structs/classes) to feed to the LLM."""
|
|
82
|
+
if self.collection.count() == 0:
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
results = self.collection.query(
|
|
86
|
+
query_texts=[code_snippet],
|
|
87
|
+
n_results=n_results
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not results['documents'] or not results['documents'][0]:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
context_blocks = []
|
|
94
|
+
for doc, meta in zip(results['documents'][0], results['metadatas'][0]):
|
|
95
|
+
context_blocks.append(f"// From {meta['file']} ({meta['name']}):\n{doc}")
|
|
96
|
+
|
|
97
|
+
return "\n\n".join(context_blocks)
|