formatting

formatting
This commit is contained in:
Kevin Turcios 2025-12-22 02:56:59 -05:00
parent 1febc07527
commit d0400c0907
25 changed files with 200 additions and 289 deletions

View file

@ -1,5 +1,6 @@
{
"python.analysis.typeCheckingMode": "basic",
"python.defaultInterpreterPath": "${workspaceFolder}/django/aiservice/.venv/bin/python",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {

View file

@ -23,6 +23,7 @@ RATE_LIMIT_MAX = int(os.getenv("RATE_LIMIT_MAX", "40"))
# - Consider using asyncio.PriorityQueue or celery with priority tasks
# - Reference: authapp/models.py:Subscriptions.plan_type, CFAPIKeys.tier
@async_only_middleware
class RateLimitMiddleware:
def __init__(self, get_response) -> None:

View file

@ -14,19 +14,9 @@ Main components:
"""
# Models
from .models import (
LLMCall,
OptimizationError,
)
# Logging utilities
from .logger import (
StructuredFormatter,
get_observability_logger,
log_with_context,
LogContext,
EventType,
)
from .logger import EventType, LogContext, StructuredFormatter, get_observability_logger, log_with_context
from .models import LLMCall, OptimizationError
__all__ = [
# Models
@ -38,4 +28,4 @@ __all__ = [
"log_with_context",
"LogContext",
"EventType",
]
]

View file

@ -4,13 +4,10 @@ Simple recorders for manually tracking LLM calls, steps, and errors.
No decorators - just call the methods directly where needed.
"""
import hashlib
import uuid
from datetime import datetime, timezone
from typing import Any
from asgiref.sync import sync_to_async
from django.utils import timezone as django_timezone
from .models import LLMCall, OptimizationError
@ -67,6 +64,7 @@ class LLMCallRecorder:
Returns:
llm_call_id: UUID of the created record
"""
llm_call_id = str(uuid.uuid4())
@ -150,12 +148,7 @@ class LLMCallRecorder:
@sync_to_async
def record_llm_call_failure(
self,
llm_call_id: str,
error_type: str,
error_message: str,
latency_ms: int | None = None,
retry_count: int = 0,
self, llm_call_id: str, error_type: str, error_message: str, latency_ms: int | None = None, retry_count: int = 0
) -> None:
"""Update LLM call record with failure information."""
LLMCall.objects.filter(id=llm_call_id).update(
@ -200,6 +193,7 @@ class ErrorRecorder:
Returns:
error_id: UUID of the created error record
"""
error_id = str(uuid.uuid4())
@ -239,6 +233,7 @@ async def track_llm_call_simple(
Returns:
llm_call_id: UUID of the created record
"""
recorder = LLMCallRecorder()
@ -266,4 +261,4 @@ async def track_llm_call_simple(
candidates_valid=candidates_valid,
)
return llm_call_id
return llm_call_id

View file

@ -1,5 +1,4 @@
"""
Observability decorators for automatic LLM call tracking.
"""Observability decorators for automatic LLM call tracking.
Usage:
@observe_llm_call(call_type="optimization")
@ -18,22 +17,20 @@ The decorator automatically:
import asyncio
import logging
import time
from collections.abc import Callable
from functools import wraps
from typing import Any, Callable, Optional
from typing import Any
from aiservice.models.aimodels import calculate_llm_cost
from aiservice.observability.database import LLMCallRecorder, ErrorRecorder
from aiservice.observability.database import ErrorRecorder, LLMCallRecorder
logger = logging.getLogger(__name__)
def observe_llm_call(
call_type: str,
extract_trace_id: Optional[Callable] = None,
extract_model_name: Optional[Callable] = None,
call_type: str, extract_trace_id: Callable | None = None, extract_model_name: Callable | None = None
):
"""
Decorator to automatically observe LLM calls.
"""Decorator to automatically observe LLM calls.
Args:
call_type: Type of LLM call ('optimization', 'test_generation', etc.)
@ -51,6 +48,7 @@ def observe_llm_call(
):
# Just the LLM call - observability is automatic
return await llm_client.chat.completions.create(...)
"""
def decorator(func: Callable) -> Callable:
@ -154,12 +152,12 @@ async def _record_start_background(
system_prompt: str,
user_prompt: str,
messages: list,
temperature: Optional[float],
temperature: float | None,
n_candidates: int,
user_id: Optional[str],
python_version: Optional[str],
context: Optional[dict],
) -> Optional[str]:
user_id: str | None,
python_version: str | None,
context: dict | None,
) -> str | None:
"""Record LLM call start in background (non-blocking)."""
try:
# If messages not provided, construct from prompts
@ -190,12 +188,7 @@ async def _record_start_background(
def _record_completion_background(
llm_recorder: LLMCallRecorder,
llm_call_id: str,
status: str,
result: Any,
latency_ms: int,
model_name: str,
llm_recorder: LLMCallRecorder, llm_call_id: str, status: str, result: Any, latency_ms: int, model_name: str
):
"""Record LLM call completion in background (non-blocking)."""
@ -256,7 +249,7 @@ def _record_error_background(
error_recorder: ErrorRecorder,
llm_recorder: LLMCallRecorder,
trace_id: str,
llm_call_id: Optional[str],
llm_call_id: str | None,
error: Exception,
latency_ms: int,
model_name: str,
@ -274,10 +267,7 @@ def _record_error_background(
severity="error",
error_message=str(error),
error_code=type(error).__name__,
context={
"model": model_name,
"n_candidates": n_candidates,
},
context={"model": model_name, "n_candidates": n_candidates},
)
# Update LLM call status to failed
@ -296,7 +286,7 @@ def _record_error_background(
asyncio.create_task(_record())
def _extract_trace_id(args: tuple, kwargs: dict, custom_extractor: Optional[Callable]) -> str:
def _extract_trace_id(args: tuple, kwargs: dict, custom_extractor: Callable | None) -> str:
"""Extract trace_id from function arguments."""
if custom_extractor:
return custom_extractor(args, kwargs)
@ -317,7 +307,7 @@ def _extract_trace_id(args: tuple, kwargs: dict, custom_extractor: Optional[Call
return "unknown"
def _extract_model_name(args: tuple, kwargs: dict, custom_extractor: Optional[Callable]) -> str:
def _extract_model_name(args: tuple, kwargs: dict, custom_extractor: Callable | None) -> str:
"""Extract model name from function arguments."""
if custom_extractor:
return custom_extractor(args, kwargs)
@ -336,4 +326,4 @@ def _extract_model_name(args: tuple, kwargs: dict, custom_extractor: Optional[Ca
return model
logger.warning("Observability: Could not extract model_name from function arguments")
return "unknown"
return "unknown"

View file

@ -6,7 +6,7 @@ that can be easily ingested into log analysis systems.
import json
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
@ -29,7 +29,7 @@ class StructuredFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
"""Format log record as JSON."""
log_data: dict[str, Any] = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
"log_level": record.levelname,
"logger_name": record.name,
"message": record.getMessage(),
@ -81,6 +81,7 @@ def get_observability_logger(name: str, component: str = "aiservice") -> logging
"metadata": {"model": "gpt-4", "n_candidates": 5}
}
)
"""
logger_name = f"{component}.{name}"
logger = logging.getLogger(logger_name)
@ -121,6 +122,7 @@ def log_with_context(
event_type='llm.call_success',
metadata={'model': 'gpt-4', 'latency_ms': 1250}
)
"""
extra: dict[str, Any] = {}
@ -151,13 +153,7 @@ class LogContext:
# All logs will include trace_id and user_id
"""
def __init__(
self,
logger: logging.Logger,
trace_id: str | None = None,
user_id: str | None = None,
**kwargs: Any
):
def __init__(self, logger: logging.Logger, trace_id: str | None = None, user_id: str | None = None, **kwargs: Any):
"""Initialize log context.
Args:
@ -165,6 +161,7 @@ class LogContext:
trace_id: Optimization trace ID
user_id: User ID
**kwargs: Additional context fields to include in all logs
"""
self.logger = logger
self.context: dict[str, Any] = {}
@ -182,14 +179,9 @@ class LogContext:
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context."""
# Context is automatically released
pass
def log(
self,
level: str,
message: str,
event_type: str | None = None,
metadata: dict[str, Any] | None = None
self, level: str, message: str, event_type: str | None = None, metadata: dict[str, Any] | None = None
) -> None:
"""Log message with context."""
extra = dict(self.context)

View file

@ -3,7 +3,9 @@
These models map to Prisma-managed tables for LLM tracking, step tracing,
and error monitoring in the optimization pipeline.
"""
import uuid
from django.db import models
@ -83,7 +85,9 @@ class OptimizationError(models.Model):
llm_call_id = models.UUIDField(null=True, blank=True)
# Error classification
error_type = models.CharField(max_length=50) # 'validation', 'llm_api', 'llm_parsing', 'test_failure', 'compilation'
error_type = models.CharField(
max_length=50
) # 'validation', 'llm_api', 'llm_parsing', 'test_failure', 'compilation'
error_category = models.CharField(max_length=50) # 'user_error', 'system_error', 'llm_error', 'infrastructure'
severity = models.CharField(max_length=20) # 'critical', 'error', 'warning', 'info'

View file

@ -58,9 +58,11 @@ async def code_repair( # noqa: D417
- original code
- optimized code
- behaviour test diffs
Returns
-------
CodeRepairIntermediateResponseItemschema or CodeRepairErrorResponseSchema
"""
system_prompt = ctx.get_system_prompt()
user_prompt = ctx.get_user_prompt()

View file

@ -5,13 +5,14 @@ from enum import Enum
import libcst as cst
import sentry_sdk
from aiservice.env_specific import debug_log_sensitive_data
from ninja import Field, Schema
from pydantic import ValidationError
from aiservice.env_specific import debug_log_sensitive_data
from optimizer.context_utils.constants import REPLACE_IN_FILE_TAGS_REGEX
from optimizer.context_utils.context_helpers import group_code, is_markdown_structure_changed, split_markdown_code
from optimizer.diff_patches_utils.patches_v2 import apply_patches, group_diff_patches_by_path
from optimizer.models import CodeAndExplanation
from pydantic import ValidationError
from testgen.instrumentation.edit_generated_test import parse_module_to_cst

View file

@ -6,26 +6,28 @@ import time
from typing import TYPE_CHECKING
import sentry_sdk
from aiservice.analytics.posthog import ph
from aiservice.common_utils import validate_trace_id
from aiservice.env_specific import create_llm_client, debug_log_sensitive_data, llm_clients
from aiservice.models.aimodels import EXPLANATIONS_MODEL, LLM, calculate_llm_cost
from aiservice.observability.database import LLMCallRecorder, ErrorRecorder
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
from ninja import NinjaAPI, Schema
from openai import OpenAIError
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from packaging import version
from aiservice.analytics.posthog import ph
from aiservice.common_utils import validate_trace_id
from aiservice.env_specific import debug_log_sensitive_data, llm_clients
from aiservice.models.aimodels import EXPLANATIONS_MODEL, LLM, calculate_llm_cost
from aiservice.observability.database import ErrorRecorder, LLMCallRecorder
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
if TYPE_CHECKING:
from aiservice.models.aimodels import LLM
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionFunctionMessageParam,
ChatCompletionToolMessageParam,
)
from aiservice.models.aimodels import LLM
explanations_api = NinjaAPI(urls_namespace="explanations")
explain_regex_pattern = re.compile(r"<explain>(.*)<\/explain>", re.DOTALL | re.IGNORECASE)
@ -235,10 +237,7 @@ async def explain_optimizations( # noqa: D417
temperature=None,
n_candidates=1,
user_id=user_id,
context={
"optimization_id": data.optimization_id,
"speedup": data.speedup,
},
context={"optimization_id": data.optimization_id, "speedup": data.speedup},
)
except Exception as obs_error:
logging.warning(f"Observability recording failed (explain start): {obs_error}")
@ -265,10 +264,7 @@ async def explain_optimizations( # noqa: D417
severity="error",
error_message=str(e),
error_code=type(e).__name__,
context={
"model": explanations_model.name,
"optimization_id": data.optimization_id,
},
context={"model": explanations_model.name, "optimization_id": data.optimization_id},
)
if llm_call_id:
await llm_recorder.record_llm_call_completion(

View file

@ -7,13 +7,14 @@ from enum import Enum
from typing import TYPE_CHECKING, cast
import sentry_sdk
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from packaging import version
from aiservice.env_specific import debug_log_sensitive_data, llm_clients
from aiservice.models.aimodels import OPTIMIZATION_REVIEW_MODEL, calculate_llm_cost
from aiservice.observability.decorators import observe_llm_call
from log_features.log_event import update_optimization_cost, update_optimization_features_review
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from packaging import version
if TYPE_CHECKING:
from aiservice.models.aimodels import LLM
@ -144,14 +145,9 @@ Output as a json markdown block with the key named as 'rating' and value being o
@observe_llm_call("optimization_review")
async def call_optimization_review_llm(
trace_id: str,
model: LLM,
messages: list[dict[str, str]],
user_id: str | None = None,
context: dict | None = None,
trace_id: str, model: LLM, messages: list[dict[str, str]], user_id: str | None = None, context: dict | None = None
):
"""
Call LLM for optimization review with automatic observability.
"""Call LLM for optimization review with automatic observability.
This function is decorated with @observe_llm_call which automatically:
- Records call start (non-blocking)
@ -165,9 +161,7 @@ async def call_optimization_review_llm(
if llm_client is None:
raise ValueError(f"LLM client for model type '{model.model_type}' is not available")
return await llm_client.with_options(max_retries=2).chat.completions.create(
model=model.name, messages=messages
)
return await llm_client.with_options(max_retries=2).chat.completions.create(model=model.name, messages=messages)
async def get_optimization_review(
@ -187,9 +181,7 @@ async def get_optimization_review(
model=optimization_review_model,
messages=messages,
user_id=request.user,
context={
"speedup": data.speedup,
},
context={"speedup": data.speedup},
)
# Calculate and update cost

View file

@ -2,10 +2,9 @@ from dataclasses import dataclass
from typing import Never
import libcst as cst
from aiservice.env_specific import debug_log_sensitive_data
from pydantic import ValidationError
from testgen.instrumentation.edit_generated_test import parse_module_to_cst
from aiservice.env_specific import debug_log_sensitive_data
from optimizer.context_utils.constants import (
MULTI_REPLACE_IN_FILE_TAGS_REGEX,
REPLACE_IN_FILE_REGEX,
@ -19,6 +18,7 @@ from optimizer.context_utils.context_helpers import (
)
from optimizer.diff_patches_utils.patches_v2 import apply_patches
from optimizer.models import CodeAndExplanation
from testgen.instrumentation.edit_generated_test import parse_module_to_cst
@dataclass()

View file

@ -48,17 +48,13 @@ def parse_diff(diff: str) -> list[SearchReplaceBlock]:
replace_end = idx
if idx >= n:
raise ValueError(
"Invalid diff format: Missing '>>>>>>> REPLACE' marker"
)
raise ValueError("Invalid diff format: Missing '>>>>>>> REPLACE' marker")
search_content = "".join(lines[search_start:search_end]).rstrip()
replace_content = "".join(lines[replace_start:replace_end]).rstrip()
try:
block = SearchReplaceBlock.from_block(
search=search_content, replace=replace_content
)
block = SearchReplaceBlock.from_block(search=search_content, replace=replace_content)
blocks.append(block)
except ValidationError as ve:
raise ValueError(f"Invalid block format: {ve}")
@ -111,7 +107,5 @@ def apply_patches(diff_str: str, content: str) -> str:
start_char_idx = content.find(block.search)
if start_char_idx != -1:
end_char_idx = start_char_idx + len(block.search)
content = (
f"{content[:start_char_idx]}{block.replace}{content[end_char_idx:]}"
)
content = f"{content[:start_char_idx]}{block.replace}{content[end_char_idx:]}"
return content

View file

@ -10,7 +10,6 @@ import libcst as cst
import sentry_sdk
from ninja import NinjaAPI
from ninja.errors import HttpError
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from pydantic import ValidationError
from aiservice.analytics.posthog import ph
@ -32,11 +31,6 @@ from optimizer.models import OptimizedCandidateSource, OptimizeSchema # noqa: T
if TYPE_CHECKING:
from django.http import HttpRequest
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionFunctionMessageParam,
ChatCompletionToolMessageParam,
)
from aiservice.models.aimodels import LLM
@ -129,8 +123,7 @@ async def call_optimization_llm(
user_id: str | None = None,
python_version: str | None = None,
):
"""
Call LLM for code optimization with automatic observability.
"""Call LLM for code optimization with automatic observability.
This function is decorated with @observe_llm_call which automatically:
- Records call start (non-blocking)
@ -146,10 +139,7 @@ async def call_optimization_llm(
return await llm_client.with_options(max_retries=3).chat.completions.create(
model=model.name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
n=n,
)
@ -386,7 +376,10 @@ async def optimize(
},
explanations_post={cei.optimization_id: cei.explanation for cei in optimization_response_items},
experiment_metadata=data.experiment_metadata if data.experiment_metadata else None,
optimizations_origin={cei.optimization_id: {"source": OptimizedCandidateSource.OPTIMIZE, "parent": None} for cei in optimization_response_items},
optimizations_origin={
cei.optimization_id: {"source": OptimizedCandidateSource.OPTIMIZE, "parent": None}
for cei in optimization_response_items
},
)
)

View file

@ -52,8 +52,7 @@ async def call_line_profiler_llm(
python_version: str | None = None,
context: dict | None = None,
):
"""
Call LLM for line profiler optimization with automatic observability.
"""Call LLM for line profiler optimization with automatic observability.
This function is decorated with @observe_llm_call which automatically:
- Records call start (non-blocking)
@ -130,9 +129,7 @@ async def optimize_python_code_line_profiler( # noqa: D417
n=n,
user_id=user_id,
python_version=python_version_str,
context={
"lsp_mode": lsp_mode,
},
context={"lsp_mode": lsp_mode},
)
await update_optimization_cost(trace_id=trace_id, cost=calculate_llm_cost(output, optimize_model))
except Exception as e:
@ -231,8 +228,10 @@ async def optimize(request, data: OptimizeSchemaLP) -> tuple[int, OptimizeRespon
},
explanations_post={cei.optimization_id: cei.explanation for cei in optimization_response_items},
experiment_metadata=data.experiment_metadata if data.experiment_metadata else None,
optimizations_origin={cei.optimization_id: {"source": OptimizedCandidateSource.OPTIMIZE_LP, "parent": None} for cei in optimization_response_items},
optimizations_origin={
cei.optimization_id: {"source": OptimizedCandidateSource.OPTIMIZE_LP, "parent": None}
for cei in optimization_response_items
},
)
response = OptimizeResponseSchema(optimizations=optimization_response_items)

View file

@ -274,7 +274,6 @@ def _strip_comments_from_code(code: str) -> str:
The same code with all comments removed, preserving string content
"""
try:
lines = code.splitlines(keepends=True)
tokens = tokenize.generate_tokens(io.StringIO(code).readline)
@ -437,91 +436,100 @@ def clean_extraneous_comments(original_module: cst.Module, optimized_module: cst
elif is_comment_only and not is_near_change:
# Comment line not near a code change - skip it
pass
# Code line - check if it changed or not
# If changed, use optimized version. If unchanged, use original version.
elif opt_idx in code_changed_lines:
# Code changed - just use the optimized line as-is, no restoration
result_lines.append(opt_line)
else:
# Code line - check if it changed or not
# If changed, use optimized version. If unchanged, use original version.
if opt_idx in code_changed_lines:
# Code changed - just use the optimized line as-is, no restoration
result_lines.append(opt_line)
else:
# Code didn't change - find and use original (including any preceding comments)
found_orig = None
orig_line_idx = None
for orig_idx_search in range(orig_idx, len(orig_lines)):
orig_code = orig_code_only[orig_idx_search] if orig_idx_search < len(orig_code_only) else ""
if orig_code == opt_code:
found_orig = orig_lines[orig_idx_search]
orig_line_idx = orig_idx_search
orig_idx = orig_idx_search + 1
break
# Code didn't change - find and use original (including any preceding comments)
found_orig = None
orig_line_idx = None
for orig_idx_search in range(orig_idx, len(orig_lines)):
orig_code = orig_code_only[orig_idx_search] if orig_idx_search < len(orig_code_only) else ""
if orig_code == opt_code:
found_orig = orig_lines[orig_idx_search]
orig_line_idx = orig_idx_search
orig_idx = orig_idx_search + 1
break
if found_orig:
# Check if there are comment-only/blank lines in the original that come before this line
# BUT ONLY restore them if this code line is UNCHANGED (not in code_changed_lines)
# Don't restore original comments before CHANGED code lines
if orig_line_idx is not None and orig_line_idx > 0 and opt_idx not in code_changed_lines:
# Look backwards for ALL consecutive comment-only or blank lines
# Collect them all, then decide which ones to restore
preceding_lines = []
check_idx = orig_line_idx - 1
if found_orig:
# Check if there are comment-only/blank lines in the original that come before this line
# BUT ONLY restore them if this code line is UNCHANGED (not in code_changed_lines)
# Don't restore original comments before CHANGED code lines
if orig_line_idx is not None and orig_line_idx > 0 and opt_idx not in code_changed_lines:
# Look backwards for ALL consecutive comment-only or blank lines
# Collect them all, then decide which ones to restore
preceding_lines = []
check_idx = orig_line_idx - 1
while check_idx >= 0:
check_code = orig_code_only[check_idx] if check_idx < len(orig_code_only) else ""
if not check_code.strip():
# This is a comment-only or blank line in the original
# Add it if not already restored and if it was removed from optimized
if check_idx not in orig_to_opt_mapping and check_idx not in restored_orig_indices:
preceding_lines.insert(0, orig_lines[check_idx])
restored_orig_indices.add(check_idx)
check_idx -= 1
else:
# Hit a line with actual code - stop looking backwards
break
while check_idx >= 0:
check_code = orig_code_only[check_idx] if check_idx < len(orig_code_only) else ""
if not check_code.strip():
# This is a comment-only or blank line in the original
# Add it if not already restored and if it was removed from optimized
if (
check_idx not in orig_to_opt_mapping
and check_idx not in restored_orig_indices
):
preceding_lines.insert(0, orig_lines[check_idx])
restored_orig_indices.add(check_idx)
check_idx -= 1
else:
# Hit a line with actual code - stop looking backwards
break
# Add the restored comments/blank lines before the actual line
result_lines.extend(preceding_lines)
# Add the restored comments/blank lines before the actual line
result_lines.extend(preceding_lines)
# Use the original line (preserves original comments or lack thereof)
result_lines.append(found_orig)
restored_orig_indices.add(orig_line_idx)
# Use the original line (preserves original comments or lack thereof)
result_lines.append(found_orig)
restored_orig_indices.add(orig_line_idx)
# Also check for trailing blank/comment lines after this line that were removed
# BUT: only restore them if this code line is UNCHANGED and they don't come before a changed line
# (in that case, the new comment from optimized should be kept)
if orig_line_idx is not None and orig_line_idx < len(orig_lines) - 1 and opt_idx not in code_changed_lines:
trailing_lines = []
check_idx = orig_line_idx + 1
found_changed_line = False
# Also check for trailing blank/comment lines after this line that were removed
# BUT: only restore them if this code line is UNCHANGED and they don't come before a changed line
# (in that case, the new comment from optimized should be kept)
if (
orig_line_idx is not None
and orig_line_idx < len(orig_lines) - 1
and opt_idx not in code_changed_lines
):
trailing_lines = []
check_idx = orig_line_idx + 1
found_changed_line = False
while check_idx < len(orig_lines):
check_code = orig_code_only[check_idx] if check_idx < len(orig_code_only) else ""
if not check_code.strip():
# This is a comment-only or blank line in the original
# Check if it was removed (not in the optimized version)
if check_idx not in orig_to_opt_mapping and check_idx not in restored_orig_indices:
trailing_lines.append(orig_lines[check_idx])
restored_orig_indices.add(check_idx)
check_idx += 1
else:
# Hit a line with actual code
# Check if this code line was changed in the optimized version
if check_idx in orig_to_opt_mapping:
next_opt_idx = orig_to_opt_mapping[check_idx]
if next_opt_idx in code_changed_lines:
found_changed_line = True
else:
# This original line is not in the mapping, which means
# it was either deleted or modified. In either case,
# this is a changed line.
while check_idx < len(orig_lines):
check_code = orig_code_only[check_idx] if check_idx < len(orig_code_only) else ""
if not check_code.strip():
# This is a comment-only or blank line in the original
# Check if it was removed (not in the optimized version)
if (
check_idx not in orig_to_opt_mapping
and check_idx not in restored_orig_indices
):
trailing_lines.append(orig_lines[check_idx])
restored_orig_indices.add(check_idx)
check_idx += 1
else:
# Hit a line with actual code
# Check if this code line was changed in the optimized version
if check_idx in orig_to_opt_mapping:
next_opt_idx = orig_to_opt_mapping[check_idx]
if next_opt_idx in code_changed_lines:
found_changed_line = True
break
else:
# This original line is not in the mapping, which means
# it was either deleted or modified. In either case,
# this is a changed line.
found_changed_line = True
break
# Only add trailing comments if they're NOT immediately before a changed line
if not found_changed_line:
result_lines.extend(trailing_lines)
else:
# Keep it (shouldn't happen but be safe)
result_lines.append(opt_line)
# Only add trailing comments if they're NOT immediately before a changed line
if not found_changed_line:
result_lines.extend(trailing_lines)
else:
# Keep it (shouldn't happen but be safe)
result_lines.append(opt_line)
# Parse the cleaned code back into a CST module
cleaned_code = "".join(result_lines)

View file

@ -189,14 +189,9 @@ Here is the function_references
@observe_llm_call("refinement")
async def call_refinement_llm(
trace_id: str,
model: LLM,
messages: list[dict[str, str]],
user_id: str | None = None,
context: dict | None = None,
trace_id: str, model: LLM, messages: list[dict[str, str]], user_id: str | None = None, context: dict | None = None
):
"""
Call LLM for refinement with automatic observability.
"""Call LLM for refinement with automatic observability.
This function is decorated with @observe_llm_call which automatically:
- Records call start (non-blocking)
@ -270,10 +265,7 @@ async def refinement( # noqa: D417
model=optimize_model,
messages=messages,
user_id=user_id,
context={
"optimization_id": optimization_id,
"speedup": ctx.data.speedup,
},
context={"optimization_id": optimization_id, "speedup": ctx.data.speedup},
)
llm_cost = calculate_llm_cost(output, optimize_model)
except Exception as e:

View file

@ -4,6 +4,9 @@ import re
from typing import TYPE_CHECKING
import sentry_sdk
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from aiservice.analytics.posthog import ph
from aiservice.common_utils import validate_trace_id
from aiservice.env_specific import debug_log_sensitive_data, llm_clients
@ -11,17 +14,16 @@ from aiservice.models.aimodels import LLM, RANKING_MODEL, calculate_llm_cost
from aiservice.observability.decorators import observe_llm_call
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
if TYPE_CHECKING:
from aiservice.models.aimodels import LLM
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionFunctionMessageParam,
ChatCompletionToolMessageParam,
)
from aiservice.models.aimodels import LLM
# from google import genai
# from pydantic import BaseModel
#
@ -75,14 +77,9 @@ Here are the function references
@observe_llm_call("ranking")
async def call_ranker_llm(
trace_id: str,
model: LLM,
messages: list[dict[str, str]],
user_id: str | None = None,
context: dict | None = None,
trace_id: str, model: LLM, messages: list[dict[str, str]], user_id: str | None = None, context: dict | None = None
):
"""
Call LLM for ranking with automatic observability.
"""Call LLM for ranking with automatic observability.
This function is decorated with @observe_llm_call which automatically:
- Records call start (non-blocking)

View file

@ -1,9 +1,9 @@
from __future__ import annotations
from functools import lru_cache
from typing import TYPE_CHECKING, cast
from libcst import CSTTransformer, ImportAlias, ImportFrom, MetadataWrapper, Name, parse_expression, parse_module
from functools import lru_cache
if TYPE_CHECKING:
from libcst import (

View file

@ -10,6 +10,10 @@ from typing import TYPE_CHECKING, SupportsIndex
import sentry_sdk
import stamina
from ninja import NinjaAPI
from ninja.errors import HttpError
from openai import OpenAIError
from aiservice.analytics.posthog import ph
from aiservice.common_utils import parse_python_version, safe_isort, should_hack_for_demo, validate_trace_id
from aiservice.env_specific import IS_PRODUCTION, debug_log_sensitive_data, llm_clients
@ -17,10 +21,6 @@ from aiservice.models.aimodels import EXECUTE_MODEL, calculate_llm_cost
from aiservice.observability.decorators import observe_llm_call
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
from ninja import NinjaAPI
from ninja.errors import HttpError
from openai import OpenAIError
from testgen.instrumentation.edit_generated_test import parse_module_to_cst, replace_definition_with_import
from testgen.instrumentation.instrument_new_tests import instrument_test_source
from testgen.models import (
@ -199,8 +199,7 @@ async def call_testgen_llm(
user_id: str | None = None,
python_version: str | None = None,
):
"""
Call LLM for test generation with automatic observability.
"""Call LLM for test generation with automatic observability.
This function is decorated with @observe_llm_call which automatically:
- Records call start (non-blocking)

View file

@ -1,11 +1,8 @@
from code_repair.code_repair_context import CodeRepairContext, CodeRepairContextData
from optimizer.diff_patches_utils.patches_v2 import apply_patches
def test_code_repair_single_file():
original_code = """```python:demo.py
import math
from typing import List, Tuple, Optional
@ -93,7 +90,6 @@ def calculate_portfolio_metrics(
```
"""
llm_response = """Looking at the test failure and comparing the original and modified code, I can see several issues in the modified implementation:
1. **Hardcoded volatility**: The volatility is hardcoded to `2` instead of being calculated
@ -211,7 +207,7 @@ The key changes I made:
6. **Fixed dictionary formatting**: Changed from double quotes to single quotes to match original formatting
These changes align the modified code with the original implementation's behavior, ensuring that the test for zero volatility passes (when cash investment has 0% return, the volatility should indeed be 0.0)."""
ctx = CodeRepairContext(CodeRepairContextData(original_code, optimized_code, ""), "" , "")
ctx = CodeRepairContext(CodeRepairContextData(original_code, optimized_code, ""), "", "")
diff_patches = ctx.extract_diff_patches_from_llm_res(llm_response)
refined_optimization = ctx.apply_patches_to_optimized_code(diff_patches)
@ -386,4 +382,4 @@ def calculate_portfolio_metrics(
}
"""
new_code = apply_patches(patch, code)
print(new_code)
print(new_code)

View file

@ -8,6 +8,7 @@ documentation to unchanged code.
import ast
import libcst as cst
from optimizer.postprocess import clean_extraneous_comments

View file

@ -1,2 +1 @@
"""Workflow generation module for GitHub Actions."""

View file

@ -4,4 +4,3 @@ from django.apps import AppConfig
class WorkflowGenConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "workflow_gen"

View file

@ -8,14 +8,11 @@ from typing import TYPE_CHECKING, Any
import sentry_sdk
from ninja import NinjaAPI, Schema
from openai.types.chat import (
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam,
)
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from aiservice.analytics.posthog import ph
from aiservice.env_specific import debug_log_sensitive_data, llm_clients
from aiservice.models.aimodels import EXECUTE_MODEL, calculate_llm_cost
from aiservice.models.aimodels import EXECUTE_MODEL
from aiservice.observability.decorators import observe_llm_call
if TYPE_CHECKING:
@ -81,8 +78,7 @@ async def call_workflow_gen_llm(
user_id: str | None = None,
context: dict | None = None,
):
"""
Call LLM for workflow generation with automatic observability.
"""Call LLM for workflow generation with automatic observability.
This function is decorated with @observe_llm_call which automatically:
- Records call start (non-blocking)
@ -110,32 +106,22 @@ async def generate_workflow_steps_llm(
) -> str | None:
"""Generate workflow steps using LLM."""
# Format repo files for prompt
files_text = "\n".join(
[f"**{path}:**\n{content[:8000]}\n" for path, content in repo_files.items()]
)
files_text = "\n".join([f"**{path}:**\n{content[:8000]}\n" for path, content in repo_files.items()])
# Format directory structure
structure_text = json.dumps(directory_structure, indent=2)
# Format codeflash config
config_text = (
json.dumps(codeflash_config, indent=2) if codeflash_config else "Not available"
)
config_text = json.dumps(codeflash_config, indent=2) if codeflash_config else "Not available"
user_prompt = USER_PROMPT.format(
repo_files=files_text,
directory_structure=structure_text,
codeflash_config=config_text,
repo_files=files_text, directory_structure=structure_text, codeflash_config=config_text
)
system_message = ChatCompletionSystemMessageParam(
role="system", content=SYSTEM_PROMPT
)
system_message = ChatCompletionSystemMessageParam(role="system", content=SYSTEM_PROMPT)
user_message = ChatCompletionUserMessageParam(role="user", content=user_prompt)
debug_log_sensitive_data(
f"Generating workflow steps with prompt length: {len(user_prompt)}"
)
debug_log_sensitive_data(f"Generating workflow steps with prompt length: {len(user_prompt)}")
try:
# Call LLM with automatic observability (decorator handles everything)
@ -146,9 +132,7 @@ async def generate_workflow_steps_llm(
temperature=0,
n=1,
user_id=user_id,
context={
"num_files": len(repo_files),
},
context={"num_files": len(repo_files)},
)
if not response.choices or not response.choices[0].message.content:
@ -160,14 +144,10 @@ async def generate_workflow_steps_llm(
# Extract YAML steps
steps_yaml = _extract_yaml_steps(response_text)
if steps_yaml:
logger.info(
f"Successfully generated workflow steps ({len(steps_yaml)} chars)"
)
logger.info(f"Successfully generated workflow steps ({len(steps_yaml)} chars)")
return steps_yaml
logger.warning(
f"Could not extract valid YAML steps from LLM response: {response_text[:200]}"
)
logger.warning(f"Could not extract valid YAML steps from LLM response: {response_text[:200]}")
return None
except Exception as e:
@ -193,11 +173,7 @@ class WorkflowGenErrorResponseSchema(Schema):
@workflow_gen_api.post(
"/",
response={
200: WorkflowGenResponseSchema,
400: WorkflowGenErrorResponseSchema,
500: WorkflowGenErrorResponseSchema,
},
response={200: WorkflowGenResponseSchema, 400: WorkflowGenErrorResponseSchema, 500: WorkflowGenErrorResponseSchema},
)
async def generate_workflow_steps(
request: HttpRequest, data: WorkflowGenInputSchema
@ -227,16 +203,10 @@ async def generate_workflow_steps(
error="Failed to generate workflow steps. Please try again or use the static template."
)
ph(
request.user,
"aiservice-workflow-gen-success",
properties={"steps_length": len(workflow_steps)},
)
ph(request.user, "aiservice-workflow-gen-success", properties={"steps_length": len(workflow_steps)})
return 200, WorkflowGenResponseSchema(workflow_steps=workflow_steps)
except Exception as e:
logger.error(f"Error in generate_workflow_steps endpoint: {e}")
sentry_sdk.capture_exception(e)
return 500, WorkflowGenErrorResponseSchema(
error="Internal server error while generating workflow steps"
)
return 500, WorkflowGenErrorResponseSchema(error="Internal server error while generating workflow steps")