Add CLI and services modules
This commit is contained in:
207
src/codexchange/services/conversion_service.py
Normal file
207
src/codexchange/services/conversion_service.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
"""Conversion service for CodeXchange CLI."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from codexchange.models import (
|
||||||
|
ConversionRequest,
|
||||||
|
ConversionResult,
|
||||||
|
Language,
|
||||||
|
)
|
||||||
|
from codexchange.services.ollama_service import OllamaService
|
||||||
|
|
||||||
|
|
||||||
|
class ConversionService:
|
||||||
|
"""Service for converting code between programming languages."""
|
||||||
|
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
|
||||||
|
def __init__(self, ollama_service: Optional[OllamaService] = None):
|
||||||
|
"""Initialize the conversion service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ollama_service: Optional OllamaService instance.
|
||||||
|
"""
|
||||||
|
self.ollama_service = ollama_service or OllamaService()
|
||||||
|
|
||||||
|
def build_conversion_prompt(
|
||||||
|
self,
|
||||||
|
source_code: str,
|
||||||
|
source_language: Language,
|
||||||
|
target_language: Language
|
||||||
|
) -> str:
|
||||||
|
"""Build a prompt for code conversion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_code: Source code to convert.
|
||||||
|
source_language: Source programming language.
|
||||||
|
target_language: Target programming language.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted prompt for the LLM.
|
||||||
|
"""
|
||||||
|
prompt = f"""You are an expert code converter. Convert the following code from {source_language.value} to {target_language.value}.
|
||||||
|
|
||||||
|
IMPORTANT REQUIREMENTS:
|
||||||
|
1. Preserve ALL comments, docstrings, and inline documentation exactly as they are
|
||||||
|
2. Maintain the original code style and formatting as much as possible
|
||||||
|
3. Keep the same logic and functionality
|
||||||
|
4. Use idiomatic patterns for the target language
|
||||||
|
5. Do not add any explanations or comments beyond what was in the original code
|
||||||
|
6. Return ONLY the converted code, no markdown formatting, no explanations
|
||||||
|
|
||||||
|
Source code:
|
||||||
|
```{source_language.value}
|
||||||
|
{source_code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Converted code:
|
||||||
|
```{target_language.value}
|
||||||
|
"""
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
def convert(self, request: ConversionRequest) -> ConversionResult:
|
||||||
|
"""Convert code from one language to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: ConversionRequest with source code and language info.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ConversionResult with converted code or error.
|
||||||
|
"""
|
||||||
|
for attempt in range(self.MAX_RETRIES):
|
||||||
|
try:
|
||||||
|
prompt = self.build_conversion_prompt(
|
||||||
|
request.source_code,
|
||||||
|
request.source_language,
|
||||||
|
request.target_language
|
||||||
|
)
|
||||||
|
|
||||||
|
converted_code = self.ollama_service.generate(
|
||||||
|
prompt=prompt,
|
||||||
|
model=request.model
|
||||||
|
)
|
||||||
|
|
||||||
|
converted_code = self._clean_conversion_output(converted_code)
|
||||||
|
|
||||||
|
return ConversionResult(
|
||||||
|
success=True,
|
||||||
|
converted_code=converted_code,
|
||||||
|
original_code=request.source_code,
|
||||||
|
source_language=request.source_language,
|
||||||
|
target_language=request.target_language,
|
||||||
|
model=request.model
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if attempt == self.MAX_RETRIES - 1:
|
||||||
|
return ConversionResult(
|
||||||
|
success=False,
|
||||||
|
original_code=request.source_code,
|
||||||
|
source_language=request.source_language,
|
||||||
|
target_language=request.target_language,
|
||||||
|
model=request.model,
|
||||||
|
error_message=f"Conversion failed after {self.MAX_RETRIES} attempts: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ConversionResult(
|
||||||
|
success=False,
|
||||||
|
original_code=request.source_code,
|
||||||
|
source_language=request.source_language,
|
||||||
|
target_language=request.target_language,
|
||||||
|
model=request.model,
|
||||||
|
error_message="Conversion failed: unknown error"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clean_conversion_output(self, output: str) -> str:
|
||||||
|
"""Clean up the conversion output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output: Raw output from LLM.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned code string.
|
||||||
|
"""
|
||||||
|
output = output.strip()
|
||||||
|
|
||||||
|
code_block_pattern = r"```(?:\w+)?\n([\s\S]*?)\n```"
|
||||||
|
matches = re.findall(code_block_pattern, output)
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
output = matches[0]
|
||||||
|
|
||||||
|
output = output.strip()
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def count_comments(self, code: str) -> int:
|
||||||
|
"""Count the number of comment lines in code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Source code.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of comment lines.
|
||||||
|
"""
|
||||||
|
lines = code.split("\n")
|
||||||
|
comment_count = 0
|
||||||
|
|
||||||
|
in_multiline_comment = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if in_multiline_comment:
|
||||||
|
comment_count += 1
|
||||||
|
if "*/" in stripped:
|
||||||
|
in_multiline_comment = False
|
||||||
|
stripped_after = stripped.split("*/", 1)[1].strip()
|
||||||
|
if stripped_after and not stripped_after.startswith("//"):
|
||||||
|
comment_count -= 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if stripped.startswith("//"):
|
||||||
|
comment_count += 1
|
||||||
|
elif stripped.startswith("/*"):
|
||||||
|
comment_count += 1
|
||||||
|
if "*/" not in stripped:
|
||||||
|
in_multiline_comment = True
|
||||||
|
else:
|
||||||
|
comment_content = stripped.split("/*")[1].split("*/")[0]
|
||||||
|
if comment_content.strip():
|
||||||
|
comment_count += 1
|
||||||
|
elif stripped.startswith("#"):
|
||||||
|
comment_count += 1
|
||||||
|
elif stripped.startswith('"""') or stripped.startswith("'''"):
|
||||||
|
if stripped.count('"""') == 2 or stripped.count("'''") == 2:
|
||||||
|
comment_count += 1
|
||||||
|
|
||||||
|
return comment_count
|
||||||
|
|
||||||
|
def verify_comment_preservation(
|
||||||
|
self,
|
||||||
|
original_code: str,
|
||||||
|
converted_code: str,
|
||||||
|
tolerance: float = 0.1
|
||||||
|
) -> bool:
|
||||||
|
"""Verify that comments were preserved in conversion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
original_code: Original source code.
|
||||||
|
converted_code: Converted source code.
|
||||||
|
tolerance: Allowed difference in comment count (10% by default).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if comments appear to be preserved.
|
||||||
|
"""
|
||||||
|
original_comments = self.count_comments(original_code)
|
||||||
|
converted_comments = self.count_comments(converted_code)
|
||||||
|
|
||||||
|
if original_comments == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if converted_comments == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ratio = abs(original_comments - converted_comments) / original_comments
|
||||||
|
return ratio <= tolerance
|
||||||
Reference in New Issue
Block a user