mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 10:16:00 +00:00
Refactored prompt-template related code (new package interprompt)
This commit is contained in:
1 parent
0884fc7a8c
commit
13a1d236dc
24 files changed
+624
-505
No files matched your search
@@ -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.
|
||||
@@ -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}
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
from .promt_template import autogenerate_prompt_factory_module
|
||||
|
||||
__all__ = ["autogenerate_prompt_factory_module"]
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
Whitespace-only changes.
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user