From aec6f619278e6f43bbaa8cb7070b236cf2ac52e2 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Fri, 30 Jan 2026 18:58:57 +0000 Subject: [PATCH] Add CLI and services modules --- .../services/conversion_service.py | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/codexchange/services/conversion_service.py diff --git a/src/codexchange/services/conversion_service.py b/src/codexchange/services/conversion_service.py new file mode 100644 index 0000000..dcb8487 --- /dev/null +++ b/src/codexchange/services/conversion_service.py @@ -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