Skip to content

Commit f77423c

Browse files
authored
Merge branch 'main' into feat/verify-and-submit-api-key-in-lsp
2 parents a0ff82f + baee96e commit f77423c

File tree

95 files changed

+3746
-3120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+3746
-3120
lines changed

.github/workflows/deploy-docs-to-azure.yaml

Lines changed: 0 additions & 31 deletions
This file was deleted.

.github/workflows/unit-tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
python-version: ["3.9", "3.10", "3.11", "3.12"]
14+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1515
continue-on-error: true
1616
runs-on: ubuntu-latest
1717
steps:
@@ -30,4 +30,4 @@ jobs:
3030
run: uv sync
3131

3232
- name: Unit tests
33-
run: uv run pytest tests/ --benchmark-skip -m "not ci_skip"
33+
run: uv run pytest tests/

.pre-commit-config.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
repos:
2-
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: "v0.11.0"
4-
hooks:
5-
- id: ruff
6-
args: [--fix, --exit-non-zero-on-fix, --config=pyproject.toml]
7-
- id: ruff-format
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.12.7
4+
hooks:
5+
# Run the linter.
6+
- id: ruff-check
7+
# Run the formatter.
8+
- id: ruff-format
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""CodeFlash Benchmark - Pytest benchmarking plugin for codeflash.ai."""
2+
3+
__version__ = "0.1.0"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
import importlib.util
4+
5+
import pytest
6+
7+
from codeflash.benchmarking.plugin.plugin import codeflash_benchmark_plugin
8+
9+
PYTEST_BENCHMARK_INSTALLED = importlib.util.find_spec("pytest_benchmark") is not None
10+
11+
12+
def pytest_configure(config: pytest.Config) -> None:
13+
"""Register the benchmark marker and disable conflicting plugins."""
14+
config.addinivalue_line("markers", "benchmark: mark test as a benchmark that should be run with codeflash tracing")
15+
16+
if config.getoption("--codeflash-trace") and PYTEST_BENCHMARK_INSTALLED:
17+
config.option.benchmark_disable = True
18+
config.pluginmanager.set_blocked("pytest_benchmark")
19+
config.pluginmanager.set_blocked("pytest-benchmark")
20+
21+
22+
def pytest_addoption(parser: pytest.Parser) -> None:
23+
parser.addoption(
24+
"--codeflash-trace", action="store_true", default=False, help="Enable CodeFlash tracing for benchmarks"
25+
)
26+
27+
28+
@pytest.fixture
29+
def benchmark(request: pytest.FixtureRequest) -> object:
30+
"""Benchmark fixture that works with or without pytest-benchmark installed."""
31+
config = request.config
32+
33+
# If --codeflash-trace is enabled, use our implementation
34+
if config.getoption("--codeflash-trace"):
35+
return codeflash_benchmark_plugin.Benchmark(request)
36+
37+
# If pytest-benchmark is installed and --codeflash-trace is not enabled,
38+
# return the normal pytest-benchmark fixture
39+
if PYTEST_BENCHMARK_INSTALLED:
40+
from pytest_benchmark.fixture import BenchmarkFixture as BSF # noqa: N814
41+
42+
bs = getattr(config, "_benchmarksession", None)
43+
if bs and bs.skip:
44+
pytest.skip("Benchmarks are skipped (--benchmark-skip was used).")
45+
46+
node = request.node
47+
marker = node.get_closest_marker("benchmark")
48+
options = dict(marker.kwargs) if marker else {}
49+
50+
if bs:
51+
return BSF(
52+
node,
53+
add_stats=bs.benchmarks.append,
54+
logger=bs.logger,
55+
warner=request.node.warn,
56+
disabled=bs.disabled,
57+
**dict(bs.options, **options),
58+
)
59+
return lambda func, *args, **kwargs: func(*args, **kwargs)
60+
61+
return lambda func, *args, **kwargs: func(*args, **kwargs)

codeflash-benchmark/pyproject.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[project]
2+
name = "codeflash-benchmark"
3+
version = "0.1.0"
4+
description = "Pytest benchmarking plugin for codeflash.ai - automatic code performance optimization"
5+
authors = [{ name = "CodeFlash Inc.", email = "[email protected]" }]
6+
requires-python = ">=3.9"
7+
readme = "README.md"
8+
license = {text = "BSL-1.1"}
9+
keywords = [
10+
"codeflash",
11+
"benchmark",
12+
"pytest",
13+
"performance",
14+
"testing",
15+
]
16+
dependencies = [
17+
"pytest>=7.0.0,!=8.3.4",
18+
]
19+
20+
[project.urls]
21+
Homepage = "https://codeflash.ai"
22+
Repository = "https://github.com/codeflash-ai/codeflash-benchmark"
23+
24+
[project.entry-points.pytest11]
25+
codeflash-benchmark = "codeflash_benchmark.plugin"
26+
27+
[build-system]
28+
requires = ["setuptools>=45", "wheel", "setuptools_scm"]
29+
build-backend = "setuptools.build_meta"
30+
31+
[tool.setuptools]
32+
packages = ["codeflash_benchmark"]

codeflash/api/aiservice.py

Lines changed: 154 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from codeflash.cli_cmds.console import console, logger
1313
from codeflash.code_utils.env_utils import get_codeflash_api_key, is_LSP_enabled
1414
from codeflash.code_utils.git_utils import get_last_commit_author_if_pr_exists, get_repo_owner_and_name
15-
from codeflash.models.models import OptimizedCandidate
15+
from codeflash.models.ExperimentMetadata import ExperimentMetadata
16+
from codeflash.models.models import AIServiceRefinerRequest, OptimizedCandidate
1617
from codeflash.telemetry.posthog_cf import ph
1718
from codeflash.version import __version__ as codeflash_version
1819

@@ -21,6 +22,7 @@
2122

2223
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
2324
from codeflash.models.ExperimentMetadata import ExperimentMetadata
25+
from codeflash.models.models import AIServiceRefinerRequest
2426

2527

2628
class AiServiceClient:
@@ -36,7 +38,11 @@ def get_aiservice_base_url(self) -> str:
3638
return "https://app.codeflash.ai"
3739

3840
def make_ai_service_request(
39-
self, endpoint: str, method: str = "POST", payload: dict[str, Any] | None = None, timeout: float | None = None
41+
self,
42+
endpoint: str,
43+
method: str = "POST",
44+
payload: dict[str, Any] | list[dict[str, Any]] | None = None,
45+
timeout: float | None = None,
4046
) -> requests.Response:
4147
"""Make an API request to the given endpoint on the AI service.
4248
@@ -98,11 +104,7 @@ def optimize_python_code( # noqa: D417
98104
99105
"""
100106
start_time = time.perf_counter()
101-
try:
102-
git_repo_owner, git_repo_name = get_repo_owner_and_name()
103-
except Exception as e:
104-
logger.warning(f"Could not determine repo owner and name: {e}")
105-
git_repo_owner, git_repo_name = None, None
107+
git_repo_owner, git_repo_name = safe_get_repo_owner_and_name()
106108

107109
payload = {
108110
"source_code": source_code,
@@ -219,13 +221,145 @@ def optimize_python_code_line_profiler( # noqa: D417
219221
console.rule()
220222
return []
221223

224+
def optimize_python_code_refinement(self, request: list[AIServiceRefinerRequest]) -> list[OptimizedCandidate]:
225+
"""Optimize the given python code for performance by making a request to the Django endpoint.
226+
227+
Args:
228+
request: A list of optimization candidate details for refinement
229+
230+
Returns:
231+
-------
232+
- List[OptimizationCandidate]: A list of Optimization Candidates.
233+
234+
"""
235+
payload = [
236+
{
237+
"optimization_id": opt.optimization_id,
238+
"original_source_code": opt.original_source_code,
239+
"read_only_dependency_code": opt.read_only_dependency_code,
240+
"original_line_profiler_results": opt.original_line_profiler_results,
241+
"original_code_runtime": opt.original_code_runtime,
242+
"optimized_source_code": opt.optimized_source_code,
243+
"optimized_explanation": opt.optimized_explanation,
244+
"optimized_line_profiler_results": opt.optimized_line_profiler_results,
245+
"optimized_code_runtime": opt.optimized_code_runtime,
246+
"speedup": opt.speedup,
247+
"trace_id": opt.trace_id,
248+
}
249+
for opt in request
250+
]
251+
logger.info(f"Refining {len(request)} optimizations…")
252+
console.rule()
253+
try:
254+
response = self.make_ai_service_request("/refinement", payload=payload, timeout=600)
255+
except requests.exceptions.RequestException as e:
256+
logger.exception(f"Error generating optimization refinements: {e}")
257+
ph("cli-optimize-error-caught", {"error": str(e)})
258+
return []
259+
260+
if response.status_code == 200:
261+
refined_optimizations = response.json()["refinements"]
262+
logger.info(f"Generated {len(refined_optimizations)} candidate refinements.")
263+
console.rule()
264+
return [
265+
OptimizedCandidate(
266+
source_code=opt["source_code"],
267+
explanation=opt["explanation"],
268+
optimization_id=opt["optimization_id"][:-4] + "refi",
269+
)
270+
for opt in refined_optimizations
271+
]
272+
try:
273+
error = response.json()["error"]
274+
except Exception:
275+
error = response.text
276+
logger.error(f"Error generating optimized candidates: {response.status_code} - {error}")
277+
ph("cli-optimize-error-response", {"response_status_code": response.status_code, "error": error})
278+
console.rule()
279+
return []
280+
281+
def get_new_explanation( # noqa: D417
282+
self,
283+
source_code: str,
284+
optimized_code: str,
285+
dependency_code: str,
286+
trace_id: str,
287+
original_line_profiler_results: str,
288+
optimized_line_profiler_results: str,
289+
original_code_runtime: str,
290+
optimized_code_runtime: str,
291+
speedup: str,
292+
annotated_tests: str,
293+
optimization_id: str,
294+
original_explanation: str,
295+
) -> str:
296+
"""Optimize the given python code for performance by making a request to the Django endpoint.
297+
298+
Parameters
299+
----------
300+
- source_code (str): The python code to optimize.
301+
- optimized_code (str): The python code generated by the AI service.
302+
- dependency_code (str): The dependency code used as read-only context for the optimization
303+
- original_line_profiler_results: str - line profiler results for the baseline code
304+
- optimized_line_profiler_results: str - line profiler results for the optimized code
305+
- original_code_runtime: str - runtime for the baseline code
306+
- optimized_code_runtime: str - runtime for the optimized code
307+
- speedup: str - speedup of the optimized code
308+
- annotated_tests: str - test functions annotated with runtime
309+
- optimization_id: str - unique id of opt candidate
310+
- original_explanation: str - original_explanation generated for the opt candidate
311+
312+
Returns
313+
-------
314+
- List[OptimizationCandidate]: A list of Optimization Candidates.
315+
316+
"""
317+
payload = {
318+
"trace_id": trace_id,
319+
"source_code": source_code,
320+
"optimized_code": optimized_code,
321+
"original_line_profiler_results": original_line_profiler_results,
322+
"optimized_line_profiler_results": optimized_line_profiler_results,
323+
"original_code_runtime": original_code_runtime,
324+
"optimized_code_runtime": optimized_code_runtime,
325+
"speedup": speedup,
326+
"annotated_tests": annotated_tests,
327+
"optimization_id": optimization_id,
328+
"original_explanation": original_explanation,
329+
"dependency_code": dependency_code,
330+
}
331+
logger.info("Generating explanation")
332+
console.rule()
333+
try:
334+
response = self.make_ai_service_request("/explain", payload=payload, timeout=60)
335+
except requests.exceptions.RequestException as e:
336+
logger.exception(f"Error generating explanations: {e}")
337+
ph("cli-optimize-error-caught", {"error": str(e)})
338+
return ""
339+
340+
if response.status_code == 200:
341+
explanation: str = response.json()["explanation"]
342+
logger.debug(f"New Explanation: {explanation}")
343+
console.rule()
344+
return explanation
345+
try:
346+
error = response.json()["error"]
347+
except Exception:
348+
error = response.text
349+
logger.error(f"Error generating optimized candidates: {response.status_code} - {error}")
350+
ph("cli-optimize-error-response", {"response_status_code": response.status_code, "error": error})
351+
console.rule()
352+
return ""
353+
222354
def log_results( # noqa: D417
223355
self,
224356
function_trace_id: str,
225357
speedup_ratio: dict[str, float | None] | None,
226358
original_runtime: float | None,
227359
optimized_runtime: dict[str, float | None] | None,
228360
is_correct: dict[str, bool] | None,
361+
optimized_line_profiler_results: dict[str, str] | None,
362+
metadata: dict[str, Any] | None,
229363
) -> None:
230364
"""Log features to the database.
231365
@@ -236,6 +370,8 @@ def log_results( # noqa: D417
236370
- original_runtime (Optional[Dict[str, float]]): The original runtime.
237371
- optimized_runtime (Optional[Dict[str, float]]): The optimized runtime.
238372
- is_correct (Optional[Dict[str, bool]]): Whether the optimized code is correct.
373+
- optimized_line_profiler_results: line_profiler results for every candidate mapped to their optimization_id
374+
- metadata: contains the best optimization id
239375
240376
"""
241377
payload = {
@@ -245,6 +381,8 @@ def log_results( # noqa: D417
245381
"optimized_runtime": optimized_runtime,
246382
"is_correct": is_correct,
247383
"codeflash_version": codeflash_version,
384+
"optimized_line_profiler_results": optimized_line_profiler_results,
385+
"metadata": metadata,
248386
}
249387
try:
250388
self.make_ai_service_request("/log_features", payload=payload, timeout=5)
@@ -331,3 +469,12 @@ class LocalAiServiceClient(AiServiceClient):
331469
def get_aiservice_base_url(self) -> str:
332470
"""Get the base URL for the local AI service."""
333471
return "http://localhost:8000"
472+
473+
474+
def safe_get_repo_owner_and_name() -> tuple[str | None, str | None]:
475+
try:
476+
git_repo_owner, git_repo_name = get_repo_owner_and_name()
477+
except Exception as e:
478+
logger.warning(f"Could not determine repo owner and name: {e}")
479+
git_repo_owner, git_repo_name = None, None
480+
return git_repo_owner, git_repo_name

0 commit comments

Comments
 (0)