mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 20:15:20 +00:00
Merge pull request #126 from oraios/feature/context-and-mode
Feature/context and mode
This commit is contained in:
39 files changed
+1353
-578
No files matched your search
@@ -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!**
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: []
|
||||
@@ -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
|
||||
@@ -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: []
|
||||
@@ -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: []
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
@@ -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: []
|
||||
@@ -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"
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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")
|
||||
@@ -0,0 +1,4 @@
|
||||
from serena.agent import ToolRegistry
|
||||
|
||||
if __name__ == "__main__":
|
||||
ToolRegistry.print_tool_overview()
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# The interprompt Package
|
||||
|
||||
This package is independent of Serena. It contains code by Oraios AI that we developed for
|
||||
convenient handling of prompt templating in various situations, in particular in multi-language scenarios
|
||||
(hence the name as portmanteau from internationalization prompting)
|
||||
|
||||
It is not yet released as separate package and will be developed together with Serena.
|
||||
|
||||
While Serena itself does not need the multi-language aspect (for now), `interprompt` helps
|
||||
with loading and rendering of the templates through the autogenerated `prompt_factory` module.
|
||||
|
||||
The main (and usually only) entry point one needs is the function `interprompt.autogenerate_prompt_factory_module`
|
||||
that will generate a module with a `PromptFactory` class that contains one method per prompt template and prompt list.
|
||||
For a single-language case, the main added value from that class are the method names and the parameter names that are read off
|
||||
from the jinja templates, allowing clean and safe templating within python without having to write any code.
|
||||
|
||||
For multi-language use cases, the `PromptFactory` adds even more value by offering simple and unified interfaces for
|
||||
internationalization in prompting.
|
||||
@@ -0,0 +1,3 @@
|
||||
from .promt_template import autogenerate_prompt_factory_module
|
||||
|
||||
__all__ = ["autogenerate_prompt_factory_module"]
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import Any
|
||||
|
||||
import jinja2
|
||||
import jinja2.meta
|
||||
import jinja2.nodes
|
||||
import jinja2.visitor
|
||||
|
||||
from interprompt.util.class_decorators import singleton
|
||||
|
||||
|
||||
class ParameterizedTemplateInterface:
|
||||
def get_parameters(self) -> list[str]: ...
|
||||
|
||||
|
||||
@singleton
|
||||
class _JinjaEnvProvider:
|
||||
def __init__(self) -> None:
|
||||
self._env: jinja2.Environment | None = None
|
||||
|
||||
def get_env(self) -> jinja2.Environment:
|
||||
if self._env is None:
|
||||
self._env = jinja2.Environment()
|
||||
return self._env
|
||||
|
||||
|
||||
class JinjaTemplate(ParameterizedTemplateInterface):
|
||||
def __init__(self, template_string: str) -> None:
|
||||
self._template_string = template_string
|
||||
self._template = _JinjaEnvProvider().get_env().from_string(self._template_string)
|
||||
parsed_content = self._template.environment.parse(self._template_string)
|
||||
self._parameters = sorted(jinja2.meta.find_undeclared_variables(parsed_content))
|
||||
|
||||
def render(self, **kwargs: Any) -> str:
|
||||
"""Renders the template with the given kwargs. You can find out which parameters are required by calling get_parameter_names()."""
|
||||
return self._template.render(**kwargs)
|
||||
|
||||
def get_parameters(self) -> list[str]:
|
||||
"""A sorted list of parameter names that are extracted from the template string. It is impossible to know the types of the parameter
|
||||
values, they can be primitives, dicts or dict-like objects.
|
||||
|
||||
:return: the list of parameter names
|
||||
"""
|
||||
return self._parameters
|
||||
@@ -0,0 +1,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}")
|
||||
Whitespace-only changes.
@@ -0,0 +1,13 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
def singleton(cls: type[Any]) -> Any:
|
||||
instance = None
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Any:
|
||||
nonlocal instance
|
||||
if instance is None:
|
||||
instance = cls(*args, **kwargs)
|
||||
return instance
|
||||
|
||||
return get_instance
|
||||
+225
-54
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)))
|
||||
@@ -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)
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
import jinja2
|
||||
import jinja2.meta
|
||||
import jinja2.nodes
|
||||
import jinja2.visitor
|
||||
|
||||
from serena.util.class_decorators import singleton
|
||||
|
||||
|
||||
@singleton
|
||||
class JinjaEnvProvider:
|
||||
def __init__(self) -> None:
|
||||
self._env: jinja2.Environment | None = None
|
||||
|
||||
def get_env(self) -> jinja2.Environment:
|
||||
if self._env is None:
|
||||
self._env = jinja2.Environment()
|
||||
return self._env
|
||||
|
||||
|
||||
class JinjaTemplate:
|
||||
def __init__(self, template_string: str) -> None:
|
||||
self._template_string = template_string
|
||||
self._template = JinjaEnvProvider().get_env().from_string(self._template_string)
|
||||
|
||||
def render(self, **kwargs: Any) -> str:
|
||||
return self._template.render(**kwargs)
|
||||
|
||||
def get_parameters(self) -> set[str]:
|
||||
parsed_content = self._template.environment.parse(self._template_string)
|
||||
return jinja2.meta.find_undeclared_variables(parsed_content)
|
||||
@@ -1,204 +0,0 @@
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
import yaml
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
from .jinja_template import JinjaTemplate
|
||||
|
||||
LANG_CODES = ["en", "de"]
|
||||
|
||||
|
||||
class PromptTemplate(ToStringMixin):
|
||||
def __init__(self, name: str, jinja_template_string: str) -> None:
|
||||
self.name = name
|
||||
self.jinja_template = JinjaTemplate(jinja_template_string.strip())
|
||||
self.parameters = self.jinja_template.get_parameters()
|
||||
|
||||
def _tostring_excludes(self) -> list[str]:
|
||||
return ["jinja_template"]
|
||||
|
||||
def instantiate(self, **kwargs: Any) -> str:
|
||||
return self.jinja_template.render(**kwargs)
|
||||
|
||||
|
||||
class PromptList:
|
||||
def __init__(self, items: list[str]) -> None:
|
||||
self.items = [x.strip() for x in items]
|
||||
|
||||
def to_string(self) -> str:
|
||||
bullet = " * "
|
||||
indent = " " * len(bullet)
|
||||
items = [x.replace("\n", "\n" + indent) for x in self.items]
|
||||
return "\n * ".join(items)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MultiLangContainer(Generic[T], ToStringMixin):
|
||||
"""
|
||||
Represents a container of items which are associated with different languages
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.lang2item: dict[str, T] = {}
|
||||
|
||||
def _tostring_excludes(self) -> list[str]:
|
||||
return ["lang2item"]
|
||||
|
||||
def _tostring_additional_entries(self) -> dict[str, Any]:
|
||||
return dict(languages=list(self.lang2item.keys()))
|
||||
|
||||
class FallbackMode(Enum):
|
||||
ANY = "any"
|
||||
"""
|
||||
If the requested language is not found, return the item for any language
|
||||
"""
|
||||
EXCEPTION = "exception"
|
||||
"""
|
||||
If the requested language is not found, raise an exception
|
||||
"""
|
||||
|
||||
def add_item(self, item: T, lang: str = "") -> None:
|
||||
self.lang2item[lang] = item
|
||||
|
||||
def get_item(self, lang: str, fallback_mode: FallbackMode = FallbackMode.EXCEPTION) -> T:
|
||||
"""
|
||||
Gets the item for the given language.
|
||||
|
||||
:param lang: the language shortcode for which to obtain the prompt template
|
||||
:param fallback_mode: defines what to do if there is no item for the given language
|
||||
:return: the item
|
||||
"""
|
||||
item = self.lang2item.get(lang)
|
||||
if item is not None:
|
||||
return item
|
||||
else:
|
||||
if fallback_mode == self.FallbackMode.EXCEPTION:
|
||||
raise KeyError(f"Item for language '{lang}' not found for name '{self.name}'")
|
||||
if fallback_mode == self.FallbackMode.ANY:
|
||||
return next(iter(self.lang2item.values()))
|
||||
else:
|
||||
raise ValueError(f"Invalid fallback mode '{fallback_mode}'")
|
||||
|
||||
|
||||
class MultiLangPromptTemplate(MultiLangContainer[PromptTemplate]):
|
||||
"""
|
||||
Represents a prompt template which potentially supports multiple languages.
|
||||
"""
|
||||
|
||||
def get_parameters(self) -> list[str]:
|
||||
"""
|
||||
Gets the parameters of the prompt template (which must be the same for all languages).
|
||||
|
||||
:return: the list of parameters required to instantiate the template
|
||||
"""
|
||||
prev_params = None
|
||||
for template in self.lang2item.values():
|
||||
params = set(template.parameters)
|
||||
if prev_params is not None:
|
||||
assert (
|
||||
params == prev_params
|
||||
), f"Parameters of MLPT '{self.name}' are inconsistent: {sorted(params)} vs {sorted(prev_params)}"
|
||||
prev_params = params
|
||||
assert prev_params is not None
|
||||
return sorted(prev_params)
|
||||
|
||||
|
||||
class MultiLangPromptList(MultiLangContainer[PromptList]):
|
||||
pass
|
||||
|
||||
|
||||
class MultiLangPromptTemplateCollection:
|
||||
"""
|
||||
Represents a collection of multi-language prompts, which are read from the prompts/ directory.
|
||||
The language of a prompt is determined by suffix in the prompt name, e.g. "_de" for German.
|
||||
For yaml files, the language can also be set globally for all prompts within it.
|
||||
|
||||
The prompts/ folder may contain:
|
||||
- .txt files with a single prompt template (filename defines prompt name, content is the prompt template)
|
||||
- .yml files with multiple prompt templates under the top-level key 'prompts'.
|
||||
Each key under 'prompts' defines a prompt name and the corresponding value is the prompt template.
|
||||
The value can also be a list of prompts instead of a single template.
|
||||
The language of all can be set by specifying the key 'lang' in addition to 'prompts'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.prompt_templates: dict[str, MultiLangPromptTemplate] = {}
|
||||
self.prompt_lists: dict[str, MultiLangPromptList] = {}
|
||||
prompts_dir = self._prompt_template_folder()
|
||||
self._read_prompt_templates(prompts_dir)
|
||||
|
||||
@classmethod
|
||||
def _prompt_template_folder(cls) -> str:
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
prompts_dir = None
|
||||
for i in range(5):
|
||||
dir_path = os.path.join(dir_path, "..")
|
||||
prompts_dir = os.path.join(dir_path, "prompts")
|
||||
if os.path.isdir(prompts_dir):
|
||||
break
|
||||
if prompts_dir is None or not os.path.isdir(prompts_dir):
|
||||
raise FileNotFoundError("Could not find the 'prompts' directory")
|
||||
return prompts_dir
|
||||
|
||||
@staticmethod
|
||||
def _container_lang(prompt_name: str, collection: dict[str, T], container_factory: Callable[[str], T]) -> tuple[T, str]:
|
||||
lang = ""
|
||||
if prompt_name[-2:] in LANG_CODES and prompt_name[-3] == "_":
|
||||
lang = prompt_name[-2:]
|
||||
prompt_name = prompt_name[:-3]
|
||||
|
||||
container = collection.get(prompt_name)
|
||||
if container is None:
|
||||
container = container_factory(prompt_name)
|
||||
collection[prompt_name] = container
|
||||
|
||||
return container, lang
|
||||
|
||||
def _add_prompt_template(self, prompt_name: str, jinja_prompt_template: str) -> None:
|
||||
"""
|
||||
:param prompt_name: a prompt name, which may have a language shortcode suffix (e.g. "_de")
|
||||
:param jinja_prompt_template: the actual prompt string which may contain placeholders/parameters (e.g. "{name}")
|
||||
"""
|
||||
multilang_prompt_template, lang = self._container_lang(prompt_name, self.prompt_templates, MultiLangPromptTemplate)
|
||||
multilang_prompt_template.add_item(PromptTemplate(prompt_name, jinja_prompt_template), lang=lang)
|
||||
|
||||
def _add_prompt_list(self, prompt_name: str, prompt_list: list[str]) -> None:
|
||||
"""
|
||||
:param prompt_name: a prompt name, which may have a language shortcode suffix (e.g. "_de")
|
||||
:param prompt_list: a list of prompts
|
||||
"""
|
||||
multilang_prompt_list, lang = self._container_lang(prompt_name, self.prompt_lists, MultiLangPromptList)
|
||||
multilang_prompt_list.add_item(PromptList(prompt_list), lang=lang)
|
||||
|
||||
def _read_prompt_templates(self, prompts_dir: str) -> None:
|
||||
for fn in os.listdir(prompts_dir):
|
||||
path = os.path.join(prompts_dir, fn)
|
||||
if fn.endswith(".txt"):
|
||||
with open(path, 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]
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
Whitespace-only changes.
@@ -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."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user