mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 20:15:56 +00:00
Manage project-specific prompt provision per session (MCP client session ID)
This allows the stateful handling introduced for #1370 #1372 to correctly apply to HTTP mode as well, by adjusting tools to be session-aware: * InitialInstructionsTool * ActivateProjectTool
This commit is contained in:
1 parent
b3c92d32d8
commit
3d31512107
5 files changed
+72
-37
No files matched your search
+7
-4
@@ -7,10 +7,13 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350
|
||||
- `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`)
|
||||
- Fix: Duplicate comments in re-saved YAML configuration files #1285
|
||||
- Fix: Prompts of dynamically activated modes (upon project activation) were not necessary passed to the LLM (only in the system prompt via
|
||||
`initial_instructions`). Now they are passed directly in the activation message (and excluded from a subsequent `initial_instructions` call).
|
||||
- Fix: Project activation message was provided more than once for case of dynamic project activation followed
|
||||
by `initial_instructions` #1372
|
||||
- Prompt provision improvements (project activation, initial instructions):
|
||||
- Prompt provision is now session-aware, i.e. when using the MCP server in HTTP mode, prompts are provided for each session separately,
|
||||
ensuring that the necessary information is always provided to the LLM
|
||||
- Fix: Prompts of dynamically activated modes (upon project activation) were not necessarily passed to the LLM (only in the system prompt via
|
||||
`initial_instructions`). Now they are passed directly in the activation message (and excluded from a subsequent `initial_instructions` call).
|
||||
- Fix: Project activation message was provided more than once for case of dynamic project activation followed
|
||||
by `initial_instructions` #1372
|
||||
|
||||
* JetBrains:
|
||||
- `Move` and `SafeDelete` tools: transform empty string to None (counteracts client errors)
|
||||
|
||||
+53
-23
@@ -9,8 +9,10 @@ import platform
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from logging import Logger
|
||||
from typing import TYPE_CHECKING, Optional, TypeVar
|
||||
|
||||
@@ -239,6 +241,11 @@ class ProjectPromptProvisionStatus:
|
||||
Manages the status of the provision of project-specific prompts
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class SessionStatus:
|
||||
mode_prompts_provided: bool = False
|
||||
project_activation_message_provided: bool = False
|
||||
|
||||
def __init__(self, newly_activated_mode_names: set[str] | None = None):
|
||||
"""
|
||||
:param newly_activated_mode_names: list of mode names that have been newly activated (by dynamic project activation)
|
||||
@@ -247,49 +254,60 @@ class ProjectPromptProvisionStatus:
|
||||
if newly_activated_mode_names is None:
|
||||
newly_activated_mode_names = set()
|
||||
self._newly_activated_mode_names: set[str] = newly_activated_mode_names
|
||||
self._mode_prompts_provided = False
|
||||
self._project_activation_message_provided = False
|
||||
self._session_status_dict: dict[str, ProjectPromptProvisionStatus.SessionStatus] = defaultdict(lambda: self.SessionStatus())
|
||||
|
||||
def is_mode_prompt_already_provided(self, mode_name: str) -> bool:
|
||||
def _get_session_status(self, session_id: str) -> SessionStatus:
|
||||
return self._session_status_dict[session_id]
|
||||
|
||||
def is_mode_prompt_already_provided(self, mode_name: str, session_id: str) -> bool:
|
||||
"""
|
||||
:param mode_name: the mode name
|
||||
:param session_id: the client session ID
|
||||
:return: whether the mode name was already provided (in a project-specific activation message) and therefore
|
||||
should not be included again (in the Serena instructions manual)
|
||||
"""
|
||||
if not self._mode_prompts_provided:
|
||||
if not self._get_session_status(session_id).mode_prompts_provided:
|
||||
return False
|
||||
return mode_name in self._newly_activated_mode_names
|
||||
|
||||
def get_modes_with_prompts_to_be_provided_for_project_activation(self) -> list[SerenaAgentMode]:
|
||||
def get_modes_with_prompts_to_be_provided_for_project_activation(self, session_id: str) -> list[SerenaAgentMode]:
|
||||
"""
|
||||
Gets the modes that have been newly activated and for which prompts still need to be provided
|
||||
(in dynamic project activation message).
|
||||
|
||||
:param: session_id: the client session ID
|
||||
:return: the modes
|
||||
"""
|
||||
result = []
|
||||
if not self._mode_prompts_provided:
|
||||
if not self._get_session_status(session_id).mode_prompts_provided:
|
||||
for mode_name in self._newly_activated_mode_names:
|
||||
mode = ActiveModes.get_mode_instance(mode_name)
|
||||
if mode.has_prompt():
|
||||
result.append(mode)
|
||||
return result
|
||||
|
||||
def mark_mode_prompts_as_provided(self) -> None:
|
||||
def mark_mode_prompts_as_provided(self, session_id: str) -> None:
|
||||
"""
|
||||
Marks the prompts for all newly activated modes as provided, so that they will not be included in the project activation message.
|
||||
"""
|
||||
self._mode_prompts_provided = True
|
||||
|
||||
def mark_project_activation_message_as_provided(self) -> None:
|
||||
:param session_id: the client session ID
|
||||
"""
|
||||
self._get_session_status(session_id).mode_prompts_provided = True
|
||||
|
||||
def mark_project_activation_message_as_provided(self, session_id: str) -> None:
|
||||
"""
|
||||
Marks the project activation message as provided, so that it will not be included again in case of multiple activations of the same project.
|
||||
"""
|
||||
self._project_activation_message_provided = True
|
||||
|
||||
def is_project_activation_message_already_provided(self) -> bool:
|
||||
:param session_id: the client session ID
|
||||
"""
|
||||
self._get_session_status(session_id).project_activation_message_provided = True
|
||||
|
||||
def is_project_activation_message_already_provided(self, session_id: str) -> bool:
|
||||
"""
|
||||
:param session_id: the client session ID
|
||||
:return: whether the project activation message was already provided and therefore should not be included again
|
||||
"""
|
||||
return self._project_activation_message_provided
|
||||
return self._get_session_status(session_id).project_activation_message_provided
|
||||
|
||||
|
||||
class SerenaAgent:
|
||||
@@ -736,7 +754,11 @@ class SerenaAgent:
|
||||
template = JinjaTemplate(prompt_template)
|
||||
return template.render(available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names)
|
||||
|
||||
def create_system_prompt(self) -> str:
|
||||
def create_system_prompt(self, session_id: str = "global") -> str:
|
||||
"""
|
||||
:param session_id: the client session ID for the case where this is run from a tool; "global" for the connection time case
|
||||
:return: the prompt
|
||||
"""
|
||||
available_tools = self._active_tools
|
||||
available_markers = available_tools.tool_marker_names
|
||||
global_memories = MemoriesManager(
|
||||
@@ -750,9 +772,9 @@ class SerenaAgent:
|
||||
relevant_modes = []
|
||||
for mode in self.get_active_modes():
|
||||
if mode.has_prompt():
|
||||
if not self._project_prompt_status.is_mode_prompt_already_provided(mode.name):
|
||||
if not self._project_prompt_status.is_mode_prompt_already_provided(mode.name, session_id):
|
||||
relevant_modes.append(mode)
|
||||
self._project_prompt_status.mark_mode_prompts_as_provided()
|
||||
self._project_prompt_status.mark_mode_prompts_as_provided(session_id)
|
||||
|
||||
system_prompt = self.prompt_factory.create_system_prompt(
|
||||
context_system_prompt=self._format_prompt(self._context.prompt),
|
||||
@@ -763,19 +785,25 @@ class SerenaAgent:
|
||||
)
|
||||
|
||||
# provide the project activation message if it hasn't yet been provided
|
||||
if self._active_project is not None and not self._project_prompt_status.is_project_activation_message_already_provided():
|
||||
system_prompt += "\n\n" + self.get_project_activation_message()
|
||||
if self._active_project is not None and not self._project_prompt_status.is_project_activation_message_already_provided(session_id):
|
||||
system_prompt += "\n\n" + self.get_project_activation_message(session_id)
|
||||
|
||||
log.info("System prompt:\n%s", system_prompt)
|
||||
return system_prompt
|
||||
|
||||
def get_project_activation_message(self) -> str:
|
||||
def get_project_activation_message(self, session_id: str) -> str:
|
||||
"""
|
||||
:return: a message providing information about the project upon activation (e.g. programming language, memories, initial prompt)
|
||||
:raise: AssertionError if no project is active
|
||||
"""
|
||||
proj = self._active_project
|
||||
assert proj is not None, "A project must be active before calling this."
|
||||
|
||||
# if activation message was already provided in the current session, don't provide it again
|
||||
if self._project_prompt_status.is_project_activation_message_already_provided(session_id):
|
||||
return "Project was already activated."
|
||||
|
||||
# provide basic project information (name, location, languages, encoding)
|
||||
if proj.is_newly_created:
|
||||
msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}. "
|
||||
else:
|
||||
@@ -785,6 +813,7 @@ class SerenaAgent:
|
||||
msg += f"\nProgramming languages: {languages_str}."
|
||||
msg += f"File encoding: {proj.project_config.encoding}."
|
||||
|
||||
# add list of memories (if memories are enabled)
|
||||
include_memories = self._active_tools.contains_tool_class(ReadMemoryTool)
|
||||
if include_memories:
|
||||
project_memories = proj.memories_manager.list_project_memories()
|
||||
@@ -795,17 +824,18 @@ class SerenaAgent:
|
||||
)
|
||||
|
||||
# add prompts for modes that were dynamically activated by the project
|
||||
modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation()
|
||||
modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation(session_id)
|
||||
if modes_with_prompts:
|
||||
msg += "\nNewly applicable mode instructions:"
|
||||
for mode in modes_with_prompts:
|
||||
msg += f"\n{mode.prompt}"
|
||||
self._project_prompt_status.mark_mode_prompts_as_provided()
|
||||
self._project_prompt_status.mark_mode_prompts_as_provided(session_id)
|
||||
|
||||
# add project-specific prompt
|
||||
if proj.project_config.initial_prompt:
|
||||
msg += f"\nProject-specific instructions:\n {proj.project_config.initial_prompt}"
|
||||
|
||||
self._project_prompt_status.mark_project_activation_message_as_provided()
|
||||
self._project_prompt_status.mark_project_activation_message_as_provided(session_id)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
+1
-3
@@ -396,7 +396,6 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
) -> None:
|
||||
prefix = "You will receive access to Serena's symbolic tools. Below are instructions for using them, take them into account."
|
||||
postfix = "You begin by acknowledging that you understood the above instructions and are ready to receive tasks."
|
||||
from serena.tools.workflow_tools import InitialInstructionsTool
|
||||
|
||||
lvl = logging.getLevelNamesMapping()[log_level.upper()]
|
||||
logging.configure(level=lvl)
|
||||
@@ -410,8 +409,7 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
context=context_instance,
|
||||
modes=modes_selection_def,
|
||||
)
|
||||
tool = agent.get_tool(InitialInstructionsTool)
|
||||
instr = tool.apply()
|
||||
instr = agent.create_system_prompt()
|
||||
if only_instructions:
|
||||
print(instr)
|
||||
else:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from sensai.util.helper import mark_used
|
||||
|
||||
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
|
||||
|
||||
|
||||
@@ -22,17 +24,17 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
Activates a project based on the project name or path.
|
||||
"""
|
||||
|
||||
def apply(self, project: str) -> str:
|
||||
# noinspection PyIncorrectDocstring
|
||||
# (session_id is injected via apply_ex)
|
||||
def apply(self, project: str, session_id: str) -> str:
|
||||
"""
|
||||
Activates the project with the given name or path.
|
||||
|
||||
:param project: the name of a registered project to activate or a path to a project directory
|
||||
"""
|
||||
is_new_activation = self.agent.activate_project_from_path_or_name(project)
|
||||
if not is_new_activation:
|
||||
result = "Project was already active."
|
||||
else:
|
||||
result = self.agent.get_project_activation_message()
|
||||
mark_used(is_new_activation)
|
||||
result = self.agent.get_project_activation_message(session_id)
|
||||
result += "\nIMPORTANT: If you have not yet read the 'Serena Instructions Manual', do it now before continuing!"
|
||||
return result
|
||||
|
||||
|
||||
@@ -63,10 +63,12 @@ class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
for clients that do not read the initial instructions when the MCP server is connected.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
# noinspection PyIncorrectDocstring
|
||||
# (session_id is injected via apply_ex)
|
||||
def apply(self, session_id: str) -> str:
|
||||
"""
|
||||
Provides the 'Serena Instructions Manual', which contains essential information on how to use the Serena toolbox.
|
||||
IMPORTANT: If you have not yet read the manual, call this tool immediately after you are given your task by the user,
|
||||
as it will critically inform you!
|
||||
"""
|
||||
return self.agent.create_system_prompt()
|
||||
return self.agent.create_system_prompt(session_id=session_id)
|
||||
Reference in new issue
Block a user