Merge pull request #126 from oraios/feature/context-and-mode

Feature/context and mode
This commit is contained in:
Michael Panchenko authored and GitHub committed 2025-05-24 18:23:25 +02:00
commit fea2a37169
39 files changed
+1353 -578

No files matched your search

+6
View File
@@ -1,6 +1,12 @@
# Latest
Changes prior to the next official version change will appear here.
# 2025-05-24
Important new feature: configurability of mode and context, allowing better integration in a variety of clients.
Also some improvements to prompts.
# 2025-05-21
**Signficant improvement in symbol finding!**
+96 -40
View File
@@ -89,6 +89,7 @@ implementation.
* [API-Based Coding Agents](#api-based-coding-agents)
* [Other MCP-Based Coding Agents](#other-mcp-based-coding-agents)
- [Onboarding and Memories](#onboarding-and-memories)
- [Modes and Contexts](#modes-and-contexts)
- [Combination with Other MCP Servers](#combination-with-other-mcp-servers)
- [Recommendations on Using Serena](#recommendations-on-using-serena)
* [Which Model to Choose?](#which-model-to-choose)
@@ -185,14 +186,18 @@ want to use Serena.
"mcpServers": {
"serena": {
"command": "/abs/path/to/uv",
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "--project-file", "/abs/path/to/myproject.yml"]
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "--project", "/abs/path/to/project"]
}
}
}
```
:info: passing the project file is optional if you have set `enable_project_activation` in your configuration,
as this setting will allow you to simply instruct Claude to activate the project you want to work on.
:info: Passing the project file is optional if you have set `enable_project_activation` in your configuration, as this setting will allow you to simply instruct Claude to activate the project you want to work on.
* For **Claude Desktop** (as shown above), Serena's default context (`desktop-app`) and modes (e.g., `interactive`, `editing`) are typically suitable for general use. You usually don't need to specify them explicitly in the `args` unless you want to override the defaults.
* For **IDE integrations** (like VSCode, Cursor, Cline, etc., configured by adding Serena as an MCP server), you **should explicitly pass the `ide-assistant` context** by adding `"--context", "ide-assistant"` to the `args` in your MCP client's configuration. You can also specify initial modes if desired (e.g., `"--mode", "editing"`).
* For specific **one-shot tasks** across any client (e.g., generating a report or a plan in a single interaction), you might want to instruct Serena (once connected) to switch to modes like `planning` and `one-shot` using the `switch_modes` tool, or set these initially via the `--mode` flags if configuring the server launch command directly for such tasks.
See the [Modes and Contexts](#modes-and-contexts) section for more details on available options and customization.
If you are using paths containing backslashes for paths on Windows
(note that you can also just use forward slashes), be sure to escape them correctly (`\\`).
@@ -205,9 +210,9 @@ Some client/OS/setup configurations were reported to cause issues when using Ser
If you experience such problems, you can start Serena in `sse` mode by running, e.g.,
```shell
uv run --directory /path/to/serena serena-mcp-server --transport sse --port 9121 --project-file /path/to/project.yml
uv run --directory /path/to/serena serena-mcp-server --transport sse --port 9121 --project /path/to/project
```
(the `--project-file` option is optional). Then configure your client to connect to `http://localhost:9121`.
(the `--project` option is optional). Then configure your client to connect to `http://localhost:9121`.
Note: on Windows and macOS there are official Claude Desktop applications by Anthropic, for Linux there is an [open-source
community version](https://github.com/aaddrick/claude-desktop-debian).
@@ -237,16 +242,16 @@ several examples for that and have heard very positive feedback so far. Claude C
add serena with
```shell
claude mcp add serena -- /path/to/uv "run" --directory /path/to/serena serena-mcp-server --project-file /path/to/project.yml
claude mcp add serena -- /path/to/uv "run" --directory /path/to/serena serena-mcp-server --project-file /path/to/project
```
### Other MCP Clients - Cline, Roo-Code, Cursor, Windsurf etc.
Being an MCP Server, Serena can be included in any MCP Client. The same config as above,
maybe with small client-specific modifications should work. Most of the popular
maybe with small client-specific modifications, should work. Most of the popular
existing coding assistants (IDE extensions or VSCode-like IDEs) accept connecting
to MCP Servers. Including Serena generally boosts their performance
to MCP Servers. It is ** recommended to use the `ide-assistant` context** for these integrations by adding `"--context", "ide-assistant"` to the `args` in your MCP client's configuration. Including Serena generally boosts their performance
by providing them tools for symbolic operations.
In this case, the billing for the usage continues to be controlled by the client of your choice
@@ -274,7 +279,7 @@ Follow the instructions [here](https://block.github.io/goose/docs/getting-starte
After that, use `goose configure` to add an extension. For adding Serena, choose the option `Command-line Extension`, name it `Serena` and add the following as command:
```
/abs/path/to/uv run --directory /abs/path/to/serena serena-mcp-server /optional/abs/path/to/project.yml
/abs/path/to/uv run --directory /abs/path/to/serena serena-mcp-server --project /optional/abs/path/to/project
```
Since Serena can do all necessary editing and command operations, you should disable the `developer` extension that goose enables by default.
@@ -365,7 +370,8 @@ it was done by us for agno in the [SerenaAgnoToolkit](/src/serena/agno.py).
## Serena's Tools and Configuration
Serena combines tools for semantic code retrieval with editing capabilities and shell execution.
Find the complete list of tools [below](#serenas-tools-and-configuration).
Serena's behavior can be further customized through [Modes and Contexts](#modes-and-contexts).
Find the complete list of tools [below](#full-list-of-tools).
The use of all tools is generally recommended, as this allows Serena to provide the most value:
Only by executing shell commands (in particular, tests) can Serena identify and correct mistakes
@@ -463,6 +469,51 @@ Every file in the `.serena/memories/` directory is a memory file.
We found the memories to significantly improve the user experience with Serena.
By itself, Serena is instructed to create new memories whenever appropriate.
## Modes and Contexts
Serena's behavior and toolset can be adjusted using **contexts** and **modes**. These allow for a high degree of customization to best suit your workflow and the environment Serena is operating in.
### Contexts
A **context** defines the general environment in which Serena is operating. It influences the initial system prompt and the set of available tools. A context is set at startup when launching Serena (e.g., via CLI options for an MCP server or in the agent script) and cannot be changed during an active session.
Serena comes with pre-defined contexts:
* `desktop-app`: Tailored for use with desktop applications like Claude Desktop. This is often the default.
* `agent`: Designed for scenarios where Serena acts as a more autonomous agent, for example, when used with Agno.
* `ide-assistant`: Optimized for integration into IDEs like VSCode, Cursor, or Cline, focusing on in-editor coding assistance.
You should choose the context that best matches your integration.
### Modes
**Modes** further refine Serena's behavior for specific types of tasks or interaction styles. Multiple modes can be active simultaneously, allowing you to combine their effects. Modes influence the system prompt and can also alter the set of available tools by excluding certain ones.
Examples of built-in modes include:
* `planning`: Focuses Serena on planning and analysis tasks.
* `editing`: Optimizes Serena for direct code modification tasks.
* `interactive`: Suitable for a conversational, back-and-forth interaction style.
* `one-shot`: Configures Serena for tasks that should be completed in a single response, often used with `planning` for generating reports or initial plans.
* `no-onboarding`: Skips the initial onboarding process if it's not needed for a particular session.
* `onboarding`: (Usually triggered automatically) Focuses on the project onboarding process.
Modes can be set at startup (similar to contexts) but can also be **switched dynamically** during a session. You can instruct the LLM to use the `switch_modes` tool to activate a different set of modes (e.g., "switch to planning and one-shot modes").
:warning: **Mode Compatibility**: While you can combine modes, some may be semantically incompatible (e.g., `interactive` and `one-shot`). Serena currently does not prevent incompatible combinations; it is up to the user to choose sensible mode configurations.
### Customizing Contexts and Modes
You can create your own contexts and modes to precisely tailor Serena to your needs:
1. **Adding to your Serena clone**: Create new `.yml` files in the `config/contexts/` or `config/modes/` directories within your local Serena repository. These custom contexts/modes will be automatically registered and available for use by their filename (without the `.yml` extension). They will also appear in listings of available contexts/modes.
2. **Using external YAML files**: When starting Serena, you can provide an absolute path to a custom `.yml` file for a context or mode.
A context or mode YAML file typically defines:
* `name`: (Optional if filename is used) The name of the context/mode.
* `prompt`: A string that will be incorporated into Serena's system prompt.
* `description`: (Optional) A brief description.
* `excluded_tools`: A list of tool names (strings) to disable when this context/mode is active.
This customization allows for deep integration and adaptation of Serena to specific project requirements or personal preferences.
## Combination with Other MCP Servers
When using Serena through an MCP Client, you can use it together with other MCP servers.
@@ -603,7 +654,7 @@ manually, if needed.
### Serena Logging
To help with troubleshooting, we have written a small GUI utility for logging. For most clients, we recommend that you enable it
through the project configuration (`myproject.yml`) if you encounter problems. Many clients also write MCP logs that can help identify issues.
through the project configuration (`project.yml`) if you encounter problems. Many clients also write MCP logs that can help identify issues.
The logging GUI may not work for all clients and on all systems. Currently, it does not work on macOS or within VSCode extensions like Cline.
@@ -642,32 +693,37 @@ For details on contributing, see [here](/CONTRIBUTING.md).
Here is the full list of Serena's tools with a short description (output of `uv run serena-list-tools`):
* `activate_project`: Activates a project by name.
* `check_onboarding_performed`: Checks whether the onboarding was already performed.
* `create_text_file`: Creates/overwrites a file in the project directory.
* `delete_lines`: Deletes a range of lines within a file.
* `delete_memory`: Deletes a memory from Serena's project-specific memory store.
* `execute_shell_command`: Executes a shell command.
* `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
* `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
* `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
* `get_active_project`: Gets the name of the currently active project (if any) and lists existing projects
* `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory.
* `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
* `insert_at_line`: Inserts content at a given line in a file.
* `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
* `list_dir`: Lists files and directories in the given directory (optionally with recursion).
* `list_memories`: Lists memories in Serena's project-specific memory store.
* `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
* `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
* `read_file`: Reads a file within the project directory.
* `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
* `replace_lines`: Replaces a range of lines within a file with new content.
* `replace_symbol_body`: Replaces the full definition of a symbol.
* `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
* `search_for_pattern`: Performs a search for a pattern in the project.
* `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
* `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
* `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
* `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
* `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
* `activate_project`: Activates a project by name.
* `check_onboarding_performed`: Checks whether project onboarding was already performed.
* `create_text_file`: Creates/overwrites a file in the project directory.
* `delete_lines`: Deletes a range of lines within a file.
* `delete_memory`: Deletes a memory from Serena's project-specific memory store.
* `execute_shell_command`: Executes a shell command.
* `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
* `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
* `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
* `get_active_project`: Gets the name of the currently active project (if any) and lists existing projects
* `get_current_config`: Prints the current configuration of the agent, including the active modes, tools, and context.
* `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory.
* `initial_instructions`: Gets the initial instructions for the current project.
Should only be used in settings where the system prompt cannot be set,
e.g. in clients you have no control over, like Claude Desktop.
* `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
* `insert_at_line`: Inserts content at a given line in a file.
* `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
* `list_dir`: Lists files and directories in the given directory (optionally with recursion).
* `list_memories`: Lists memories in Serena's project-specific memory store.
* `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
* `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
* `read_file`: Reads a file within the project directory.
* `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
* `replace_lines`: Replaces a range of lines within a file with new content.
* `replace_symbol_body`: Replaces the full definition of a symbol.
* `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
* `search_for_pattern`: Performs a search for a pattern in the project.
* `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
* `switch_modes`: Activates modes by providing a list of their names
* `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
* `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
* `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
* `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
-25
View File
@@ -1,25 +0,0 @@
# ruff: noqa
# black: skip
# mypy: ignore-errors
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
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)
# methods
+6
View File
@@ -0,0 +1,6 @@
description: All tools except InitialInstructionsTool for agent context
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:
- initial_instructions
+13
View File
@@ -0,0 +1,13 @@
description: All tools included for desktop app context
prompt: |
You are running in desktop app context where the tools give you access to the code base as well as some
access to the file system, if configured. You interact with the user through a chat interface that is separated
from the code base. As a consequence, if you are in interactive mode, your communication with the user should
involve high-level thinking and planning as well as some summarization of any code edits that you make.
For viewing the code edits the user will view them in a separate code editor window, and the back-and-forth
between the chat and the code editor should be minimized as well as facilitated by you.
If complex changes have been made, advise the user on how to review them in the code editor.
If complex relationships that the user asked for should be visualized or explained, consider creating
a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering
options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions.
excluded_tools: []
+12
View File
@@ -0,0 +1,12 @@
description: Non-symbolic editing tools and general shell tool are excluded
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.
excluded_tools:
- create_text_file
- read_file
- delete_lines
- replace_lines
- insert_at_line
- execute_shell_command
+7
View File
@@ -0,0 +1,7 @@
description: All tools, with detailed instructions for code editing
prompt: |
You are operating in editing mode. You can edit files with the provided tools
to implement the requested changes to the code base while adhering to the project's code style and patterns.
Use symbolic editing tools whenever possible for precise code modifications.
If no editing task has yet been provided, wait for the user to provide one.
excluded_tools: []
+11
View File
@@ -0,0 +1,11 @@
description: Interactive mode for clarification and step-by-step work
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.
Break down complex tasks into smaller steps and explain your thinking at each stage. When you're uncertain about
a decision, present options to the user and ask for guidance rather than making assumptions.
Focus on providing informative results for intermediate steps so the user can follow along with your progress and
provide feedback as needed.
excluded_tools: []
+8
View File
@@ -0,0 +1,8 @@
description: Onboarding was already performed, exclude all onboarding tools
prompt: |
You have already performed onboarding, meaning that memories have already been created.
Read a list of available memories using the `list_memories` tool before proceeding with the task.
You don't need to read the actual memories, just remember that they exist and that you can read them later if they are relevant for your task.
excluded_tools:
- onboarding
- check_onboarding_performed
+16
View File
@@ -0,0 +1,16 @@
description: Only read-only tools, focused on analysis and planning
prompt: |
You are operating in onboarding mode. This is the first time you are seeing the project.
Your task is to collect relevant information about it and to save memories using the tools provided.
Call relevant onboarding tools for more instructions on how to do this.
In this mode, you should not be modifying any existing files.
If you are also in interactive mode and something about the project is unclear, ask the user for clarification.
excluded_tools:
- create_text_file
- replace_symbol_body
- insert_after_symbol
- insert_before_symbol
- delete_lines
- replace_lines
- insert_at_line
- execute_shell_command
+18
View File
@@ -0,0 +1,18 @@
description: Focus on completely finishing a task without interaction
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
should be working code with tests passing and all requirements fulfilled. Complete all necessary steps including
linting, formatting, and testing to ensure your solution is production-ready.
Only abort the task if absolutely necessary, such as when critical information is missing that cannot be inferred
from the codebase.
It may be that you have not received a task yet. In this case, wait for the user to provide a task, this will be the
only time you should wait for user interaction.
excluded_tools:
- get_current_config
- activate_project
- switch_modes
+23
View File
@@ -0,0 +1,23 @@
description: Only read-only tools, focused on analysis and planning
prompt: |
You are operating in planning mode. Your task is to analyze code but not write any code.
The user may ask you to assist in creating a comprehensive plan, or to learn something about the codebase -
either a small aspect of it or about the whole project.
When reading code in order to answer a user question or task, you should try reading only the necessary code.
Some tasks may require you to understand the architecture of large parts of the codebase, while for others,
it may be enough to read a single file or a small set of symbols.
You can achieve the intelligent reading of code by using the symbolic tools for getting an overview of symbols and
the relations between them, and then only reading the bodies of symbols that are necessary to answer the question
or complete the task. You can also use the standard tools like list_dir and search_for_pattern if you need to.
You generally have access to memories and it may be useful for you to read them, but also only if they help you
to answer the question or complete the task. You can infer which memories are relevant to the current task by reading
the memory names and descriptions.
excluded_tools:
- create_text_file
- replace_symbol_body
- insert_after_symbol
- insert_before_symbol
- delete_lines
- replace_lines
- insert_at_line
- execute_shell_command
@@ -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.
+20
View File
@@ -0,0 +1,20 @@
# 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.
The context and modes of operation are described below. From them you can infer how to interact with your user
and which tasks and kinds of interactions are expected of you.
Context description:
{{ context_system_prompt }}
Modes descriptions:
{% for prompt in mode_system_prompts %}
- {{ prompt }}
{% endfor %}
+35 -26
View File
@@ -25,31 +25,40 @@ read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run serena-list-tools`.
# execute `uv run scripts/print_tool_overview.py`.
#
# check_onboarding_performed
# create_text_file
# delete_lines
# delete_memory
# execute_shell_command
# find_referencing_symbols
# find_symbol
# get_dir_overview
# get_document_overview
# insert_after_symbol
# insert_at_line
# insert_before_symbol
# list_dir
# list_memories
# onboarding
# prepare_for_new_conversation
# read_file
# read_memory
# replace_symbol_body
# search_in_all_code
# summarize_changes
# think_about_collected_information
# think_about_task_adherence
# think_about_whether_you_are_done
# write_memory
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_active_project`: Gets the name of the currently active project (if any) and lists existing projects
# * `get_current_config`: Prints the current configuration of the agent, including the active modes, tools, and context.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
-1
View File
@@ -38,7 +38,6 @@ dependencies = [
[project.scripts]
serena-mcp-server = "serena.mcp:start_mcp_server"
serena-list-tools = "serena.agent:print_tool_overview"
[project.license]
text = "MIT"
+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", encoding="utf-8") 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", encoding="utf-8") 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" / "generated" / "generated_prompt_factory.py"),
)
if __name__ == "__main__":
+13
View File
@@ -0,0 +1,13 @@
from serena.config import SerenaAgentContext, SerenaAgentMode
if __name__ == "__main__":
print("---------- Available modes: ----------")
for mode_name in SerenaAgentMode.list_registered_mode_names():
mode = SerenaAgentMode.load(mode_name)
mode.print_overview()
print("\n")
print("---------- Available contexts: ----------")
for context_name in SerenaAgentContext.list_registered_context_names():
context = SerenaAgentContext.load(context_name)
context.print_overview()
print("\n")
+4
View File
@@ -0,0 +1,4 @@
from serena.agent import ToolRegistry
if __name__ == "__main__":
ToolRegistry.print_tool_overview()
+5 -5
View File
@@ -8,15 +8,15 @@ enable_project_activation: True
# Add your list of projects here (which you can switch between using "activate_project").
# Every list item must be either
# - a path to a .yml file, absolute or relative to the directory this
# - [Recommended] a path to the project's root directory (e.g. "/path/to/myproject"), with the
# configuration file being located in "/path/to/myproject/.serena/project.yml".
# In this case, the name of the project will be the name of the directory ("myproject").
# - or a path to a .yml file, absolute or relative to the directory this
# configuration file is in (e.g. "myproject.yml" or "/path/to/wherever/myproject.yml").
# In this case, the name of the project will be the base filename of the .yml file
# (e.g. "myproject" for "myproject.yml").
# - or a path to the project's root directory (e.g. "/path/to/myproject"), with the
# configuration file being located in "/path/to/myproject/.serena/project.yml".
# In this case, the name of the project will be the name of the directory ("myproject").
projects:
- myproject.yml
- /path/to/myproject
# Whether to open a graphical window with Serena's logs (not supported on macOS).
# This is useful both for troubleshooting and for monitoring the tool calls,
+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
+391
View File
@@ -0,0 +1,391 @@
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
```yaml
lang: <language_code> # optional, defaults to "default"
prompts:
<prompt_name>:
<prompt_template_string>
<prompt_list_name>: [<prompt_string_1>, <prompt_string_2>, ...]
```
When specifying prompt templates for multiple languages, make sure that the Jinja template parameters
(inferred from the things inside the `{{ }}` in the template strings) 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 = """
# ruff: noqa
# black: skip
# mypy: ignore-errors
# NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
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}')
"""
os.makedirs(os.path.dirname(target_module_path), exist_ok=True)
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
+225 -54
View File
@@ -11,6 +11,7 @@ import traceback
from abc import ABC
from collections import defaultdict
from collections.abc import Callable, Generator, Iterable, Sequence
from copy import copy
from dataclasses import dataclass, field
from logging import Logger
from pathlib import Path
@@ -26,7 +27,8 @@ 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.config import SerenaAgentContext, SerenaAgentMode
from serena.prompt_factory import PromptFactory, SerenaPromptFactory
from serena.symbol import SymbolLocation, SymbolManager
from serena.text_utils import search_files
from serena.util.file_system import scan_directory
@@ -164,6 +166,9 @@ class ProjectConfig(ToStringMixin):
def get_serena_managed_dir(self) -> str:
return os.path.join(self.project_root, self.SERENA_MANAGED_DIR)
def get_excluded_tool_classes(self) -> set[type["Tool"]]:
return set(ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools)
@dataclass
class SerenaConfig:
@@ -250,13 +255,19 @@ class SerenaAgent:
project_config: ProjectConfig | str | Path | None = None,
project_activation_callback: Callable[[], None] | None = None,
serena_config: SerenaConfig | None = None,
) -> None:
context: SerenaAgentContext | None = None,
modes: list[SerenaAgentMode] | None = None,
):
"""
:param project_config: the project to load immediately; may be a path to the project configuration file
or a configuration instance;
if None, do not load any project (must use project selection tool to activate a project).
If a project is provided, the corresponding language server will be started.
:param project_activation_callback: a callback function to be called when a project is activated.
:param context: the context in which the agent is operating, None for default context.
The context may adjust prompts, tool availability, and tool descriptions.
:param modes: list of modes in which the agent is operating (they will be combined), None for default modes.
The modes may adjust prompts, tool availability, and tool descriptions.
:param serena_config: the Serena configuration or None to read the configuration from the default location.
"""
# obtain serena configuration
@@ -282,7 +293,8 @@ class SerenaAgent:
log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})")
log.info("Available projects: {}".format(", ".join(self.serena_config.project_names)))
self.prompt_factory = PromptFactory()
# Initialize the prompt factory
self.prompt_factory = SerenaPromptFactory()
self._project_activation_callback = project_activation_callback
# project-specific instances, which will be initialized upon project activation
@@ -294,15 +306,27 @@ class SerenaAgent:
# find all tool classes and instantiate them
self._all_tools: dict[type[Tool], Tool] = {}
for tool_class in iter_tool_classes():
"""maps tool classes to their instances (which are linked to the agent instance)"""
for tool_class in ToolRegistry.get_all_tool_classes():
tool_instance = tool_class(self)
if not self.serena_config.enable_project_activation:
if tool_class in (GetActiveProjectTool, ActivateProjectTool):
log.info(f"Excluding tool '{tool_instance.get_name()}' because project activation is disabled in configuration")
continue
self._all_tools[tool_class] = tool_instance
self._active_tools = dict(self._all_tools)
# Apply context and mode tool configurations
if context is None:
context = SerenaAgentContext.load_default()
if modes is None:
modes = SerenaAgentMode.load_default_modes()
self._context = context
self._modes = modes
log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name() for tool in self._all_tools.values()])}")
self._active_tools: dict[type[Tool], Tool] = {}
self._update_active_tools()
# If GUI log window is enabled, set the tool names for highlighting
if self._gui_log_handler is not None:
@@ -340,26 +364,59 @@ class SerenaAgent:
# When project activation is not enabled, we only expose the active tools
return list(self._active_tools.values())
def set_modes(self, modes: list[SerenaAgentMode]) -> None:
"""
Set the current mode configurations.
:param modes: List of mode names or paths to use
"""
self._modes = modes
self._update_active_tools()
log.info(f"Set modes to {[mode.name for mode in modes]}")
def get_active_modes(self) -> list[SerenaAgentMode]:
"""
:return: the list of active modes
"""
return list(self._modes)
def create_system_prompt(self) -> str:
return self.prompt_factory.create_system_prompt(
context_system_prompt=self._context.prompt,
mode_system_prompts=[mode.prompt for mode in self._modes],
)
def _update_active_tools(self) -> None:
"""
Update the active tools based on context, modes, and project configuration.
All tool exclusions are merged together.
"""
excluded_tool_classes: set[type[Tool]] = set()
# modes
for mode in self._modes:
excluded_tool_classes.update(mode.get_excluded_tool_classes())
# context
excluded_tool_classes.update(self._context.get_excluded_tool_classes())
# project config
if self.project_config is not None:
excluded_tool_classes.update(self.project_config.get_excluded_tool_classes())
if self.project_config.read_only:
for tool_class in self._all_tools:
if tool_class.can_edit():
excluded_tool_classes.add(tool_class)
self._active_tools = {
tool_class: tool_instance for tool_class, tool_instance in self._all_tools.items() if tool_class not in excluded_tool_classes
}
log.info(f"Active tools after all exclusions ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}")
def activate_project(self, project_config: ProjectConfig) -> None:
log.info(f"Activating {project_config}")
self.project_config = project_config
# handle project-specific tool exclusions (if any)
if self.project_config.excluded_tools:
self._active_tools = {
key: tool for key, tool in self._all_tools.items() if tool.get_name() not in project_config.excluded_tools
}
log.info(f"Active tools after exclusions ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}")
else:
self._active_tools = dict(self._all_tools)
# if read_only mode is enabled, exclude all editing tools
if self.project_config.read_only:
self._active_tools = {key: tool for key, tool in self._active_tools.items() if not key.can_edit()}
log.info(
f"Project is in read-only mode. Editing tools excluded. Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}"
)
self._update_active_tools()
# start the language server
self.reset_language_server()
assert self.language_server is not None
@@ -372,11 +429,66 @@ class SerenaAgent:
if self._project_activation_callback is not None:
self._project_activation_callback()
def get_active_tool_classes(self) -> list[type["Tool"]]:
"""
:return: the list of active tool classes for the current project
"""
return list(self._active_tools.keys())
def get_active_tool_names(self) -> list[str]:
"""
:return: the list of names of the active tools for the current project
"""
return sorted([tool.get_name() for tool in self._active_tools.values()])
return sorted([tool.get_name() for tool in self.get_active_tool_classes()])
def tool_is_active(self, tool_class: type["Tool"] | str) -> bool:
"""
:param tool_class: the class or name of the tool to check
:return: True if the tool is active, False otherwise
"""
if isinstance(tool_class, str):
return tool_class in self.get_active_tool_names()
else:
return tool_class in self.get_active_tool_classes()
def get_current_config_overview(self) -> str:
"""
:return: a string overview of the current configuration, including the active project, context, modes, and tools
"""
result_str = "Current configuration:\n"
if self.project_config is not None:
result_str += f"Active project: {self.project_config.project_name}\n"
result_str += f"Active context: {self._context.name}\n"
# Active modes
active_mode_names = [mode.name for mode in self.get_active_modes()]
result_str += "Active modes: {}\n".format(", ".join(active_mode_names))
# Available but not active modes
all_available_modes = SerenaAgentMode.list_registered_mode_names()
inactive_modes = [mode for mode in all_available_modes if mode not in active_mode_names]
if inactive_modes:
result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes))
# Active tools
result_str += "Active tools (after all exclusions from the project, context, and modes):\n"
active_tool_names = self.get_active_tool_names()
# print the tool names in chunks
chunk_size = 4
for i in range(0, len(active_tool_names), chunk_size):
chunk = active_tool_names[i : i + chunk_size]
result_str += " " + ", ".join(chunk) + "\n"
# Available but not active tools
all_tool_names = sorted([tool.get_name() for tool in self._all_tools.values()])
inactive_tool_names = [tool for tool in all_tool_names if tool not in active_tool_names]
if inactive_tool_names:
result_str += "Available but not active tools:\n"
for i in range(0, len(inactive_tool_names), chunk_size):
chunk = inactive_tool_names[i : i + chunk_size]
result_str += " " + ", ".join(chunk) + "\n"
return result_str
def is_language_server_running(self) -> bool:
return self.language_server is not None and self.language_server.is_running()
@@ -410,7 +522,7 @@ class SerenaAgent:
return self._all_tools[tool_class] # type: ignore
def print_tool_overview(self) -> None:
_print_tool_overview(self._active_tools.values())
ToolRegistry.print_tool_overview(self._active_tools.values())
def mark_file_modified(self, relativ_path: str) -> None:
assert self.lines_read is not None
@@ -585,15 +697,23 @@ class Tool(Component):
)
return result
def is_active(self) -> bool:
return self.agent.tool_is_active(self.__class__)
def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs) -> str: # type: ignore
"""
Applies the tool with the given arguments
"""
apply_fn = self.get_apply_fn()
try:
if not self.is_active():
return f"Error: Tool '{self.get_name()}' is not active. Active tools: {self.agent.get_active_tool_names()}"
except Exception as e:
return f"RuntimeError while checking if tool {self.get_name()} is active: {e}"
if log_call:
self._log_tool_application(inspect.currentframe())
try:
# check whether the tool requires an active project and language server
if not isinstance(self, ToolMarkerDoesNotRequireActiveProject):
@@ -605,21 +725,6 @@ class Tool(Component):
if not self.agent.is_language_server_running():
log.info("Language server is not running. Starting it ...")
self.agent.reset_language_server()
# check whether the tool is enabled
if self.agent.project_config is not None and self.get_name() in self.agent.project_config.excluded_tools:
return (
f"Error: Tool '{self.get_name()}' is disabled for the active project ('{self.project_config.project_name}'); "
f"active tools: {self.agent.get_active_tool_names()}"
)
# check if the project is in read-only mode and this is an editing tool
if self.agent.project_config is not None and self.agent.project_config.read_only and self.__class__.can_edit():
return (
f"Error: Tool '{self.get_name()}' cannot be used because the project '{self.project_config.project_name}' "
f"is in read-only mode. Editing operations are not allowed."
)
# apply the actual tool
result = apply_fn(**kwargs)
@@ -1127,8 +1232,6 @@ class CheckOnboardingPerformedTool(Tool):
Checks whether project onboarding was already performed.
You should always call this tool before beginning to actually work on the project/after activating a project,
but after calling the initial instructions tool.
If onboarding was already performed, you will receive a list of available memories.
Don't read the memories immediately after if not needed, just remember that they exist and that you can read them later.
"""
list_memories_tool = self.agent.get_tool(ListMemoriesTool)
memories = json.loads(list_memories_tool.apply())
@@ -1138,7 +1241,13 @@ class CheckOnboardingPerformedTool(Tool):
+ "You should perform onboarding by calling the `onboarding` tool before proceeding with the task."
)
else:
return json.dumps({"result": "Onboarding already performed.", "available_memories": memories})
return f"""The onboarding was already performed, below is the list of available memories.
Do not read them immediately, just remember that they exist and that you can read them later, if it is necessary
for the current task.
Some memories may be based on previous conversations, others may be general for the current project.
You should be able to tell which one you need based on the name of the memory.
{memories}"""
class OnboardingTool(Tool):
@@ -1436,6 +1545,39 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
return SUCCESS_RESULT
class SwitchModesTool(Tool):
"""
Activates modes by providing a list of their names
"""
def apply(self, modes: list[str]) -> str:
"""
Activates the desired modes, like ["editing", "interactive"] or ["planning", "one-shot"]
:param modes: the names of the modes to activate
"""
mode_instances = [SerenaAgentMode.load(mode) for mode in modes]
self.agent.set_modes(mode_instances)
# Inform the Agent about the activated modes and the currently active tools
result_str = f"Successfully activated modes: {', '.join([mode.name for mode in mode_instances])}" + "\n"
result_str += "\n".join([mode_instance.prompt for mode_instance in mode_instances]) + "\n"
result_str += f"Currently active tools: {', '.join(self.agent.get_active_tool_names())}"
return result_str
class GetCurrentConfigTool(Tool):
"""
Prints the current configuration of the agent, including the active modes, tools, and context.
"""
def apply(self) -> str:
"""
Print the current configuration of the agent, including the active modes, tools, and context.
"""
return self.agent.get_current_config_overview()
class InitialInstructionsTool(Tool):
"""
Gets the initial instructions for the current project.
@@ -1448,10 +1590,10 @@ class InitialInstructionsTool(Tool):
Get the initial instructions for the current coding project.
You should always call this tool before starting to work (including using any other tool) on any programming task!
"""
return self.agent.prompt_factory.create_system_prompt()
return self.agent.create_system_prompt()
def iter_tool_classes(same_module_only: bool = True) -> Generator[type[Tool], None, None]:
def _iter_tool_classes(same_module_only: bool = True) -> Generator[type[Tool], None, None]:
"""
Iterate over Tool subclasses.
@@ -1464,17 +1606,46 @@ def iter_tool_classes(same_module_only: bool = True) -> Generator[type[Tool], No
yield tool_class
def print_tool_overview() -> None:
_print_tool_overview(iter_tool_classes())
_TOOL_REGISTRY_DICT: dict[str, type[Tool]] = {tool_class.get_name(): tool_class for tool_class in _iter_tool_classes()}
"""maps tool name to the corresponding tool class"""
def _print_tool_overview(tools: Iterable[type[Tool] | Tool]) -> None:
tool_dict: dict[str, type[Tool] | Tool] = {}
for tool in tools:
tool_dict[tool.get_name()] = tool
for tool_name in sorted(tool_dict.keys()):
tool = tool_dict[tool_name]
print(f" * `{tool_name}`: {tool.get_tool_description().strip()}")
class ToolRegistry:
@staticmethod
def get_tool_class_by_name(tool_name: str) -> type[Tool]:
try:
return _TOOL_REGISTRY_DICT[tool_name]
except KeyError as e:
available_tools = "\n".join(ToolRegistry.get_tool_names())
raise ValueError(f"Tool with name {tool_name} not found. Available tools:\n{available_tools}") from e
@staticmethod
def get_all_tool_classes() -> list[type[Tool]]:
return list(_TOOL_REGISTRY_DICT.values())
@staticmethod
def get_tool_names() -> list[str]:
return list(_TOOL_REGISTRY_DICT.keys())
@staticmethod
def tool_dict() -> dict[str, type[Tool]]:
"""Maps tool name to the corresponding tool class"""
return copy(_TOOL_REGISTRY_DICT)
@staticmethod
def print_tool_overview(tools: Iterable[type[Tool] | Tool] | None = None) -> None:
"""
Print a summary of the tools. If no tools are passed, a summary of all tools is printed.
"""
if tools is None:
tools = _TOOL_REGISTRY_DICT.values()
tool_dict: dict[str, type[Tool] | Tool] = {}
for tool_class in tools:
tool_dict[tool_class.get_name()] = tool_class
for tool_name in sorted(tool_dict.keys()):
tool_class = tool_dict[tool_name]
print(f" * `{tool_name}`: {tool_class.get_tool_description().strip()}")
def _tuple_to_info(name: str, symbol_type: SymbolKind, line: int, column: int) -> dict[str, int | str]:
+6 -3
View File
@@ -16,6 +16,7 @@ from sensai.util.logging import LogTime
from serena import serena_root_path
from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe
from serena.config import SerenaAgentContext
log = logging.getLogger(__name__)
@@ -70,7 +71,9 @@ class SerenaAgnoAgentProvider:
parser = argparse.ArgumentParser(description="Serena coding assistant")
parser.add_argument(
"--project-file", required=False, help="Path to the project file, either absolute or relative to the root directory"
"--project",
required=False,
help="Path to the project (or project.yml file).",
)
args = parser.parse_args()
@@ -89,7 +92,7 @@ class SerenaAgnoAgentProvider:
with LogTime("Loading Serena agent"):
try:
serena_agent = SerenaAgent(project_file)
serena_agent = SerenaAgent(project_file, context=SerenaAgentContext.load("agent"))
except Exception as e:
show_fatal_exception_safe(e)
raise
@@ -117,7 +120,7 @@ class SerenaAgnoAgentProvider:
# To see detailed logs, you should use the serena logger (configure it in the project file path)
show_tool_calls=False,
markdown=True,
system_message=serena_agent.prompt_factory.create_system_prompt(),
system_message=serena_agent.create_system_prompt(),
telemetry=False,
memory=AgentMemory(),
add_history_to_messages=True,
+140
View File
@@ -0,0 +1,140 @@
"""
Context and Mode configuration loader
"""
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Self
import yaml
from sensai.util import logging
from serena.constants import CONTEXT_YAMLS_DIR, DEFAULT_CONTEXT, DEFAULT_MODES, MODE_YAMLS_DIR
if TYPE_CHECKING:
from serena.agent import Tool
log = logging.getLogger(__name__)
@dataclass
class SerenaAgentMode:
"""Represents a mode of operation for the agent, typically read off a YAML file.
An agent can be in multiple modes simultaneously as long as they are not mutually exclusive.
The modes can be adjusted after the agent is running, for example for switching from planning to editing.
"""
name: str
prompt: str
description: str = ""
excluded_tools: set[str] = field(default_factory=set)
def print_overview(self) -> None:
"""Print an overview of the mode."""
print(f"{self.name}:\n {self.description}")
if self.excluded_tools:
print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools)))
def get_excluded_tool_classes(self) -> list[type["Tool"]]:
"""Get the list of tool classes that are excluded from the mode."""
from serena.agent import ToolRegistry
return [ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools]
@classmethod
def from_yaml(cls, yaml_path: str | Path) -> Self:
"""Load a mode from a YAML file."""
with open(yaml_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
name = data.pop("name", Path(yaml_path).stem)
return cls(name=name, **data)
@classmethod
def from_name(cls, name: str) -> Self:
"""Load a registered Serena mode."""
yaml_path = os.path.join(MODE_YAMLS_DIR, f"{name}.yml")
if not os.path.exists(yaml_path):
raise FileNotFoundError(
f"Mode {name} not found in {MODE_YAMLS_DIR}. You can load custom modes by using from_yaml() instead. "
f"Available modes: {cls.list_registered_mode_names()}"
)
return cls.from_yaml(yaml_path)
@classmethod
def list_registered_mode_names(cls) -> list[str]:
"""Names of all registered modes (from the corresponding YAML files in the serena repo)."""
return sorted([f.stem for f in Path(MODE_YAMLS_DIR).glob("*.yml")])
@classmethod
def load_default_modes(cls) -> list[Self]:
"""Load the default modes (interactive and editing)."""
return [cls.from_name(mode) for mode in DEFAULT_MODES]
@classmethod
def load(cls, name_or_path: str | Path) -> Self:
try:
return cls.from_name(str(name_or_path))
except FileNotFoundError:
return cls.from_yaml(name_or_path)
@dataclass
class SerenaAgentContext:
"""Represents a context where the agent is operating (an IDE, a chat, etc.), typically read off a YAML file.
An agent can only be in a single context at a time.
The contexts cannot be changed after the agent is running.
"""
name: str
prompt: str
description: str = ""
excluded_tools: set[str] = field(default_factory=set)
def get_excluded_tool_classes(self) -> list[type["Tool"]]:
"""Get the list of tool classes that are excluded from the context."""
from serena.agent import ToolRegistry
return [ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools]
@classmethod
def from_yaml(cls, yaml_path: str | Path) -> Self:
"""Load a context from a YAML file."""
with open(yaml_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
name = data.pop("name", Path(yaml_path).stem)
return cls(name=name, **data)
@classmethod
def from_name(cls, name: str) -> Self:
"""Load a registered Serena context."""
yaml_path = os.path.join(CONTEXT_YAMLS_DIR, f"{name}.yml")
if not os.path.exists(yaml_path):
raise FileNotFoundError(
f"Context {Path(yaml_path).stem} not found in {CONTEXT_YAMLS_DIR}. You can load a custom context by using from_yaml() instead.\n"
f"Available contexts:\n{cls.list_registered_context_names()}"
)
return cls.from_yaml(yaml_path)
@classmethod
def load(cls, name_or_path: str | Path) -> Self:
try:
return cls.from_name(str(name_or_path))
except FileNotFoundError:
return cls.from_yaml(name_or_path)
@classmethod
def list_registered_context_names(cls) -> list[str]:
"""Names of all registered contexts (from the corresponding YAML files in the serena repo)."""
return sorted([f.stem for f in Path(CONTEXT_YAMLS_DIR).glob("*.yml")])
@classmethod
def load_default(cls) -> Self:
"""Load the default context."""
return cls.from_name(DEFAULT_CONTEXT)
def print_overview(self) -> None:
"""Print an overview of the mode."""
print(f"{self.name}:\n {self.description}")
if self.excluded_tools:
print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools)))
+11
View File
@@ -0,0 +1,11 @@
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")
CONTEXT_YAMLS_DIR = str(_repo_root_path / "config" / "contexts")
MODE_YAMLS_DIR = str(_repo_root_path / "config" / "modes")
DEFAULT_CONTEXT = "desktop-app"
DEFAULT_MODES = ("interactive", "editing")
@@ -0,0 +1,35 @@
# ruff: noqa
# black: skip
# mypy: ignore-errors
# NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
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, *, context_system_prompt: Any, mode_system_prompts: Any) -> str:
return self._render_prompt("system_prompt", context_system_prompt=context_system_prompt, mode_system_prompts=mode_system_prompts)
-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, encoding="utf-8") 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]
-44
View File
@@ -1,44 +0,0 @@
# ruff: noqa
# black: skip
# mypy: ignore-errors
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
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 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())
+73 -17
View File
@@ -3,7 +3,7 @@ The Serena Model Context Protocol (MCP) Server
"""
import sys
from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from dataclasses import dataclass
from logging import Formatter, Logger, StreamHandler
@@ -19,6 +19,8 @@ from sensai.util import logging
from sensai.util.helper import mark_used
from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe
from serena.config import SerenaAgentContext, SerenaAgentMode
from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES
log = logging.getLogger(__name__)
LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
@@ -93,21 +95,33 @@ def make_tool(
)
def create_mcp_server(project_file_path: str | None, host: str = "0.0.0.0", port: int = 8000) -> FastMCP:
def create_mcp_server_and_agent(
project: str | None,
host: str = "0.0.0.0",
port: int = 8000,
context: str = DEFAULT_CONTEXT,
modes: Sequence[str] = DEFAULT_MODES,
) -> tuple[FastMCP, SerenaAgent]:
"""
Create an MCP server.
:param project_file_path: The path to the project file, or None.
:param project: The path to the project directory or the `project.yml` file therein, or None.
:param host: The host to bind to
:param port: The port to bind to
:param context: The context name or path to context file
:param modes: List of mode names or paths to mode files
"""
mcp: FastMCP | None = None
context_instance = SerenaAgentContext.load(context)
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
try:
agent = SerenaAgent(
project_file_path,
project_config=project,
# Callback disabled for the time being (see above)
# project_activation_callback=update_tools
context=context_instance,
modes=modes_instances,
)
except Exception as e:
show_fatal_exception_safe(e)
@@ -138,25 +152,55 @@ def create_mcp_server(project_file_path: str | None, host: str = "0.0.0.0", port
update_tools()
return mcp
return mcp, agent
@click.command()
# Add --project option as the primary, more intuitive interface
@click.option(
"--project-file",
"project_file_opt", # Rename to avoid conflict with argument
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
"--project",
"project_file_opt", # Use same destination variable to avoid conflicts
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
default=None,
help="Optional path to the .yml project file via option."
help="Path to the .yml project file. "
"Does not need to be provided at startup since you can activate a project later by simply asking the agent to do so "
"(there is a dedicated tool for this purpose).",
)
# Keep --project-file for backwards compatibility
@click.option(
"--project-file",
"project_file_opt", # Use same destination variable to avoid conflicts
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
default=None,
help="[DEPRECATED] Use --project instead. Optional path to the .yml project file via option."
"Does not need to be provided at startup since you can activate a project later by simply asking the agent to do so "
"(there is a dedicated tool for this purpose).",
)
# Positional argument for backwards compatibility
@click.argument(
"project_file_arg",
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
required=False,
default=None,
)
@click.option(
"--context",
type=str,
show_default=True,
default=DEFAULT_CONTEXT,
help="Context to use. This can be a name of a built-in context ('desktop-app', 'agent', 'ide-assistant') "
"or a path to a custom context YAML file.",
)
@click.option(
"--mode",
"modes",
type=str,
multiple=True,
default=DEFAULT_MODES,
show_default=True,
help="Mode(s) to use. This can be names of built-in modes ('planning', 'editing', 'one-shot', 'interactive') "
"or paths to custom mode YAML files. Can be specified multiple times to combine modes.",
)
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
@@ -167,34 +211,46 @@ def create_mcp_server(project_file_path: str | None, host: str = "0.0.0.0", port
@click.option(
"--host",
type=str,
default="0.0.0.0",
show_default=True,
default="0.0.0.0",
help="Host to bind to (for SSE transport).",
)
@click.option(
"--port",
type=int,
default=8000,
show_default=True,
default=8000,
help="Port to bind to (for SSE transport).",
)
def start_mcp_server(
project_file_opt: str | None, project_file_arg: str | None, transport: Literal["stdio", "sse"], host: str, port: int
project_file_opt: str | None,
project_file_arg: str | None,
context: str = DEFAULT_CONTEXT,
modes: tuple[str, ...] = DEFAULT_MODES,
transport: Literal["stdio", "sse"] = "stdio",
host: str = "0.0.0.0",
port: int = 8000,
) -> None:
"""Starts the Serena MCP server.
Accepts the project file path either via the --project-file option or as a positional argument.
Accepts a path to the project directory or the `project.yml` file therein via the --project option.
Use --context to specify the execution environment and --mode to specify behavior mode(s).
"""
# Prioritize the positional argument if provided
# This is for backward compatibility with the old CLI, should be removed in the future!
project_file = project_file_arg if project_file_arg is not None else project_file_opt
mcp_server = create_mcp_server(project_file_path=project_file, host=host, port=port)
mcp_server, agent = create_mcp_server_and_agent(project=project_file, host=host, port=port, context=context, modes=modes)
# log after server creation such that the log appears in the GUI
if project_file_arg is not None:
log.warning(
"The positional argument for the project file path is deprecated and will be removed in the future!"
"Please pass the project file path via the `--project-file` option instead.\n"
"The positional argument for the project file path is deprecated and will be removed in the future! "
"Please pass the project file path via the `--project` option instead.\n"
f"Used path: {project_file}"
)
log.info(f"Starting serena agent in MCP server with config:\n{agent.get_current_config_overview()}")
mcp_server.run(transport=transport)
+11
View File
@@ -0,0 +1,11 @@
from serena.constants import PROMPT_TEMPLATES_DIR
from serena.generated.generated_prompt_factory import PromptFactory
class SerenaPromptFactory(PromptFactory):
"""
A class for retrieving and rendering prompt templates and prompt lists.
"""
def __init__(self) -> None:
super().__init__(PROMPT_TEMPLATES_DIR)
+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
View File
Whitespace-only changes.
+2 -2
View File
@@ -3,7 +3,7 @@
import pytest
from mcp.server.fastmcp.tools.base import Tool as MCPTool
from serena.agent import Tool, iter_tool_classes
from serena.agent import Tool, ToolRegistry
from serena.mcp import make_tool
@@ -269,7 +269,7 @@ def is_test_mock_class(tool_class: type) -> bool:
)
@pytest.mark.parametrize("tool_class", list(iter_tool_classes()))
@pytest.mark.parametrize("tool_class", ToolRegistry.get_all_tool_classes())
def test_make_tool_all_tools(tool_class) -> None:
"""Test that make_tool works for all tools in the codebase."""