From 15f85e7c46d668797b1219c352acfd480d472866 Mon Sep 17 00:00:00 2001 From: fl1pcoin Date: Sun, 28 Sep 2025 19:17:30 +0300 Subject: [PATCH 1/4] add README translation --- osa_tool/config/settings/arguments.yaml | 10 +++ osa_tool/config/settings/config.toml | 1 + osa_tool/config/settings/prompts.toml | 34 +++++++ osa_tool/readmegen/prompts/prompts_builder.py | 12 +++ osa_tool/run.py | 6 ++ osa_tool/translation/readme_translator.py | 88 +++++++++++++++++++ 6 files changed, 151 insertions(+) create mode 100644 osa_tool/translation/readme_translator.py diff --git a/osa_tool/config/settings/arguments.yaml b/osa_tool/config/settings/arguments.yaml index 12dfe822..48aa3030 100644 --- a/osa_tool/config/settings/arguments.yaml +++ b/osa_tool/config/settings/arguments.yaml @@ -94,6 +94,16 @@ convert_notebooks: Provide one or multiple paths, or leave empty for repo directory. example: path/to/file1, path/to/file2 +translate_readme: + aliases: [ "--translate-readme" ] + type: list + description: | + List of target languages to translate the project's main README into. + Each language should be specified by its name (e.g., "Russian", "Chinese"). + The translated README files will be saved separately in the repository folder + with language-specific suffixes (e.g., README_ru.md, README_zh.md). + example: Russian, Chinese + delete_dir: aliases: [ "--delete-dir" ] type: flag diff --git a/osa_tool/config/settings/config.toml b/osa_tool/config/settings/config.toml index dc88d557..53bab930 100644 --- a/osa_tool/config/settings/config.toml +++ b/osa_tool/config/settings/config.toml @@ -27,6 +27,7 @@ web_mode = false # branch = "" # article = "" translate_dirs = false +translate_readme = [] # convert_notebooks = [] delete_dir = false # ensure_license = "" diff --git a/osa_tool/config/settings/prompts.toml b/osa_tool/config/settings/prompts.toml index 68d8d969..594d64f7 100644 --- a/osa_tool/config/settings/prompts.toml +++ b/osa_tool/config/settings/prompts.toml @@ -258,4 +258,38 @@ SPECIAL RULES: - All content before the first meaningful markdown header (starting with `## `) in the original README should go to the `badges` section **only if it contains the project title, logos, or status badges**. - Textual descriptions, images, or code examples before the first `## ` header should be evaluated and placed into the appropriate sections (like "Overview", "Usage", or "Installation"). - Avoid placing unrelated text or images in `badges`. +""" + +translate = """ +TASK: +Translate the provided README content into the target language. + +RULES: + +- Translate only natural language parts (titles, descriptions, paragraphs, lists). +- DO NOT translate: + * project name, + * code blocks, + * shell commands, + * configuration snippets, + * links, badges, and image references. +- Preserve original Markdown formatting. +- Return only valid JSON, no explanations. +- The "suffix" must be the correct ISO 639-1 or common abbreviation of the target language. + +OUTPUT FORMAT: +Return a JSON object with the following structure: + +{{ + "content": "translated README text", + "suffix": "short language code (e.g., en, ru, es, fr, de)" +}} + +INPUT DATA: + +1. TARGET LANGUAGE: {target_language} + +2. README CONTENT: + +{readme_content} """ \ No newline at end of file diff --git a/osa_tool/readmegen/prompts/prompts_builder.py b/osa_tool/readmegen/prompts/prompts_builder.py index 094cdc22..593a710e 100644 --- a/osa_tool/readmegen/prompts/prompts_builder.py +++ b/osa_tool/readmegen/prompts/prompts_builder.py @@ -152,6 +152,18 @@ def get_prompt_algorithms_article(self, key_files: list[FileContext], pdf_summar logger.error(f"Failed to build algorithms prompt: {e}") raise + def get_prompt_translate_readme(self, readme_content: str, target_language: str) -> str: + """Builds a prompt to translate README into target language""" + try: + formatted_prompt = self.prompts["translate"].format( + target_language=target_language, + readme_content=readme_content, + ) + return formatted_prompt + except Exception as e: + logger.error(f"Failed to build readme translation prompt: {e}") + raise + @staticmethod def serialize_file_contexts(files: list[FileContext]) -> str: """ diff --git a/osa_tool/run.py b/osa_tool/run.py index 51408729..76fd676c 100644 --- a/osa_tool/run.py +++ b/osa_tool/run.py @@ -30,6 +30,7 @@ update_workflow_config, ) from osa_tool.translation.dir_translator import DirectoryTranslator +from osa_tool.translation.readme_translator import ReadmeTranslator from osa_tool.utils import ( delete_repository, logger, @@ -136,6 +137,11 @@ def main(): rich_section("README generation") readme_agent(config, plan.get("article"), plan.get("refine_readme")) + # Readme translation + if plan.get("translate_readme"): + rich_section("README translation") + ReadmeTranslator(config, plan.get("translate_readme")).translate_readme() + # About section generation about_gen = None if plan.get("about"): diff --git a/osa_tool/translation/readme_translator.py b/osa_tool/translation/readme_translator.py new file mode 100644 index 00000000..567f406b --- /dev/null +++ b/osa_tool/translation/readme_translator.py @@ -0,0 +1,88 @@ +import asyncio +import json +import os + +from osa_tool.config.settings import ConfigLoader +from osa_tool.models.models import ModelHandlerFactory, ModelHandler +from osa_tool.readmegen.postprocessor.response_cleaner import process_text +from osa_tool.readmegen.prompts.prompts_builder import PromptBuilder +from osa_tool.readmegen.utils import read_file, save_sections, remove_extra_blank_lines +from osa_tool.utils import parse_folder_name, logger + + +class ReadmeTranslator: + def __init__(self, config_loader: ConfigLoader, languages: list[str]): + self.config_loader = config_loader + self.config = self.config_loader.config + self.rate_limit = self.config.llm.rate_limit + self.languages = languages + self.repo_url = self.config.git.repository + self.model_handler: ModelHandler = ModelHandlerFactory.build(self.config) + self.base_path = os.path.join(os.getcwd(), parse_folder_name(self.repo_url)) + + async def translate_readme_request_async( + self, readme_content: str, target_language: str, semaphore: asyncio.Semaphore + ) -> dict: + """Asynchronous request to translate README content via LLM.""" + prompt = PromptBuilder(self.config_loader).get_prompt_translate_readme(readme_content, target_language) + async with semaphore: + response = await self.model_handler.async_request(prompt) + response = process_text(response) + try: + result = json.loads(response) + except json.JSONDecodeError: + logger.warning(f"LLM response for '{target_language}' is not valid JSON, applying fallback") + result = { + "content": response.strip(), + "suffix": target_language[:2].lower(), + } + return result + + async def translate_readme_async(self) -> None: + """ + Asynchronously translate the main README into all target languages. + """ + readme_content = self.get_main_readme_file() + if not readme_content: + logger.warning("No README content found, skipping translation") + return + + semaphore = asyncio.Semaphore(self.rate_limit) + + async def translate_and_save(lang: str): + translation = await self.translate_readme_request_async(readme_content, lang, semaphore) + self.save_translated_readme(translation) + + await asyncio.gather(*(translate_and_save(lang) for lang in self.languages)) + + def save_translated_readme(self, translation: dict) -> None: + """ + Save a single translated README to a file. + + Args: + translation (dict): Dictionary with keys: + - "content": translated README text + - "suffix": language code + """ + suffix = translation.get("suffix", "unknown") + content = translation.get("content", "") + + if not content: + logger.warning(f"Translation for '{suffix}' is empty, skipping save.") + return + + filename = f"README_{suffix}.md" + file_path = os.path.join(self.base_path, filename) + + save_sections(content, file_path) + remove_extra_blank_lines(file_path) + logger.info(f"Saved translated README: {file_path}") + + def get_main_readme_file(self) -> str: + """Return the content of the main README.md in the repository root, or empty string if not found.""" + readme_path = os.path.join(self.base_path, "README.md") + return read_file(readme_path) + + def translate_readme(self) -> None: + """Synchronous wrapper around async translation.""" + asyncio.run(self.translate_readme_async()) From 75c8ec24052d34e3852ce25d83d2fba2732b6911 Mon Sep 17 00:00:00 2001 From: fl1pcoin Date: Mon, 29 Sep 2025 21:14:25 +0300 Subject: [PATCH 2/4] add set default readme for github --- osa_tool/translation/readme_translator.py | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osa_tool/translation/readme_translator.py b/osa_tool/translation/readme_translator.py index 567f406b..647f17ff 100644 --- a/osa_tool/translation/readme_translator.py +++ b/osa_tool/translation/readme_translator.py @@ -1,6 +1,7 @@ import asyncio import json import os +import shutil from osa_tool.config.settings import ConfigLoader from osa_tool.models.models import ModelHandlerFactory, ModelHandler @@ -36,6 +37,8 @@ async def translate_readme_request_async( "content": response.strip(), "suffix": target_language[:2].lower(), } + + result["target_language"] = target_language return result async def translate_readme_async(self) -> None: @@ -49,12 +52,22 @@ async def translate_readme_async(self) -> None: semaphore = asyncio.Semaphore(self.rate_limit) + results = {} + async def translate_and_save(lang: str): translation = await self.translate_readme_request_async(readme_content, lang, semaphore) self.save_translated_readme(translation) + results[lang] = translation await asyncio.gather(*(translate_and_save(lang) for lang in self.languages)) + if self.languages: + first_lang = self.languages[0] + if first_lang in results: + self.set_default_translated_readme(results[first_lang]) + else: + logger.warning(f"No translation found for first language '{first_lang}'") + def save_translated_readme(self, translation: dict) -> None: """ Save a single translated README to a file. @@ -78,6 +91,37 @@ def save_translated_readme(self, translation: dict) -> None: remove_extra_blank_lines(file_path) logger.info(f"Saved translated README: {file_path}") + def set_default_translated_readme(self, translation: dict) -> None: + """ + Create a .github/README.md symlink (or copy fallback) + pointing to the first translated README. + """ + suffix = translation.get("suffix") + if not suffix: + logger.warning("No suffix for first translated README, skipping default setup.") + return + + source_path = os.path.join(self.base_path, f"README_{suffix}.md") + if not os.path.exists(source_path): + logger.warning(f"Translated README not found at {source_path}, skipping setup.") + return + + github_dir = os.path.join(self.base_path, ".github") + os.makedirs(github_dir, exist_ok=True) + + target_path = os.path.join(github_dir, "README.md") + + try: + if os.path.exists(target_path): + os.remove(target_path) + + os.symlink(source_path, target_path) + logger.info(f"Created symlink: {target_path} -> {source_path}") + except (OSError, NotImplementedError) as e: + logger.warning(f"Symlink not supported ({e}), copying file instead") + shutil.copyfile(source_path, target_path) + logger.info(f"Copied file: {target_path}") + def get_main_readme_file(self) -> str: """Return the content of the main README.md in the repository root, or empty string if not found.""" readme_path = os.path.join(self.base_path, "README.md") From e2eee62f0bd9a5f2c50882eff6485cc05ceeb183 Mon Sep 17 00:00:00 2001 From: fl1pcoin Date: Fri, 3 Oct 2025 02:35:42 +0300 Subject: [PATCH 3/4] changes after review + add unit tests --- osa_tool/readmegen/prompts/prompts_builder.py | 54 +++++--- osa_tool/run.py | 5 +- osa_tool/translation/readme_translator.py | 4 +- .../readmegen/prompts/test_prompts_builder.py | 3 +- .../translation/test_readme_translator.py | 131 ++++++++++++++++++ 5 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 tests/unit/translation/test_readme_translator.py diff --git a/osa_tool/readmegen/prompts/prompts_builder.py b/osa_tool/readmegen/prompts/prompts_builder.py index 593a710e..1b887d91 100644 --- a/osa_tool/readmegen/prompts/prompts_builder.py +++ b/osa_tool/readmegen/prompts/prompts_builder.py @@ -34,7 +34,7 @@ def get_prompt_preanalysis(self) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build preanalysis prompt: {e}") - raise + raise PromptFormatError("Could not build preanalysis prompt") from e def get_prompt_core_features(self, key_files: list[FileContext]) -> str: """Builds a core features prompt using project metadata, README content, and key files.""" @@ -48,7 +48,7 @@ def get_prompt_core_features(self, key_files: list[FileContext]) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build core features prompt: {e}") - raise + raise PromptFormatError("Could not build core features prompt") from e def get_prompt_overview(self, core_features: str) -> str: """Builds an overview prompt using metadata, README content, and extracted core features.""" @@ -62,7 +62,7 @@ def get_prompt_overview(self, core_features: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build overview prompt: {e}") - raise + raise PromptFormatError("Could not build overview prompt") from e def get_prompt_getting_started(self, examples_files: list[FileContext]) -> str: """Builds a getting started prompt using metadata, README content, and example files.""" @@ -75,7 +75,7 @@ def get_prompt_getting_started(self, examples_files: list[FileContext]) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build getting started prompt: {e}") - raise + raise PromptFormatError("Could not build getting started prompt") from e def get_prompt_deduplicated_install_and_start(self, installation: str, getting_started: str) -> str: """Builds a deduplicating prompt using Installation and Getting Started sections of README.""" @@ -87,7 +87,7 @@ def get_prompt_deduplicated_install_and_start(self, installation: str, getting_s return formatted_prompt except Exception as e: logger.error(f"Failed to build deduplicating prompt: {e}") - raise + raise PromptFormatError("Could not build deduplicating prompt") from e def get_prompt_files_summary(self, files_content: list[FileContext]) -> str: """Builds a files summary prompt using serialized file contents.""" @@ -99,7 +99,7 @@ def get_prompt_files_summary(self, files_content: list[FileContext]) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build files summary prompt: {e}") - raise + raise PromptFormatError("Could not build summary prompt") from e def get_prompt_pdf_summary(self, pdf_content: str) -> str: """Builds a PDF summary prompt using the provided PDF content.""" @@ -108,7 +108,7 @@ def get_prompt_pdf_summary(self, pdf_content: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build PDF summary prompt: {e}") - raise + raise PromptFormatError("Could not build PDF summary prompt") from e def get_prompt_overview_article(self, files_summary: str, pdf_summary: str) -> str: """Builds an article overview prompt using metadata, file summary, and PDF summary.""" @@ -122,7 +122,7 @@ def get_prompt_overview_article(self, files_summary: str, pdf_summary: str) -> s return formatted_prompt except Exception as e: logger.error(f"Failed to build overview prompt: {e}") - raise + raise PromptFormatError("Could not build article overview prompt") from e def get_prompt_content_article(self, files_summary: str, pdf_summary: str) -> str: """Builds a content article prompt using metadata, key file content, and PDF summary.""" @@ -136,7 +136,7 @@ def get_prompt_content_article(self, files_summary: str, pdf_summary: str) -> st return formatted_prompt except Exception as e: logger.error(f"Failed to build content prompt: {e}") - raise + raise PromptFormatError("Could not build content prompt") from e def get_prompt_algorithms_article(self, key_files: list[FileContext], pdf_summary: str) -> str: """Builds an algorithms article prompt using metadata, file summary, and PDF summary.""" @@ -150,7 +150,7 @@ def get_prompt_algorithms_article(self, key_files: list[FileContext], pdf_summar return formatted_prompt except Exception as e: logger.error(f"Failed to build algorithms prompt: {e}") - raise + raise PromptFormatError("Could not build algorithms prompt") from e def get_prompt_translate_readme(self, readme_content: str, target_language: str) -> str: """Builds a prompt to translate README into target language""" @@ -162,7 +162,7 @@ def get_prompt_translate_readme(self, readme_content: str, target_language: str) return formatted_prompt except Exception as e: logger.error(f"Failed to build readme translation prompt: {e}") - raise + raise PromptFormatError("Could not build translation prompt") from e @staticmethod def serialize_file_contexts(files: list[FileContext]) -> str: @@ -200,15 +200,29 @@ def load_prompts(path: str, section: str = "prompts") -> dict: Returns: dict: Dictionary with prompts from the specified section. """ - if not os.path.exists(path): - logger.error(f"Prompts file {path} not found.") - raise FileNotFoundError(f"Prompts file {path} not found.") + try: + if not os.path.exists(path): + raise FileNotFoundError(f"Prompts file {path} not found.") + + with open(path, "rb") as f: + toml_data = tomli.load(f) + + if section not in toml_data: + raise KeyError(f"Section '{section}' not found in {path}.") + + return toml_data[section] + except Exception as e: + logger.error(f"Failed to load prompts from {path}: {e}") + raise PromptLoadError(f"Could not load prompts from {path}") from e + + +class PromptBuilderError(Exception): + """Base exception for PromptBuilder errors.""" + - with open(path, "rb") as f: - toml_data = tomli.load(f) +class PromptLoadError(PromptBuilderError): + """Raised when loading prompts from a file fails.""" - if section not in toml_data: - logger.error(f"Section '{section}' not found in {path}.") - raise KeyError(f"Section '{section}' not found in {path}.") - return toml_data[section] +class PromptFormatError(PromptBuilderError): + """Raised when building a specific prompt fails.""" diff --git a/osa_tool/run.py b/osa_tool/run.py index 76fd676c..d2fc2299 100644 --- a/osa_tool/run.py +++ b/osa_tool/run.py @@ -138,9 +138,10 @@ def main(): readme_agent(config, plan.get("article"), plan.get("refine_readme")) # Readme translation - if plan.get("translate_readme"): + translate_readme = plan.get("translate_readme") + if translate_readme: rich_section("README translation") - ReadmeTranslator(config, plan.get("translate_readme")).translate_readme() + ReadmeTranslator(config, translate_readme).translate_readme() # About section generation about_gen = None diff --git a/osa_tool/translation/readme_translator.py b/osa_tool/translation/readme_translator.py index 647f17ff..d62ecaec 100644 --- a/osa_tool/translation/readme_translator.py +++ b/osa_tool/translation/readme_translator.py @@ -28,10 +28,10 @@ async def translate_readme_request_async( prompt = PromptBuilder(self.config_loader).get_prompt_translate_readme(readme_content, target_language) async with semaphore: response = await self.model_handler.async_request(prompt) - response = process_text(response) try: + response = process_text(response) result = json.loads(response) - except json.JSONDecodeError: + except (json.JSONDecodeError, ValueError): logger.warning(f"LLM response for '{target_language}' is not valid JSON, applying fallback") result = { "content": response.strip(), diff --git a/tests/unit/readmegen/prompts/test_prompts_builder.py b/tests/unit/readmegen/prompts/test_prompts_builder.py index 21484128..b498ff99 100644 --- a/tests/unit/readmegen/prompts/test_prompts_builder.py +++ b/tests/unit/readmegen/prompts/test_prompts_builder.py @@ -1,6 +1,7 @@ import pytest from osa_tool.readmegen.context.files_contents import FileContext +from osa_tool.readmegen.prompts.prompts_builder import PromptLoadError def test_serialize_file_contexts(prompt_builder): @@ -32,7 +33,7 @@ def test_load_prompts_valid(prompt_builder): def test_load_prompts_missing_file(prompt_builder): # Assert - with pytest.raises(FileNotFoundError): + with pytest.raises(PromptLoadError): prompt_builder.load_prompts("non_existing_file.toml") diff --git a/tests/unit/translation/test_readme_translator.py b/tests/unit/translation/test_readme_translator.py new file mode 100644 index 00000000..7f3749ea --- /dev/null +++ b/tests/unit/translation/test_readme_translator.py @@ -0,0 +1,131 @@ +import asyncio +import json +import os +from unittest.mock import patch, AsyncMock + +import pytest + +import osa_tool.translation.readme_translator as rt + + +@pytest.fixture +def translator(tmp_path, mock_config_loader, load_metadata_prompts): + t = rt.ReadmeTranslator(mock_config_loader, ["fr", "de"]) + t.base_path = tmp_path + return t + + +def test_get_main_readme_file_found(translator, tmp_path): + # Arrange + readme = tmp_path / "README.md" + readme.write_text("hello readme") + + # Assert + assert translator.get_main_readme_file() == "hello readme" + + +def test_get_main_readme_file_missing(translator): + # Assert + assert translator.get_main_readme_file() == "" + + +def test_save_translated_readme_creates_file(translator): + # Arrange + translation = {"suffix": "fr", "content": "bonjour"} + + # Act + translator.save_translated_readme(translation) + + # Assert + file_path = os.path.join(translator.base_path, "README_fr.md") + assert os.path.exists(file_path) + assert "bonjour" in open(file_path).read() + + +def test_save_translated_readme_skips_empty(translator, caplog): + # Act + translator.save_translated_readme({"suffix": "fr", "content": ""}) + + # Assert + assert "skipping save" in caplog.text.lower() + + +def test_set_default_translated_readme_symlink(translator): + # Arrange + source = os.path.join(translator.base_path, "README_fr.md") + with open(source, "w") as f: + f.write("content") + + translation = {"suffix": "fr"} + + # Act + translator.set_default_translated_readme(translation) + + # Assert + target = os.path.join(translator.base_path, ".github", "README.md") + assert os.path.exists(target) + + +def test_set_default_translated_readme_copy_on_error(translator): + # Arrange + source = os.path.join(translator.base_path, "README_fr.md") + with open(source, "w") as f: + f.write("content") + + translation = {"suffix": "fr"} + + # Act + with patch("os.symlink", side_effect=OSError("no symlink")): + translator.set_default_translated_readme(translation) + + # Assert + target = os.path.join(translator.base_path, ".github", "README.md") + assert os.path.exists(target) + assert not os.path.islink(target) + + +@pytest.mark.asyncio +async def test_translate_readme_request_async_valid_json(translator): + # Arrange + response = json.dumps({"content": "text", "suffix": "fr"}) + translator.model_handler.async_request = AsyncMock(return_value=response) + + # Act + result = await translator.translate_readme_request_async("hello", "French", asyncio.Semaphore(1)) + + # Assert + assert result["content"] == "text" + assert result["target_language"] == "French" + + +@pytest.mark.asyncio +async def test_translate_readme_request_async_invalid_json(translator, caplog): + # Arrange + translator.model_handler.async_request = AsyncMock(return_value="not a json") + + # Act + result = await translator.translate_readme_request_async("hello", "French", asyncio.Semaphore(1)) + + # Assert + assert "fallback" in caplog.text.lower() + assert result["suffix"] == "fr" + assert result["target_language"] == "French" + + +@pytest.mark.asyncio +async def test_translate_readme_async_runs(translator, tmp_path): + # Arrange + readme = tmp_path / "README.md" + readme.write_text("hello readme") + + resp = json.dumps({"content": "bonjour", "suffix": "fr"}) + translator.model_handler.async_request = AsyncMock(return_value=resp) + + # Act + await translator.translate_readme_async() + + # Assert + readme_fr = tmp_path / "README_fr.md" + assert readme_fr.exists() + target = tmp_path / ".github" / "README.md" + assert target.exists() From 951fa358a164fbfe38fbf5438c8c9862fb672ec7 Mon Sep 17 00:00:00 2001 From: fl1pcoin Date: Fri, 3 Oct 2025 22:06:56 +0300 Subject: [PATCH 4/4] updates after merge --- osa_tool/readmegen/prompts/prompts_builder.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osa_tool/readmegen/prompts/prompts_builder.py b/osa_tool/readmegen/prompts/prompts_builder.py index 068b1e7d..68f89008 100644 --- a/osa_tool/readmegen/prompts/prompts_builder.py +++ b/osa_tool/readmegen/prompts/prompts_builder.py @@ -159,7 +159,7 @@ def get_prompt_detect_citation(self) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build detection of citation prompt: {e}") - raise + raise PromptFormatError("Could not build detection of citation prompt") from e def get_prompt_refine_readme_step1(self, generated_readme: str) -> str: """Builds a prompt to merge original README details into the generated structure.""" @@ -170,7 +170,7 @@ def get_prompt_refine_readme_step1(self, generated_readme: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build refine readme step 1 prompt: {e}") - raise + raise PromptFormatError("Could not build refine readme step 1 prompt") from e def get_prompt_refine_readme_step2(self, readme: str) -> str: """Builds a prompt to clean duplicates and normalize formatting in README.""" @@ -179,7 +179,7 @@ def get_prompt_refine_readme_step2(self, readme: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build refine readme step 2 prompt: {e}") - raise + raise PromptFormatError("Could not build refine readme step 2 prompt") from e def get_prompt_refine_readme_step3(self, readme: str) -> str: """Builds a prompt to finalize README headings, ToC, and formatting consistency.""" @@ -188,7 +188,7 @@ def get_prompt_refine_readme_step3(self, readme: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build refine readme step 3 prompt: {e}") - raise + raise PromptFormatError("Could not build refine readme step 3 prompt") from e def get_prompt_clean_readme_step1(self, readme: str) -> str: """Builds a prompt to remove duplicate commands, text, and media from README.""" @@ -197,7 +197,7 @@ def get_prompt_clean_readme_step1(self, readme: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build cleaning readme step 1 prompt: {e}") - raise + raise PromptFormatError("Could not build cleaning readme step 1 prompt") from e def get_prompt_clean_readme_step2(self, readme: str) -> str: """Builds a prompt to delete semantically duplicated content across README sections.""" @@ -206,7 +206,7 @@ def get_prompt_clean_readme_step2(self, readme: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build cleaning readme step 2 prompt: {e}") - raise + raise PromptFormatError("Could not build cleaning readme step 2 prompt") from e def get_prompt_clean_readme_step3(self, readme: str) -> str: """Builds a prompt to finalize README formatting and ensure GFM compliance.""" @@ -215,7 +215,7 @@ def get_prompt_clean_readme_step3(self, readme: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build cleaning readme step 3 prompt: {e}") - raise + raise PromptFormatError("Could not build cleaning readme step 3 prompt") from e def get_prompt_article_name_extraction(self, pdf_content: str) -> str: """Builds an article name extraction prompt""" @@ -224,7 +224,7 @@ def get_prompt_article_name_extraction(self, pdf_content: str) -> str: return formatted_prompt except Exception as e: logger.error(f"Failed to build article name extraction prompt: {e}") - raise + raise PromptFormatError("Could not build article name extraction prompt") from e @staticmethod def serialize_file_contexts(files: list[FileContext]) -> str: