From 13a1d236dce7d8de4b8cd646513149586ed28f08 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 22 May 2025 21:46:18 +0200 Subject: [PATCH] Refactored prompt-template related code (new package interprompt) --- code_templates/prompt_factory_template.py | 57 --- {prompts => config}/contexts/agent.yml | 2 +- {prompts => config}/contexts/desktop-app.yml | 2 +- .../contexts/ide-assistant.yml | 2 +- {prompts => config}/modes/editing.yml | 2 +- {prompts => config}/modes/interactive.yml | 2 +- {prompts => config}/modes/one-shot.yml | 3 +- {prompts => config}/modes/planning.yml | 2 +- .../prompt_templates/simple_tool_outputs.yml | 165 ++++---- config/prompt_templates/system_prompt.yml | 14 + scripts/gen_prompt_factory.py | 47 +-- src/interprompt/README.md | 18 + src/interprompt/__init__.py | 3 + src/interprompt/jinja_template.py | 43 ++ src/interprompt/promt_template.py | 396 ++++++++++++++++++ src/interprompt/util/__init__.py | 0 src/interprompt/util/class_decorators.py | 13 + src/serena/agent.py | 2 +- src/serena/constants.py | 6 + src/serena/llm/jinja_template.py | 32 -- src/serena/llm/multilang_prompt.py | 204 --------- src/serena/llm/prompt_factory.py | 78 ---- src/serena/prompt_factory.py | 34 ++ src/serena/util/class_decorators.py | 2 + 24 files changed, 624 insertions(+), 505 deletions(-) delete mode 100644 code_templates/prompt_factory_template.py rename {prompts => config}/contexts/agent.yml (92%) rename {prompts => config}/contexts/desktop-app.yml (92%) rename {prompts => config}/contexts/ide-assistant.yml (94%) rename {prompts => config}/modes/editing.yml (98%) rename {prompts => config}/modes/interactive.yml (96%) rename {prompts => config}/modes/one-shot.yml (82%) rename {prompts => config}/modes/planning.yml (98%) rename prompts/mcp_prompts.yml => config/prompt_templates/simple_tool_outputs.yml (72%) create mode 100644 config/prompt_templates/system_prompt.yml create mode 100644 src/interprompt/README.md create mode 100644 src/interprompt/__init__.py create mode 100644 src/interprompt/jinja_template.py create mode 100644 src/interprompt/promt_template.py create mode 100644 src/interprompt/util/__init__.py create mode 100644 src/interprompt/util/class_decorators.py create mode 100644 src/serena/constants.py delete mode 100644 src/serena/llm/jinja_template.py delete mode 100644 src/serena/llm/multilang_prompt.py delete mode 100644 src/serena/llm/prompt_factory.py create mode 100644 src/serena/prompt_factory.py diff --git a/code_templates/prompt_factory_template.py b/code_templates/prompt_factory_template.py deleted file mode 100644 index d3ea586d..00000000 --- a/code_templates/prompt_factory_template.py +++ /dev/null @@ -1,57 +0,0 @@ -# ruff: noqa -# black: skip -# mypy: ignore-errors - -from typing import List, Optional - -from .multilang_prompt import MultiLangContainer, MultiLangPromptTemplateCollection, PromptList - - -class PromptFactory: - # NOTE: This class is auto-generated by gen_prompt_factory.py - - def __init__(self, lang_shortcode: str = "en", fallback_mode=MultiLangContainer.FallbackMode.EXCEPTION): - self.lang_shortcode = lang_shortcode - self.collection = MultiLangPromptTemplateCollection() - self.fallback_mode = fallback_mode - self.context_extension = "" - self.mode_extensions = [] - - def _format_prompt(self, prompt_name: str, kwargs) -> str: - del kwargs["self"] - mpt = self.collection.get_multilang_prompt_template(prompt_name) - return mpt.get_item(self.lang_shortcode, self.fallback_mode).instantiate(**kwargs) - - def _get_list(self, prompt_name: str) -> PromptList: - mpl = self.collection.get_multilang_prompt_list(prompt_name) - return mpl.get_item(self.lang_shortcode, self.fallback_mode) - - def set_context(self, context_extension: str) -> None: - """Set the context extension for the system prompt.""" - self.context_extension = context_extension - - def set_modes(self, mode_extensions: List[str]) -> None: - """Set the mode extensions for the system prompt.""" - self.mode_extensions = mode_extensions - - def create_system_prompt(self) -> str: - """Create the system prompt with context and mode extensions.""" - base_prompt = self._format_prompt("system_prompt", locals()) - - # Add context and mode extensions - extensions = [] - if self.context_extension: - extensions.append(self.context_extension) - - for mode_extension in self.mode_extensions: - extensions.append(mode_extension) - - # If no extensions, return the base prompt - if not extensions: - return base_prompt - - # Combine the base prompt with the extensions - combined_prompt = base_prompt + "\n\n" + "\n\n".join(extensions) - return combined_prompt - - # methods \ No newline at end of file diff --git a/prompts/contexts/agent.yml b/config/contexts/agent.yml similarity index 92% rename from prompts/contexts/agent.yml rename to config/contexts/agent.yml index b4ac8b0d..0f1d1bc5 100644 --- a/prompts/contexts/agent.yml +++ b/config/contexts/agent.yml @@ -1,6 +1,6 @@ name: agent description: All tools except InitialInstructionsTool for agent context -system_prompt_extension: | +system_prompt: | You are running in agent context where the system prompt is provided externally. You should use symbolic tools when possible for code understanding and modification. excluded_tools: diff --git a/prompts/contexts/desktop-app.yml b/config/contexts/desktop-app.yml similarity index 92% rename from prompts/contexts/desktop-app.yml rename to config/contexts/desktop-app.yml index e12a8f4a..6f338068 100644 --- a/prompts/contexts/desktop-app.yml +++ b/config/contexts/desktop-app.yml @@ -1,6 +1,6 @@ name: desktop-app description: All tools included for desktop app context -system_prompt_extension: | +system_prompt: | You are running in desktop app context with all tools available. You should use the symbolic tools when possible, but you also have access to file and shell operations for more complex tasks. excluded_tools: [] diff --git a/prompts/contexts/ide-assistant.yml b/config/contexts/ide-assistant.yml similarity index 94% rename from prompts/contexts/ide-assistant.yml rename to config/contexts/ide-assistant.yml index 27a72fd7..1a743714 100644 --- a/prompts/contexts/ide-assistant.yml +++ b/config/contexts/ide-assistant.yml @@ -1,6 +1,6 @@ name: ide-assistant description: Non-symbolic editing tools and general shell tool are excluded -system_prompt_extension: | +system_prompt: | You are running in IDE assistant context where file operations and shell commands are handled by the IDE. You should exclusively use symbolic tools for exploring and modifying the code, as the IDE handles file-level operations. diff --git a/prompts/modes/editing.yml b/config/modes/editing.yml similarity index 98% rename from prompts/modes/editing.yml rename to config/modes/editing.yml index 96eb914a..0f5002e0 100644 --- a/prompts/modes/editing.yml +++ b/config/modes/editing.yml @@ -1,6 +1,6 @@ name: editing description: All tools, with detailed instructions for code editing -system_prompt_extension: | +system_prompt: | You are operating in editing mode. Your task is to implement the requested changes while adhering to the project's code style and patterns. Use symbolic editing tools whenever possible for precise code modifications. excluded_tools: [] diff --git a/prompts/modes/interactive.yml b/config/modes/interactive.yml similarity index 96% rename from prompts/modes/interactive.yml rename to config/modes/interactive.yml index 2de63352..bc1dd5dc 100644 --- a/prompts/modes/interactive.yml +++ b/config/modes/interactive.yml @@ -1,6 +1,6 @@ name: interactive description: Interactive mode for clarification and step-by-step work -system_prompt_extension: | +system_prompt: | You are operating in interactive mode. You should engage with the user throughout the task, asking for clarification whenever anything is unclear, insufficiently specified, or ambiguous. diff --git a/prompts/modes/one-shot.yml b/config/modes/one-shot.yml similarity index 82% rename from prompts/modes/one-shot.yml rename to config/modes/one-shot.yml index 5923df89..08732918 100644 --- a/prompts/modes/one-shot.yml +++ b/config/modes/one-shot.yml @@ -1,7 +1,6 @@ name: one-shot description: Focus on completely finishing a task without interaction -system_prompt_extension: | - You are operating in one-shot mode. Your goal is to complete the entire task autonomously without further user interaction. +system_prompt: You are operating in one-shot mode. Your goal is to complete the entire task autonomously without further user interaction. You should assume auto-approval for all tools and continue working until the task is completely finished. If the task is planning, your final result should be a comprehensive plan. If the task is coding, your final result diff --git a/prompts/modes/planning.yml b/config/modes/planning.yml similarity index 98% rename from prompts/modes/planning.yml rename to config/modes/planning.yml index abfdc405..7a378337 100644 --- a/prompts/modes/planning.yml +++ b/config/modes/planning.yml @@ -1,6 +1,6 @@ name: planning description: Only read-only tools, focused on analysis and planning -system_prompt_extension: | +system_prompt: | You are operating in planning mode. Your task is to analyze code and create a comprehensive plan but not write any code. Focus on understanding the existing codebase structure, architecture, and functionality to create detailed planning documents that can be used for future implementation. diff --git a/prompts/mcp_prompts.yml b/config/prompt_templates/simple_tool_outputs.yml similarity index 72% rename from prompts/mcp_prompts.yml rename to config/prompt_templates/simple_tool_outputs.yml index 641f08d2..41291165 100644 --- a/prompts/mcp_prompts.yml +++ b/config/prompt_templates/simple_tool_outputs.yml @@ -1,90 +1,75 @@ -lang: en -prompts: - onboarding_prompt: | - You are viewing the project for the first time. - Your task is to assemble relevant high-level information about the project which - will be saved to memory files in the following steps. - The information should be sufficient to understand what the project is about, - and the most important commands for developing code. - The project is being developed on the system: {{ system }}. - - You need to identify at least the following information: - * the project's purpose - * the tech stack used - * the code style and conventions used (including naming, type hints, docstrings, etc.) - * which commands to run when a task is completed (linting, formatting, testing, etc.) - * the rough structure of the codebase - * the commands for testing, formatting, and linting - * the commands for running the entrypoints of the project - * the util commands for the system, like `git`, `ls`, `cd`, `grep`, `find`, etc. Keep in mind that the system is {{ system }}, - so the commands might be different than on a regular unix system. - * whether there are particular guidelines, styles, design patterns, etc. that one should know about - - This list is not exhaustive, you can add more information if you think it is relevant. - - For doing that, you will need to acquire information about the project with the corresponding tools. - Read only the necessary files and directories to avoid loading too much data into memory. - If you cannot find everything you need from the project itself, you should ask the user for more information. - - After collecting all the information, you will use the `write_memory` tool (in multiple calls) to save it to various memory files. - A particularly important memory file will be the `suggested_commands.md` file, which should contain - a list of commands that the user should know about to develop code in this project. - Moreover, you should create memory files for the style and conventions and a dedicated memory file for - what should be done when a task is completed. - **Important**: after done with the onboarding task, remember to call the `write_memory` to save the collected information! - - think_about_collected_information: | - Have you collected all the information you need for solving the current task? If not, can the missing information be acquired by using the available tools, - in particular the tools related to symbol discovery? Or do you need to ask the user for more information? - Think about it step by step and give a summary of the missing information and how it could be acquired. - - think_about_task_adherence: | - Are you deviating from the task at hand? Do you need any additional information to proceed? - Have you loaded all relevant memory files to see whether your implementation is fully aligned with the - code style, conventions, and guidelines of the project? If not, adjust your implementation accordingly - before modifying any code into the codebase. - Note that it is better to stop and ask the user for clarification - than to perform large changes which might not be aligned with the user's intentions. - If you feel like the conversation is deviating too much from the original task, apologize and suggest to the user - how to proceed. If the conversation became too long, create a summary of the current progress and suggest to the user - to start a new conversation based on that summary. - - think_about_whether_you_are_done: | - Have you already performed all the steps required by the task? Is it appropriate to run tests and linting, and if so, - have you done that already? Is it appropriate to adjust non-code files like documentation and config and have you done that already? - Should new tests be written to cover the changes? - Note that a task that is just about exploring the codebase does not require running tests or linting. - Read the corresponding memory files to see what should be done when a task is completed. - - summarize_changes: | - Summarize all the changes you have made to the codebase over the course of the conversation. - Explore the diff if needed (e.g. by using `git diff`) to ensure that you have not missed anything. - Explain whether and how the changes are covered by tests. Explain how to best use the new code, how to understand it, - which existing code it affects and interacts with. Are there any dangers (like potential breaking changes or potential new problems) - that the user should be aware of? Should any new documentation be written or existing documentation updated? - You can use tools to explore the codebase prior to writing the summary, but don't write any new code in this step until - the summary is complete. - - prepare_for_new_conversation: | - You have not yet completed the current task but we are running out of context. You should succinctly summarize the current state of the conversation - and all relevant files, symbols and entities that you have discovered so far. Respond with a summary that can be used in the next conversation. - Recommend which tools to use and the next steps to take. Imagine that you are handing over the task to another person who has access to the - same tools and memory files as you do, but has not been part of the conversation so far. If you have discovered anything general about the project - in the course of the conversation, consider writing a new memory file to store that information. - - system_prompt: | - You are a professional coding agent who is concerned with one particular codebase. You have tools to explore the codebase - iteratively, to edit files and to execute arbitrary commands. You heavily rely on these tools to solve the tasks given to you, - and you operate in a frugal and intelligent manner, always - keeping in mind to not load content that is not needed for the task at hand. - Whenever you want to read or edit code, you try using the symbolic tools first instead of just reading or editing files or lines. - For understanding symbol locations, you make sure to have read them using find symbol or - the overview tools. You only use read_file, replace_lines and so on if the - symbolic tools are not appropriate for some reason (e.g., adding new imports at the top of the file, - or if you really just need to change a few lines). - - If you cannot find everything you need in the codebase - and generally when in doubt, you ask the user clarifying questions instead of guessing. - - You never make assumptions about code that you failed to read or find, instead you stop - and ask the user for clarification. +# Some of Serena's tools are just outputting a fixed text block without doing anything else. +# Such tools are meant to encourage the agent to think in a certain way, to stay on track +# and so on. The (templates for) outputs of these tools are contained here. +prompts: + onboarding_prompt: | + You are viewing the project for the first time. + Your task is to assemble relevant high-level information about the project which + will be saved to memory files in the following steps. + The information should be sufficient to understand what the project is about, + and the most important commands for developing code. + The project is being developed on the system: {{ system }}. + + You need to identify at least the following information: + * the project's purpose + * the tech stack used + * the code style and conventions used (including naming, type hints, docstrings, etc.) + * which commands to run when a task is completed (linting, formatting, testing, etc.) + * the rough structure of the codebase + * the commands for testing, formatting, and linting + * the commands for running the entrypoints of the project + * the util commands for the system, like `git`, `ls`, `cd`, `grep`, `find`, etc. Keep in mind that the system is {{ system }}, + so the commands might be different than on a regular unix system. + * whether there are particular guidelines, styles, design patterns, etc. that one should know about + + This list is not exhaustive, you can add more information if you think it is relevant. + + For doing that, you will need to acquire information about the project with the corresponding tools. + Read only the necessary files and directories to avoid loading too much data into memory. + If you cannot find everything you need from the project itself, you should ask the user for more information. + + After collecting all the information, you will use the `write_memory` tool (in multiple calls) to save it to various memory files. + A particularly important memory file will be the `suggested_commands.md` file, which should contain + a list of commands that the user should know about to develop code in this project. + Moreover, you should create memory files for the style and conventions and a dedicated memory file for + what should be done when a task is completed. + **Important**: after done with the onboarding task, remember to call the `write_memory` to save the collected information! + + think_about_collected_information: | + Have you collected all the information you need for solving the current task? If not, can the missing information be acquired by using the available tools, + in particular the tools related to symbol discovery? Or do you need to ask the user for more information? + Think about it step by step and give a summary of the missing information and how it could be acquired. + + think_about_task_adherence: | + Are you deviating from the task at hand? Do you need any additional information to proceed? + Have you loaded all relevant memory files to see whether your implementation is fully aligned with the + code style, conventions, and guidelines of the project? If not, adjust your implementation accordingly + before modifying any code into the codebase. + Note that it is better to stop and ask the user for clarification + than to perform large changes which might not be aligned with the user's intentions. + If you feel like the conversation is deviating too much from the original task, apologize and suggest to the user + how to proceed. If the conversation became too long, create a summary of the current progress and suggest to the user + to start a new conversation based on that summary. + + think_about_whether_you_are_done: | + Have you already performed all the steps required by the task? Is it appropriate to run tests and linting, and if so, + have you done that already? Is it appropriate to adjust non-code files like documentation and config and have you done that already? + Should new tests be written to cover the changes? + Note that a task that is just about exploring the codebase does not require running tests or linting. + Read the corresponding memory files to see what should be done when a task is completed. + + summarize_changes: | + Summarize all the changes you have made to the codebase over the course of the conversation. + Explore the diff if needed (e.g. by using `git diff`) to ensure that you have not missed anything. + Explain whether and how the changes are covered by tests. Explain how to best use the new code, how to understand it, + which existing code it affects and interacts with. Are there any dangers (like potential breaking changes or potential new problems) + that the user should be aware of? Should any new documentation be written or existing documentation updated? + You can use tools to explore the codebase prior to writing the summary, but don't write any new code in this step until + the summary is complete. + + prepare_for_new_conversation: | + You have not yet completed the current task but we are running out of context. + {mode_prepare_for_new_conversation} + Imagine that you are handing over the task to another person who has access to the + same tools and memory files as you do, but has not been part of the conversation so far. + Write a summary that can be used in the next conversation to a memory file using the `write_memory` tool. diff --git a/config/prompt_templates/system_prompt.yml b/config/prompt_templates/system_prompt.yml new file mode 100644 index 00000000..494471a1 --- /dev/null +++ b/config/prompt_templates/system_prompt.yml @@ -0,0 +1,14 @@ +# The system prompt template. Note that many clients will not allow configuration of the actual system prompt, +# in which case this prompt will be given as a regular message on the call of a simple tool which the agent +# is encouraged (via the tool description) to call at the beginning of the conversation. +prompts: + system_prompt: | + You are a professional coding agent concerned with one particular codebase. You have + access to semantic coding tools on which you rely heavily for all your work, as well as collection of memory + files containing general information about the codebase. You operate in a frugal and intelligent manner, always + keeping in mind to not read or generate content that is not needed for the task at hand. + + {context_system_prompt} + + {mode_system_prompt} + \ No newline at end of file diff --git a/scripts/gen_prompt_factory.py b/scripts/gen_prompt_factory.py index a1f477bf..c4e4f696 100644 --- a/scripts/gen_prompt_factory.py +++ b/scripts/gen_prompt_factory.py @@ -1,45 +1,22 @@ +""" +Autogenerates the `prompt_factory.py` module +""" + +from pathlib import Path + from sensai.util import logging -from serena.llm.multilang_prompt import MultiLangPromptTemplateCollection +from interprompt import autogenerate_prompt_factory_module +from serena.constants import PROMPT_TEMPLATES_DIR, REPO_ROOT log = logging.getLogger(__name__) def main(): - coll = MultiLangPromptTemplateCollection() - - package = "src/serena" - - # collect methods to generate - indent = " " - methods = [] - for mpt in coll.prompt_templates.values(): - prompt_name = mpt.name - params = mpt.get_parameters() - if len(params) == 0: - params_str = "" - else: - params_str = ", *, " + ", ".join(params) - methods.append( - f"def create_{prompt_name}(self{params_str}) -> str:" - + f"\n{indent}{indent}return self._format_prompt('{prompt_name}', locals())\n\n{indent}" - ) - for mpl in coll.prompt_lists.values(): - prompt_name = mpl.name - methods.append( - f"def get_list_{prompt_name}(self) -> PromptList:" + f"\n{indent}{indent}return self._get_list('{prompt_name}')\n\n{indent}" - ) - - # write prompt factory with added methods - with open("code_templates/prompt_factory_template.py") as f: - code = f.read() - methods_str = "".join(methods) - code = code.replace("# methods", methods_str) - - prompt_factory_module = f"{package}/llm/prompt_factory.py" - with open(prompt_factory_module, "w") as f: - f.write(code) - log.info(f"Prompt factory generated successfully in {prompt_factory_module}") + autogenerate_prompt_factory_module( + prompts_dir=PROMPT_TEMPLATES_DIR, + target_module_path=str(Path(REPO_ROOT) / "src" / "serena" / "prompt_factory.py"), + ) if __name__ == "__main__": diff --git a/src/interprompt/README.md b/src/interprompt/README.md new file mode 100644 index 00000000..c8de32de --- /dev/null +++ b/src/interprompt/README.md @@ -0,0 +1,18 @@ +# The interprompt Package + +This package is independent of Serena. It contains code by Oraios AI that we developed for +convenient handling of prompt templating in various situations, in particular in multi-language scenarios +(hence the name as portmanteau from internationalization prompting) + +It is not yet released as separate package and will be developed together with Serena. + +While Serena itself does not need the multi-language aspect (for now), `interprompt` helps +with loading and rendering of the templates through the autogenerated `prompt_factory` module. + +The main (and usually only) entry point one needs is the function `interprompt.autogenerate_prompt_factory_module` +that will generate a module with a `PromptFactory` class that contains one method per prompt template and prompt list. +For a single-language case, the main added value from that class are the method names and the parameter names that are read off +from the jinja templates, allowing clean and safe templating within python without having to write any code. + +For multi-language use cases, the `PromptFactory` adds even more value by offering simple and unified interfaces for +internationalization in prompting. diff --git a/src/interprompt/__init__.py b/src/interprompt/__init__.py new file mode 100644 index 00000000..06b2ac51 --- /dev/null +++ b/src/interprompt/__init__.py @@ -0,0 +1,3 @@ +from .promt_template import autogenerate_prompt_factory_module + +__all__ = ["autogenerate_prompt_factory_module"] diff --git a/src/interprompt/jinja_template.py b/src/interprompt/jinja_template.py new file mode 100644 index 00000000..bb945f46 --- /dev/null +++ b/src/interprompt/jinja_template.py @@ -0,0 +1,43 @@ +from typing import Any + +import jinja2 +import jinja2.meta +import jinja2.nodes +import jinja2.visitor + +from interprompt.util.class_decorators import singleton + + +class ParameterizedTemplateInterface: + def get_parameters(self) -> list[str]: ... + + +@singleton +class _JinjaEnvProvider: + def __init__(self) -> None: + self._env: jinja2.Environment | None = None + + def get_env(self) -> jinja2.Environment: + if self._env is None: + self._env = jinja2.Environment() + return self._env + + +class JinjaTemplate(ParameterizedTemplateInterface): + def __init__(self, template_string: str) -> None: + self._template_string = template_string + self._template = _JinjaEnvProvider().get_env().from_string(self._template_string) + parsed_content = self._template.environment.parse(self._template_string) + self._parameters = sorted(jinja2.meta.find_undeclared_variables(parsed_content)) + + def render(self, **kwargs: Any) -> str: + """Renders the template with the given kwargs. You can find out which parameters are required by calling get_parameter_names().""" + return self._template.render(**kwargs) + + def get_parameters(self) -> list[str]: + """A sorted list of parameter names that are extracted from the template string. It is impossible to know the types of the parameter + values, they can be primitives, dicts or dict-like objects. + + :return: the list of parameter names + """ + return self._parameters diff --git a/src/interprompt/promt_template.py b/src/interprompt/promt_template.py new file mode 100644 index 00000000..2cdb43ee --- /dev/null +++ b/src/interprompt/promt_template.py @@ -0,0 +1,396 @@ +import logging +import os +from enum import Enum +from typing import Any, ClassVar, Generic, TypeVar + +import yaml +from sensai.util.string import ToStringMixin + +from .jinja_template import JinjaTemplate, ParameterizedTemplateInterface + +log = logging.getLogger(__name__) + + +class PromptTemplate(ToStringMixin, ParameterizedTemplateInterface): + def __init__(self, name: str, jinja_template_string: str) -> None: + self.name = name + self._jinja_template = JinjaTemplate(jinja_template_string.strip()) + + def _tostring_excludes(self) -> list[str]: + return ["jinja_template"] + + def render(self, **kwargs: Any) -> str: + return self._jinja_template.render(**kwargs) + + def get_parameters(self) -> list[str]: + return self._jinja_template.get_parameters() + + +class PromptList: + def __init__(self, items: list[str]) -> None: + self.items = [x.strip() for x in items] + + def to_string(self) -> str: + bullet = " * " + indent = " " * len(bullet) + items = [x.replace("\n", "\n" + indent) for x in self.items] + return "\n * ".join(items) + + +T = TypeVar("T") +DEFAULT_LANG_CODE = "default" + + +class LanguageFallbackMode(Enum): + """ + Defines what to do if there is no item for the given language. + """ + + ANY = "any" + """ + Return the item for any language (the first one found) + """ + EXCEPTION = "exception" + """ + If the requested language is not found, raise an exception + """ + USE_DEFAULT_LANG = "use_default_lang" + """ + If the requested language is not found, use the default language + """ + + +class _MultiLangContainer(Generic[T], ToStringMixin): + """ + A container of items (usually, all having the same semantic meaning) which are associated with different languages. + Can also be used for single-language purposes by always using the default language code. + """ + + def __init__(self, name: str) -> None: + self.name = name + self._lang2item: dict[str, T] = {} + """Maps language codes to items""" + + def _tostring_excludes(self) -> list[str]: + return ["lang2item"] + + def _tostring_additional_entries(self) -> dict[str, Any]: + return dict(languages=list(self._lang2item.keys())) + + def get_language_codes(self) -> list[str]: + """The language codes for which items are registered in the container.""" + return list(self._lang2item.keys()) + + def add_item(self, item: T, lang_code: str = DEFAULT_LANG_CODE, allow_overwrite: bool = False) -> None: + """Adds an item to the container, representing the same semantic entity as the other items in the container but in a different language. + + :param item: the item to add + :param lang_code: the language shortcode for which to add the item. Use the default for single-language use cases. + :param allow_overwrite: if True, allow overwriting an existing entry for the same language + """ + if not allow_overwrite and lang_code in self._lang2item: + raise KeyError(f"Item for language '{lang_code}' already registered for name '{self.name}'") + self._lang2item[lang_code] = item + + def get_item(self, lang: str = DEFAULT_LANG_CODE, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> T: + """ + Gets the item for the given language. + + :param lang: the language shortcode for which to obtain the prompt template. A default language can be specified. + :param fallback_mode: defines what to do if there is no item for the given language + :return: the item + """ + try: + return self._lang2item[lang] + except KeyError as outer_e: + if fallback_mode == LanguageFallbackMode.EXCEPTION: + raise KeyError(f"Item for language '{lang}' not found for name '{self.name}'") from outer_e + if fallback_mode == LanguageFallbackMode.ANY: + try: + return next(iter(self._lang2item.values())) + except StopIteration as e: + raise KeyError(f"No items registered for any language in container '{self.name}'") from e + if fallback_mode == LanguageFallbackMode.USE_DEFAULT_LANG: + try: + return self._lang2item[DEFAULT_LANG_CODE] + except KeyError as e: + raise KeyError( + f"Item not found neither for {lang=} nor for the default language '{DEFAULT_LANG_CODE}' in container '{self.name}'" + ) from e + + def __len__(self) -> int: + return len(self._lang2item) + + +class MultiLangPromptTemplate(ParameterizedTemplateInterface): + _FORBIDDEN_PARAM_NAMES: ClassVar[set[str]] = {"lang_code", "fallback_mode"} # avoid collisions with **kwargs in calls to render + + """ + Represents a prompt template with support for multiple languages. + The parameters of all prompt templates (for all languages) are (must be) the same. + """ + + def __init__(self, name: str) -> None: + self._prompts_container = _MultiLangContainer[PromptTemplate](name) + + def __len__(self) -> int: + return len(self._prompts_container) + + @property + def name(self) -> str: + return self._prompts_container.name + + def add_prompt_template( + self, prompt_template: PromptTemplate, lang_code: str = DEFAULT_LANG_CODE, allow_overwrite: bool = False + ) -> None: + """ + Adds a prompt template for a new language. + The parameters of all prompt templates (for all languages) are (must be) the same, so if a prompt template is already registered, + the parameters of the new prompt template should be the same as the existing ones. + + :param prompt_template: the prompt template to add + :param lang_code: the language code for which to add the prompt template. For single-language use cases, you should always use the default language code. + :param allow_overwrite: whether to allow overwriting an existing entry for the same language + """ + incoming_parameters = prompt_template.get_parameters() + if contained_fobidden_parameters := self._FORBIDDEN_PARAM_NAMES.intersection(incoming_parameters): + raise ValueError( + f"Cannot add prompt template for language '{lang_code}' to MultiLangPromptTemplate '{self.name}'" + f"since it contains forbidden parameter names: {contained_fobidden_parameters}" + ) + if len(self) > 0: + parameters = self.get_parameters() + if parameters != incoming_parameters: + raise ValueError( + f"Cannot add prompt template for language '{lang_code}' to MultiLangPromptTemplate '{self.name}'" + f"because the parameters are inconsistent: {parameters} vs {prompt_template.get_parameters()}" + ) + + self._prompts_container.add_item(prompt_template, lang_code, allow_overwrite) + + def get_prompt_template( + self, lang_code: str = DEFAULT_LANG_CODE, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION + ) -> PromptTemplate: + return self._prompts_container.get_item(lang_code, fallback_mode) + + def get_parameters(self) -> list[str]: + if len(self) == 0: + raise RuntimeError( + f"No prompt templates registered for MultiLangPromptTemplate '{self.name}', make sure to register a prompt template before accessing the parameters" + ) + first_prompt_template = next(iter(self._prompts_container._lang2item.values())) + return first_prompt_template.get_parameters() + + def render( + self, lang_code: str = DEFAULT_LANG_CODE, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION, **kwargs: Any + ) -> str: + prompt_template = self.get_prompt_template(lang_code, fallback_mode) + return prompt_template.render(**kwargs) + + +class MultiLangPromptList(_MultiLangContainer[PromptList]): + pass + + +class PromptCollection: + """ + Main class for managing a collection of prompt templates and prompt lists, with support for multiple languages. + All data will be read from the yamls directly contained in the given directory on initialization. + It is thus assumed that you manage one directory per prompt collection. + + The yamls are assumed to be either of the form (for single-language support): + + ```yaml + : + : [, , ...] + ``` + + or (necessary for multi-language support): + + ```yaml + lang: # optional, defaults to "default" + prompts: + : + + : [, , ...] + + ``` + + When specifying prompt templates for multiple languages, make sure that the parameters are the same for all languages + (you will get an exception otherwise). + + The prompt names must be unique (for the same language) within the collection. + """ + + def __init__(self, prompts_dir: str, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> None: + """ + :param prompts_dir: the directory containing the prompt templates and prompt lists + :param fallback_mode: the fallback mode to use when a prompt template or prompt list is not found for the requested language. + May be reset after initialization. + """ + self._multi_lang_prompt_templates: dict[str, MultiLangPromptTemplate] = {} + self._multi_lang_prompt_lists: dict[str, MultiLangPromptList] = {} + self._load_from_disc(prompts_dir) + self.fallback_mode = fallback_mode + + def _add_prompt_template(self, name: str, template_str: str, lang_code: str = DEFAULT_LANG_CODE) -> None: + """ + :param name: name of the prompt template + :param template_str: the Jinja template string + :param lang_code: the language code for which to add the prompt template. + """ + prompt_template = PromptTemplate(name, template_str) + mlpt = self._multi_lang_prompt_templates.get(name) + if mlpt is None: + mlpt = MultiLangPromptTemplate(name) + self._multi_lang_prompt_templates[name] = mlpt + mlpt.add_prompt_template(prompt_template, lang_code=lang_code) + + def _add_prompt_list(self, name: str, prompt_list: list[str], lang_code: str = DEFAULT_LANG_CODE) -> None: + """ + :param name: name of the prompt list + :param prompt_list: a list of prompts + :param lang_code: the language code for which to add the prompt list. + """ + multilang_prompt_list = self._multi_lang_prompt_lists.get(name) + if multilang_prompt_list is None: + multilang_prompt_list = MultiLangPromptList(name) + self._multi_lang_prompt_lists[name] = multilang_prompt_list + multilang_prompt_list.add_item(PromptList(prompt_list), lang_code=lang_code) + + def _load_from_disc(self, prompts_dir: str) -> None: + """Loads all prompt templates and prompt lists from yaml files in the given directory.""" + for fn in os.listdir(prompts_dir): + if not fn.endswith((".yml", ".yaml")): + log.debug(f"Skipping non-YAML file: {fn}") + continue + path = os.path.join(prompts_dir, fn) + with open(path, encoding="utf-8") as f: + data = yaml.safe_load(f) + try: + prompts_data = data["prompts"] + except KeyError as e: + raise KeyError(f"Invalid yaml structure (missing 'prompts' key) in file {path}") from e + + lang_code = prompts_data.get("lang", DEFAULT_LANG_CODE) + # add the data to the collection + for prompt_name, prompt_template_or_list in prompts_data.items(): + if isinstance(prompt_template_or_list, list): + self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code) + elif isinstance(prompt_template_or_list, str): + self._add_prompt_template(prompt_name, prompt_template_or_list, lang_code=lang_code) + else: + raise ValueError( + f"Invalid prompt type for {prompt_name} in file {path} (should be str or list): {prompt_template_or_list}" + ) + + def get_prompt_template_names(self) -> list[str]: + return list(self._multi_lang_prompt_templates.keys()) + + def get_prompt_list_names(self) -> list[str]: + return list(self._multi_lang_prompt_lists.keys()) + + def __len__(self) -> int: + return len(self._multi_lang_prompt_templates) + + def get_multilang_prompt_template(self, prompt_name: str) -> MultiLangPromptTemplate: + """The MultiLangPromptTemplate object for the given prompt name. For single-language use cases, you should use the `get_prompt_template` method instead.""" + return self._multi_lang_prompt_templates[prompt_name] + + def get_multilang_prompt_list(self, prompt_name: str) -> MultiLangPromptList: + return self._multi_lang_prompt_lists[prompt_name] + + def get_prompt_template( + self, + prompt_name: str, + lang_code: str = DEFAULT_LANG_CODE, + ) -> PromptTemplate: + """The PromptTemplate object for the given prompt name and language code.""" + return self.get_multilang_prompt_template(prompt_name).get_prompt_template(lang_code=lang_code, fallback_mode=self.fallback_mode) + + def get_prompt_template_parameters(self, prompt_name: str) -> list[str]: + """The parameters of the PromptTemplate object for the given prompt name.""" + return self.get_multilang_prompt_template(prompt_name).get_parameters() + + def get_prompt_list(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> PromptList: + """The PromptList object for the given prompt name and language code.""" + return self.get_multilang_prompt_list(prompt_name).get_item(lang_code) + + def render_prompt_template( + self, + prompt_name: str, + lang_code: str = DEFAULT_LANG_CODE, + **kwargs: Any, + ) -> str: + """Renders the prompt template for the given prompt name and language code.""" + return self.get_prompt_template(prompt_name, lang_code=lang_code).render(**kwargs) + + +class PromptFactoryBase: + """A base class for auto-generated prompt factory classes.""" + + def __init__(self, prompts_dir: str, lang_code: str = DEFAULT_LANG_CODE, fallback_mode=LanguageFallbackMode.EXCEPTION): + """ + :param prompts_dir: the directory containing the prompt templates and prompt lists + :param lang_code: the language code to use for retrieving the prompt templates and prompt lists. + Leave as `default` for single-language use cases. + :param fallback_mode: the fallback mode to use when a prompt template or prompt list is not found for the requested language. + Irrelevant for single-language use cases. + """ + self.lang_code = lang_code + self._prompt_collection = PromptCollection(prompts_dir, fallback_mode=fallback_mode) + + def _render_prompt(self, prompt_name: str, **kwargs) -> str: + return self._prompt_collection.render_prompt_template(prompt_name, self.lang_code, **kwargs) + + def _get_prompt_list(self, prompt_name: str) -> PromptList: + return self._prompt_collection.get_prompt_list(prompt_name, self.lang_code) + + +def autogenerate_prompt_factory_module(prompts_dir: str, target_module_path: str) -> None: + """ + Autogenerates a prompt factory module for the given prompt collection directory. + The generated `PromptFactory` class is meant to be the central entry class for retrieving and rendering prompt templates and prompt lists + in your application. It will contain one method per prompt template and prompt list, and is useful for both single- and multi-language use cases. + + :param prompts_dir: the directory containing the prompt templates and prompt lists + :param target_module_path: the path to the target module file. Important: The module will be overwritten! + """ + generated_code = """ +# NOTE: This class is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually! +# ruff: noqa +# black: skip +# mypy: ignore-errors + +from interprompt.promt_template import PromptFactoryBase, PromptList +from typing import Any + +class PromptFactory(PromptFactoryBase): + \""" + A class for retrieving and rendering prompt templates and prompt lists. + \""" +""" + # ---- add methods based on prompt template names and parameters and prompt list names ---- + prompt_collection = PromptCollection(prompts_dir) + + for template_name in prompt_collection.get_prompt_template_names(): + template_parameters = prompt_collection.get_prompt_template_parameters(template_name) + render_call_str = f'"{template_name}"' + if len(template_parameters) == 0: + method_params_str = "" + else: + method_params_str = ", *, " + ", ".join([f"{param}: Any" for param in template_parameters]) + render_call_str += ", " + ", ".join([f"{param}={param}" for param in template_parameters]) + generated_code += f""" + def create_{template_name}(self{method_params_str}) -> str: + return self._render_prompt({render_call_str}) +""" + for prompt_list_name in prompt_collection.get_prompt_list_names(): + generated_code += f""" + def get_list_{prompt_list_name}(self) -> PromptList: + return self._get_prompt_list('{prompt_list_name}') +""" + + with open(target_module_path, "w", encoding="utf-8") as f: + f.write(generated_code) + log.info(f"Prompt factory generated successfully in {target_module_path}") diff --git a/src/interprompt/util/__init__.py b/src/interprompt/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/interprompt/util/class_decorators.py b/src/interprompt/util/class_decorators.py new file mode 100644 index 00000000..2aeb3f8d --- /dev/null +++ b/src/interprompt/util/class_decorators.py @@ -0,0 +1,13 @@ +from typing import Any + + +def singleton(cls: type[Any]) -> Any: + instance = None + + def get_instance(*args: Any, **kwargs: Any) -> Any: + nonlocal instance + if instance is None: + instance = cls(*args, **kwargs) + return instance + + return get_instance diff --git a/src/serena/agent.py b/src/serena/agent.py index c90c01dd..34cbe1d4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -29,7 +29,7 @@ from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind from serena import serena_root_path, serena_version -from serena.llm.prompt_factory import PromptFactory +from serena.prompt_factory import PromptFactory from serena.symbol import SymbolLocation, SymbolManager from serena.text_utils import search_files from serena.util.class_decorators import singleton diff --git a/src/serena/constants.py b/src/serena/constants.py new file mode 100644 index 00000000..1f456514 --- /dev/null +++ b/src/serena/constants.py @@ -0,0 +1,6 @@ +from pathlib import Path + +_repo_root_path = Path(__file__).parent.parent.parent + +REPO_ROOT = str(_repo_root_path) +PROMPT_TEMPLATES_DIR = str(_repo_root_path / "config" / "prompt_templates") diff --git a/src/serena/llm/jinja_template.py b/src/serena/llm/jinja_template.py deleted file mode 100644 index 8dc5e992..00000000 --- a/src/serena/llm/jinja_template.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Any - -import jinja2 -import jinja2.meta -import jinja2.nodes -import jinja2.visitor - -from serena.util.class_decorators import singleton - - -@singleton -class JinjaEnvProvider: - def __init__(self) -> None: - self._env: jinja2.Environment | None = None - - def get_env(self) -> jinja2.Environment: - if self._env is None: - self._env = jinja2.Environment() - return self._env - - -class JinjaTemplate: - def __init__(self, template_string: str) -> None: - self._template_string = template_string - self._template = JinjaEnvProvider().get_env().from_string(self._template_string) - - def render(self, **kwargs: Any) -> str: - return self._template.render(**kwargs) - - def get_parameters(self) -> set[str]: - parsed_content = self._template.environment.parse(self._template_string) - return jinja2.meta.find_undeclared_variables(parsed_content) diff --git a/src/serena/llm/multilang_prompt.py b/src/serena/llm/multilang_prompt.py deleted file mode 100644 index 4092e735..00000000 --- a/src/serena/llm/multilang_prompt.py +++ /dev/null @@ -1,204 +0,0 @@ -import os -from collections.abc import Callable -from enum import Enum -from typing import Any, Generic, TypeVar - -import yaml -from sensai.util.string import ToStringMixin - -from .jinja_template import JinjaTemplate - -LANG_CODES = ["en", "de"] - - -class PromptTemplate(ToStringMixin): - def __init__(self, name: str, jinja_template_string: str) -> None: - self.name = name - self.jinja_template = JinjaTemplate(jinja_template_string.strip()) - self.parameters = self.jinja_template.get_parameters() - - def _tostring_excludes(self) -> list[str]: - return ["jinja_template"] - - def instantiate(self, **kwargs: Any) -> str: - return self.jinja_template.render(**kwargs) - - -class PromptList: - def __init__(self, items: list[str]) -> None: - self.items = [x.strip() for x in items] - - def to_string(self) -> str: - bullet = " * " - indent = " " * len(bullet) - items = [x.replace("\n", "\n" + indent) for x in self.items] - return "\n * ".join(items) - - -T = TypeVar("T") - - -class MultiLangContainer(Generic[T], ToStringMixin): - """ - Represents a container of items which are associated with different languages - """ - - def __init__(self, name: str) -> None: - self.name = name - self.lang2item: dict[str, T] = {} - - def _tostring_excludes(self) -> list[str]: - return ["lang2item"] - - def _tostring_additional_entries(self) -> dict[str, Any]: - return dict(languages=list(self.lang2item.keys())) - - class FallbackMode(Enum): - ANY = "any" - """ - If the requested language is not found, return the item for any language - """ - EXCEPTION = "exception" - """ - If the requested language is not found, raise an exception - """ - - def add_item(self, item: T, lang: str = "") -> None: - self.lang2item[lang] = item - - def get_item(self, lang: str, fallback_mode: FallbackMode = FallbackMode.EXCEPTION) -> T: - """ - Gets the item for the given language. - - :param lang: the language shortcode for which to obtain the prompt template - :param fallback_mode: defines what to do if there is no item for the given language - :return: the item - """ - item = self.lang2item.get(lang) - if item is not None: - return item - else: - if fallback_mode == self.FallbackMode.EXCEPTION: - raise KeyError(f"Item for language '{lang}' not found for name '{self.name}'") - if fallback_mode == self.FallbackMode.ANY: - return next(iter(self.lang2item.values())) - else: - raise ValueError(f"Invalid fallback mode '{fallback_mode}'") - - -class MultiLangPromptTemplate(MultiLangContainer[PromptTemplate]): - """ - Represents a prompt template which potentially supports multiple languages. - """ - - def get_parameters(self) -> list[str]: - """ - Gets the parameters of the prompt template (which must be the same for all languages). - - :return: the list of parameters required to instantiate the template - """ - prev_params = None - for template in self.lang2item.values(): - params = set(template.parameters) - if prev_params is not None: - assert ( - params == prev_params - ), f"Parameters of MLPT '{self.name}' are inconsistent: {sorted(params)} vs {sorted(prev_params)}" - prev_params = params - assert prev_params is not None - return sorted(prev_params) - - -class MultiLangPromptList(MultiLangContainer[PromptList]): - pass - - -class MultiLangPromptTemplateCollection: - """ - Represents a collection of multi-language prompts, which are read from the prompts/ directory. - The language of a prompt is determined by suffix in the prompt name, e.g. "_de" for German. - For yaml files, the language can also be set globally for all prompts within it. - - The prompts/ folder may contain: - - .txt files with a single prompt template (filename defines prompt name, content is the prompt template) - - .yml files with multiple prompt templates under the top-level key 'prompts'. - Each key under 'prompts' defines a prompt name and the corresponding value is the prompt template. - The value can also be a list of prompts instead of a single template. - The language of all can be set by specifying the key 'lang' in addition to 'prompts'. - """ - - def __init__(self) -> None: - self.prompt_templates: dict[str, MultiLangPromptTemplate] = {} - self.prompt_lists: dict[str, MultiLangPromptList] = {} - prompts_dir = self._prompt_template_folder() - self._read_prompt_templates(prompts_dir) - - @classmethod - def _prompt_template_folder(cls) -> str: - dir_path = os.path.dirname(os.path.realpath(__file__)) - prompts_dir = None - for i in range(5): - dir_path = os.path.join(dir_path, "..") - prompts_dir = os.path.join(dir_path, "prompts") - if os.path.isdir(prompts_dir): - break - if prompts_dir is None or not os.path.isdir(prompts_dir): - raise FileNotFoundError("Could not find the 'prompts' directory") - return prompts_dir - - @staticmethod - def _container_lang(prompt_name: str, collection: dict[str, T], container_factory: Callable[[str], T]) -> tuple[T, str]: - lang = "" - if prompt_name[-2:] in LANG_CODES and prompt_name[-3] == "_": - lang = prompt_name[-2:] - prompt_name = prompt_name[:-3] - - container = collection.get(prompt_name) - if container is None: - container = container_factory(prompt_name) - collection[prompt_name] = container - - return container, lang - - def _add_prompt_template(self, prompt_name: str, jinja_prompt_template: str) -> None: - """ - :param prompt_name: a prompt name, which may have a language shortcode suffix (e.g. "_de") - :param jinja_prompt_template: the actual prompt string which may contain placeholders/parameters (e.g. "{name}") - """ - multilang_prompt_template, lang = self._container_lang(prompt_name, self.prompt_templates, MultiLangPromptTemplate) - multilang_prompt_template.add_item(PromptTemplate(prompt_name, jinja_prompt_template), lang=lang) - - def _add_prompt_list(self, prompt_name: str, prompt_list: list[str]) -> None: - """ - :param prompt_name: a prompt name, which may have a language shortcode suffix (e.g. "_de") - :param prompt_list: a list of prompts - """ - multilang_prompt_list, lang = self._container_lang(prompt_name, self.prompt_lists, MultiLangPromptList) - multilang_prompt_list.add_item(PromptList(prompt_list), lang=lang) - - def _read_prompt_templates(self, prompts_dir: str) -> None: - for fn in os.listdir(prompts_dir): - path = os.path.join(prompts_dir, fn) - if fn.endswith(".txt"): - with open(path) as file: - prompt_template_string = file.read() - self._add_prompt_template(fn[:-4], prompt_template_string) - elif fn.endswith(".yml"): - with open(path, encoding="utf-8") as f: - data = yaml.safe_load(f) - assert "prompts" in data, f"YAML file {fn} must contain key 'prompts'" - lang_suffix = ("_" + data["lang"]) if "lang" in data else "" - for prompt_name, prompt in data["prompts"].items(): - prompt_name += lang_suffix - if isinstance(prompt, list): - self._add_prompt_list(prompt_name, prompt) - elif isinstance(prompt, str): - self._add_prompt_template(prompt_name, prompt) - else: - raise ValueError(f"Invalid prompt type: {prompt}") - - def get_multilang_prompt_template(self, prompt_name: str) -> MultiLangPromptTemplate: - return self.prompt_templates[prompt_name] - - def get_multilang_prompt_list(self, prompt_name: str) -> MultiLangPromptList: - return self.prompt_lists[prompt_name] diff --git a/src/serena/llm/prompt_factory.py b/src/serena/llm/prompt_factory.py deleted file mode 100644 index fce9245a..00000000 --- a/src/serena/llm/prompt_factory.py +++ /dev/null @@ -1,78 +0,0 @@ -# ruff: noqa -# black: skip -# mypy: ignore-errors - -from typing import List, Optional - -from .multilang_prompt import MultiLangContainer, MultiLangPromptTemplateCollection, PromptList - - -class PromptFactory: - # NOTE: This class is auto-generated by gen_prompt_factory.py - - def __init__(self, lang_shortcode: str = "en", fallback_mode=MultiLangContainer.FallbackMode.EXCEPTION): - self.lang_shortcode = lang_shortcode - self.collection = MultiLangPromptTemplateCollection() - self.fallback_mode = fallback_mode - self.context_extension = "" - self.mode_extensions = [] - - def _format_prompt(self, prompt_name: str, kwargs) -> str: - del kwargs["self"] - mpt = self.collection.get_multilang_prompt_template(prompt_name) - return mpt.get_item(self.lang_shortcode, self.fallback_mode).instantiate(**kwargs) - - def _get_list(self, prompt_name: str) -> PromptList: - mpl = self.collection.get_multilang_prompt_list(prompt_name) - return mpl.get_item(self.lang_shortcode, self.fallback_mode) - - def set_context(self, context_extension: str) -> None: - """Set the context extension for the system prompt.""" - self.context_extension = context_extension - - def set_modes(self, mode_extensions: List[str]) -> None: - """Set the mode extensions for the system prompt.""" - self.mode_extensions = mode_extensions - - def create_system_prompt(self) -> str: - """Create the system prompt with context and mode extensions.""" - base_prompt = self._format_prompt("system_prompt", locals()) - - # Add context and mode extensions - extensions = [] - if self.context_extension: - extensions.append(self.context_extension) - - for mode_extension in self.mode_extensions: - extensions.append(mode_extension) - - # If no extensions, return the base prompt - if not extensions: - return base_prompt - - # Combine the base prompt with the extensions - combined_prompt = base_prompt + "\n\n" + "\n\n".join(extensions) - return combined_prompt - - def create_onboarding_prompt(self, *, system) -> str: - return self._format_prompt('onboarding_prompt', locals()) - - def create_think_about_collected_information(self) -> str: - return self._format_prompt('think_about_collected_information', locals()) - - def create_think_about_task_adherence(self) -> str: - return self._format_prompt('think_about_task_adherence', locals()) - - def create_think_about_whether_you_are_done(self) -> str: - return self._format_prompt('think_about_whether_you_are_done', locals()) - - def create_summarize_changes(self) -> str: - return self._format_prompt('summarize_changes', locals()) - - def create_prepare_for_new_conversation(self) -> str: - return self._format_prompt('prepare_for_new_conversation', locals()) - - def create_system_prompt(self) -> str: - return self._format_prompt('system_prompt', locals()) - - \ No newline at end of file diff --git a/src/serena/prompt_factory.py b/src/serena/prompt_factory.py new file mode 100644 index 00000000..504cd6d1 --- /dev/null +++ b/src/serena/prompt_factory.py @@ -0,0 +1,34 @@ + +# NOTE: This class is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually! +# ruff: noqa +# black: skip +# mypy: ignore-errors + +from interprompt.promt_template import PromptFactoryBase, PromptList +from typing import Any + +class PromptFactory(PromptFactoryBase): + """ + A class for retrieving and rendering prompt templates and prompt lists. + """ + + def create_onboarding_prompt(self, *, system: Any) -> str: + return self._render_prompt("onboarding_prompt", system=system) + + def create_think_about_collected_information(self) -> str: + return self._render_prompt("think_about_collected_information") + + def create_think_about_task_adherence(self) -> str: + return self._render_prompt("think_about_task_adherence") + + def create_think_about_whether_you_are_done(self) -> str: + return self._render_prompt("think_about_whether_you_are_done") + + def create_summarize_changes(self) -> str: + return self._render_prompt("summarize_changes") + + def create_prepare_for_new_conversation(self) -> str: + return self._render_prompt("prepare_for_new_conversation") + + def create_system_prompt(self) -> str: + return self._render_prompt("system_prompt") diff --git a/src/serena/util/class_decorators.py b/src/serena/util/class_decorators.py index 948ad72a..6beadbde 100644 --- a/src/serena/util/class_decorators.py +++ b/src/serena/util/class_decorators.py @@ -1,6 +1,8 @@ from typing import Any +# duplicate of interprompt.class_decorators +# We don't want to depend on interprompt for this in serena, so we duplicate it here def singleton(cls: type[Any]) -> Any: instance = None