Skip to content

Commit 6f6c039

Browse files
authored
Merge pull request #1886 from codeflash-ai/codeflash-core
feat: introduce codeflash_core engine with Rich console UI
2 parents cab52ba + 347316f commit 6f6c039

27 files changed

Lines changed: 6004 additions & 3094 deletions

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ dependencies = [
3030
"tree-sitter-kotlin>=1.0.0",
3131
"pytest-timeout>=2.1.0",
3232
"tomlkit>=0.11.7",
33+
"attrs>=23.1.0",
34+
"requests>=2.28.0",
3335
"junitparser>=3.1.0",
3436
"pydantic>=1.10.1",
3537
"humanize>=4.0.0",
@@ -102,7 +104,7 @@ tests = [
102104
]
103105

104106
[tool.hatch.build.targets.sdist]
105-
include = ["codeflash"]
107+
include = ["codeflash", "src/codeflash_core"]
106108
exclude = [
107109
"docs/*",
108110
"experiments/*",

src/codeflash_core/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from codeflash_core.config import CoreConfig, TestConfig
2+
from codeflash_core.models import (
3+
BenchmarkResults,
4+
Candidate,
5+
CodeContext,
6+
FunctionToOptimize,
7+
HelperFunction,
8+
OptimizationResult,
9+
ScoredCandidate,
10+
TestOutcome,
11+
TestOutcomeStatus,
12+
TestResults,
13+
)
14+
from codeflash_core.optimizer import Optimizer
15+
from codeflash_core.protocols import LanguagePlugin
16+
17+
__all__ = [
18+
"BenchmarkResults",
19+
"Candidate",
20+
"CodeContext",
21+
"CoreConfig",
22+
"FunctionToOptimize",
23+
"HelperFunction",
24+
"LanguagePlugin",
25+
"OptimizationResult",
26+
"Optimizer",
27+
"ScoredCandidate",
28+
"TestConfig",
29+
"TestOutcome",
30+
"TestOutcomeStatus",
31+
"TestResults",
32+
]

src/codeflash_core/ai/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from codeflash_core.ai.client import AIClient
2+
3+
__all__ = ["AIClient"]

src/codeflash_core/ai/client.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import TYPE_CHECKING
5+
6+
import requests
7+
8+
if TYPE_CHECKING:
9+
from codeflash_core.config import AIConfig
10+
from codeflash_core.models import Candidate, CodeContext
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class AIClient:
16+
"""Client for the Codeflash AI optimization service."""
17+
18+
def __init__(self, config: AIConfig) -> None:
19+
self.base_url = config.base_url.rstrip("/")
20+
self.api_key = config.api_key
21+
self.timeout = config.timeout
22+
self.session = requests.Session()
23+
if self.api_key:
24+
self.session.headers["Authorization"] = f"Bearer {self.api_key}"
25+
26+
def get_candidates(self, context: CodeContext) -> list[Candidate]:
27+
"""Request optimization candidates from the AI service."""
28+
from codeflash_core.models import Candidate
29+
30+
payload = {
31+
"function_name": context.target_function.qualified_name,
32+
"source_code": context.target_code,
33+
"helper_functions": [
34+
{"name": h.qualified_name, "source_code": h.source_code} for h in context.helper_functions
35+
],
36+
"read_only_context": context.read_only_context,
37+
"imports": context.imports,
38+
}
39+
40+
try:
41+
resp = self.session.post(f"{self.base_url}/ai/optimize", json=payload, timeout=self.timeout)
42+
resp.raise_for_status()
43+
data = resp.json()
44+
except requests.RequestException:
45+
logger.exception("AI service request failed")
46+
return []
47+
48+
candidates = []
49+
for item in data.get("candidates", []):
50+
candidates.append(Candidate(code=item["code"], explanation=item.get("explanation", "")))
51+
return candidates
52+
53+
def close(self) -> None:
54+
self.session.close()

src/codeflash_core/cli.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""CLI entry point for codeflash."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import logging
7+
import os
8+
import sys
9+
from pathlib import Path
10+
from typing import TYPE_CHECKING
11+
12+
if TYPE_CHECKING:
13+
from codeflash_core.strategy_utils import OptimizationStrategy
14+
15+
16+
def build_parser() -> argparse.ArgumentParser:
17+
parser = argparse.ArgumentParser(prog="cfnext", description="Optimize Python code with AI.")
18+
parser.add_argument("--version", action="store_true", help="Print the version and exit.")
19+
parser.add_argument(
20+
"--show-config", action="store_true", help="Show current or auto-detected configuration and exit."
21+
)
22+
sub = parser.add_subparsers(dest="command")
23+
24+
opt = sub.add_parser("optimize", help="Optimize functions in the given files.")
25+
opt.add_argument("files", nargs="*", help="Python files to optimize.")
26+
opt.add_argument("--file", dest="target_file", default=None, help="Single file to optimize.")
27+
opt.add_argument("--function", dest="target_function", default=None, help="Specific function name to optimize.")
28+
opt.add_argument("--all", action="store_true", dest="optimize_all", help="Optimize all files in module-root.")
29+
opt.add_argument("--project-root", type=Path, default=None, help="Override project root directory.")
30+
opt.add_argument("--module-root", default=None, help="Override module root (relative to project root).")
31+
opt.add_argument("--tests-root", default=None, help="Override tests root (relative to project root).")
32+
opt.add_argument("--benchmarks-root", default=None, help="Override benchmarks root (relative to project root).")
33+
opt.add_argument("--api-key", default=None, help="Codeflash API key (default: $CODEFLASH_API_KEY).")
34+
opt.add_argument("--effort", choices=["low", "medium", "high"], default=None, help="Effort level for optimization.")
35+
opt.add_argument(
36+
"--server",
37+
choices=["local", "prod"],
38+
default=None,
39+
help="AI service server: 'local' for localhost:8000, 'prod' for app.codeflash.ai.",
40+
)
41+
opt.add_argument(
42+
"--benchmark", action="store_true", help="Trace benchmark tests and calculate optimization impact."
43+
)
44+
opt.add_argument("--no-pr", action="store_true", help="Do not create a PR, only update code locally.")
45+
opt.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts.")
46+
opt.add_argument(
47+
"--formatter-cmds", nargs="*", default=None, help="Override formatter commands (each applied to $file)."
48+
)
49+
opt.add_argument("--pytest-cmd", default=None, help="Override the pytest command to use.")
50+
opt.add_argument("--disable-telemetry", action="store_true", help="Disable telemetry.")
51+
opt.add_argument(
52+
"--strategy", choices=["default"], default="default", help="Optimization strategy to use (default: default)."
53+
)
54+
opt.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging.")
55+
56+
return parser
57+
58+
59+
def collect_files(config_project_root: Path, config_module_root: str) -> list[Path]:
60+
"""Collect all .py files under the module root."""
61+
module_dir = config_project_root / config_module_root if config_module_root else config_project_root
62+
if not module_dir.is_dir():
63+
return []
64+
return sorted(
65+
p for p in module_dir.rglob("*.py") if not p.name.startswith("test_") and not p.name.endswith("_test.py")
66+
)
67+
68+
69+
SERVER_URLS = {"local": "http://localhost:8000", "prod": "https://app.codeflash.ai"}
70+
71+
72+
def main(argv: list[str] | None = None) -> int:
73+
parser = build_parser()
74+
args = parser.parse_args(argv)
75+
76+
# -- Top-level flags (no subcommand needed) ------------------------------
77+
if args.version:
78+
from importlib.metadata import version
79+
80+
print(f"cfnext {version('codeflash-core')}")
81+
return 0
82+
83+
# -- Config (needed for --show-config and optimize) ----------------------
84+
from codeflash_core.config import CoreConfig
85+
86+
start_dir = getattr(args, "project_root", None) or Path.cwd()
87+
config = CoreConfig.find_and_load(start_dir)
88+
89+
if args.show_config:
90+
import json
91+
92+
info = {
93+
"project_root": str(config.project_root),
94+
"module_root": config.module_root,
95+
"tests_root": config.tests_root,
96+
"benchmarks_root": config.benchmarks_root,
97+
"effort": config.effort,
98+
"formatter_cmds": config.formatter_cmds,
99+
"ignore_paths": config.ignore_paths,
100+
"ai_base_url": config.ai.base_url,
101+
"disable_telemetry": config.disable_telemetry,
102+
}
103+
print(json.dumps(info, indent=2))
104+
return 0
105+
106+
if args.command is None:
107+
parser.print_help()
108+
return 0
109+
110+
if args.command != "optimize":
111+
parser.print_help()
112+
return 1
113+
114+
# -- Logging -------------------------------------------------------------
115+
from codeflash_core.ui import setup_logging
116+
117+
setup_logging(level=logging.DEBUG if args.verbose else logging.WARNING)
118+
119+
# CLI overrides
120+
if args.project_root:
121+
config.project_root = args.project_root.resolve()
122+
if args.module_root is not None:
123+
config.module_root = args.module_root
124+
if args.tests_root is not None:
125+
config.tests_root = args.tests_root
126+
if args.benchmarks_root is not None:
127+
config.benchmarks_root = args.benchmarks_root
128+
if args.effort is not None:
129+
config.effort = args.effort
130+
if args.server is not None:
131+
config.ai.base_url = SERVER_URLS[args.server]
132+
if args.formatter_cmds is not None:
133+
config.formatter_cmds = args.formatter_cmds
134+
if args.disable_telemetry:
135+
config.disable_telemetry = True
136+
137+
# API key: CLI flag > env var > config file
138+
api_key = args.api_key or os.environ.get("CODEFLASH_API_KEY", "") or config.ai.api_key
139+
if not api_key:
140+
print("Error: No API key provided. Set CODEFLASH_API_KEY or pass --api-key.")
141+
return 1
142+
config.ai.api_key = api_key
143+
144+
# -- Telemetry -----------------------------------------------------------
145+
from codeflash_core.telemetry import PostHogClient, init_sentry
146+
147+
if not config.disable_telemetry:
148+
PostHogClient.initialize(config.telemetry.posthog_api_key, enabled=config.telemetry.enabled)
149+
init_sentry(config.telemetry.sentry_dsn, enabled=config.telemetry.enabled)
150+
151+
# -- Resolve files -------------------------------------------------------
152+
if args.target_file:
153+
files = [Path(args.target_file).resolve()]
154+
elif args.optimize_all:
155+
files = collect_files(config.project_root, config.module_root)
156+
elif args.files:
157+
files = [Path(f).resolve() for f in args.files]
158+
else:
159+
print("Error: Provide files to optimize, use --file, or use --all.")
160+
return 1
161+
162+
if not files:
163+
print("Error: No Python files found.")
164+
return 1
165+
166+
# -- Build plugin & validate environment ---------------------------------
167+
from codeflash_core.optimizer import Optimizer
168+
169+
try:
170+
from codeflash.plugin import PythonPlugin
171+
except ImportError:
172+
print("Error: codeflash package not installed. Install it to use the Python plugin.")
173+
return 1
174+
175+
plugin = PythonPlugin(config.project_root)
176+
177+
if hasattr(plugin, "validate_environment") and not plugin.validate_environment(config):
178+
return 1
179+
180+
# -- Resolve strategy ----------------------------------------------------
181+
strategy = _resolve_strategy(args.strategy)
182+
183+
optimizer = Optimizer(config, plugin, strategy=strategy)
184+
results = optimizer.run(files, function_filter=args.target_function)
185+
186+
# -- Shutdown telemetry --------------------------------------------------
187+
if PostHogClient.instance is not None:
188+
PostHogClient.instance.shutdown()
189+
190+
return 0 if results else 2
191+
192+
193+
def _resolve_strategy(name: str) -> OptimizationStrategy:
194+
"""Return the strategy instance for the given CLI name."""
195+
from codeflash_core.strategy import DefaultStrategy
196+
197+
return DefaultStrategy()
198+
199+
200+
if __name__ == "__main__":
201+
sys.exit(main())

0 commit comments

Comments
 (0)