mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-20 02:18:13 +00:00
Support onboarding task (initial demo)
* Add LLM prompt factory (yaml-based)
* Add list_dir tool
* Add onboarding tool (which returns prompt only, as registered MCP prompts
do not appear to be usable by Claude Desktop)
This commit is contained in:
1 parent
3eb33d2050
commit
61f7c15b15
12 files changed
+520
-8
No files matched your search
@@ -0,0 +1,43 @@
|
||||
from sensai.util import logging
|
||||
|
||||
from serena.llm.multilang_prompt import MultiLangPromptTemplateCollection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
coll = MultiLangPromptTemplateCollection()
|
||||
|
||||
package = "src/serena"
|
||||
|
||||
# collect methods to generate
|
||||
indent = " "
|
||||
methods = []
|
||||
for mpt in coll.prompt_templates.values():
|
||||
prompt_name = mpt.name
|
||||
params = mpt.get_parameters()
|
||||
if len(params) == 0:
|
||||
params_str = ""
|
||||
else:
|
||||
params_str = ", *, " + ", ".join(params)
|
||||
methods.append(
|
||||
f"def create_{prompt_name}(self{params_str}) -> str:"
|
||||
+ f"\n{indent}{indent}return self._format_prompt('{prompt_name}', locals())\n\n{indent}"
|
||||
)
|
||||
for mpl in coll.prompt_lists.values():
|
||||
prompt_name = mpl.name
|
||||
methods.append(
|
||||
f"def get_list_{prompt_name}(self) -> PromptList:" + f"\n{indent}{indent}return self._get_list('{prompt_name}')\n\n{indent}"
|
||||
)
|
||||
|
||||
# write prompt factory with added methods
|
||||
with open("code_templates/prompt_factory_template.py") as f:
|
||||
code = f.read()
|
||||
methods_str = "".join(methods)
|
||||
code = code.replace("# methods", methods_str)
|
||||
with open(f"{package}/llm/prompt_factory.py", "w") as f:
|
||||
f.write(code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.run_main(main)
|
||||
+92
-8
@@ -2,21 +2,25 @@
|
||||
The Serena Model Context Protocol (MCP) Server
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import yaml
|
||||
from mcp.server.fastmcp.prompts.base import Message, UserMessage
|
||||
from mcp.server.fastmcp.server import Context, FastMCP, Settings
|
||||
from sensai.util import logging
|
||||
|
||||
from multilspy import SyncLanguageServer
|
||||
from multilspy.multilspy_config import Language, MultilspyConfig
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from serena.file_utils import scan_directory
|
||||
from serena.llm.prompt_factory import PromptFactory
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,6 +28,9 @@ log = logging.getLogger(__name__)
|
||||
@dataclass
|
||||
class SerenaMCPRequestContext:
|
||||
language_server: SyncLanguageServer
|
||||
project_root: str
|
||||
project_config: dict[str, Any]
|
||||
prompt_factory: PromptFactory
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -41,9 +48,9 @@ async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[SerenaMCPRequest
|
||||
|
||||
# read project configuration
|
||||
with open(project_file, encoding="utf-8") as f:
|
||||
project = yaml.safe_load(f)
|
||||
language = Language(project["language"])
|
||||
project_root = project["project_root"]
|
||||
project_config = yaml.safe_load(f)
|
||||
language = Language(project_config["language"])
|
||||
project_root = project_config["project_root"]
|
||||
|
||||
# create and start the language server instance
|
||||
config = MultilspyConfig(code_language=language)
|
||||
@@ -51,7 +58,9 @@ async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[SerenaMCPRequest
|
||||
language_server = SyncLanguageServer.create(config, logger, project_root)
|
||||
language_server.start()
|
||||
try:
|
||||
yield SerenaMCPRequestContext(language_server=language_server)
|
||||
yield SerenaMCPRequestContext(
|
||||
language_server=language_server, project_root=project_root, project_config=project_config, prompt_factory=PromptFactory()
|
||||
)
|
||||
finally:
|
||||
language_server.stop()
|
||||
|
||||
@@ -60,11 +69,16 @@ mcp_settings = Settings(lifespan=server_lifespan)
|
||||
mcp = FastMCP(**mcp_settings.model_dump())
|
||||
|
||||
|
||||
class Tool:
|
||||
class Component(ABC):
|
||||
def __init__(self, ctx: Context):
|
||||
lifespan_context = cast(SerenaMCPRequestContext, ctx.request_context.lifespan_context)
|
||||
self.langsrv = lifespan_context.language_server
|
||||
self.project_root = lifespan_context.project_root
|
||||
self.project_config = lifespan_context.project_config
|
||||
self.prompt_factory = lifespan_context.prompt_factory
|
||||
|
||||
|
||||
class Tool(Component):
|
||||
def execute(self) -> str:
|
||||
try:
|
||||
return self._execute()
|
||||
@@ -73,12 +87,47 @@ class Tool:
|
||||
return f"Error executing tool: {e}"
|
||||
|
||||
@abstractmethod
|
||||
def _execute(self):
|
||||
def _execute(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
class SimplePrompt(Component):
|
||||
def __init__(self, ctx: Context):
|
||||
super().__init__(ctx)
|
||||
self.messages = []
|
||||
|
||||
def create(self) -> str:
|
||||
return self._create_prompt()
|
||||
|
||||
@abstractmethod
|
||||
def _create_prompt(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
class SequentialPrompt(Component):
|
||||
def __init__(self, ctx: Context):
|
||||
super().__init__(ctx)
|
||||
self.messages = []
|
||||
|
||||
def create(self) -> list[Message]:
|
||||
self._add_messages()
|
||||
return self.messages
|
||||
|
||||
@abstractmethod
|
||||
def _add_messages(self) -> None:
|
||||
pass
|
||||
|
||||
def _add_user_message(self, msg: str) -> None:
|
||||
self.messages.append(UserMessage(content=msg))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def read_file(ctx: Context, relative_path: str) -> str:
|
||||
"""
|
||||
:param ctx: the context object, which will be created and provided automatically
|
||||
:param relative_path: the relative path to the file to read
|
||||
:return: the full text of the file at the given relative path
|
||||
"""
|
||||
log.info(f"read_file: {relative_path=}")
|
||||
|
||||
class ReadFileTool(Tool):
|
||||
@@ -89,6 +138,41 @@ def read_file(ctx: Context, relative_path: str) -> str:
|
||||
return ReadFileTool(ctx).execute()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_dir(ctx: Context, relative_path: str, recursive: bool) -> str:
|
||||
"""
|
||||
:param ctx: the context object, which will be created and provided automatically
|
||||
:param relative_path: the relative path to the directory to list; pass "." to scan the project root
|
||||
:param recursive: whether to scan subdirectories recursively
|
||||
:return: a JSON object with the names of directories and files within the given directory
|
||||
"""
|
||||
log.info(f"list_dir: {relative_path=}")
|
||||
|
||||
class ListDirTool(Tool):
|
||||
def _execute(self):
|
||||
dirs, files = scan_directory(
|
||||
os.path.join(self.project_root, relative_path), recursive=recursive, ignored_dirs=self.project_config["ignored_dirs"]
|
||||
)
|
||||
return json.dumps({"dirs": dirs, "files": files})
|
||||
|
||||
return ListDirTool(ctx).execute()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def onboarding(ctx: Context) -> str:
|
||||
"""
|
||||
:param ctx: the context object, which will be created and provided automatically
|
||||
:return: instructions on how to create the onboarding information
|
||||
"""
|
||||
onboarding_file = "serena_onboarding.md"
|
||||
|
||||
class OnboardingPrompt(SimplePrompt):
|
||||
def _create_prompt(self):
|
||||
return self.prompt_factory.create_onboarding_prompt(onboarding_file=onboarding_file)
|
||||
|
||||
return OnboardingPrompt(ctx).create()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, stream=sys.stderr, format="%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||
|
||||
Reference in new issue
Block a user