coreinsight-cli 0.2.7__tar.gz → 0.2.8__tar.gz
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_cli-0.2.7/coreinsight_cli.egg-info → coreinsight_cli-0.2.8}/PKG-INFO +1 -1
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/analyzer.py +106 -15
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/main.py +31 -4
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/memory.py +71 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/tui.py +281 -53
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8/coreinsight_cli.egg-info}/PKG-INFO +1 -1
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/pyproject.toml +1 -1
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/LICENSE +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/README.md +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/Dockerfile.cpp-sandbox +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/Dockerfile.python-sandbox +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/__init__.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/config.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/__init__.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/bad_loop.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/data_processor.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/slow.cpp +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/hardware.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/indexer.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/parser.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/profiler.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/prompts.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/sandbox.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/scanner.py +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/SOURCES.txt +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/dependency_links.txt +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/entry_points.txt +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/requires.txt +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/top_level.txt +0 -0
- {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/setup.cfg +0 -0
|
@@ -14,6 +14,35 @@ from langchain_anthropic import ChatAnthropic
|
|
|
14
14
|
|
|
15
15
|
from coreinsight.prompts import SYSTEM_PROMPT, ANALYSIS_TEMPLATE, HARNESS_ADDENDUM
|
|
16
16
|
|
|
17
|
+
# Phrases that appear at the start of a truncated LLM response
|
|
18
|
+
_TRUNCATION_HINTS = (
|
|
19
|
+
"context length",
|
|
20
|
+
"context_length_exceeded",
|
|
21
|
+
"maximum context",
|
|
22
|
+
"token limit",
|
|
23
|
+
"finish_reason: length",
|
|
24
|
+
"finish_reason\":\"length",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def _is_truncated(raw: str) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Returns True if the raw LLM output looks like it was cut off mid-generation.
|
|
30
|
+
Catches both explicit error messages and structural truncation signs.
|
|
31
|
+
"""
|
|
32
|
+
if not raw or len(raw.strip()) < 20:
|
|
33
|
+
return True
|
|
34
|
+
low = raw.lower()
|
|
35
|
+
if any(hint in low for hint in _TRUNCATION_HINTS):
|
|
36
|
+
return True
|
|
37
|
+
stripped = raw.strip()
|
|
38
|
+
# JSON truncation: opened but never closed
|
|
39
|
+
if stripped.startswith("{") and not stripped.endswith("}"):
|
|
40
|
+
return True
|
|
41
|
+
# Code truncation: opens a block but ends mid-statement
|
|
42
|
+
if stripped.endswith(("...", "/*", "//", "\"", "'")):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
17
46
|
logger = logging.getLogger(__name__)
|
|
18
47
|
|
|
19
48
|
|
|
@@ -163,12 +192,15 @@ class AnalyzerAgent:
|
|
|
163
192
|
self.json_llm = self.base_llm
|
|
164
193
|
|
|
165
194
|
elif provider == "local_server":
|
|
166
|
-
|
|
195
|
+
from coreinsight.prompts import ModelTier
|
|
196
|
+
base_url = api_keys.get("local_url", "http://localhost:1234/v1")
|
|
197
|
+
_max_tokens = 2048 if model_tier == ModelTier.SMALL else 4096
|
|
167
198
|
self.base_llm = ChatOpenAI(
|
|
168
199
|
model=model_name,
|
|
169
200
|
api_key="not-needed",
|
|
170
201
|
base_url=base_url,
|
|
171
202
|
temperature=0.1,
|
|
203
|
+
max_tokens=_max_tokens,
|
|
172
204
|
model_kwargs={"response_format": {"type": "json_object"}},
|
|
173
205
|
)
|
|
174
206
|
self.json_llm = self.base_llm
|
|
@@ -196,11 +228,20 @@ class AnalyzerAgent:
|
|
|
196
228
|
self.json_llm = self.base_llm
|
|
197
229
|
|
|
198
230
|
else: # Ollama default
|
|
231
|
+
from coreinsight.prompts import ModelTier
|
|
232
|
+
# Small models (7B) typically have 4096 native context.
|
|
233
|
+
# Asking for more causes silent degradation or OOM on the host.
|
|
234
|
+
# Medium/large local models can handle 8192 comfortably.
|
|
235
|
+
_ctx = 4096 if model_tier == ModelTier.SMALL else 8192
|
|
236
|
+
# num_predict: small models need room for JSON + code in one shot.
|
|
237
|
+
# Capping at 2048 for small prevents runaway generation that hits
|
|
238
|
+
# the limit mid-JSON and returns truncated garbage.
|
|
239
|
+
_predict = 2048 if model_tier == ModelTier.SMALL else 4096
|
|
199
240
|
self.base_llm = ChatOllama(
|
|
200
241
|
model=model_name,
|
|
201
242
|
temperature=0.1,
|
|
202
|
-
num_predict=
|
|
203
|
-
num_ctx=
|
|
243
|
+
num_predict=_predict,
|
|
244
|
+
num_ctx=_ctx,
|
|
204
245
|
)
|
|
205
246
|
self.json_llm = self.base_llm.bind(format="json")
|
|
206
247
|
|
|
@@ -258,14 +299,31 @@ class AnalyzerAgent:
|
|
|
258
299
|
def _invoke_code_chain(self, template: str, variables: dict, language: str) -> str:
|
|
259
300
|
"""Shared invocation + extraction logic for harness and fix chains."""
|
|
260
301
|
chain = PromptTemplate.from_template(template) | self.base_llm
|
|
261
|
-
|
|
302
|
+
try:
|
|
303
|
+
result = chain.invoke(variables)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
err = str(e).lower()
|
|
306
|
+
if any(h in err for h in _TRUNCATION_HINTS):
|
|
307
|
+
raise RuntimeError(
|
|
308
|
+
f"Model hit its context limit. Try a smaller file, fewer functions, "
|
|
309
|
+
f"or a model with a larger context window. Detail: {e}"
|
|
310
|
+
) from e
|
|
311
|
+
raise
|
|
262
312
|
raw = result.content if hasattr(result, "content") else str(result)
|
|
263
|
-
# Handle Anthropic returning a list of content blocks
|
|
264
313
|
if isinstance(raw, list):
|
|
265
314
|
raw = "\n".join(
|
|
266
315
|
item["text"] if isinstance(item, dict) and "text" in item else str(item)
|
|
267
316
|
for item in raw
|
|
268
317
|
)
|
|
318
|
+
if _is_truncated(raw):
|
|
319
|
+
logger.warning(
|
|
320
|
+
f"LLM output appears truncated (len={len(raw)}). "
|
|
321
|
+
f"Model likely hit its context/predict limit."
|
|
322
|
+
)
|
|
323
|
+
raise RuntimeError(
|
|
324
|
+
"Model output was truncated — hit context or token limit. "
|
|
325
|
+
"Try a model with a larger context window, or reduce the function size."
|
|
326
|
+
)
|
|
269
327
|
return self._extract_executable_code(raw)
|
|
270
328
|
|
|
271
329
|
def generate_harness(
|
|
@@ -421,12 +479,14 @@ def _build_llm(provider: str, model_name: str, api_keys: dict):
|
|
|
421
479
|
return llm, llm
|
|
422
480
|
|
|
423
481
|
if provider == "local_server":
|
|
424
|
-
base_url
|
|
482
|
+
base_url = api_keys.get("local_url", "http://localhost:1234/v1")
|
|
483
|
+
_max_tokens = api_keys.pop("_predict", 4096) # reuse same key as Ollama path
|
|
425
484
|
llm = ChatOpenAI(
|
|
426
485
|
model=model_name,
|
|
427
486
|
api_key="not-needed",
|
|
428
487
|
base_url=base_url,
|
|
429
488
|
temperature=0.1,
|
|
489
|
+
max_tokens=_max_tokens,
|
|
430
490
|
model_kwargs={"response_format": {"type": "json_object"}},
|
|
431
491
|
)
|
|
432
492
|
return llm, llm
|
|
@@ -452,16 +512,33 @@ def _build_llm(provider: str, model_name: str, api_keys: dict):
|
|
|
452
512
|
)
|
|
453
513
|
return llm, llm
|
|
454
514
|
|
|
455
|
-
# Ollama default
|
|
515
|
+
# Ollama default — context and predict budget are passed in from the
|
|
516
|
+
# calling agent which knows its own model_tier.
|
|
517
|
+
# Default to medium-safe values; callers override via kwargs if needed.
|
|
518
|
+
_ctx = api_keys.pop("_ctx", 8192)
|
|
519
|
+
_predict = api_keys.pop("_predict", 4096)
|
|
456
520
|
base = ChatOllama(
|
|
457
521
|
model=model_name,
|
|
458
522
|
temperature=0.1,
|
|
459
|
-
num_predict=
|
|
460
|
-
num_ctx=
|
|
523
|
+
num_predict=_predict,
|
|
524
|
+
num_ctx=_ctx,
|
|
461
525
|
)
|
|
462
526
|
return base, base.bind(format="json")
|
|
463
527
|
|
|
464
528
|
|
|
529
|
+
def _build_llm_tiered(provider: str, model_name: str, api_keys: dict, model_tier: str):
|
|
530
|
+
"""Wraps _build_llm with tier-aware context settings for local providers."""
|
|
531
|
+
from coreinsight.prompts import ModelTier
|
|
532
|
+
keys = dict(api_keys or {})
|
|
533
|
+
if provider == "ollama":
|
|
534
|
+
keys["_ctx"] = 4096 if model_tier == ModelTier.SMALL else 8192
|
|
535
|
+
keys["_predict"] = 2048 if model_tier == ModelTier.SMALL else 4096
|
|
536
|
+
elif provider == "local_server":
|
|
537
|
+
# max_tokens controls response length — context window is server-side
|
|
538
|
+
keys["_predict"] = 2048 if model_tier == ModelTier.SMALL else 4096
|
|
539
|
+
return _build_llm(provider, model_name, keys)
|
|
540
|
+
|
|
541
|
+
|
|
465
542
|
class BottleneckAgent:
|
|
466
543
|
"""
|
|
467
544
|
Agent 1 — analysis only.
|
|
@@ -480,7 +557,7 @@ class BottleneckAgent:
|
|
|
480
557
|
from coreinsight.prompts import BOTTLENECK_TEMPLATE, SYSTEM_PROMPT
|
|
481
558
|
self.model_tier = model_tier
|
|
482
559
|
self.parser = JsonOutputParser(pydantic_object=AuditResult)
|
|
483
|
-
self._base_llm, self._json_llm =
|
|
560
|
+
self._base_llm, self._json_llm = _build_llm_tiered(provider, model_name, api_keys, model_tier)
|
|
484
561
|
|
|
485
562
|
self._prompt = PromptTemplate(
|
|
486
563
|
template=BOTTLENECK_TEMPLATE,
|
|
@@ -544,7 +621,7 @@ class OptimizerAgent:
|
|
|
544
621
|
) -> None:
|
|
545
622
|
from coreinsight.prompts import OPTIMIZER_TEMPLATE
|
|
546
623
|
self.model_tier = model_tier
|
|
547
|
-
self._base_llm, _ =
|
|
624
|
+
self._base_llm, _ = _build_llm_tiered(provider, model_name, api_keys, model_tier)
|
|
548
625
|
self._template = OPTIMIZER_TEMPLATE
|
|
549
626
|
|
|
550
627
|
def _extract_code(self, raw: str) -> str:
|
|
@@ -620,7 +697,7 @@ class HarnessAgent:
|
|
|
620
697
|
HARNESS_ADDENDUM_MULTI,
|
|
621
698
|
)
|
|
622
699
|
self.model_tier = model_tier
|
|
623
|
-
self._base_llm, _ =
|
|
700
|
+
self._base_llm, _ = _build_llm_tiered(provider, model_name, api_keys, model_tier)
|
|
624
701
|
self._harness_tmpl = HARNESS_TEMPLATE_MULTI + HARNESS_ADDENDUM_MULTI.get(model_tier, "")
|
|
625
702
|
self._fix_tmpl = FIX_TEMPLATE_MULTI + HARNESS_ADDENDUM_MULTI.get(model_tier, "")
|
|
626
703
|
|
|
@@ -638,14 +715,28 @@ class HarnessAgent:
|
|
|
638
715
|
|
|
639
716
|
def _invoke(self, template: str, variables: dict) -> str:
|
|
640
717
|
chain = PromptTemplate.from_template(template) | self._base_llm
|
|
641
|
-
|
|
642
|
-
|
|
718
|
+
try:
|
|
719
|
+
result = chain.invoke(variables)
|
|
720
|
+
except Exception as e:
|
|
721
|
+
err = str(e).lower()
|
|
722
|
+
if any(h in err for h in _TRUNCATION_HINTS):
|
|
723
|
+
raise RuntimeError(
|
|
724
|
+
f"Model hit its context limit during harness generation. "
|
|
725
|
+
f"Detail: {e}"
|
|
726
|
+
) from e
|
|
727
|
+
raise
|
|
728
|
+
raw = result.content if hasattr(result, "content") else str(result)
|
|
643
729
|
if isinstance(raw, list):
|
|
644
730
|
raw = "\n".join(
|
|
645
731
|
item["text"] if isinstance(item, dict) and "text" in item
|
|
646
732
|
else str(item)
|
|
647
733
|
for item in raw
|
|
648
734
|
)
|
|
735
|
+
if _is_truncated(raw):
|
|
736
|
+
raise RuntimeError(
|
|
737
|
+
"Harness output was truncated — model hit its token limit. "
|
|
738
|
+
"Switching to fix loop with truncation note."
|
|
739
|
+
)
|
|
649
740
|
return self._extract_code(raw)
|
|
650
741
|
|
|
651
742
|
def _check_speedup(self, success: bool, logs: str) -> bool:
|
|
@@ -738,7 +829,7 @@ class TestCaseAgent:
|
|
|
738
829
|
model_tier: str,
|
|
739
830
|
) -> None:
|
|
740
831
|
self.model_tier = model_tier
|
|
741
|
-
self._base_llm, _ =
|
|
832
|
+
self._base_llm, _ = _build_llm_tiered(provider, model_name, api_keys, model_tier)
|
|
742
833
|
|
|
743
834
|
def generate(
|
|
744
835
|
self,
|
|
@@ -272,8 +272,15 @@ def process_function(func: dict, language: str, agent: AnalyzerAgent, sandbox: C
|
|
|
272
272
|
return func_name, result, (success, logs, plot_data), None, verification, profiler_result, None, is_valid_optimization
|
|
273
273
|
|
|
274
274
|
except Exception as e:
|
|
275
|
+
err_str = str(e)
|
|
276
|
+
if "context" in err_str.lower() and "limit" in err_str.lower():
|
|
277
|
+
_log(func_name, f"Context limit hit: {e}", style="bold yellow")
|
|
278
|
+
return func_name, None, None, (
|
|
279
|
+
f"⚠️ Context limit: {err_str}\n"
|
|
280
|
+
f"Try a model with a larger context window, or split the function."
|
|
281
|
+
), None, None, None, False
|
|
275
282
|
_log(func_name, f"Failed: {e}", style="bold red")
|
|
276
|
-
return func_name, None, None, f"❌ Analysis failed: {
|
|
283
|
+
return func_name, None, None, f"❌ Analysis failed: {err_str}", None, None, None, False
|
|
277
284
|
|
|
278
285
|
def parse_csv_logs(logs: str):
|
|
279
286
|
"""Safely extracts CSV data from the sandbox logs."""
|
|
@@ -803,12 +810,23 @@ def run_demo(lang: str = "python", no_docker: bool = False):
|
|
|
803
810
|
|
|
804
811
|
run_analysis(str(demo_dir / entry_file), no_docker=no_docker)
|
|
805
812
|
|
|
806
|
-
def _run_memory_cmd(clear: bool):
|
|
813
|
+
def _run_memory_cmd(clear: bool, export_path: str = None, export_fmt: str = "csv"):
|
|
807
814
|
from coreinsight.memory import OptimizationMemory, MEMORY_DIR
|
|
808
815
|
import shutil
|
|
809
816
|
|
|
810
817
|
mem = OptimizationMemory()
|
|
811
818
|
|
|
819
|
+
if export_path:
|
|
820
|
+
count = mem.export(export_path, fmt=export_fmt)
|
|
821
|
+
if count:
|
|
822
|
+
console.print(
|
|
823
|
+
f"[bold green]✅ Exported {count} optimization(s) to "
|
|
824
|
+
f"[cyan]{export_path}[/cyan][/bold green]"
|
|
825
|
+
)
|
|
826
|
+
else:
|
|
827
|
+
console.print("[yellow]Nothing to export — memory store is empty.[/yellow]")
|
|
828
|
+
return
|
|
829
|
+
|
|
812
830
|
if clear:
|
|
813
831
|
if MEMORY_DIR.exists():
|
|
814
832
|
shutil.rmtree(MEMORY_DIR, ignore_errors=True)
|
|
@@ -930,7 +948,12 @@ def main_cli():
|
|
|
930
948
|
index_parser.add_argument("--dir", default=".", help="Directory to index")
|
|
931
949
|
|
|
932
950
|
memory_parser = subparsers.add_parser("memory", help="Inspect or clear the local optimization memory")
|
|
933
|
-
memory_parser.add_argument("--clear",
|
|
951
|
+
memory_parser.add_argument("--clear", action="store_true", help="Wipe the memory store")
|
|
952
|
+
memory_parser.add_argument("--export", dest="export_path", default=None,
|
|
953
|
+
help="Export memory to file (e.g. --export optimizations.csv)")
|
|
954
|
+
memory_parser.add_argument("--format", dest="export_fmt", default="csv",
|
|
955
|
+
choices=["csv", "md"],
|
|
956
|
+
help="Export format: csv (default) or md")
|
|
934
957
|
|
|
935
958
|
view_parser = subparsers.add_parser("view", help="Launch the interactive TUI")
|
|
936
959
|
view_parser.add_argument("--dir", default=".", help="Starting directory (default: current)")
|
|
@@ -954,7 +977,11 @@ def main_cli():
|
|
|
954
977
|
elif args.command == "analyze":
|
|
955
978
|
run_analysis(args.file, no_docker=getattr(args, "no_docker", False))
|
|
956
979
|
elif args.command == "memory":
|
|
957
|
-
_run_memory_cmd(
|
|
980
|
+
_run_memory_cmd(
|
|
981
|
+
clear=getattr(args, "clear", False),
|
|
982
|
+
export_path=getattr(args, "export_path", None),
|
|
983
|
+
export_fmt=getattr(args, "export_fmt", "csv"),
|
|
984
|
+
)
|
|
958
985
|
elif args.command == "scan":
|
|
959
986
|
scanner = ProjectScanner(args.dir)
|
|
960
987
|
scanner.scan_project(max_results=args.top)
|
|
@@ -186,6 +186,77 @@ class OptimizationMemory:
|
|
|
186
186
|
logger.debug(f"Memory lookup failed: {exc}")
|
|
187
187
|
return None
|
|
188
188
|
|
|
189
|
+
def export(self, output_path: str, fmt: str = "csv") -> int:
|
|
190
|
+
"""
|
|
191
|
+
Export all stored optimizations to CSV or Markdown.
|
|
192
|
+
Returns the number of records written, or 0 on failure.
|
|
193
|
+
"""
|
|
194
|
+
import csv as _csv
|
|
195
|
+
from pathlib import Path as _Path
|
|
196
|
+
|
|
197
|
+
if not self._ensure_db():
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
all_records = self._collection.get(include=["metadatas"])
|
|
202
|
+
metadatas = all_records.get("metadatas", []) or []
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.error(f"Export failed reading store: {e}")
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
if not metadatas:
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
# Sort most recent first
|
|
211
|
+
metadatas = sorted(
|
|
212
|
+
metadatas,
|
|
213
|
+
key=lambda m: m.get("timestamp", ""),
|
|
214
|
+
reverse=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
out = _Path(output_path)
|
|
218
|
+
|
|
219
|
+
if fmt == "csv":
|
|
220
|
+
with open(out, "w", newline="", encoding="utf-8") as f:
|
|
221
|
+
writer = _csv.writer(f)
|
|
222
|
+
writer.writerow([
|
|
223
|
+
"func_name", "language", "severity", "issue",
|
|
224
|
+
"avg_speedup", "correctness_cases",
|
|
225
|
+
"hardware_evidence", "verified_at", "optimized_code",
|
|
226
|
+
])
|
|
227
|
+
for m in metadatas:
|
|
228
|
+
writer.writerow([
|
|
229
|
+
m.get("func_name", ""),
|
|
230
|
+
m.get("language", ""),
|
|
231
|
+
m.get("severity", ""),
|
|
232
|
+
m.get("issue", ""),
|
|
233
|
+
m.get("avg_speedup", ""),
|
|
234
|
+
m.get("correctness_cases",""),
|
|
235
|
+
m.get("profiler_summary", ""),
|
|
236
|
+
m.get("timestamp", "")[:19].replace("T", " "),
|
|
237
|
+
m.get("optimized_code", ""),
|
|
238
|
+
])
|
|
239
|
+
|
|
240
|
+
elif fmt == "md":
|
|
241
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
242
|
+
f.write("# CoreInsight Optimization Memory Export\n\n")
|
|
243
|
+
for i, m in enumerate(metadatas, 1):
|
|
244
|
+
lang = m.get("language", "")
|
|
245
|
+
f.write(f"## {i}. `{m.get('func_name', 'unknown')}` ({lang})\n\n")
|
|
246
|
+
f.write(f"- **Severity:** {m.get('severity', '')}\n")
|
|
247
|
+
f.write(f"- **Issue:** {m.get('issue', '')}\n")
|
|
248
|
+
f.write(f"- **Avg speedup:** {float(m.get('avg_speedup', 0)):.2f}x\n")
|
|
249
|
+
f.write(f"- **Correctness cases:** {m.get('correctness_cases', '')}\n")
|
|
250
|
+
f.write(f"- **Verified at:** {m.get('timestamp', '')[:19].replace('T', ' ')}\n")
|
|
251
|
+
if m.get("profiler_summary"):
|
|
252
|
+
f.write(f"- **Hardware evidence:** {m.get('profiler_summary')}\n")
|
|
253
|
+
code = m.get("optimized_code", "").strip()
|
|
254
|
+
if code:
|
|
255
|
+
f.write(f"\n```{lang}\n{code}\n```\n")
|
|
256
|
+
f.write("\n---\n\n")
|
|
257
|
+
|
|
258
|
+
return len(metadatas)
|
|
259
|
+
|
|
189
260
|
def store(
|
|
190
261
|
self,
|
|
191
262
|
original_code: str,
|
|
@@ -22,8 +22,10 @@ from textual.widgets import (
|
|
|
22
22
|
DirectoryTree,
|
|
23
23
|
Footer,
|
|
24
24
|
Header,
|
|
25
|
+
Input,
|
|
25
26
|
Label,
|
|
26
27
|
RichLog,
|
|
28
|
+
Select,
|
|
27
29
|
Static,
|
|
28
30
|
Switch,
|
|
29
31
|
)
|
|
@@ -107,9 +109,17 @@ class MemoryModal(ModalScreen):
|
|
|
107
109
|
MemoryModal #memory-log {
|
|
108
110
|
height: 1fr;
|
|
109
111
|
}
|
|
110
|
-
MemoryModal #
|
|
112
|
+
MemoryModal #memory-buttons {
|
|
111
113
|
margin-top: 1;
|
|
112
|
-
|
|
114
|
+
align: center middle;
|
|
115
|
+
}
|
|
116
|
+
MemoryModal #memory-buttons Button {
|
|
117
|
+
margin: 0 1;
|
|
118
|
+
}
|
|
119
|
+
MemoryModal #export-status {
|
|
120
|
+
height: 1;
|
|
121
|
+
margin-top: 1;
|
|
122
|
+
color: $success;
|
|
113
123
|
}
|
|
114
124
|
"""
|
|
115
125
|
|
|
@@ -117,7 +127,11 @@ class MemoryModal(ModalScreen):
|
|
|
117
127
|
with Container():
|
|
118
128
|
yield Label("Optimization Memory", id="memory-title")
|
|
119
129
|
yield RichLog(id="memory-log", highlight=True, markup=True)
|
|
120
|
-
|
|
130
|
+
with Horizontal(id="memory-buttons"):
|
|
131
|
+
yield Button("Export CSV", id="export-csv", variant="primary")
|
|
132
|
+
yield Button("Export MD", id="export-md", variant="primary")
|
|
133
|
+
yield Button("Close [Esc]",id="close-memory",variant="default")
|
|
134
|
+
yield Label("", id="export-status")
|
|
121
135
|
|
|
122
136
|
def on_mount(self) -> None:
|
|
123
137
|
log = self.query_one("#memory-log", RichLog)
|
|
@@ -194,6 +208,27 @@ class MemoryModal(ModalScreen):
|
|
|
194
208
|
@on(Button.Pressed, "#close-memory")
|
|
195
209
|
def close(self) -> None:
|
|
196
210
|
self.dismiss()
|
|
211
|
+
|
|
212
|
+
@on(Button.Pressed, "#export-csv")
|
|
213
|
+
def export_csv(self) -> None:
|
|
214
|
+
self._do_export("csv")
|
|
215
|
+
|
|
216
|
+
@on(Button.Pressed, "#export-md")
|
|
217
|
+
def export_md(self) -> None:
|
|
218
|
+
self._do_export("md")
|
|
219
|
+
|
|
220
|
+
def _do_export(self, fmt: str) -> None:
|
|
221
|
+
from pathlib import Path
|
|
222
|
+
from coreinsight.memory import OptimizationMemory
|
|
223
|
+
|
|
224
|
+
out = Path.cwd() / f"coreinsight_memory_export.{fmt}"
|
|
225
|
+
mem = OptimizationMemory()
|
|
226
|
+
count = mem.export(str(out), fmt=fmt)
|
|
227
|
+
status = self.query_one("#export-status", Label)
|
|
228
|
+
if count:
|
|
229
|
+
status.update(f"[green]Exported {count} record(s) to {out.name}[/green]")
|
|
230
|
+
else:
|
|
231
|
+
status.update("[yellow]Nothing to export.[/yellow]")
|
|
197
232
|
|
|
198
233
|
|
|
199
234
|
# ---------------------------------------------------------------------------
|
|
@@ -253,65 +288,219 @@ class ConfirmModal(ModalScreen[bool]):
|
|
|
253
288
|
# Settings modal
|
|
254
289
|
# ---------------------------------------------------------------------------
|
|
255
290
|
|
|
256
|
-
class
|
|
257
|
-
"""
|
|
291
|
+
class ConfigureModal(ModalScreen):
|
|
292
|
+
"""Full configure screen — replaces coreinsight configure for TUI users."""
|
|
258
293
|
|
|
259
|
-
BINDINGS = [Binding("escape
|
|
294
|
+
BINDINGS = [Binding("escape", "dismiss", "Cancel")]
|
|
260
295
|
|
|
261
296
|
DEFAULT_CSS = """
|
|
262
|
-
|
|
297
|
+
ConfigureModal {
|
|
263
298
|
align: center middle;
|
|
264
299
|
}
|
|
265
|
-
|
|
266
|
-
width:
|
|
267
|
-
height:
|
|
300
|
+
ConfigureModal > Container {
|
|
301
|
+
width: 70;
|
|
302
|
+
height: 36;
|
|
268
303
|
background: $surface;
|
|
269
304
|
border: thick $primary;
|
|
270
305
|
padding: 1 2;
|
|
271
306
|
}
|
|
272
|
-
|
|
273
|
-
|
|
307
|
+
ConfigureModal .field-label {
|
|
308
|
+
color: $text-muted;
|
|
309
|
+
margin-top: 1;
|
|
310
|
+
}
|
|
311
|
+
ConfigureModal Input {
|
|
312
|
+
margin-bottom: 1;
|
|
313
|
+
}
|
|
314
|
+
ConfigureModal Select {
|
|
274
315
|
margin-bottom: 1;
|
|
275
316
|
}
|
|
276
|
-
|
|
317
|
+
ConfigureModal #configure-buttons {
|
|
318
|
+
margin-top: 1;
|
|
319
|
+
align: center middle;
|
|
320
|
+
}
|
|
321
|
+
ConfigureModal Button {
|
|
322
|
+
margin: 0 1;
|
|
323
|
+
min-width: 16;
|
|
324
|
+
}
|
|
325
|
+
ConfigureModal #configure-status {
|
|
326
|
+
height: 1;
|
|
327
|
+
color: $success;
|
|
277
328
|
margin-top: 1;
|
|
278
|
-
width: 100%;
|
|
279
329
|
}
|
|
280
330
|
"""
|
|
281
331
|
|
|
332
|
+
def __init__(self, **kwargs):
|
|
333
|
+
super().__init__(**kwargs)
|
|
334
|
+
self._config = load_config()
|
|
335
|
+
|
|
282
336
|
def compose(self) -> ComposeResult:
|
|
283
|
-
config
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
model = config.get("model_name", "llama3.2")
|
|
287
|
-
tier = "Pro" if pro_user else "Free"
|
|
288
|
-
tier_col = "bold green" if pro_user else "bold yellow"
|
|
337
|
+
config = self._config
|
|
338
|
+
provider = config.get("provider", "ollama")
|
|
339
|
+
model = config.get("model_name", "llama3.2")
|
|
289
340
|
agent_mode = config.get("agent_mode", "auto")
|
|
341
|
+
local_url = config.get("api_keys", {}).get("local_url", "http://localhost:1234/v1")
|
|
342
|
+
api_keys = config.get("api_keys", {})
|
|
290
343
|
|
|
291
344
|
with Container():
|
|
292
|
-
yield Label("
|
|
293
|
-
|
|
294
|
-
yield
|
|
295
|
-
yield
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
yield Label("Configure CoreInsight", id="configure-title")
|
|
346
|
+
|
|
347
|
+
yield Label("Provider", classes="field-label")
|
|
348
|
+
yield Select(
|
|
349
|
+
[
|
|
350
|
+
("Ollama (local, free)", "ollama"),
|
|
351
|
+
("Local Server (LM Studio etc)","local_server"),
|
|
352
|
+
("OpenAI", "openai"),
|
|
353
|
+
("Anthropic", "anthropic"),
|
|
354
|
+
("Google Gemini", "google"),
|
|
355
|
+
],
|
|
356
|
+
value=provider,
|
|
357
|
+
id="provider-select",
|
|
302
358
|
)
|
|
303
|
-
if not pro_user:
|
|
304
|
-
yield Static(
|
|
305
|
-
f"\n [yellow]Unlock Pro (free during beta):[/yellow]\n"
|
|
306
|
-
f" [cyan underline]{PRO_WAITLIST_URL}[/cyan underline]",
|
|
307
|
-
classes="setting-row",
|
|
308
|
-
)
|
|
309
|
-
yield Button("Close [Esc]", id="close-settings", variant="default")
|
|
310
359
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
360
|
+
yield Label("Model name", classes="field-label")
|
|
361
|
+
yield Input(value=model, placeholder="e.g. llama3.2, gpt-4o, claude-opus-4-5", id="model-input")
|
|
362
|
+
|
|
363
|
+
yield Label("Agent mode", classes="field-label")
|
|
364
|
+
yield Select(
|
|
365
|
+
[
|
|
366
|
+
("Auto (recommended)", "auto"),
|
|
367
|
+
("Single agent", "single"),
|
|
368
|
+
("Multi agent", "multi"),
|
|
369
|
+
],
|
|
370
|
+
value=agent_mode,
|
|
371
|
+
id="agent-mode-select",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Local server URL — shown only for local_server
|
|
375
|
+
yield Label("Local server URL", classes="field-label", id="local-url-label")
|
|
376
|
+
yield Input(
|
|
377
|
+
value=local_url,
|
|
378
|
+
placeholder="http://localhost:1234/v1",
|
|
379
|
+
id="local-url-input",
|
|
380
|
+
)
|
|
314
381
|
|
|
382
|
+
# API key fields — shown per provider
|
|
383
|
+
yield Label("OpenAI API key", classes="field-label", id="openai-label")
|
|
384
|
+
yield Input(
|
|
385
|
+
value=api_keys.get("openai", ""),
|
|
386
|
+
placeholder="sk-...",
|
|
387
|
+
password=True,
|
|
388
|
+
id="openai-input",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
yield Label("Anthropic API key", classes="field-label", id="anthropic-label")
|
|
392
|
+
yield Input(
|
|
393
|
+
value=api_keys.get("anthropic", ""),
|
|
394
|
+
placeholder="sk-ant-...",
|
|
395
|
+
password=True,
|
|
396
|
+
id="anthropic-input",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
yield Label("Google API key", classes="field-label", id="google-label")
|
|
400
|
+
yield Input(
|
|
401
|
+
value=api_keys.get("google", ""),
|
|
402
|
+
placeholder="AIza...",
|
|
403
|
+
password=True,
|
|
404
|
+
id="google-input",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
yield Label("Pro key", classes="field-label")
|
|
408
|
+
yield Input(
|
|
409
|
+
value=config.get("pro_key", ""),
|
|
410
|
+
placeholder="Your CoreInsight Pro key",
|
|
411
|
+
password=True,
|
|
412
|
+
id="pro-key-input",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
with Horizontal(id="configure-buttons"):
|
|
416
|
+
yield Button("Save", id="configure-save", variant="success")
|
|
417
|
+
yield Button("Cancel", id="configure-cancel", variant="default")
|
|
418
|
+
|
|
419
|
+
yield Label("", id="configure-status")
|
|
420
|
+
|
|
421
|
+
def on_mount(self) -> None:
|
|
422
|
+
self._refresh_visibility(self._config.get("provider", "ollama"))
|
|
423
|
+
|
|
424
|
+
@on(Select.Changed, "#provider-select")
|
|
425
|
+
def provider_changed(self, event: Select.Changed) -> None:
|
|
426
|
+
self._refresh_visibility(str(event.value))
|
|
427
|
+
|
|
428
|
+
def _refresh_visibility(self, provider: str) -> None:
|
|
429
|
+
"""Show only the fields relevant to the selected provider."""
|
|
430
|
+
local_ids = ["local-url-label", "local-url-input"]
|
|
431
|
+
openai_ids = ["openai-label", "openai-input"]
|
|
432
|
+
anthropic_ids= ["anthropic-label", "anthropic-input"]
|
|
433
|
+
google_ids = ["google-label", "google-input"]
|
|
434
|
+
|
|
435
|
+
all_conditional = local_ids + openai_ids + anthropic_ids + google_ids
|
|
436
|
+
|
|
437
|
+
# Hide everything first
|
|
438
|
+
for wid in all_conditional:
|
|
439
|
+
try:
|
|
440
|
+
self.query_one(f"#{wid}").display = False
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
# Show relevant ones
|
|
445
|
+
show = {
|
|
446
|
+
"local_server": local_ids,
|
|
447
|
+
"openai": openai_ids,
|
|
448
|
+
"anthropic": anthropic_ids,
|
|
449
|
+
"google": google_ids,
|
|
450
|
+
}.get(provider, [])
|
|
451
|
+
|
|
452
|
+
for wid in show:
|
|
453
|
+
try:
|
|
454
|
+
self.query_one(f"#{wid}").display = True
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
@on(Button.Pressed, "#configure-save")
|
|
459
|
+
def save(self) -> None:
|
|
460
|
+
import json
|
|
461
|
+
from pathlib import Path
|
|
462
|
+
|
|
463
|
+
provider = str(self.query_one("#provider-select", Select).value)
|
|
464
|
+
model = self.query_one("#model-input", Input).value.strip()
|
|
465
|
+
agent_mode = str(self.query_one("#agent-mode-select", Select).value)
|
|
466
|
+
pro_key = self.query_one("#pro-key-input", Input).value.strip()
|
|
467
|
+
|
|
468
|
+
if not model:
|
|
469
|
+
self.query_one("#configure-status", Label).update(
|
|
470
|
+
"[red]Model name cannot be empty.[/red]"
|
|
471
|
+
)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# Build updated config
|
|
475
|
+
config = dict(self._config)
|
|
476
|
+
config["provider"] = provider
|
|
477
|
+
config["model_name"] = model
|
|
478
|
+
config["agent_mode"] = agent_mode
|
|
479
|
+
|
|
480
|
+
api_keys = dict(config.get("api_keys", {}))
|
|
481
|
+
api_keys["local_url"] = self.query_one("#local-url-input", Input).value.strip()
|
|
482
|
+
api_keys["openai"] = self.query_one("#openai-input", Input).value.strip()
|
|
483
|
+
api_keys["anthropic"] = self.query_one("#anthropic-input", Input).value.strip()
|
|
484
|
+
api_keys["google"] = self.query_one("#google-input", Input).value.strip()
|
|
485
|
+
config["api_keys"] = api_keys
|
|
486
|
+
|
|
487
|
+
if pro_key:
|
|
488
|
+
config["pro_key"] = pro_key
|
|
489
|
+
|
|
490
|
+
config_dir = Path.home() / ".coreinsight"
|
|
491
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
492
|
+
config_path = config_dir / "config.json"
|
|
493
|
+
with open(config_path, "w") as f:
|
|
494
|
+
json.dump(config, f, indent=2)
|
|
495
|
+
|
|
496
|
+
self.query_one("#configure-status", Label).update(
|
|
497
|
+
"[green]Saved. Restart coreinsight view to apply changes.[/green]"
|
|
498
|
+
)
|
|
499
|
+
self._config = config
|
|
500
|
+
|
|
501
|
+
@on(Button.Pressed, "#configure-cancel")
|
|
502
|
+
def cancel(self) -> None:
|
|
503
|
+
self.dismiss()
|
|
315
504
|
|
|
316
505
|
# ---------------------------------------------------------------------------
|
|
317
506
|
# Main TUI App
|
|
@@ -323,12 +512,13 @@ class CoreInsightApp(App):
|
|
|
323
512
|
CSS_PATH = None # all CSS inline below
|
|
324
513
|
|
|
325
514
|
BINDINGS = [
|
|
326
|
-
Binding("q",
|
|
327
|
-
Binding("a",
|
|
328
|
-
Binding("i",
|
|
329
|
-
Binding("m",
|
|
330
|
-
Binding("
|
|
331
|
-
Binding("
|
|
515
|
+
Binding("q", "quit", "Quit"),
|
|
516
|
+
Binding("a", "analyze", "Analyze"),
|
|
517
|
+
Binding("i", "index", "Index"),
|
|
518
|
+
Binding("m", "view_memory", "Memory"),
|
|
519
|
+
Binding("c", "configure", "Configure"),
|
|
520
|
+
Binding("d", "demo", "Demo"),
|
|
521
|
+
Binding("ctrl+c", "quit", "Quit", show=False),
|
|
332
522
|
]
|
|
333
523
|
|
|
334
524
|
CSS = """
|
|
@@ -456,10 +646,11 @@ class CoreInsightApp(App):
|
|
|
456
646
|
|
|
457
647
|
with Vertical(id="action-panel"):
|
|
458
648
|
yield Label("Actions", id="action-label")
|
|
459
|
-
yield Button("Analyze
|
|
460
|
-
yield Button("Index
|
|
461
|
-
yield Button("
|
|
462
|
-
yield Button("
|
|
649
|
+
yield Button("Analyze [a]", id="btn-analyze", variant="success")
|
|
650
|
+
yield Button("Index [i]", id="btn-index", variant="primary")
|
|
651
|
+
yield Button("Demo [d]", id="btn-demo", variant="primary")
|
|
652
|
+
yield Button("Memory [m]", id="btn-memory", variant="default")
|
|
653
|
+
yield Button("Configure [c]", id="btn-configure", variant="default")
|
|
463
654
|
with Horizontal(id="no-docker-row"):
|
|
464
655
|
yield Switch(value=False, id="no-docker-switch")
|
|
465
656
|
yield Label("Skip Docker", id="no-docker-label")
|
|
@@ -562,9 +753,46 @@ class CoreInsightApp(App):
|
|
|
562
753
|
def action_view_memory(self) -> None:
|
|
563
754
|
self.push_screen(MemoryModal())
|
|
564
755
|
|
|
565
|
-
@on(Button.Pressed, "#btn-
|
|
566
|
-
def
|
|
567
|
-
self.push_screen(
|
|
756
|
+
@on(Button.Pressed, "#btn-configure")
|
|
757
|
+
def action_configure(self) -> None:
|
|
758
|
+
self.push_screen(ConfigureModal())
|
|
759
|
+
|
|
760
|
+
@on(Button.Pressed, "#btn-demo")
|
|
761
|
+
def action_demo(self) -> None:
|
|
762
|
+
if self._busy:
|
|
763
|
+
self._set_status("Already running — please wait.")
|
|
764
|
+
return
|
|
765
|
+
no_docker = self.query_one("#no-docker-switch", Switch).value
|
|
766
|
+
self._start_demo(no_docker)
|
|
767
|
+
|
|
768
|
+
@work(thread=True)
|
|
769
|
+
def _start_demo(self, no_docker: bool) -> None:
|
|
770
|
+
from coreinsight.main import run_demo
|
|
771
|
+
|
|
772
|
+
log = self.query_one("#output-log", RichLog)
|
|
773
|
+
tui_console = TuiConsole(log)
|
|
774
|
+
self._busy = True
|
|
775
|
+
|
|
776
|
+
self.call_from_thread(self._set_status, "Running demo...")
|
|
777
|
+
self.call_from_thread(
|
|
778
|
+
log.write,
|
|
779
|
+
"\n[bold cyan]Running built-in Python demo...[/bold cyan]\n"
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# Temporarily patch the demo's console output into the TUI
|
|
783
|
+
import coreinsight.main as _main
|
|
784
|
+
_prev = _main.console
|
|
785
|
+
_main.console = tui_console
|
|
786
|
+
try:
|
|
787
|
+
run_demo(lang="python", no_docker=no_docker)
|
|
788
|
+
except SystemExit:
|
|
789
|
+
pass
|
|
790
|
+
except Exception as exc:
|
|
791
|
+
self.call_from_thread(log.write, f"[red]Demo error: {exc}[/red]")
|
|
792
|
+
finally:
|
|
793
|
+
_main.console = _prev
|
|
794
|
+
self._busy = False
|
|
795
|
+
self.call_from_thread(self._set_status, "Demo complete.")
|
|
568
796
|
|
|
569
797
|
# ── Workers ───────────────────────────────────────────────────────────
|
|
570
798
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coreinsight-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "Local-first AI performance profiler that mathematically verifies optimizations for Python, C++, and CUDA"
|
|
9
9
|
license = {text = "GPL-3.0-or-later"}
|
|
10
10
|
authors = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|