Refactored prompt-template related code (new package interprompt)

This commit is contained in:
Michael Panchenko committed 2025-05-22 22:11:04 +02:00
1 parent 0884fc7a8c
commit 13a1d236dc
24 files changed
+624 -505

No files matched your search

-57
View File
@@ -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
@@ -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:
@@ -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: []
@@ -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.
@@ -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: []
@@ -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.
@@ -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
@@ -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.
@@ -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.
+14
View File
@@ -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}
+12 -35
View File
@@ -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__":
+18
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
from .promt_template import autogenerate_prompt_factory_module
__all__ = ["autogenerate_prompt_factory_module"]
+43
View File
@@ -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
+396
View File
@@ -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
<prompt_name>: <prompt_template_string>
<prompt_list_name>: [<prompt_template_string_1>, <prompt_template_string_2>, ...]
```
or (necessary for multi-language support):
```yaml
lang: <language_code> # optional, defaults to "default"
prompts:
<prompt_name>:
<prompt_template_string>
<prompt_list_name>: [<prompt_template_string_1>, <prompt_template_string_2>, ...]
```
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}")
View File
Whitespace-only changes.
+13
View File
@@ -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
+1 -1
View File
@@ -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
+6
View File
@@ -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")
-32
View File
@@ -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)
-204
View File
@@ -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]
-78
View File
@@ -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())
+34
View File
@@ -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")
+2
View File
@@ -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