mirror of
https://github.com/tiennm99/serena.git
synced 2026-09-20 04:23:43 +00:00
Proper first implementation of mode and context setting, WIP
This commit is contained in:
1 parent
1c06dee660
commit
3584ce47eb
22 files changed
+347
-467
No files matched your search
@@ -1,8 +1,6 @@
|
||||
name: agent
|
||||
description: All tools except InitialInstructionsTool for agent context
|
||||
system_prompt: |
|
||||
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
|
||||
tool_description_overrides: {}
|
||||
- initial_instructions
|
||||
@@ -1,7 +1,13 @@
|
||||
name: desktop-app
|
||||
description: All tools included for desktop app context
|
||||
system_prompt: |
|
||||
You are running in desktop app context with all tools available. You should use the symbolic tools
|
||||
when possible, but you also have access to file and shell operations for more complex tasks.
|
||||
excluded_tools: []
|
||||
tool_description_overrides: {}
|
||||
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: []
|
||||
@@ -1,13 +1,12 @@
|
||||
name: ide-assistant
|
||||
description: Non-symbolic editing tools and general shell tool are excluded
|
||||
system_prompt: |
|
||||
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
|
||||
tool_description_overrides: {}
|
||||
- execute_shell_command
|
||||
@@ -1,25 +1,5 @@
|
||||
name: editing
|
||||
description: All tools, with detailed instructions for code editing
|
||||
system_prompt: |
|
||||
prompt: |
|
||||
You are operating in editing mode. Your task is to implement the requested changes while adhering to the project's
|
||||
code style and patterns. Use symbolic editing tools whenever possible for precise code modifications.
|
||||
excluded_tools: []
|
||||
tool_description_overrides:
|
||||
replace_symbol_body: |
|
||||
Replaces the body of the symbol at the given location. This is a powerful tool for refactoring an entire class, method,
|
||||
or function while maintaining its interface.
|
||||
|
||||
Example: To refactor a method to improve performance or readability, first find the symbol using find_symbol,
|
||||
then use this tool to replace the entire implementation with an improved version.
|
||||
insert_after_symbol: |
|
||||
Inserts the given body/content after the end of the definition of the given symbol. This is ideal for adding
|
||||
new methods to a class, new functions to a module, or new fields to a class.
|
||||
|
||||
Example: To add a new method to an existing class, first find the class using find_symbol,
|
||||
then use this tool to insert the new method at the appropriate location.
|
||||
insert_before_symbol: |
|
||||
Inserts the given body/content before the beginning of the definition of the given symbol. This is useful
|
||||
for adding imports, new classes, or documentation above an existing symbol.
|
||||
|
||||
Example: To add missing imports at the top of a file, find the first symbol in the file using get_symbols_overview,
|
||||
then use this tool to insert the imports before that symbol.
|
||||
@@ -1,6 +1,5 @@
|
||||
name: interactive
|
||||
description: Interactive mode for clarification and step-by-step work
|
||||
system_prompt: |
|
||||
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.
|
||||
|
||||
@@ -9,5 +8,4 @@ system_prompt: |
|
||||
|
||||
Focus on providing informative results for intermediate steps so the user can follow along with your progress and
|
||||
provide feedback as needed.
|
||||
excluded_tools: []
|
||||
tool_description_overrides: {}
|
||||
excluded_tools: []
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
name: one-shot
|
||||
description: Focus on completely finishing a task without interaction
|
||||
system_prompt: You are operating in one-shot mode. Your goal is to complete the entire task autonomously without further user 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
|
||||
@@ -9,5 +9,7 @@ system_prompt: You are operating in one-shot mode. Your goal is to complete th
|
||||
|
||||
Only abort the task if absolutely necessary, such as when critical information is missing that cannot be inferred
|
||||
from the codebase.
|
||||
excluded_tools: []
|
||||
tool_description_overrides: {}
|
||||
excluded_tools:
|
||||
- get_current_config
|
||||
- activate_project
|
||||
- activate_modes
|
||||
@@ -1,6 +1,5 @@
|
||||
name: planning
|
||||
description: Only read-only tools, focused on analysis and planning
|
||||
system_prompt: |
|
||||
prompt: |
|
||||
You are operating in planning mode. Your task is to analyze code and create a comprehensive plan but not write any code.
|
||||
Focus on understanding the existing codebase structure, architecture, and functionality to create detailed planning
|
||||
documents that can be used for future implementation.
|
||||
@@ -13,17 +12,3 @@ excluded_tools:
|
||||
- replace_lines
|
||||
- insert_at_line
|
||||
- execute_shell_command
|
||||
tool_description_overrides:
|
||||
find_symbol: |
|
||||
Retrieves information on all symbols/code entities, i.e. classes, methods, attributes, variables, etc.
|
||||
with the given name. In planning mode, this tool is essential for understanding the codebase structure.
|
||||
Use this tool to explore the codebase thoroughly before creating a comprehensive plan.
|
||||
|
||||
Example: To understand class hierarchies, use this tool to find base classes, then follow up with find_referencing_symbols
|
||||
to locate derived classes.
|
||||
find_referencing_symbols: |
|
||||
Finds symbols that reference the symbol at the given location. In planning mode, this tool is crucial for
|
||||
understanding dependencies and relationships between different parts of the codebase.
|
||||
|
||||
Example: To understand how a service class is used throughout the application, find the service class symbol first,
|
||||
then use this tool to identify all consumers of the service.
|
||||
@@ -8,6 +8,13 @@ prompts:
|
||||
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 }}
|
||||
|
||||
{{ mode_system_prompt }}
|
||||
Modes descriptions:
|
||||
{% for prompt in mode_system_prompts %}
|
||||
- {{ prompt }}
|
||||
{% endfor %}
|
||||
@@ -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 = "GPL-2.0"
|
||||
|
||||
@@ -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()
|
||||
+115
-152
@@ -11,14 +11,11 @@ import traceback
|
||||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Generator, Iterable
|
||||
from copy import copy
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Self, TypeVar, Union, cast
|
||||
|
||||
# Import here so we have the type
|
||||
if TYPE_CHECKING:
|
||||
from serena.util.config_loader import ConfigData
|
||||
|
||||
import yaml
|
||||
from sensai.util import logging
|
||||
from sensai.util.logging import FallbackHandler
|
||||
@@ -29,6 +26,7 @@ from multilspy.multilspy_config import Language, MultilspyConfig
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from multilspy.multilspy_types import SymbolKind
|
||||
from serena import serena_root_path, serena_version
|
||||
from serena.config import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.prompt_factory import PromptFactory, SerenaPromptFactory
|
||||
from serena.symbol import SymbolLocation, SymbolManager
|
||||
from serena.text_utils import search_files
|
||||
@@ -122,6 +120,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)
|
||||
|
||||
|
||||
@singleton
|
||||
class SerenaConfig:
|
||||
@@ -190,16 +191,18 @@ class SerenaAgent:
|
||||
self,
|
||||
project_file_path: str | None = None,
|
||||
project_activation_callback: Callable[[], None] | None = None,
|
||||
context: str | None = None,
|
||||
modes: list[str] | None = None,
|
||||
context: SerenaAgentContext | None = None,
|
||||
modes: list[SerenaAgentMode] | None = None,
|
||||
):
|
||||
"""
|
||||
:param project_file_path: the configuration file (.yml) of the project to load immediately;
|
||||
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 name or path to context file to use
|
||||
:param modes: list of mode names or paths to mode files to use
|
||||
: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.
|
||||
"""
|
||||
# obtain serena configuration
|
||||
self.serena_config = SerenaConfig()
|
||||
@@ -228,26 +231,6 @@ class SerenaAgent:
|
||||
self.prompt_factory = SerenaPromptFactory()
|
||||
self._project_activation_callback = project_activation_callback
|
||||
|
||||
# Load context and mode configuration
|
||||
from serena.util.config_loader import ConfigData, ConfigLoader
|
||||
|
||||
self.config_loader = ConfigLoader()
|
||||
self.current_context: ConfigData | None = None
|
||||
self.current_modes: list[ConfigData] = []
|
||||
|
||||
# Set context and modes if provided
|
||||
if context is not None:
|
||||
self.set_context(context)
|
||||
else:
|
||||
# Default to desktop-app context if none provided
|
||||
self.set_context("desktop-app")
|
||||
|
||||
if modes is not None:
|
||||
self.set_modes(modes)
|
||||
else:
|
||||
# Default to interactive mode if none provided
|
||||
self.set_modes(["interactive"])
|
||||
|
||||
# project-specific instances, which will be initialized upon project activation
|
||||
self.project_config: ProjectConfig | None = None
|
||||
self.language_server: SyncLanguageServer | None = None
|
||||
@@ -257,7 +240,9 @@ 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):
|
||||
@@ -266,6 +251,12 @@ class SerenaAgent:
|
||||
self._all_tools[tool_class] = tool_instance
|
||||
|
||||
# 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
|
||||
self._update_active_tools()
|
||||
log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name() for tool in self._all_tools.values()])}")
|
||||
|
||||
@@ -305,72 +296,23 @@ class SerenaAgent:
|
||||
# When project activation is not enabled, we only expose the active tools
|
||||
return list(self._active_tools.values())
|
||||
|
||||
def set_context(self, context: str) -> None:
|
||||
"""
|
||||
Set the current context configuration.
|
||||
|
||||
:param context: Name or path of the context to use
|
||||
"""
|
||||
context_config = self.config_loader.get_context(context)
|
||||
self.current_context = context_config
|
||||
|
||||
# Update tool configurations
|
||||
self._update_active_tools()
|
||||
|
||||
log.info(f"Set context to '{context_config.name}': {context_config.description}")
|
||||
|
||||
def set_modes(self, modes: list[str]) -> None:
|
||||
def set_modes(self, modes: list[SerenaAgentMode]) -> None:
|
||||
"""
|
||||
Set the current mode configurations.
|
||||
|
||||
:param modes: List of mode names or paths to use
|
||||
"""
|
||||
mode_configs = [self.config_loader.get_mode(mode) for mode in modes]
|
||||
self._check_mode_conflicts(mode_configs)
|
||||
self.current_modes = mode_configs
|
||||
self.current_modes = modes
|
||||
self._update_active_tools()
|
||||
|
||||
log.info(f"Set modes to {[mode.name for mode in mode_configs]}")
|
||||
|
||||
def _get_modes_system_prompt(self) -> str:
|
||||
return "\n".join([mode.system_prompt_extension for mode in self.current_modes])
|
||||
|
||||
def _get_context_system_prompt(self) -> str:
|
||||
return self.current_context.system_prompt_extension
|
||||
log.info(f"Set modes to {[mode.name for mode in modes]}")
|
||||
|
||||
def create_system_prompt(self) -> str:
|
||||
context_system_prompt = self._get_context_system_prompt()
|
||||
mode_system_prompt = self._get_modes_system_prompt()
|
||||
return self.prompt_factory.create_system_prompt(
|
||||
context_system_prompt=context_system_prompt,
|
||||
mode_system_prompt=mode_system_prompt,
|
||||
context_system_prompt=self.context.prompt,
|
||||
mode_system_prompts=[mode.prompt for mode in self.current_modes],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _check_mode_conflicts(mode_configs: list["ConfigData"]) -> None:
|
||||
"""
|
||||
Check for conflicts in tool exclusions between modes.
|
||||
|
||||
:param mode_configs: List of mode configurations to check
|
||||
:raises ValueError: If there are conflicts between modes
|
||||
"""
|
||||
if not mode_configs:
|
||||
return
|
||||
|
||||
# Check for conflicts in tool exclusions
|
||||
tools_excluded_by_mode: dict[str, str] = {}
|
||||
for mode in mode_configs:
|
||||
for tool in mode.excluded_tools:
|
||||
if tool in tools_excluded_by_mode:
|
||||
# Another mode already excludes this tool
|
||||
other_mode = tools_excluded_by_mode[tool]
|
||||
if other_mode != mode.name:
|
||||
raise ValueError(
|
||||
f"Conflict between modes: Tool '{tool}' is excluded in mode '{other_mode}' but also in mode '{mode.name}'"
|
||||
)
|
||||
else:
|
||||
tools_excluded_by_mode[tool] = mode.name
|
||||
|
||||
def _update_active_tools(self) -> None:
|
||||
"""
|
||||
Update the active tools based on context, modes, and project configuration.
|
||||
@@ -380,47 +322,19 @@ class SerenaAgent:
|
||||
2. Context exclusions
|
||||
3. Mode exclusions
|
||||
"""
|
||||
# Start with all tools
|
||||
self._active_tools = dict(self._all_tools)
|
||||
|
||||
# Collect all excluded tools
|
||||
excluded_tools = set()
|
||||
|
||||
# Apply mode exclusions
|
||||
# Collect all excluded tools with the desired priority mode < context < project
|
||||
excluded_tool_classes: set[type[Tool]] = set()
|
||||
for mode in self.current_modes:
|
||||
excluded_tools.update(mode.excluded_tools)
|
||||
excluded_tool_classes.update(mode.get_excluded_tool_classes())
|
||||
excluded_tool_classes.update(self.context.get_excluded_tool_classes())
|
||||
if self.project_config is not None:
|
||||
excluded_tool_classes.update(self.project_config.get_excluded_tool_classes())
|
||||
|
||||
# Apply context exclusions (overrides mode exclusions)
|
||||
if self.current_context:
|
||||
excluded_tools.update(self.current_context.excluded_tools)
|
||||
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
|
||||
}
|
||||
|
||||
# Apply the exclusions
|
||||
if excluded_tools:
|
||||
self._active_tools = {key: tool for key, tool in self._active_tools.items() if tool.get_name() not in excluded_tools}
|
||||
log.info(f"Tools excluded by context/mode: {sorted(excluded_tools)}")
|
||||
|
||||
# Apply tool description overrides from context and modes
|
||||
if self.current_context:
|
||||
self._apply_tool_description_overrides(self.current_context.tool_description_overrides)
|
||||
|
||||
for mode in self.current_modes:
|
||||
self._apply_tool_description_overrides(mode.tool_description_overrides)
|
||||
|
||||
log.info(f"Active tools after context/mode ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}")
|
||||
|
||||
def _apply_tool_description_overrides(self, overrides: dict[str, str]) -> None:
|
||||
"""
|
||||
Apply tool description overrides from context or mode configurations.
|
||||
|
||||
:param overrides: Dictionary of tool name to description override
|
||||
"""
|
||||
for tool_name, description in overrides.items():
|
||||
for tool_class, tool in self._all_tools.items():
|
||||
if tool.get_name() == tool_name:
|
||||
# Use monkey patching to override the docstring
|
||||
# This is hacky but effective for this use case
|
||||
tool.__class__.__doc__ = description
|
||||
break
|
||||
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}")
|
||||
@@ -492,7 +406,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
|
||||
@@ -1365,26 +1279,6 @@ class PrepareForNewConversationTool(Tool):
|
||||
return self.prompt_factory.create_prepare_for_new_conversation()
|
||||
|
||||
|
||||
class SetModesTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
"""
|
||||
Changes the current operating modes of the agent.
|
||||
"""
|
||||
|
||||
def apply(self, modes: list[str]) -> str:
|
||||
"""
|
||||
Changes the current operating modes of the agent.
|
||||
|
||||
:param modes: List of mode names to switch to (e.g. ["planning"], ["editing"], ["one-shot"], ["interactive"]),
|
||||
or paths to custom mode configuration files
|
||||
:return: Message indicating success or failure
|
||||
"""
|
||||
try:
|
||||
self.agent.set_modes(modes)
|
||||
return f"Successfully set modes to: {modes}"
|
||||
except Exception as e:
|
||||
return f"Failed to set modes: {e}"
|
||||
|
||||
|
||||
class SearchForPatternTool(Tool):
|
||||
"""
|
||||
Performs a search for a pattern in the project.
|
||||
@@ -1530,6 +1424,46 @@ 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])}"
|
||||
result_str += "\n".join([mode_instance.prompt for mode_instance in mode_instances])
|
||||
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.
|
||||
"""
|
||||
result_str = "Current configuration:\n"
|
||||
if self.agent.project_config is not None:
|
||||
result_str += f"Active project: {self.agent.project_config.project_name}\n"
|
||||
result_str += f"Active context: {self.agent.context.name}\n"
|
||||
result_str += "Active modes: {}\n".format(", ".join([mode.name for mode in self.agent.current_modes]))
|
||||
result_str += "Active tools (exclusions from the project, context, and modes):\n"
|
||||
result_str += "\n".join(self.agent.get_active_tool_names())
|
||||
return result_str
|
||||
|
||||
|
||||
class InitialInstructionsTool(Tool):
|
||||
"""
|
||||
Gets the initial instructions for the current project.
|
||||
@@ -1545,7 +1479,7 @@ class InitialInstructionsTool(Tool):
|
||||
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.
|
||||
|
||||
@@ -1558,17 +1492,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]:
|
||||
|
||||
+1
-1
@@ -117,7 +117,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, 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("interactive"), cls.from_name("editing")]
|
||||
|
||||
@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. "
|
||||
f"Available contexts: {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")
|
||||
|
||||
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)))
|
||||
@@ -4,3 +4,5 @@ _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")
|
||||
@@ -31,5 +31,5 @@ class PromptFactory(PromptFactoryBase):
|
||||
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_prompt: Any) -> str:
|
||||
return self._render_prompt("system_prompt", context_system_prompt=context_system_prompt, mode_system_prompt=mode_system_prompt)
|
||||
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)
|
||||
+16
-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,7 @@ 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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||
@@ -94,7 +95,7 @@ def make_tool(
|
||||
|
||||
|
||||
def create_mcp_server(
|
||||
project_file_path: str | None, host: str = "0.0.0.0", port: int = 8000, context: str | None = None, modes: list[str] | None = None
|
||||
project_file_path: str | None, host: str = "0.0.0.0", port: int = 8000, context: str = "default", modes: Sequence[str] = ("default",)
|
||||
) -> FastMCP:
|
||||
"""
|
||||
Create an MCP server.
|
||||
@@ -106,14 +107,16 @@ def create_mcp_server(
|
||||
: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,
|
||||
# Callback disabled for the time being (see above)
|
||||
# project_activation_callback=update_tools
|
||||
context=context,
|
||||
modes=modes,
|
||||
context=context_instance,
|
||||
modes=modes_instances,
|
||||
)
|
||||
except Exception as e:
|
||||
show_fatal_exception_safe(e)
|
||||
@@ -166,19 +169,18 @@ def create_mcp_server(
|
||||
@click.option(
|
||||
"--context",
|
||||
type=str,
|
||||
default=None,
|
||||
default="desktop-app",
|
||||
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. Defaults to 'desktop-app' if not specified.",
|
||||
"or a path to a custom context YAML file.",
|
||||
)
|
||||
@click.option(
|
||||
"--mode",
|
||||
"modes",
|
||||
type=str,
|
||||
multiple=True,
|
||||
default=[],
|
||||
default=["editing", "interactive"],
|
||||
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. "
|
||||
"Defaults to 'interactive' if not specified.",
|
||||
"or paths to custom mode YAML files. Can be specified multiple times to combine modes.",
|
||||
)
|
||||
@click.option(
|
||||
"--transport",
|
||||
@@ -204,11 +206,11 @@ def create_mcp_server(
|
||||
def start_mcp_server(
|
||||
project_file_opt: str | None,
|
||||
project_file_arg: str | None,
|
||||
context: str | None,
|
||||
context: str,
|
||||
modes: tuple[str, ...],
|
||||
transport: Literal["stdio", "sse"],
|
||||
host: str,
|
||||
port: int,
|
||||
transport: Literal["stdio", "sse"] = "stdio",
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8000,
|
||||
) -> None:
|
||||
"""Starts the Serena MCP server.
|
||||
|
||||
@@ -220,10 +222,7 @@ def start_mcp_server(
|
||||
# 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
|
||||
|
||||
# Convert modes tuple to list
|
||||
modes_list = list(modes) if modes else None
|
||||
|
||||
mcp_server = create_mcp_server(project_file_path=project_file, host=host, port=port, context=context, modes=modes_list)
|
||||
mcp_server = create_mcp_server(project_file_path=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:
|
||||
|
||||
@@ -7,5 +7,5 @@ class SerenaPromptFactory(PromptFactory):
|
||||
A class for retrieving and rendering prompt templates and prompt lists.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(PROMPT_TEMPLATES_DIR)
|
||||
@@ -1,131 +0,0 @@
|
||||
"""
|
||||
Context and Mode configuration loader
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from sensai.util import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigData:
|
||||
"""Base class for context and mode configurations."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
system_prompt_extension: str
|
||||
excluded_tools: set[str]
|
||||
tool_description_overrides: dict[str, str]
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "ConfigData":
|
||||
"""Create a ConfigData instance from a dictionary."""
|
||||
return cls(
|
||||
name=data.get("name", ""),
|
||||
description=data.get("description", ""),
|
||||
system_prompt_extension=data.get("system_prompt_extension", ""),
|
||||
excluded_tools=set(data.get("excluded_tools", [])),
|
||||
tool_description_overrides=data.get("tool_description_overrides", {}),
|
||||
)
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
"""Handles loading of context and mode configurations."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config loader."""
|
||||
self.serena_root = self._find_serena_root()
|
||||
self.contexts_dir = os.path.join(self.serena_root, "prompts", "contexts")
|
||||
self.modes_dir = os.path.join(self.serena_root, "prompts", "modes")
|
||||
|
||||
# Ensure directories exist
|
||||
os.makedirs(self.contexts_dir, exist_ok=True)
|
||||
os.makedirs(self.modes_dir, exist_ok=True)
|
||||
|
||||
# Cache loaded configs
|
||||
self.context_cache: dict[str, ConfigData] = {}
|
||||
self.mode_cache: dict[str, ConfigData] = {}
|
||||
|
||||
def _find_serena_root(self) -> str:
|
||||
"""Find the root directory of Serena."""
|
||||
current_dir = pathlib.Path(__file__).parent.parent.parent.parent
|
||||
return str(current_dir)
|
||||
|
||||
def load_config_from_file(self, file_path: str | Path) -> ConfigData:
|
||||
"""Load a configuration from a file."""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Config file not found: {file_path}")
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
return ConfigData.from_dict(data)
|
||||
|
||||
def get_context(self, context_name_or_path: str) -> ConfigData:
|
||||
"""
|
||||
Get the context configuration by name or path.
|
||||
|
||||
:param context_name_or_path: Either a context name (will be looked up in contexts directory)
|
||||
or a path to a YAML file
|
||||
:return: The context configuration
|
||||
"""
|
||||
# Return from cache if available
|
||||
if context_name_or_path in self.context_cache:
|
||||
return self.context_cache[context_name_or_path]
|
||||
|
||||
# Check if it's a file path
|
||||
if os.path.isfile(context_name_or_path):
|
||||
config = self.load_config_from_file(context_name_or_path)
|
||||
self.context_cache[context_name_or_path] = config
|
||||
return config
|
||||
|
||||
# Check if it's a known context name
|
||||
context_file = os.path.join(self.contexts_dir, f"{context_name_or_path}.yml")
|
||||
if os.path.isfile(context_file):
|
||||
config = self.load_config_from_file(context_file)
|
||||
self.context_cache[context_name_or_path] = config
|
||||
return config
|
||||
|
||||
raise ValueError(f"Context not found: {context_name_or_path}. Please provide a valid context name or file path.")
|
||||
|
||||
def get_mode(self, mode_name_or_path: str) -> ConfigData:
|
||||
"""
|
||||
Get the mode configuration by name or path.
|
||||
|
||||
:param mode_name_or_path: Either a mode name (will be looked up in modes directory)
|
||||
or a path to a YAML file
|
||||
:return: The mode configuration
|
||||
"""
|
||||
# Return from cache if available
|
||||
if mode_name_or_path in self.mode_cache:
|
||||
return self.mode_cache[mode_name_or_path]
|
||||
|
||||
# Check if it's a file path
|
||||
if os.path.isfile(mode_name_or_path):
|
||||
config = self.load_config_from_file(mode_name_or_path)
|
||||
self.mode_cache[mode_name_or_path] = config
|
||||
return config
|
||||
|
||||
# Check if it's a known mode name
|
||||
mode_file = os.path.join(self.modes_dir, f"{mode_name_or_path}.yml")
|
||||
if os.path.isfile(mode_file):
|
||||
config = self.load_config_from_file(mode_file)
|
||||
self.mode_cache[mode_name_or_path] = config
|
||||
return config
|
||||
|
||||
raise ValueError(f"Mode not found: {mode_name_or_path}. Please provide a valid mode name or file path.")
|
||||
|
||||
def list_available_contexts(self) -> list[str]:
|
||||
"""List all available context names."""
|
||||
return [f.stem for f in Path(self.contexts_dir).glob("*.yml")]
|
||||
|
||||
def list_available_modes(self) -> list[str]:
|
||||
"""List all available mode names."""
|
||||
return [f.stem for f in Path(self.modes_dir).glob("*.yml")]
|
||||
@@ -1,100 +0,0 @@
|
||||
"""
|
||||
Tests for the configuration loader for contexts and modes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from serena.util.config_loader import ConfigData, ConfigLoader
|
||||
|
||||
|
||||
def test_config_data_from_dict():
|
||||
"""Test creating ConfigData from a dictionary."""
|
||||
data = {
|
||||
"name": "test-context",
|
||||
"description": "Test context",
|
||||
"system_prompt_extension": "You are in test context",
|
||||
"excluded_tools": ["tool1", "tool2"],
|
||||
"tool_description_overrides": {"tool3": "Override desc"},
|
||||
}
|
||||
|
||||
config = ConfigData.from_dict(data)
|
||||
assert config.name == "test-context"
|
||||
assert config.description == "Test context"
|
||||
assert config.system_prompt_extension == "You are in test context"
|
||||
assert config.excluded_tools == {"tool1", "tool2"}
|
||||
assert config.tool_description_overrides == {"tool3": "Override desc"}
|
||||
|
||||
|
||||
def test_config_loader_default_dirs():
|
||||
"""Test that ConfigLoader initializes the default directories."""
|
||||
loader = ConfigLoader()
|
||||
assert os.path.exists(loader.contexts_dir)
|
||||
assert os.path.exists(loader.modes_dir)
|
||||
|
||||
|
||||
def test_load_config_from_file():
|
||||
"""Test loading a configuration from a file."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".yml", mode="w+", delete=False) as f:
|
||||
f.write(
|
||||
"""
|
||||
name: test-config
|
||||
description: Test config from file
|
||||
system_prompt_extension: Test from file
|
||||
excluded_tools:
|
||||
- tool1
|
||||
- tool2
|
||||
tool_description_overrides:
|
||||
tool3: Override from file
|
||||
"""
|
||||
)
|
||||
f.flush()
|
||||
|
||||
try:
|
||||
loader = ConfigLoader()
|
||||
config = loader.load_config_from_file(f.name)
|
||||
|
||||
assert config.name == "test-config"
|
||||
assert config.description == "Test config from file"
|
||||
assert config.system_prompt_extension == "Test from file"
|
||||
assert config.excluded_tools == {"tool1", "tool2"}
|
||||
assert config.tool_description_overrides == {"tool3": "Override from file"}
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
|
||||
def test_get_context():
|
||||
"""Test getting a context by name."""
|
||||
loader = ConfigLoader()
|
||||
|
||||
# This assumes contexts/desktop-app.yml exists in the project
|
||||
config = loader.get_context("desktop-app")
|
||||
assert config.name == "desktop-app"
|
||||
|
||||
# Test cache functionality
|
||||
assert "desktop-app" in loader.context_cache
|
||||
|
||||
|
||||
def test_get_mode():
|
||||
"""Test getting a mode by name."""
|
||||
loader = ConfigLoader()
|
||||
|
||||
# This assumes modes/interactive.yml exists in the project
|
||||
config = loader.get_mode("interactive")
|
||||
assert config.name == "interactive"
|
||||
|
||||
# Test cache functionality
|
||||
assert "interactive" in loader.mode_cache
|
||||
|
||||
|
||||
def test_list_available():
|
||||
"""Test listing available contexts and modes."""
|
||||
loader = ConfigLoader()
|
||||
|
||||
contexts = loader.list_available_contexts()
|
||||
assert len(contexts) > 0
|
||||
assert "desktop-app" in contexts
|
||||
|
||||
modes = loader.list_available_modes()
|
||||
assert len(modes) > 0
|
||||
assert "interactive" in modes
|
||||
@@ -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