Merge pull request #1 from oraios/main

Update from origin
This commit is contained in:
architectureman authored and GitHub committed 2025-08-03 22:16:39 +07:00
commit f5e6e565ee
66 files changed
+1387 -845

No files matched your search

+7 -2
View File
@@ -28,6 +28,12 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: '>=1.17.0'
- name: Ensure cached directory exist before calling cache-related actions
shell: bash
run: |
mkdir -p $HOME/.serena/language_servers/static
mkdir -p $HOME/.cache/go-build
mkdir -p $HOME/go/bin
# Add Go bin directory to PATH for this workflow
# GITHUB_PATH is a special file that GitHub Actions uses to modify PATH
# Writing to this file adds the directory to the PATH for subsequent steps
@@ -49,7 +55,6 @@ jobs:
with:
elixir-version: '1.18.4'
otp-version: '26.1'
- name: Prepare java
uses: actions/setup-java@v3
with:
@@ -77,7 +82,7 @@ jobs:
id: cache-language-servers
uses: actions/cache@v3
with:
path: src/solidlsp/language_servers/static
path: ~/.serena/language_servers/static
key: language-servers-${{ runner.os }}-v1
restore-keys: |
language-servers-${{ runner.os }}-
+7
View File
@@ -2,8 +2,15 @@
Status of the `main` branch. Changes prior to the next official version change will appear here.
* Language servers:
* Reliably detect language server termination and propagate the respective error all the way
back to the tool application, where an unexpected termination is handled by restarting the language server
and subsequently retrying the tool application.
Fixes:
* Ignore `.git` in check for ignored paths and improve performance of `find_all_non_ignored_files`
* Fix language server startup issues on Windows when using Claude Code (which was due to
default shell reconfiguration imposed by Claude Code)
# 0.1.3
+64 -62
View File
@@ -195,6 +195,16 @@ Note that no matter how you run the MCP server, Serena will, by default, start a
MCP server (since many clients fail to clean up processes correctly).
This and other settings can be adjusted in the [configuration](#configuration) and/or by providing [command-line arguments](#command-line-arguments).
##### Using uvx
`uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation.
```shell
uvx --from git+https://github.com/oraios/serena serena start-mcp-server
```
Explore the CLI to see some of the customization options that serena provides (more info on them below).
###### Local Installation
1. Clone the repository and change into it.
@@ -202,38 +212,19 @@ This and other settings can be adjusted in the [configuration](#configuration) a
git clone https://github.com/oraios/serena
cd serena
```
2. Optionally create the configuration file in your home directory, i.e.
* `~/.serena/serena_config.yml` on Linux and macOS, or
* `%USERPROFILE%\.serena\serena_config.yml` on Windows.
by copying the template and then adjusting it according to your needs:
2. Optionally edit the configuration file in your home directory with
```shell
mkdir ~/.serena
cp src/serena/resources/serena_config.template.yml ~/.serena/serena_config.yml
uv run serena config edit
```
If you just want the default config, you can skip this part, and a config file will be created when you first run Serena.
3. Run the server with `uv`:
```shell
uv run serena-mcp-server
uv run serena start-mcp-server
```
When running from outside the serena installation directory, be sure to pass it, i.e. use
When running from outside the serena installation directory, be sure to pass it, i.e., use
```shell
uv run --directory /abs/path/to/serena serena-mcp-server
```
##### Using uvx
`uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation.
* Windows:
```shell
uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe
```
* Other operating systems:
```shell
uvx --from git+https://github.com/oraios/serena serena-mcp-server
```
uv run --directory /abs/path/to/serena serena start-mcp-server
```
##### Using Docker (Experimental)
@@ -243,7 +234,7 @@ You can run the Serena MCP server directly via docker as follows,
assuming that the projects you want to work on are all located in `/path/to/your/projects`:
```shell
docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena-mcp-server --transport stdio
docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena start-mcp-server --transport stdio
```
Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides:
@@ -263,15 +254,15 @@ therefore needs to be configured with a launch command.
When using instead the SSE mode, which uses HTTP-based communication, you control the server lifecycle yourself,
i.e. you start the server and provide the client with the URL to connect to it.
Simply provide `serena-mcp-server` with the `--transport sse` option and optionally provide the port.
Simply provide `start-mcp-server` with the `--transport sse` option and optionally provide the port.
For example, to run the Serena MCP server in SSE mode on port 9121 using a local installation,
you would run this command from the Serena directory,
```shell
uv run serena-mcp-server --transport sse --port 9121
uv run serena start-mcp-server --transport sse --port 9121
```
and then configure your client to connect to `http://localhost/sse:9121`.
and then configure your client to connect to `http://localhost:9121/sse`.
#### Command-Line Arguments
@@ -289,12 +280,22 @@ Serena's behavior (active tools and prompts as well as logging configuration, et
1. The `serena_config.yml` for general settings that apply to all clients and projects.
It is located in your user directory under `.serena/serena_config.yml`.
If you do not explicitly create the file, it will be auto-generated when you first run Serena.
2. In the arguments passed to the `serena-mcp-server` in your client's config (see below),
You can edit it directly or use
```shell
uvx --from git+https://github.com/oraios/serena serena config edit
```
(or use the `--directory` command version).
2. In the arguments passed to the `start-mcp-server` in your client's config (see below),
which will apply to all sessions started by the respective client. In particular, the [context](#contexts) parameter
should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client.
See for a detailed explanation. You can override all entries from the `serena_config.yml` through command line arguments.
3. In the `.serena/project.yml` file within your project. This will hold project-level configuration that is used whenever
that project is activated.
that project is activated. This file will be autogenerated when you first use Serena on that project, but you can also
generate it explicitly with
```shell
uvx --from git+https://github.com/oraios/serena serena project generate-yml
```
(or use the `--directory` command version).
4. Through the currently active set of [modes](#modes).
@@ -323,20 +324,17 @@ project, the file `.serena/project.yml` will be generated. You can adjust the la
same name.
If you are mostly working with the same project, you can also configure to always activate a project at startup
by passing `--project <path_or_name>` to the `serena-mcp-server` command in your client's MCP config.
by passing `--project <path_or_name>` to the `start-mcp-server` command in your client's MCP config.
ℹ️ For larger projects, we recommend that you index your project to accelerate Serena's tools; otherwise the first
tool application may be very slow.
To do so, run one of these commands the project directory or pass the path to the project as an argument:
To do so, run this from the project directory (or pass the path to the project as an argument):
* When using a local installation:
```shell
uv run --directory /abs/path/to/serena index-project
```
* When using uvx:
```shell
uvx --from git+https://github.com/oraios/serena index-project
```
```shell
uvx --from git+https://github.com/oraios/serena serena project index
```
(or use the `--directory` command version).
### Claude Code
@@ -351,15 +349,16 @@ claude mcp add serena -- <serena-mcp-server> --context ide-assistant --project $
where `<serena-mcp-server>` is your way of [running the Serena MCP server](#running-the-serena-mcp-server).
For example, when using `uvx`, you would run
```shell
claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd)
claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context ide-assistant --project $(pwd)
```
ℹ️ Serena comes with an instruction text, and Claude needs to read it to properly use Serena's tools.
Once in Claude Code, you can ask to "read Serena's initial instructions" or run `/mcp__serena__initial_instructions` to load the instruction text.
Do this whenever you start a new conversation and after any compacting operation to ensure Claude remains properly configured to use Serena's tools.
ℹ️ **NEW**: an alternative to the above is adding the instructions as part of the system prompt, then you will not need to run the command above or to remember re-running it after compacting.
This can be achieved through starting claude code with `claude --append-system-prompt $(uvx --from git+https://github.com/oraios/serena serena print-system-prompt)`. Note that this is **experimental**, Claude may not understand the instructions correctly in this way, and we haven't thoroughly tested the resulting behavior. Please report any issues you encounter.
ℹ️ Serena comes with an instruction text, and Claude needs to read it to properly use Serena's tools.
As of version `v1.0.52`, claude code reads the instructions of the MCP server, so this **is handled automatically**.
If you are using an older version, or if Claude fails to read the instructions, you can ask it explicitly
to "read Serena's initial instructions" or run `/mcp__serena__initial_instructions` to load the instruction text.
If you want to make use of that, you will have to enable the corresponding tool explicitly by adding `initial_instructions` to the `included_optional_tools`
in your config.
Note that you may have to make Claude read the instructions you start a new conversation and after any compacting operation to ensure Claude remains properly configured to use Serena's tools.
### Claude Desktop
@@ -374,7 +373,7 @@ Add the `serena` MCP server configuration, using a [run command](#running-the-se
"mcpServers": {
"serena": {
"command": "/abs/path/to/uv",
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"]
"args": ["run", "--directory", "/abs/path/to/serena", "serena", "start-mcp-server"]
}
}
}
@@ -385,7 +384,7 @@ Add the `serena` MCP server configuration, using a [run command](#running-the-se
"mcpServers": {
"serena": {
"command": "/abs/path/to/uvx",
"args": ["--from", "git+https://github.com/oraios/serena", "serena-mcp-server"]
"args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"]
}
}
}
@@ -396,7 +395,7 @@ Add the `serena` MCP server configuration, using a [run command](#running-the-se
"mcpServers": {
"serena": {
"command": "docker",
"args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena-mcp-server", "--transport", "stdio"]
"args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena", "start-mcp-server", "--transport", "stdio"]
}
}
}
@@ -578,14 +577,20 @@ When launching Serena, specify modes using `--mode <mode-name>`; multiple modes
#### Customization
You can create your own contexts and modes to precisely tailor Serena to your needs in two ways:
* **Adding to Serena's configuration directory**: 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 name (filename without the `.yml` extension). They will also appear in listings of available contexts/modes.
* **Using external YAML files**: When starting Serena, you can provide an absolute path to a custom `.yml` file for a context or mode.
* You can use Serena's CLI to manage modes and contexts. Check out
```shell
uvx --from git+https://github.com/oraios/serena serena mode --help
uvx --from git+https://github.com/oraios/serena serena context --help
```
*NOTE*: Custom contexts/modes are simply YAML files in `<home>/.serena`, they are automatically registered and available for use by their name (filename without the `.yml` extension). If you don't want to use Serena's CLI, you can create and manage them in any way you see fit.
* **Using external YAML files**: When starting Serena, you can also 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.
* `description`: (Optional) A brief description, not passed to the LLM.
* `excluded_tools`: A list of tool names (strings) to disable when this context/mode is active.
* `included_optional_tools`: A list of tool names that are disabled by default and have to be explicitly enabled by the user.
This customization allows for deep integration and adaptation of Serena to specific project requirements or personal preferences.
@@ -738,11 +743,10 @@ subscriptions.
### Subscription-Based Coding Agents
The most prominent subscription-based coding agents are parts of IDEs like
Many prominent subscription-based coding agents are parts of IDEs like
Windsurf, Cursor and VSCode.
Serena's functionality is similar to Cursor's Agent, Windsurf's Cascade or
VSCode's
upcoming [agent mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
VSCode's agent mode.
Serena has the advantage of not requiring a subscription.
A potential disadvantage is that it
@@ -750,7 +754,7 @@ is not directly integrated into an IDE, so the inspection of newly written code
is not as seamless.
More technical differences are:
* Serena is not bound to a specific IDE.
* Serena is not bound to a specific IDE or CLI.
Serena's MCP server can be used with any MCP client (including some IDEs),
and the Agno-based agent provides additional ways of applying its functionality.
* Serena is not bound to a specific large language model or API.
@@ -826,7 +830,7 @@ For details on contributing, see [here](/CONTRIBUTING.md).
## Full List of Tools
Here is the full list of Serena's tools with a short description (output of `uv run serena-list-tools`):
Here is the full list of Serena's tools with a short description (output of `uv run serena tools list`):
* `activate_project`: Activates a project by name.
* `check_onboarding_performed`: Checks whether project onboarding was already performed.
@@ -840,9 +844,7 @@ Here is the full list of Serena's tools with a short description (output of `uv
* `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.
* `initial_instructions`: Gets the initial instructions for the current project. Disabled by default, has to be enabled explicitly by using `included_optional_tools`
* `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.
+102
View File
@@ -0,0 +1,102 @@
# Connecting Serena MCP Server to ChatGPT via MCPO & Cloudflare Tunnel
This guide explains how to expose a **locally running Serena MCP server** (powered by MCPO) to the internet using **Cloudflare Tunnel**, and how to connect it to **ChatGPT as a Custom GPT with tool access**.
Once configured, ChatGPT becomes a powerful **coding agent** with direct access to your codebase, shell, and file system — so **read the security notes carefully**.
---
## Prerequisites
Make sure you have [uv](https://docs.astral.sh/uv/getting-started/installation/)
and [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed.
## 1. Start the Serena MCP Server via MCPO
Run the following command to launch Serena as http server (assuming port 8000):
```bash
uvx mcpo --port 8000 --api-key <YOUR_SECRET_KEY> -- \
uvx --from git+https://github.com/oraios/serena \
serena start-mcp-server --context chatgpt --project $(pwd)
```
- `--api-key` is required to secure the server.
- `--project` should point to the root of your codebase.
You can also use other options, and you don't have to pass `--project` if you want to work on multiple projects
or want to activate it later. See
```shell
uvx --from git+https://github.com/oraios/serena serena start-mcp-server --help
```
---
## 2. Expose the Server Using Cloudflare Tunnel
Run:
```bash
cloudflared tunnel --url http://localhost:8000
```
This will give you a **public HTTPS URL** like:
```
https://serena-agent-tunnel.trycloudflare.com
```
Your server is now securely exposed to the internet.
---
## 3. Connect It to ChatGPT (Custom GPT)
### Steps:
1. Go to [ChatGPT → Explore GPTs → Create](https://chat.openai.com/gpts/editor)
2. During setup, click **“Add APIs”**
3. Set up **API Key authentication** with the auth type as **Bearer** and enter the api key you used to start the MCPO server.
4. In the **Schema** section, click on **import from URL** and paste `<cloudflared_url>/openapi.json` with the URL you got from the previous step.
5. Add the following line to the top of the imported JSON schema:
```
"servers": ["url": "<cloudflared_url>"],
```
**Important**: don't include a trailing slash at the end of the URL!
ChatGPT will read the schema and create functions automatically.
---
## Security Warning — Read Carefully
Depending on your configuration and enabled tools, Serena's MCP server may:
- Execute **arbitrary shell commands**
- Read, write, and modify **files in your codebase**
This gives ChatGPT the same powers as a remote developer on your machine.
### ⚠️ Key Rules:
- **NEVER expose your API key**
- **Only expose this server when needed**, and monitor its use.
In your project’s `.serena/project.yml` or global config, you can disable tools like:
```yaml
excluded_tools:
- execute_shell_command
- ...
read_only: true
```
This is strongly recommended if you want a read-only or safer agent.
---
## Final Thoughts
With this setup, ChatGPT becomes a coding assistant **running on your local code** — able to index, search, edit, and even run shell commands depending on your configuration.
Use responsibly, and keep security in mind.
+2 -2
View File
@@ -18,9 +18,9 @@ dependencies = [
"pyright>=1.1.396,<2",
"overrides>=7.7.0,<8",
"python-dotenv>=1.0.0, <2",
"mcp>=1.5.0",
"mcp==1.12.3",
"flask>=3.0.0",
"sensai-utils>=1.4.0",
"sensai-utils>=1.5.0",
"pydantic>=2.10.6",
"types-pyyaml>=6.0.12.20241230",
"pyyaml>=6.0.2",
+3 -7
View File
@@ -9,7 +9,7 @@ from pprint import pprint
from serena.agent import SerenaAgent
from serena.config.serena_config import SerenaConfig
from serena.constants import REPO_ROOT
from serena.tools import FindFileTool, FindReferencingSymbolsTool, SearchForPatternTool
from serena.tools import FindFileTool, FindReferencingSymbolsTool, GetSymbolsOverviewTool, SearchForPatternTool
if __name__ == "__main__":
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False))
@@ -18,13 +18,9 @@ if __name__ == "__main__":
find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
find_file_tool = agent.get_tool(FindFileTool)
search_pattern_tool = agent.get_tool(SearchForPatternTool)
overview_tool = agent.get_tool(GetSymbolsOverviewTool)
result = agent.execute_task(
lambda: search_pattern_tool.apply(
r"def request_full_.*?\).*?\)",
restrict_search_to_code_files=False,
relative_path="src/solidlsp",
paths_include_glob="**/ls.py",
)
lambda: overview_tool.apply("src/solidlsp/ls.py"),
)
pprint(json.loads(result))
+2 -2
View File
@@ -7,14 +7,14 @@ from pathlib import Path
from sensai.util import logging
from interprompt import autogenerate_prompt_factory_module
from serena.constants import PROMPT_TEMPLATES_DIR, REPO_ROOT
from serena.constants import PROMPT_TEMPLATES_DIR_INTERNAL, REPO_ROOT
log = logging.getLogger(__name__)
def main():
autogenerate_prompt_factory_module(
prompts_dir=PROMPT_TEMPLATES_DIR,
prompts_dir=PROMPT_TEMPLATES_DIR_INTERNAL,
target_module_path=str(Path(REPO_ROOT) / "src" / "serena" / "generated" / "generated_prompt_factory.py"),
)
+84 -12
View File
@@ -1,7 +1,7 @@
import logging
import os
from enum import Enum
from typing import Any, Generic, TypeVar
from typing import Any, Generic, Literal, TypeVar
import yaml
from sensai.util.string import ToStringMixin
@@ -92,6 +92,9 @@ class _MultiLangContainer(Generic[T], ToStringMixin):
raise KeyError(f"Item for language '{lang_code}' already registered for name '{self.name}'")
self._lang2item[lang_code] = item
def has_item(self, lang_code: str = DEFAULT_LANG_CODE) -> bool:
return lang_code in self._lang2item
def get_item(self, lang: str = DEFAULT_LANG_CODE, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> T:
"""
Gets the item for the given language.
@@ -183,6 +186,9 @@ class MultiLangPromptTemplate(ParameterizedTemplateInterface):
prompt_template = self.get_prompt_template(lang_code, fallback_mode)
return prompt_template.render(**params)
def has_item(self, lang_code: str = DEFAULT_LANG_CODE) -> bool:
return self._prompts_container.has_item(lang_code)
class MultiLangPromptList(_MultiLangContainer[PromptList]):
pass
@@ -212,44 +218,96 @@ class MultiLangPromptCollection:
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:
def __init__(self, prompts_dir: str | list[str], fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> None:
"""
:param prompts_dir: the directory containing the prompt templates and prompt lists
:param prompts_dir: the directory containing the prompt templates and prompt lists.
If a list is provided, will look for prompt templates in the dirs from left to right
(first one containing the desired template wins).
: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)
if isinstance(prompts_dir, str):
prompts_dir = [prompts_dir]
# Add prompts from multiple directories, prioritizing names from the left.
# If name collisions appear in the first directory, an error is raised (so the first directory should have no
# internal collisions, this helps in avoiding errors)
# For all following directories, on a collision the new value will be ignored.
# This also means that for the following directories, there is no error check on collisions internal to them.
# We assume that they are correct (i.e., they have no internal collisions).
first_prompts_dir, fallback_prompt_dirs = prompts_dir[0], prompts_dir[1:]
self._load_from_disc(first_prompts_dir, on_name_collision="raise")
for fallback_prompt_dir in fallback_prompt_dirs:
# already loaded prompts have priority
self._load_from_disc(fallback_prompt_dir, on_name_collision="skip")
self.fallback_mode = fallback_mode
def _add_prompt_template(self, name: str, template_str: str, lang_code: str = DEFAULT_LANG_CODE) -> None:
def _add_prompt_template(
self,
name: str,
template_str: str,
lang_code: str = DEFAULT_LANG_CODE,
on_name_collision: Literal["skip", "overwrite", "raise"] = "raise",
) -> 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.
:param on_name_collision: how to deal with name/lang_code collisions
"""
allow_overwrite = False
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)
if mlpt.has_item(lang_code):
if on_name_collision == "raise":
raise KeyError(f"Prompt '{name}' for {lang_code} already exists!")
if on_name_collision == "skip":
log.debug(f"Skipping prompt '{name}' since it already exists.")
return
elif on_name_collision == "overwrite":
allow_overwrite = True
mlpt.add_prompt_template(prompt_template, lang_code=lang_code, allow_overwrite=allow_overwrite)
def _add_prompt_list(self, name: str, prompt_list: list[str], lang_code: str = DEFAULT_LANG_CODE) -> None:
def _add_prompt_list(
self,
name: str,
prompt_list: list[str],
lang_code: str = DEFAULT_LANG_CODE,
on_name_collision: Literal["skip", "overwrite", "raise"] = "raise",
) -> 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.
:param on_name_collision: how to deal with name/lang_code collisions
"""
allow_overwrite = False
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)
if multilang_prompt_list.has_item(lang_code):
if on_name_collision == "raise":
raise KeyError(f"Prompt '{name}' for {lang_code} already exists!")
if on_name_collision == "skip":
log.debug(f"Skipping prompt '{name}' since it already exists.")
return
elif on_name_collision == "overwrite":
allow_overwrite = True
multilang_prompt_list.add_item(PromptList(prompt_list), lang_code=lang_code, allow_overwrite=allow_overwrite)
def _load_from_disc(self, prompts_dir: str) -> None:
"""Loads all prompt templates and prompt lists from yaml files in the given directory."""
def _load_from_disc(self, prompts_dir: str, on_name_collision: Literal["skip", "overwrite", "raise"] = "raise") -> None:
"""Loads all prompt templates and prompt lists from yaml files in the given directory.
:param prompts_dir:
:param on_name_collision: how to deal with name/lang_code collisions
"""
for fn in os.listdir(prompts_dir):
if not fn.endswith((".yml", ".yaml")):
log.debug(f"Skipping non-YAML file: {fn}")
@@ -266,9 +324,11 @@ class MultiLangPromptCollection:
# 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)
self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision)
elif isinstance(prompt_template_or_list, str):
self._add_prompt_template(prompt_name, prompt_template_or_list, lang_code=lang_code)
self._add_prompt_template(
prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision
)
else:
raise ValueError(
f"Invalid prompt type for {prompt_name} in file {path} (should be str or list): {prompt_template_or_list}"
@@ -306,6 +366,18 @@ class MultiLangPromptCollection:
"""The PromptList object for the given prompt name and language code."""
return self.get_multilang_prompt_list(prompt_name).get_item(lang_code)
def _has_prompt_list(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> bool:
multi_lang_prompt_list = self._multi_lang_prompt_lists.get(prompt_name)
if multi_lang_prompt_list is None:
return False
return multi_lang_prompt_list.has_item(lang_code)
def _has_prompt_template(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> bool:
multi_lang_prompt_template = self._multi_lang_prompt_templates.get(prompt_name)
if multi_lang_prompt_template is None:
return False
return multi_lang_prompt_template.has_item(lang_code)
def render_prompt_template(
self,
prompt_name: str,
+4 -2
View File
@@ -10,9 +10,11 @@ log = logging.getLogger(__name__)
class PromptFactoryBase:
"""Base class for auto-generated prompt factory classes."""
def __init__(self, prompts_dir: str, lang_code: str = DEFAULT_LANG_CODE, fallback_mode=LanguageFallbackMode.EXCEPTION):
def __init__(self, prompts_dir: str | list[str], lang_code: str = DEFAULT_LANG_CODE, fallback_mode=LanguageFallbackMode.EXCEPTION):
"""
:param prompts_dir: the directory containing the prompt templates and prompt lists
:param prompts_dir: the directory containing the prompt templates and prompt lists.
If a list is provided, will look for prompt templates in the dirs from left to right
(first one containing the desired template wins).
: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.
+57 -6
View File
@@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Any, Optional, TypeVar
from sensai.util import logging
from sensai.util.logging import LogTime
from interprompt.jinja_template import JinjaTemplate
from serena import serena_version
from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats
from serena.config.context_mode import RegisteredContext, SerenaAgentContext, SerenaAgentMode
@@ -25,7 +26,8 @@ from serena.config.serena_config import SerenaConfig, ToolInclusionDefinition, T
from serena.dashboard import SerenaDashboardAPI
from serena.project import Project
from serena.prompt_factory import SerenaPromptFactory
from serena.tools import ActivateProjectTool, Tool, ToolRegistry
from serena.tools import ActivateProjectTool, Tool, ToolMarker, ToolRegistry
from serena.util.inspection import iter_subclasses
from serena.util.logging import MemoryLogHandler
from solidlsp import SolidLanguageServer
@@ -91,6 +93,23 @@ class MemoriesManager:
return f"Memory {name} deleted."
class AvailableTools:
def __init__(self, tools: list[Tool]):
"""
:param tools: the list of available tools
"""
self.tools = tools
self.tool_names = [tool.get_name_from_cls() for tool in tools]
self.tool_marker_names = set()
for marker_class in iter_subclasses(ToolMarker):
for tool in tools:
if isinstance(tool, marker_class):
self.tool_marker_names.add(marker_class.__name__)
def __len__(self) -> int:
return len(self.tools)
class SerenaAgent:
def __init__(
self,
@@ -181,6 +200,8 @@ class SerenaAgent:
log.info("Available projects: {}".format(", ".join(self.serena_config.project_names)))
log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}")
self._check_shell_settings()
# determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see),
# limited by the Serena config, the context (which is fixed for the session) and JetBrains mode
tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context]
@@ -190,7 +211,7 @@ class SerenaAgent:
tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains"))
self._base_tool_set = ToolSet.default().apply(*tool_inclusion_definitions)
self._exposed_tools = {tc: t for tc, t in self._all_tools.items() if self._base_tool_set.includes_name(t.get_name())}
self._exposed_tools = AvailableTools([t for t in self._all_tools.values() if self._base_tool_set.includes_name(t.get_name())])
log.info(f"Number of exposed tools: {len(self._exposed_tools)}")
# create executor for starting the language server and running tools in another thread
@@ -225,6 +246,22 @@ class SerenaAgent:
except Exception as e:
log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e)
def get_context(self) -> SerenaAgentContext:
return self._context
def get_tool_description_override(self, tool_name: str) -> str | None:
return self._context.tool_description_overrides.get(tool_name, None)
def _check_shell_settings(self) -> None:
# On Windows, Claude Code sets COMSPEC to Git-Bash (often even with a path containing spaces),
# which causes all sorts of trouble, preventing language servers from being launched correctly.
# So we make sure that COMSPEC is unset if it has been set to bash specifically.
if platform.system() == "Windows":
comspec = os.environ.get("COMSPEC", "")
if "bash" in comspec:
os.environ["COMSPEC"] = "" # force use of default shell
log.info("Adjusting COMSPEC environment variable to use the default shell instead of '%s'", comspec)
def _ide_context_tool_inclusion_definitions(self, project_root_or_name: str | None) -> list[ToolInclusionDefinition]:
"""
In the IDE assistant context, the agent is assumed to work on a single project, and we thus
@@ -289,7 +326,7 @@ class SerenaAgent:
If a client should attempt to use a tool that is dynamically disabled
(e.g. because a project is activated that disables it), it will receive an error.
"""
return list(self._exposed_tools.values())
return list(self._exposed_tools.tools)
def get_active_project(self) -> Project | None:
"""
@@ -323,11 +360,21 @@ class SerenaAgent:
"""
return list(self._modes)
def _format_prompt(self, prompt_template: str) -> str:
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:
return self.prompt_factory.create_system_prompt(
context_system_prompt=self._context.prompt,
mode_system_prompts=[mode.prompt for mode in self._modes],
available_markers = self._exposed_tools.tool_marker_names
log.info("Generating system prompt with available_tools=(see exposed tools), available_markers=%s", available_markers)
system_prompt = self.prompt_factory.create_system_prompt(
context_system_prompt=self._format_prompt(self._context.prompt),
mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self._modes],
available_tools=self._exposed_tools.tool_names,
available_markers=available_markers,
)
log.info("System prompt:\n%s", system_prompt)
return system_prompt
def _update_active_tools(self) -> None:
"""
@@ -571,3 +618,7 @@ class SerenaAgent:
if self._gui_log_viewer:
log.info("Stopping the GUI log window ...")
self._gui_log_viewer.stop()
def get_tool_by_name(self, tool_name: str) -> Tool:
tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
return self.get_tool(tool_class)
+114 -3
View File
@@ -1,3 +1,4 @@
import glob
import os
import shutil
import subprocess
@@ -16,6 +17,8 @@ from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths
from serena.constants import (
DEFAULT_CONTEXT,
DEFAULT_MODES,
PROMPT_TEMPLATES_DIR_IN_USER_HOME,
PROMPT_TEMPLATES_DIR_INTERNAL,
SERENA_LOG_FORMAT,
SERENA_MANAGED_DIR_IN_HOME,
SERENAS_OWN_CONTEXT_YAMLS_DIR,
@@ -23,7 +26,7 @@ from serena.constants import (
USER_CONTEXT_YAMLS_DIR,
USER_MODE_YAMLS_DIR,
)
from serena.mcp import SerenaMCPFactorySingleProcess
from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess
from serena.project import Project
from serena.tools import ToolRegistry
from serena.util.logging import MemoryLogHandler
@@ -435,7 +438,7 @@ class ProjectCommands(AutoRegisteringGroup):
@click.argument("project", type=click.Path(exists=True), default=os.getcwd(), required=False)
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="WARNING")
def index_deprecated(project: str, log_level: str = "WARNING") -> None:
click.echo("Deprecated! Use `project index` instead.")
click.echo("Deprecated! Use `serena project index` instead.")
ProjectCommands._index_project(project, log_level)
@staticmethod
@@ -475,6 +478,113 @@ class ToolCommands(AutoRegisteringGroup):
else:
ToolRegistry().print_tool_overview()
@staticmethod
@click.command(
"description",
help="Print the description of a tool, optionally with a specific context (the latter may modify the default description).",
)
@click.argument("tool_name", type=str)
@click.option("--context", type=str, default=None, help="Context name or path to context file.")
def description(tool_name: str, context: str | None = None) -> None:
# Load the context
serena_context = None
if context:
serena_context = SerenaAgentContext.load(context)
agent = SerenaAgent(
project=None,
serena_config=SerenaConfig(web_dashboard=False, log_level=logging.INFO),
context=serena_context,
)
tool = agent.get_tool_by_name(tool_name)
mcp_tool = SerenaMCPFactory.make_mcp_tool(tool)
click.echo(mcp_tool.description)
class PromptCommands(AutoRegisteringGroup):
def __init__(self) -> None:
super().__init__(name="prompts", help="Commands related to Serena's prompts that are outside of contexts and modes.")
@staticmethod
def _get_user_prompt_yaml_path(prompt_yaml_name: str) -> str:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
return os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, prompt_yaml_name)
@staticmethod
@click.command("list", help="Lists yamls that are used for defining prompts.")
def list() -> None:
serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
for prompt_yaml_name in serena_prompt_yaml_names:
user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
if os.path.exists(user_prompt_yaml_path):
click.echo(f"{user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}")
else:
click.echo(prompt_yaml_name)
@staticmethod
@click.command("create-override", help="Create an override of an internal prompts yaml for customizing Serena's prompts")
@click.argument("prompt_yaml_name")
def create_override(prompt_yaml_name: str) -> None:
"""
:param prompt_yaml_name: The yaml name of the prompt you want to override. Call the `list` command for discovering valid prompt yaml names.
:return:
"""
# for convenience, we can pass names without .yml
if not prompt_yaml_name.endswith(".yml"):
prompt_yaml_name = prompt_yaml_name + ".yml"
user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
if os.path.exists(user_prompt_yaml_path):
raise FileExistsError(f"{user_prompt_yaml_path} already exists.")
serena_prompt_yaml_path = os.path.join(PROMPT_TEMPLATES_DIR_INTERNAL, prompt_yaml_name)
shutil.copyfile(serena_prompt_yaml_path, user_prompt_yaml_path)
_open_in_editor(user_prompt_yaml_path)
@staticmethod
@click.command("edit-override", help="Edit an existing prompt override file")
@click.argument("prompt_yaml_name")
def edit_override(prompt_yaml_name: str) -> None:
"""
:param prompt_yaml_name: The yaml name of the prompt override to edit.
:return:
"""
# for convenience, we can pass names without .yml
if not prompt_yaml_name.endswith(".yml"):
prompt_yaml_name = prompt_yaml_name + ".yml"
user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
if not os.path.exists(user_prompt_yaml_path):
click.echo(f"Override file '{prompt_yaml_name}' not found. Create it with: prompts create-override {prompt_yaml_name}")
return
_open_in_editor(user_prompt_yaml_path)
@staticmethod
@click.command("list-overrides", help="List existing prompt override files")
def list_overrides() -> None:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
override_files = glob.glob(os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, "*.yml"))
for file_path in override_files:
if os.path.basename(file_path) in serena_prompt_yaml_names:
click.echo(file_path)
@staticmethod
@click.command("delete-override", help="Delete a prompt override file")
@click.argument("prompt_yaml_name")
def delete_override(prompt_yaml_name: str) -> None:
"""
:param prompt_yaml_name: The yaml name of the prompt override to delete."
:return:
"""
# for convenience, we can pass names without .yml
if not prompt_yaml_name.endswith(".yml"):
prompt_yaml_name = prompt_yaml_name + ".yml"
user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
if not os.path.exists(user_prompt_yaml_path):
click.echo(f"Override file '{prompt_yaml_name}' not found.")
return
os.remove(user_prompt_yaml_path)
click.echo(f"Deleted override file '{prompt_yaml_name}'.")
# Expose groups so we can reference them in pyproject.toml
mode = ModeCommands()
@@ -482,6 +592,7 @@ context = ContextCommands()
project = ProjectCommands()
config = SerenaConfigCommands()
tools = ToolCommands()
prompts = PromptCommands()
# Expose toplevel commands for the same reason
top_level = TopLevelCommands()
@@ -489,7 +600,7 @@ start_mcp_server = top_level.start_mcp_server
index_project = project.index_deprecated
# needed for the help script to work - register all subcommands to the top-level group
for subgroup in (mode, context, project, config, tools):
for subgroup in (mode, context, project, config, tools, prompts):
top_level.add_command(subgroup)
+14 -24
View File
@@ -3,8 +3,7 @@ Context and Mode configuration loader
"""
import os
from copy import copy
from dataclasses import asdict, dataclass
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Self
@@ -39,22 +38,15 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
name: str
prompt: str
"""
a Jinja2 template for the generation of the system prompt.
It is formatted by the agent (see SerenaAgent._format_prompt()).
"""
description: str = ""
def _tostring_includes(self) -> list[str]:
return ["name"]
def to_json_dict(self) -> dict[str, str | list[str]]:
result = asdict(self)
result["excluded_tools"] = list(result["excluded_tools"])
return result
@classmethod
def from_json_dict(cls, data: dict) -> Self:
data = copy(data)
data["excluded_tools"] = set(data["excluded_tools"])
return cls(**data)
def print_overview(self) -> None:
"""Print an overview of the mode."""
print(f"{self.name}:\n {self.description}")
@@ -133,28 +125,26 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
name: str
prompt: str
"""
a Jinja2 template for the generation of the system prompt.
It is formatted by the agent (see SerenaAgent._format_prompt()).
"""
description: str = ""
tool_description_overrides: dict[str, str] = field(default_factory=dict)
"""Maps tool names to custom descriptions, default descriptions are extracted from the tool docstrings."""
def _tostring_includes(self) -> list[str]:
return ["name"]
def to_json_dict(self) -> dict[str, str | list[str]]:
result = asdict(self)
result["excluded_tools"] = list(result["excluded_tools"])
return result
@classmethod
def from_json_dict(cls, data: dict) -> Self:
data = copy(data)
data["excluded_tools"] = set(data["excluded_tools"])
return cls(**data)
@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)
# Ensure backwards compatibility for tool_description_overrides
if "tool_description_overrides" not in data:
data["tool_description_overrides"] = {}
return cls(name=name, **data)
@classmethod
+2 -2
View File
@@ -47,13 +47,13 @@ class SerenaPaths:
Provides paths to various Serena-related directories and files.
"""
def __init__(self):
def __init__(self) -> None:
self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME
"""
the path to the user's Serena configuration directory, which is typically ~/.serena
"""
def get_next_log_file_path(self, prefix: str):
def get_next_log_file_path(self, prefix: str) -> str:
"""
:param prefix: the filename prefix indicating the type of the log file
:return: the full path to the log file to use
+2 -1
View File
@@ -10,7 +10,8 @@ SERENA_MANAGED_DIR_IN_HOME = str(_serena_in_home_managed_dir)
# TODO: Path-related constants should be moved to SerenaPaths; don't add further constants here.
REPO_ROOT = str(_repo_root_path)
PROMPT_TEMPLATES_DIR = str(_serena_pkg_path / "resources" / "config" / "prompt_templates")
PROMPT_TEMPLATES_DIR_INTERNAL = str(_serena_pkg_path / "resources" / "config" / "prompt_templates")
PROMPT_TEMPLATES_DIR_IN_USER_HOME = str(_serena_in_home_managed_dir / "prompt_templates")
SERENAS_OWN_CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts")
"""The contexts that are shipped with the Serena package, i.e. the default contexts."""
USER_CONTEXT_YAMLS_DIR = str(_serena_in_home_managed_dir / "contexts")
@@ -32,5 +32,7 @@ class PromptFactory(PromptFactoryBase):
def create_prepare_for_new_conversation(self) -> str:
return self._render_prompt("prepare_for_new_conversation", locals())
def create_system_prompt(self, *, context_system_prompt: Any, mode_system_prompts: Any) -> str:
def create_system_prompt(
self, *, available_markers: Any, available_tools: Any, context_system_prompt: Any, mode_system_prompts: Any
) -> str:
return self._render_prompt("system_prompt", locals())
+21 -5
View File
@@ -68,10 +68,17 @@ class SerenaMCPFactory:
# Mount the tool description as a combination of the docstring description and
# the return value description, if it exists.
if docstring.description:
func_doc = f"{docstring.description.strip().strip('.')}."
overridden_description = tool.agent.get_context().tool_description_overrides.get(func_name, None)
if overridden_description is not None:
func_doc = overridden_description
elif docstring.description:
func_doc = docstring.description
else:
func_doc = ""
func_doc = func_doc.strip().strip(".")
if func_doc:
func_doc += "."
if docstring.returns and (docstring_returns_descr := docstring.returns.description):
# Only add a space before "Returns" if func_doc is not empty
prefix = " " if func_doc else ""
@@ -113,6 +120,7 @@ class SerenaMCPFactory:
for tool in self._iter_tools():
mcp_tool = self.make_mcp_tool(tool)
mcp._tool_manager._tools[tool.get_name()] = mcp_tool
log.info(f"Starting MCP server with {len(mcp._tool_manager._tools)} tools: {list(mcp._tool_manager._tools.keys())}")
@abstractmethod
def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None:
@@ -170,9 +178,8 @@ class SerenaMCPFactory:
# `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings;
# retain only FASTMCP_ prefix for already set environment variables.
Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_")
mcp_settings: Settings = Settings(lifespan=self.server_lifespan, host=host, port=port)
mcp = FastMCP(**mcp_settings.model_dump())
instructions = self._get_initial_instructions()
mcp = FastMCP(lifespan=self.server_lifespan, host=host, port=port, instructions=instructions)
return mcp
@asynccontextmanager
@@ -181,6 +188,10 @@ class SerenaMCPFactory:
"""Manage server startup and shutdown lifecycle."""
yield None # ensures MyPy understands we yield None
@abstractmethod
def _get_initial_instructions(self) -> str:
pass
class SerenaMCPFactorySingleProcess(SerenaMCPFactory):
"""
@@ -207,6 +218,11 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory):
assert self.agent is not None
yield from self.agent.get_exposed_tool_instances()
def _get_initial_instructions(self) -> str:
assert self.agent is not None
# we don't use the tool (which at the time of writing calls this method), since the tool may be disabled by the config
return self.agent.create_system_prompt()
@asynccontextmanager
async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]:
self._set_mcp_tools(mcp_server)
+3
View File
@@ -5,11 +5,13 @@ from pathlib import Path
import pathspec
from serena.config.serena_config import DEFAULT_TOOL_TIMEOUT, ProjectConfig
from serena.constants import SERENA_MANAGED_DIR_IN_HOME
from serena.text_utils import MatchedConsecutiveLines, search_files
from serena.util.file_system import GitignoreParser, match_path
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -271,4 +273,5 @@ class Project:
ls_logger,
self.project_root,
timeout=ls_timeout,
solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME),
)
+5 -2
View File
@@ -1,4 +1,6 @@
from serena.constants import PROMPT_TEMPLATES_DIR
import os
from serena.constants import PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL
from serena.generated.generated_prompt_factory import PromptFactory
@@ -8,4 +10,5 @@ class SerenaPromptFactory(PromptFactory):
"""
def __init__(self) -> None:
super().__init__(PROMPT_TEMPLATES_DIR)
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
super().__init__(prompts_dir=[PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL])
@@ -3,4 +3,6 @@ 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
- initial_instructions
tool_description_overrides: {}
@@ -0,0 +1,28 @@
description: A configuration specific for chatgpt, which has a limit of 30 tools and requires short descriptions.
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: []
included_optional_tools:
- switch_modes
tool_description_overrides:
find_symbol: |
Retrieves symbols matching `name_path` in a file.
Use `depth > 0` to include children. `name_path` can be: "foo": any symbol named "foo"; "foo/bar": "bar" within "foo"; "/foo/bar": only top-level "foo/bar"
replace_regex: |
Replaces text using regular expressions. Preferred for smaller edits where symbol-level tools aren't appropriate.
Use wildcards (.*?) to match large sections efficiently: "beginning.*?end" instead of specifying exact content.
Essential for multi-line replacements.
search_for_pattern: |
Flexible pattern search across codebase. Prefer symbolic operations when possible.
Uses DOTALL matching. Use non-greedy quantifiers (.*?) to avoid over-matching.
Supports file filtering via globs and code-only restriction.
@@ -1,4 +1,11 @@
# See Serena's documentation for more details on concept of contexts.
description: Description of the context, not used in the code.
prompt: Prompt that will form part of the system prompt/initial instructions for agents started in this context.
excluded_tools: []
excluded_tools: []
# several tools are excluded by default and have to be explicitly included by the user
included_optional_tools: []
# mapping of tool names to an override of their descriptions (the default description is the docstring of the Tool's apply method).
# Sometimes, tool descriptions are too long (e.g., for ChatGPT), or users may want to override them for another reason.
tool_description_overrides: {}
@@ -10,4 +10,8 @@ prompt: |
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: []
excluded_tools: []
included_optional_tools:
- switch_modes
tool_description_overrides: {}
@@ -19,7 +19,8 @@ prompt: |
excluded_tools:
- create_text_file
- read_file
- delete_lines
- replace_lines
- insert_at_line
- execute_shell_command
- execute_shell_command
- prepare_for_new_conversation
tool_description_overrides: {}
@@ -14,7 +14,7 @@ prompt: |
use the regex-based approach that is described below.
Let us first discuss the symbol-based approach.
Symbols are identified by their name path and relative file path, see the description of the `find_symbols` tool for more details
Symbols are identified by their name path and relative file path, see the description of the `find_symbol` tool for more details
on how the `name_path` matches symbols.
You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file
or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible
@@ -34,7 +34,8 @@ prompt: |
You will generally be able to use the info from the snippets and the regex-based approach to adjust the references as well.
You can assume that all symbol editing tools are reliable, so you don't need to verify the results if the tool returns without error.
Now let us discuss the regex-based approach.
{% if 'replace_regex' in available_tools %}
Let us discuss the regex-based approach.
The regex-based approach is your primary tool for editing code whenever replacing or deleting a whole symbol would be a more expensive operation.
This is the case if you need to adjust just a few lines of code within a method, or a chunk that is much smaller than a whole symbol.
You use other tools to find the relevant content and
@@ -104,7 +105,8 @@ prompt: |
Generally, I remind you that you rely on the regex tool with providing you the correct feedback, no need for more verification!
IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD!
excluded_tools:
{% endif %}
excluded_tools:
- replace_lines
- insert_at_line
- delete_lines
@@ -2,3 +2,6 @@
description: Description of the mode, not used in the code.
prompt: Prompt that will form part of the message sent to the model when activating this mode.
excluded_tools: []
# several tools are excluded by default and have to be explicitly included by the user
included_optional_tools: []
@@ -5,32 +5,38 @@ 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
files containing general information about the codebase. You operate in a resource-efficient and intelligent manner, always
keeping in mind to not read or generate content that is not needed for the task at hand.
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 small set of symbols or a single file.
Generally, you should avoid reading entire files unless it is absolutely necessary, instead relying on
intelligent step-by-step acquisition of information. However, if you already read a file, it does not make
intelligent step-by-step acquisition of information. {% if 'ToolMarkerSymbolicRead' in available_markers %}However, if you already read a file, it does not make
sense to further analyse it with the symbolic tools (except for the `find_referencing_symbols` tool),
as you already have the information.
as you already have the information.{% endif %}
I WILL BE SERIOUSLY UPSET IF YOU READ ENTIRE FILES WITHOUT NEED! CONSIDER INSTEAD USING THE OVERVIEW TOOL AND
SYMBOLIC TOOLS TO READ ONLY THE NECESSARY CODE FIRST!
I WILL BE SERIOUSLY UPSET IF YOU READ ENTIRE FILES WITHOUT NEED!
{% if 'ToolMarkerSymbolicRead' in available_markers %}
CONSIDER INSTEAD USING THE OVERVIEW TOOL AND SYMBOLIC TOOLS TO READ ONLY THE NECESSARY CODE FIRST!
I WILL BE EVEN MORE UPSET IF AFTER HAVING READ AN ENTIRE FILE YOU KEEP READING THE SAME CONTENT WITH THE SYMBOLIC TOOLS!
THE PURPOSE OF THE SYMBOLIC TOOLS IS TO HAVE TO READ LESS CODE, NOT READ THE SAME CONTENT MULTIPLE TIMES!
{% endif %}
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, find_file and search_for_pattern if you need to.
or complete the task.
You can use the standard tools like list_dir, find_file and search_for_pattern if you need to.
When tools allow it, you pass the `relative_path` parameter to restrict the search to a specific file or directory.
For some tools, `relative_path` can only be a file path, so make sure to properly read the tool descriptions.
If you are unsure about a symbol's name or location (to the extent that substring_matching for the symbol name is not enough), you can use the `search_for_pattern` tool, which allows fast
and flexible search for patterns in the codebase. This way you can first find candidates for symbols or files,
and then proceed with the symbolic tools.
{% if 'search_for_pattern' in available_tools %}
If you are unsure about a symbol's name or location{% if 'find_symbol' in available_tools %} (to the extent that substring_matching for the symbol name is not enough){% endif %}, you can use the `search_for_pattern` tool, which allows fast
and flexible search for patterns in the codebase.{% if 'ToolMarkerSymbolicRead' in available_markers %}This way you can first find candidates for symbols or files,
and then proceed with the symbolic tools.{% endif %}
{% endif %}
Symbols are identified by their `name_path and `relative_path`, see the description of the `find_symbols` tool for more details
{% if 'ToolMarkerSymbolicRead' in available_markers %}
Symbols are identified by their `name_path and `relative_path`, see the description of the `find_symbol` tool for more details
on how the `name_path` matches symbols.
You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file
or directory, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible
@@ -40,10 +46,13 @@ prompts:
you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
to read the desired methods with `include_body=True`
You can understand relationships between symbols by using the `find_referencing_symbols` tool.
{% endif %}
{% if 'read_memory' in available_tools %}
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.
{% endif %}
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.
@@ -1,5 +1,6 @@
class LogMessage {
constructor(message, toolNames) {
message = this.escapeHtml(message);
const logLevel = this.determineLogLevel(message);
const highlightedMessage = this.highlightToolNames(message, toolNames);
this.$elem = $('<div>').addClass('log-' + logLevel).html(highlightedMessage + '\n');
@@ -27,6 +28,21 @@ class LogMessage {
});
return highlightedMessage;
}
escapeHtml (convertString) {
if (typeof convertString !== 'string') return convertString;
const patterns = {
'<' : '&lt;',
'>' : '&gt;',
'&' : '&amp;',
'"' : '&quot;',
'\'' : '&#x27;',
'`' : '&#x60;'
};
return convertString.replace(/[<>&"'`]/g, match => patterns[match]);
};
}
class Dashboard {
+2 -9
View File
@@ -19,15 +19,8 @@ class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit):
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
) -> str:
"""
Execute a shell command and return its output.
IMPORTANT: you should always consider the memory about suggested shell commands before using this tool.
If this memory was not loaded in the current conversation, you should load it using the `read_memory` tool
before using this tool.
You should have at least once looked at the suggested shell commands from the corresponding memory
created during the onboarding process before using this tool.
Never execute unsafe shell commands like `rm -rf /` or similar! Generally be very careful with deletions.
Execute a shell command and return its output. If there is a memory about suggested commands, read that first.
Never execute unsafe shell commands like `rm -rf /` or similar!
:param command: the shell command to execute
:param cwd: the working directory to execute the command in. If None, the project root will be used.
+4 -4
View File
@@ -1,7 +1,7 @@
import json
from serena.config.context_mode import SerenaAgentMode
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
@@ -35,7 +35,7 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
return result_str
class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional):
"""
Removes a project from the Serena configuration.
"""
@@ -50,7 +50,7 @@ class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
return f"Successfully removed project '{project_name}' from configuration."
class SwitchModesTool(Tool):
class SwitchModesTool(Tool, ToolMarkerOptional):
"""
Activates modes by providing a list of their names
"""
@@ -71,7 +71,7 @@ class SwitchModesTool(Tool):
return result_str
class GetCurrentConfigTool(Tool):
class GetCurrentConfigTool(Tool, ToolMarkerOptional):
"""
Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
"""
+6 -14
View File
@@ -14,7 +14,7 @@ from fnmatch import fnmatch
from pathlib import Path
from serena.text_utils import search_files
from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit
from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional
from serena.util.file_system import scan_directory
@@ -29,7 +29,6 @@ class ReadFileTool(Tool):
"""
Reads the given file or a chunk of it. Generally, symbolic operations
like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for.
Reading the entire file is only recommended if there is no other way to get the content required for the task.
:param relative_path: the relative path to the file to read
:param start_line: the 0-based index of the first line to be retrieved.
@@ -60,14 +59,7 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit):
def apply(self, relative_path: str, content: str) -> str:
"""
Write a new file (or overwrite an existing file). For existing files, it is strongly recommended
to use symbolic operations like replace_symbol_body or insert_after_symbol/insert_before_symbol, if possible.
You can also use insert_at_line to insert content at a specific line for existing files if the symbolic operations
are not the right choice for what you want to do.
If ever used on an existing file, the content has to be the complete content of that file (so it
may never end with something like "The remaining content of the file is left unchanged.").
For operations that just replace a part of a file, use the replace_lines or the symbolic editing tools instead.
Write a new file or overwrite an existing file.
:param relative_path: the relative path to the file to create
:param content: the (utf-8-encoded) content to write to the file
@@ -200,7 +192,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit):
return SUCCESS_RESULT
class DeleteLinesTool(Tool, ToolMarkerCanEdit):
class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
"""
Deletes a range of lines within a file.
"""
@@ -228,7 +220,7 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit):
return SUCCESS_RESULT
class ReplaceLinesTool(Tool, ToolMarkerCanEdit):
class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
"""
Replaces a range of lines within a file with new content.
"""
@@ -259,7 +251,7 @@ class ReplaceLinesTool(Tool, ToolMarkerCanEdit):
return SUCCESS_RESULT
class InsertAtLineTool(Tool, ToolMarkerCanEdit):
class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
"""
Inserts content at a given line in a file.
"""
@@ -351,7 +343,7 @@ class SearchForPatternTool(Tool):
For example, for finding classes or methods from a name pattern.
Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files,
which is why it is the default.
:return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested).
:return: A mapping of file paths to lists of matched consecutive lines.
"""
abs_path = os.path.join(self.get_project_root(), relative_path)
if not os.path.exists(abs_path):
+4 -4
View File
@@ -1,10 +1,10 @@
import json
from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerOptional
from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerOptional, ToolMarkerSymbolicRead
from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient
class JetBrainsFindSymbolTool(Tool, ToolMarkerOptional):
class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
"""
Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
"""
@@ -70,7 +70,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerOptional):
return self._limit_length(result, max_answer_chars)
class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerOptional):
class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
"""
Finds symbols that reference the given symbol
"""
@@ -100,7 +100,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerOptional):
return self._limit_length(result, max_answer_chars)
class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerOptional):
class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
"""
Retrieves an overview of the top-level symbols within a specified file
"""
+2 -9
View File
@@ -10,15 +10,8 @@ class WriteMemoryTool(Tool):
def apply(self, memory_name: str, content: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
"""
Write some information about this project that can be useful for future tasks to a memory.
Use markdown formatting for the content.
The information should be short and to the point.
The memory name should be meaningful, such that from the name you can infer what the information is about.
It is better to have multiple small memories than to have a single large one because
memories will be read one by one and we only ever want to read relevant memories.
This tool is either called during the onboarding process or when you have identified
something worth remembering about the project from the past conversation.
Write some information about this project that can be useful for future tasks to a memory in md format.
The memory name should be meaningful.
"""
if len(content) > max_answer_chars:
raise ValueError(
+35 -29
View File
@@ -4,11 +4,18 @@ Language server-related tools
import dataclasses
import json
import os
from collections.abc import Sequence
from copy import copy
from typing import Any
from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerCanEdit
from serena.tools import (
SUCCESS_RESULT,
TOOL_DEFAULT_MAX_ANSWER_LENGTH,
Tool,
ToolMarkerSymbolicEdit,
ToolMarkerSymbolicRead,
)
from solidlsp.ls_types import SymbolKind
@@ -33,44 +40,45 @@ class RestartLanguageServerTool(Tool):
def apply(self) -> str:
"""Use this tool only on explicit user request or after confirmation.
It may be necessary to restart the language server if the user performs edits
not through Serena, so the language server state becomes outdated and further editing attempts lead to errors.
If such editing errors happen, you should suggest using this tool.
It may be necessary to restart the language server if it hangs.
"""
self.agent.reset_language_server()
return SUCCESS_RESULT
class GetSymbolsOverviewTool(Tool):
class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
"""
Gets an overview of the top-level symbols defined in a given file or directory.
"""
def apply(self, relative_path: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
"""
Gets an overview of the given file or directory.
For each analyzed file, we list the top-level symbols in the file (name_path, kind).
Use this tool to get a high-level understanding of the code symbols.
Calling this is often a good idea before more targeted reading, searching or editing operations on the code symbols.
Before requesting a symbol overview, it is usually a good idea to narrow down the scope of the overview
by first understanding the basic directory structure of the repository that you can get from memories
or by using the `list_dir` and `find_file` tools (or similar).
Use this tool to get a high-level understanding of the code symbols in a file.
This should be the first tool to call when you want to understand a new file, unless you already know
what you are looking for.
:param relative_path: the relative path to the file or directory to get the overview of
:param max_answer_chars: if the overview is longer than this number of characters,
no content will be returned. Don't adjust unless there is really no other way to get the content
required for the task. If the overview is too long, you should use a smaller directory instead,
(e.g. a subdirectory).
:return: a JSON object mapping relative paths of all contained files to info about top-level symbols in the file (name_path, kind).
:return: a JSON object containing info about top-level symbols in the file
"""
symbol_retriever = self.create_language_server_symbol_retriever()
result = symbol_retriever.get_symbol_overview(relative_path)
result_json_str = json.dumps({k: [dataclasses.asdict(i) for i in l] for k, l in result.items()})
file_path = os.path.join(self.project.project_root, relative_path)
# The symbol overview is capable of working with both files and directories,
# but we want to ensure that the user provides a file path.
if not os.path.exists(file_path):
raise FileNotFoundError(f"File or directory {relative_path} does not exist in the project.")
if os.path.isdir(file_path):
raise ValueError(f"Expected a file path, but got a directory path: {relative_path}. ")
result = symbol_retriever.get_symbol_overview(relative_path)[relative_path]
result_json_str = json.dumps([dataclasses.asdict(i) for i in result])
return self._limit_length(result_json_str, max_answer_chars)
class FindSymbolTool(Tool):
class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
"""
Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
"""
@@ -132,7 +140,7 @@ class FindSymbolTool(Tool):
:param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`.
:param substring_matching: If True, use substring matching for the last segment of `name`.
:param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned.
:return: JSON string: a list of symbols (with locations) matching the name.
:return: a list of symbols (with locations) matching the name.
"""
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
@@ -150,7 +158,7 @@ class FindSymbolTool(Tool):
return self._limit_length(result, max_answer_chars)
class FindReferencingSymbolsTool(Tool):
class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
"""
Finds symbols that reference the symbol at the given location (optionally filtered by type).
"""
@@ -164,10 +172,8 @@ class FindReferencingSymbolsTool(Tool):
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
) -> str:
"""
Finds symbols that reference the symbol at the given `name_path`. The result will contain metadata about the referencing symbols
as well as a short code snippet around the reference (unless `include_body` is True, then the short snippet will be omitted).
Note that among other kinds of references, this function can be used to find (direct) subclasses of a class,
as subclasses are referencing symbols that have the kind class.
Finds references to the symbol at the given `name_path`. The result will contain metadata about the referencing symbols
as well as a short code snippet around the reference.
:param name_path: for finding the symbol to find references for, same logic as in the `find_symbol` tool.
:param relative_path: the relative path to the file containing the symbol for which to find references.
@@ -204,7 +210,7 @@ class FindReferencingSymbolsTool(Tool):
return self._limit_length(result, max_answer_chars)
class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit):
class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
"""
Replaces the full definition of a symbol.
"""
@@ -232,7 +238,7 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit):
return SUCCESS_RESULT
class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit):
class InsertAfterSymbolTool(Tool, ToolMarkerSymbolicEdit):
"""
Inserts content after the end of the definition of a given symbol.
"""
@@ -257,7 +263,7 @@ class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit):
return SUCCESS_RESULT
class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit):
class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit):
"""
Inserts content before the beginning of the definition of a given symbol.
"""
@@ -269,9 +275,9 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit):
body: str,
) -> str:
"""
Inserts the given body/content before the beginning of the definition of the given symbol (via the symbol's location).
A typical use case is to insert a new class, function, method, field or variable assignment.
It also can be used to insert a new import statement before the first symbol in the file.
Inserts the given content before the beginning of the definition of the given symbol (via the symbol's location).
A typical use case is to insert a new class, function, method, field or variable assignment; or
a new import statement before the first symbol in the file.
:param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply)
:param relative_path: the relative path to the file containing the symbol
+35 -11
View File
@@ -1,6 +1,5 @@
import inspect
import os
import traceback
from abc import ABC
from collections.abc import Iterable
from dataclasses import dataclass
@@ -16,6 +15,7 @@ from serena.prompt_factory import PromptFactory
from serena.symbol import LanguageServerSymbolRetriever
from serena.util.class_decorators import singleton
from serena.util.inspection import iter_subclasses
from solidlsp.ls_exceptions import SolidLSPException
if TYPE_CHECKING:
from serena.agent import LinesRead, MemoriesManager, SerenaAgent
@@ -73,22 +73,40 @@ class Component(ABC):
TOOL_DEFAULT_MAX_ANSWER_LENGTH = int(2e5)
class ToolMarkerCanEdit:
class ToolMarker:
"""
Base class for tool markers.
"""
class ToolMarkerCanEdit(ToolMarker):
"""
Marker class for all tools that can perform editing operations on files.
"""
class ToolMarkerDoesNotRequireActiveProject:
class ToolMarkerDoesNotRequireActiveProject(ToolMarker):
pass
class ToolMarkerOptional:
class ToolMarkerOptional(ToolMarker):
"""
Marker class for optional tools that are disabled by default.
"""
class ToolMarkerSymbolicRead(ToolMarker):
"""
Marker class for tools that perform symbol read operations.
"""
class ToolMarkerSymbolicEdit(ToolMarkerCanEdit):
"""
Marker class for tools that perform symbolic edit operations.
"""
class ApplyMethodProtocol(Protocol):
"""Callable protocol for the apply method of a tool."""
@@ -237,18 +255,24 @@ class Tool(Component):
self.agent.reset_language_server()
# apply the actual tool
result = apply_fn(**kwargs)
try:
result = apply_fn(**kwargs)
except SolidLSPException as e:
if e.is_language_server_terminated():
log.error(f"Language server terminated while executing tool ({e}). Restarting the language server and retrying ...")
self.agent.reset_language_server()
result = apply_fn(**kwargs)
else:
raise
# record tool usage
self.agent.record_tool_usage_if_enabled(kwargs, result, self)
except Exception as e:
if not catch_exceptions:
raise
msg = f"Error executing tool: {e}\n{traceback.format_exc()}"
log.error(
f"Error executing tool: {e}. "
f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)",
exc_info=e,
)
msg = f"Error executing tool: {e}"
log.error(f"Error executing tool: {e}", exc_info=e)
result = msg
if log_call:
+3 -3
View File
@@ -5,7 +5,7 @@ Tools supporting the general workflow of the agent
import json
import platform
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
class CheckOnboardingPerformedTool(Tool):
@@ -96,7 +96,7 @@ class ThinkAboutWhetherYouAreDoneTool(Tool):
return self.prompt_factory.create_think_about_whether_you_are_done()
class SummarizeChangesTool(Tool):
class SummarizeChangesTool(Tool, ToolMarkerOptional):
"""
Provides instructions for summarizing the changes made to the codebase.
"""
@@ -122,7 +122,7 @@ class PrepareForNewConversationTool(Tool):
return self.prompt_factory.create_prepare_for_new_conversation()
class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject):
class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional):
"""
Gets the initial instructions for the current project.
Should only be used in settings where the system prompt cannot be set,
@@ -5,7 +5,6 @@ Provides C/C++ specific instantiation of the LanguageServer class. Contains vari
import logging
import os
import pathlib
import stat
import threading
from solidlsp.ls import SolidLanguageServer
@@ -13,6 +12,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -24,25 +24,30 @@ class ClangdLanguageServer(SolidLanguageServer):
Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
clangd_executable_path = self._setup_runtime_dependencies(logger, config)
clangd_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path),
"cpp",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.service_ready_event = threading.Event()
self.initialize_searcher_command_available = threading.Event()
self.resolve_main_method_available = threading.Event()
@staticmethod
def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Setup runtime dependencies for ClangdLanguageServer and return the command to start the server.
"""
@@ -75,7 +80,7 @@ class ClangdLanguageServer(SolidLanguageServer):
]
)
clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clangd")
clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd")
dep = deps.single_for_current_platform()
clangd_executable_path = deps.binary_path(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
@@ -89,7 +94,7 @@ class ClangdLanguageServer(SolidLanguageServer):
f"Clangd executable not found at {clangd_executable_path}.\n"
"Make sure you have installed clangd. See https://clangd.llvm.org/installation"
)
os.chmod(clangd_executable_path, stat.S_IEXEC)
os.chmod(clangd_executable_path, 0o755)
return clangd_executable_path
+12 -8
View File
@@ -6,7 +6,6 @@ import logging
import os
import pathlib
import shutil
import stat
import subprocess
import threading
@@ -15,6 +14,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -78,34 +78,38 @@ class ClojureLSP(SolidLanguageServer):
]
)
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config)
clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path),
"clojure",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.initialize_searcher_command_available = threading.Event()
self.resolve_main_method_available = threading.Event()
self.service_ready_event = threading.Event()
@staticmethod
def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""Setup runtime dependencies for clojure-lsp and return the command to start the server."""
verify_clojure_cli()
deps = ClojureLSP.runtime_dependencies
dependency = deps.single_for_current_platform()
clojurelsp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clojure-lsp")
clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings)
clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir)
if not os.path.exists(clojurelsp_executable_path):
os.makedirs(clojurelsp_ls_dir, exist_ok=True)
logger.log(
f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}",
logging.INFO,
@@ -113,7 +117,7 @@ class ClojureLSP(SolidLanguageServer):
deps.install(logger, clojurelsp_ls_dir)
if not os.path.exists(clojurelsp_executable_path):
raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
os.chmod(clojurelsp_executable_path, stat.S_IEXEC)
os.chmod(clojurelsp_executable_path, 0o755)
return clojurelsp_executable_path
@staticmethod
+20 -9
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import logging
import os
import platform
import subprocess
from collections.abc import Sequence
from dataclasses import dataclass
@@ -21,7 +22,7 @@ class RuntimeDependency:
url: str | None = None
archive_type: str | None = None
binary_name: str | None = None
command: str | None = None
command: str | list[str] | None = None
package_name: str | None = None
package_version: str | None = None
extract_path: str | None = None
@@ -71,7 +72,7 @@ class RuntimeDependencyCollection:
return results
@staticmethod
def _run_command(command: str, cwd: str) -> None:
def _run_command(command: str | list[str], cwd: str) -> None:
kwargs = {}
if PlatformUtils.get_platform_id().is_windows():
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore
@@ -79,8 +80,16 @@ class RuntimeDependencyCollection:
import pwd
kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name
log.info("Running command '%s' in '%s'", command, cwd)
completed_proces = subprocess.run(
is_windows = platform.system() == "Windows"
if not isinstance(command, str) and not is_windows:
# Since we are using the shell, we need to convert the command list to a single string
# on Linux/macOS
command = " ".join(command)
log.info("Running command %s in '%s'", f"'{command}'" if isinstance(command, str) else command, cwd)
completed_process = subprocess.run(
command,
shell=True,
check=True,
@@ -89,11 +98,13 @@ class RuntimeDependencyCollection:
stderr=subprocess.STDOUT,
**kwargs,
)
log.log(
logging.WARNING if completed_proces.returncode else logging.INFO,
"Command completed with return code %d",
completed_proces.returncode,
)
if completed_process.returncode != 0:
log.warning("Command '%s' failed with return code %d", command, completed_process.returncode)
log.warning("Command output:\n%s", completed_process.stdout)
else:
log.info(
"Command completed successfully",
)
@staticmethod
def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None:
@@ -7,7 +7,6 @@ import logging
import os
import platform
import shutil
import stat
import subprocess
import tarfile
import threading
@@ -20,11 +19,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import LanguageServerException
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency
@@ -175,18 +175,20 @@ class CSharpLanguageServer(SolidLanguageServer):
This is the official Roslyn-based language server from Microsoft.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
dotnet_path, language_server_path = self._ensure_server_installed(logger, config)
dotnet_path, language_server_path = self._ensure_server_installed(logger, config, solidlsp_settings)
# Find solution or project file
solution_or_project = find_solution_or_project_file(repository_root_path)
# Create log directory
log_dir = Path(self.ls_resources_dir()) / "logs"
log_dir = Path(self.ls_resources_dir(solidlsp_settings)) / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
# Build command using dotnet directly
@@ -206,6 +208,7 @@ class CSharpLanguageServer(SolidLanguageServer):
repository_root_path,
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
"csharp",
solidlsp_settings,
)
self.initialization_complete = threading.Event()
@@ -215,15 +218,17 @@ class CSharpLanguageServer(SolidLanguageServer):
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
@classmethod
def _ensure_server_installed(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]:
def _ensure_server_installed(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> tuple[str, str]:
"""
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
Returns a tuple of (dotnet_path, language_server_dll_path).
"""
runtime_id = CSharpLanguageServer._get_runtime_id()
lang_server_dep, dotnet_runtime_dep = CSharpLanguageServer._get_runtime_dependencies(runtime_id)
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep)
server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep)
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep, solidlsp_settings)
server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep, solidlsp_settings)
return dotnet_path, server_dll_path
@@ -240,7 +245,7 @@ class CSharpLanguageServer(SolidLanguageServer):
elif system == "linux":
return "linux-x64" if machine in ["x86_64", "amd64"] else "linux-arm64"
else:
raise LanguageServerException(f"Unsupported platform: {system} {machine}")
raise SolidLSPException(f"Unsupported platform: {system} {machine}")
@staticmethod
def _get_runtime_dependencies(runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]:
@@ -255,14 +260,16 @@ class CSharpLanguageServer(SolidLanguageServer):
dotnet_runtime_dep = dep
if not lang_server_dep:
raise LanguageServerException(f"No C# language server dependency found for platform {runtime_id}")
raise SolidLSPException(f"No C# language server dependency found for platform {runtime_id}")
if not dotnet_runtime_dep:
raise LanguageServerException(f"No .NET runtime dependency found for platform {runtime_id}")
raise SolidLSPException(f"No .NET runtime dependency found for platform {runtime_id}")
return lang_server_dep, dotnet_runtime_dep
@staticmethod
def _ensure_dotnet_runtime(logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str:
@classmethod
def _ensure_dotnet_runtime(
cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
"""Ensure .NET runtime is available and return the dotnet executable path."""
# Check if dotnet is already available on the system
system_dotnet = shutil.which("dotnet")
@@ -277,15 +284,17 @@ class CSharpLanguageServer(SolidLanguageServer):
pass
# Download .NET 9 runtime using config
return CSharpLanguageServer._ensure_dotnet_runtime_from_config(logger, runtime_dep)
return cls._ensure_dotnet_runtime_from_config(logger, runtime_dep, solidlsp_settings)
@classmethod
def _ensure_language_server(cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency) -> str:
def _ensure_language_server(
cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
"""Ensure language server is available and return the DLL path."""
package_name = lang_server_dep.package_name
package_version = lang_server_dep.package_version
server_dir = Path(cls.ls_resources_dir()) / f"{package_name}.{package_version}"
server_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / f"{package_name}.{package_version}"
server_dll = server_dir / lang_server_dep.binary_name
if server_dll.exists():
@@ -294,17 +303,17 @@ class CSharpLanguageServer(SolidLanguageServer):
# Download and install the language server
logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO)
package_path = CSharpLanguageServer._download_nuget_package_direct(logger, package_name, package_version)
package_path = cls._download_nuget_package_direct(logger, package_name, package_version, solidlsp_settings)
# Extract and install
CSharpLanguageServer._extract_language_server(lang_server_dep, package_path, server_dir)
cls._extract_language_server(lang_server_dep, package_path, server_dir)
if not server_dll.exists():
raise LanguageServerException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")
raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")
# Make executable on Unix systems
if platform.system().lower() != "windows":
server_dll.chmod(server_dll.stat().st_mode | stat.S_IEXEC)
server_dll.chmod(0o755)
logger.log(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}", logging.INFO)
return str(server_dll)
@@ -326,14 +335,16 @@ class CSharpLanguageServer(SolidLanguageServer):
source_dir = possible_dir
break
else:
raise LanguageServerException(f"Could not find language server files in package. Searched in {package_path}")
raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}")
# Copy files to cache directory
server_dir.mkdir(parents=True, exist_ok=True)
shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)
@classmethod
def _download_nuget_package_direct(cls, logger: LanguageServerLogger, package_name: str, package_version: str) -> Path:
def _download_nuget_package_direct(
cls, logger: LanguageServerLogger, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings
) -> Path:
"""
Download a NuGet package directly from the Azure NuGet feed.
Returns the path to the extracted package directory.
@@ -341,7 +352,7 @@ class CSharpLanguageServer(SolidLanguageServer):
azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json"
# Create temporary directory for package download
temp_dir = Path(cls.ls_resources_dir()) / "temp_downloads"
temp_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "temp_downloads"
temp_dir.mkdir(parents=True, exist_ok=True)
try:
@@ -358,7 +369,7 @@ class CSharpLanguageServer(SolidLanguageServer):
break
if not package_base_address:
raise LanguageServerException("Could not find package base address in Azure NuGet feed")
raise SolidLSPException("Could not find package base address in Azure NuGet feed")
# Construct the download URL for the specific package
package_id_lower = package_name.lower()
@@ -385,12 +396,14 @@ class CSharpLanguageServer(SolidLanguageServer):
return package_extract_dir
except Exception as e:
raise LanguageServerException(
raise SolidLSPException(
f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
) from e
@classmethod
def _ensure_dotnet_runtime_from_config(cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str:
def _ensure_dotnet_runtime_from_config(
cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Ensure .NET 9 runtime is available using runtime dependency configuration.
Returns the path to the dotnet executable.
@@ -408,7 +421,7 @@ class CSharpLanguageServer(SolidLanguageServer):
pass
# Download .NET 9 runtime using config
dotnet_dir = Path(cls.ls_resources_dir()) / "dotnet-runtime-9.0"
dotnet_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "dotnet-runtime-9.0"
dotnet_exe = dotnet_dir / runtime_dep.binary_name
if dotnet_exe.exists():
@@ -442,13 +455,13 @@ class CSharpLanguageServer(SolidLanguageServer):
# Make dotnet executable on Unix
if platform.system().lower() != "windows":
dotnet_exe.chmod(dotnet_exe.stat().st_mode | stat.S_IEXEC)
dotnet_exe.chmod(0o755)
logger.log(f"Successfully installed .NET 9 runtime to {dotnet_exe}", logging.INFO)
return str(dotnet_exe)
except Exception as e:
raise LanguageServerException(f"Failed to download .NET 9 runtime from {url}: {e}") from e
raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e
def _get_initialize_params(self) -> InitializeParams:
"""
@@ -623,7 +636,7 @@ class CSharpLanguageServer(SolidLanguageServer):
self.server.start()
except Exception as e:
self.logger.log(f"Failed to start language server process: {e}", logging.ERROR)
raise LanguageServerException(f"Failed to start C# language server: {e}")
raise SolidLSPException(f"Failed to start C# language server: {e}")
# Send initialization
initialize_params = self._get_initialize_params()
@@ -633,7 +646,7 @@ class CSharpLanguageServer(SolidLanguageServer):
init_response = self.server.send.initialize(initialize_params)
self.logger.log(f"Received initialize response: {init_response}", logging.DEBUG)
except Exception as e:
raise LanguageServerException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e
raise SolidLSPException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e
# Apply diagnostic capabilities
self._force_pull_diagnostics(init_response)
@@ -1,11 +1,11 @@
import logging
import os
import pathlib
import stat
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -15,21 +15,22 @@ class DartLanguageServer(SolidLanguageServer):
Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart.
"""
def __init__(self, config, logger, repository_root_path):
def __init__(self, config, logger, repository_root_path, solidlsp_settings: SolidLSPSettings):
"""
Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
executable_path = self._setup_runtime_dependencies(logger)
executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path),
"dart",
solidlsp_settings,
)
@classmethod
def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger") -> str:
def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger", solidlsp_settings: SolidLSPSettings) -> str:
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
@@ -75,14 +76,14 @@ class DartLanguageServer(SolidLanguageServer):
]
)
dart_ls_dir = cls.ls_resources_dir()
dart_ls_dir = cls.ls_resources_dir(solidlsp_settings)
dart_executable_path = deps.binary_path(dart_ls_dir)
if not os.path.exists(dart_executable_path):
deps.install(logger, dart_ls_dir)
assert os.path.exists(dart_executable_path)
os.chmod(dart_executable_path, stat.S_IEXEC)
os.chmod(dart_executable_path, 0o755)
return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2"
+18 -13
View File
@@ -7,7 +7,6 @@ import logging
import os
import pathlib
import shutil
import stat
import threading
import uuid
from pathlib import PurePath
@@ -44,18 +43,20 @@ class EclipseJDTLS(SolidLanguageServer):
The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
runtime_dependency_paths = self._setupRuntimeDependencies(logger, config)
runtime_dependency_paths = self._setupRuntimeDependencies(logger, config, solidlsp_settings)
self.runtime_dependency_paths = runtime_dependency_paths
# ws_dir is the workspace directory for the EclipseJDTLS server
ws_dir = str(
PurePath(
SolidLSPSettings.get_language_server_directory(),
solidlsp_settings.ls_resources_dir,
"EclipseJDTLS",
"workspaces",
uuid.uuid4().hex,
@@ -63,15 +64,15 @@ class EclipseJDTLS(SolidLanguageServer):
)
# shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces
shared_cache_location = str(PurePath(SolidLSPSettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex"))
shared_cache_location = str(PurePath(solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex"))
os.makedirs(shared_cache_location, exist_ok=True)
os.makedirs(ws_dir, exist_ok=True)
jre_path = self.runtime_dependency_paths.jre_path
lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
os.makedirs(ws_dir, exist_ok=True)
data_dir = str(PurePath(ws_dir, "data_dir"))
jdtls_config_path = str(PurePath(ws_dir, "config_path"))
@@ -132,7 +133,9 @@ class EclipseJDTLS(SolidLanguageServer):
self.intellicode_enable_command_available = threading.Event()
self.initialize_searcher_command_available = threading.Event()
super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java")
super().__init__(
config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings
)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
@@ -153,7 +156,9 @@ class EclipseJDTLS(SolidLanguageServer):
]
@classmethod
def _setupRuntimeDependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths:
def _setupRuntimeDependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> RuntimeDependencyPaths:
"""
Setup runtime dependencies for EclipseJDTLS and return the paths.
"""
@@ -238,7 +243,7 @@ class EclipseJDTLS(SolidLanguageServer):
gradle_path = str(
PurePath(
cls.ls_resources_dir(),
cls.ls_resources_dir(solidlsp_settings),
"gradle-7.3.3",
)
)
@@ -254,7 +259,7 @@ class EclipseJDTLS(SolidLanguageServer):
assert os.path.exists(gradle_path)
dependency = runtime_dependencies["vscode-java"][platformId.value]
vscode_java_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"]))
vscode_java_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"]))
os.makedirs(vscode_java_path, exist_ok=True)
jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"]))
jre_path = str(PurePath(vscode_java_path, dependency["jre_path"]))
@@ -273,7 +278,7 @@ class EclipseJDTLS(SolidLanguageServer):
):
FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"])
os.chmod(jre_path, stat.S_IEXEC)
os.chmod(jre_path, 0o755)
assert os.path.exists(vscode_java_path)
assert os.path.exists(jre_home_path)
@@ -283,7 +288,7 @@ class EclipseJDTLS(SolidLanguageServer):
assert os.path.exists(jdtls_readonly_config_path)
dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
intellicode_directory_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"]))
intellicode_directory_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"]))
os.makedirs(intellicode_directory_path, exist_ok=True)
intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"]))
intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"]))
@@ -1,4 +1,3 @@
import json
import logging
import os
import pathlib
@@ -6,16 +5,18 @@ import stat
import subprocess
import threading
import time
from pathlib import PurePath
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from ..common import RuntimeDependency
class ElixirTools(SolidLanguageServer):
@@ -68,8 +69,8 @@ class ElixirTools(SolidLanguageServer):
return filtered_response
@staticmethod
def _get_elixir_version():
@classmethod
def _get_elixir_version(cls):
"""Get the installed Elixir version or None if not found."""
try:
result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False)
@@ -79,13 +80,16 @@ class ElixirTools(SolidLanguageServer):
return None
return None
def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Setup runtime dependencies for Next LS.
Downloads the Next LS binary for the current platform and returns the path to the executable.
"""
# Check if Elixir is available first
elixir_version = self._get_elixir_version()
elixir_version = cls._get_elixir_version()
if not elixir_version:
raise RuntimeError(
"Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH."
@@ -93,70 +97,78 @@ class ElixirTools(SolidLanguageServer):
logger.log(f"Found Elixir: {elixir_version}", logging.INFO)
platformId = PlatformUtils.get_platform_id()
with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f:
runtimeDependencies = json.load(f)
del runtimeDependencies["_description"]
os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True)
# Map platform IDs to runtime dependency keys
platform_mapping = {
"linux-x64": "linux-x64",
"osx-x64": "darwin-x64",
"osx-arm64": "darwin-arm64",
"darwin-x64": "darwin-x64",
"darwin-arm64": "darwin-arm64",
"win-x64": "win-x64",
}
platform_key = platform_mapping.get(platformId.value)
if not platform_key:
raise RuntimeError(f"Unsupported platform for Next LS: {platformId.value}")
platform_id = PlatformUtils.get_platform_id()
# Check for Windows and provide a helpful error message
if platformId.value.startswith("win"):
if platform_id.value.startswith("win"):
raise RuntimeError(
"Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. "
"Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS."
)
dependency = runtimeDependencies["next_ls"][platform_key]
next_ls_dir = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]))
os.makedirs(next_ls_dir, exist_ok=True)
valid_platforms = [
PlatformId.LINUX_x64,
PlatformId.OSX_x64,
PlatformId.OSX_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Next LS at the moment"
executable_path = str(PurePath(next_ls_dir, dependency["executable_name"]))
binary_path = str(PurePath(next_ls_dir, dependency["binary_name"]))
next_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "next-ls")
# Define runtime dependencies inline
runtime_deps = {
PlatformId.LINUX_x64: RuntimeDependency(
id="next_ls_linux_amd64",
platform_id="linux-x64",
url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64",
archive_type="binary",
binary_name="next_ls_linux_amd64",
extract_path="next_ls",
),
PlatformId.OSX_x64: RuntimeDependency(
id="next_ls_darwin_amd64",
platform_id="osx-x64",
url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64",
archive_type="binary",
binary_name="next_ls_darwin_amd64",
extract_path="next_ls",
),
PlatformId.OSX_arm64: RuntimeDependency(
id="next_ls_darwin_arm64",
platform_id="osx-arm64",
url="https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64",
archive_type="binary",
binary_name="next_ls_darwin_arm64",
extract_path="next_ls",
),
}
dependency = runtime_deps[platform_id]
executable_path = os.path.join(next_ls_dir, "nextls")
binary_path = os.path.join(next_ls_dir, dependency.binary_name)
if not os.path.exists(executable_path):
logger.log(f"Downloading Next LS binary from {dependency['url']}", logging.INFO)
FileUtils.download_file(logger, dependency["url"], binary_path)
logger.log(f"Downloading Next LS binary from {dependency.url}", logging.INFO)
FileUtils.download_file(logger, dependency.url, binary_path)
# Make the binary executable on Unix-like systems
if not platformId.value.startswith("win"):
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
# Create a symlink or copy with the expected name
# Create a symlink with the expected name
if binary_path != executable_path:
if os.path.exists(executable_path):
os.remove(executable_path)
if platformId.value.startswith("win"):
# On Windows, copy the file
import shutil
shutil.copy2(binary_path, executable_path)
else:
# On Unix-like systems, create a symlink
os.symlink(os.path.basename(binary_path), executable_path)
os.symlink(os.path.basename(binary_path), executable_path)
assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}"
logger.log(f"Next LS binary ready at: {executable_path}", logging.INFO)
return executable_path
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
nextls_executable_path = self.setupRuntimeDependencies(logger, config)
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
nextls_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
@@ -164,6 +176,7 @@ class ElixirTools(SolidLanguageServer):
repository_root_path,
ProcessLaunchInfo(cmd=f'"{nextls_executable_path}" --stdio', cwd=repository_root_path),
"elixir",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.request_id = 0
@@ -171,29 +184,66 @@ class ElixirTools(SolidLanguageServer):
# Set generous timeout for Next LS which can be slow to initialize and respond
self.set_request_timeout(180.0) # 60 seconds for all environments
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Next LS Language Server.
"""
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f:
d = json.load(f)
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"initializationOptions": {
"mix_env": "dev",
"mix_target": "host",
"experimental": {"completions": {"enable": False}},
"extensions": {"credo": {"enable": True, "cli_options": []}},
},
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"completion": {
"dynamicRegistration": True,
"completionItem": {"snippetSupport": True, "documentationFormat": ["markdown", "plaintext"]},
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"formatting": {"dynamicRegistration": True},
"codeAction": {
"dynamicRegistration": True,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports",
]
}
},
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"executeCommand": {"dynamicRegistration": True},
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
del d["_description"]
d["processId"] = os.getpid()
assert d["rootPath"] == "$rootPath"
d["rootPath"] = repository_absolute_path
assert d["rootUri"] == "$rootUri"
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["uri"] == "$uri"
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["name"] == "$name"
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
return d
return initialize_params
def _start_server(self):
"""Start Next LS server process"""
@@ -1,97 +0,0 @@
{
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
"processId": "os.getpid()",
"locale": "en",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"initializationOptions": {
"mix_env": "dev",
"mix_target": "host",
"experimental": {
"completions": {
"enable": false
}
},
"extensions": {
"credo": {
"enable": true,
"cli_options": []
}
}
},
"capabilities": {
"textDocument": {
"synchronization": {
"didSave": true,
"dynamicRegistration": true
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true,
"documentationFormat": [
"markdown",
"plaintext"
]
}
},
"definition": {
"dynamicRegistration": true
},
"references": {
"dynamicRegistration": true
},
"documentSymbol": {
"dynamicRegistration": true,
"hierarchicalDocumentSymbolSupport": true,
"symbolKind": {
"valueSet": [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
]
}
},
"hover": {
"dynamicRegistration": true,
"contentFormat": [
"markdown",
"plaintext"
]
},
"formatting": {
"dynamicRegistration": true
},
"codeAction": {
"dynamicRegistration": true,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports"
]
}
}
}
},
"workspace": {
"workspaceFolders": true,
"didChangeConfiguration": {
"dynamicRegistration": true
},
"executeCommand": {
"dynamicRegistration": true
}
}
},
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
@@ -1,33 +0,0 @@
{
"_description": "This file lists the runtime dependencies for the Elixir Language Server (Next LS)",
"next_ls": {
"linux-x64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_linux_amd64",
"executable_name": "nextls"
},
"darwin-x64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_darwin_amd64",
"executable_name": "nextls"
},
"darwin-arm64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_darwin_arm64",
"executable_name": "nextls"
},
"win-x64": {
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_windows_amd64.exe",
"archiveType": "binary",
"relative_extraction_path": "next_ls",
"binary_name": "next_ls_windows_amd64.exe",
"executable_name": "nextls.exe"
}
}
}
+5 -1
View File
@@ -11,6 +11,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
class Gopls(SolidLanguageServer):
@@ -70,7 +71,9 @@ class Gopls(SolidLanguageServer):
return True
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
self._setup_runtime_dependency()
super().__init__(
@@ -79,6 +82,7 @@ class Gopls(SolidLanguageServer):
repository_root_path,
ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path),
"go",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.request_id = 0
+17 -5
View File
@@ -16,6 +16,7 @@ from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -34,7 +35,9 @@ class Intelephense(SolidLanguageServer):
return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"]
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Setup runtime dependencies for Intelephense and return the command to start the server.
"""
@@ -58,7 +61,7 @@ class Intelephense(SolidLanguageServer):
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
# Install intelephense if not already installed
intelephense_ls_dir = os.path.join(cls.ls_resources_dir(), "php-lsp")
intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp")
os.makedirs(intelephense_ls_dir, exist_ok=True)
intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
if not os.path.exists(intelephense_executable_path):
@@ -79,11 +82,20 @@ class Intelephense(SolidLanguageServer):
return f"{intelephense_executable_path} --stdio"
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
# Setup runtime dependencies before initializing
intelephense_cmd = self._setup_runtime_dependencies(logger, config)
intelephense_cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php")
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path),
"php",
solidlsp_settings,
)
self.request_id = 0
@staticmethod
+5 -1
View File
@@ -13,6 +13,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
class JediServer(SolidLanguageServer):
@@ -20,7 +21,9 @@ class JediServer(SolidLanguageServer):
Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
@@ -30,6 +33,7 @@ class JediServer(SolidLanguageServer):
repository_root_path,
ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path),
"python",
solidlsp_settings,
)
@override
@@ -14,6 +14,7 @@ from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@dataclasses.dataclass
@@ -32,11 +33,13 @@ class KotlinLanguageServer(SolidLanguageServer):
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
runtime_dependency_paths = self._setup_runtime_dependencies(logger, config)
runtime_dependency_paths = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
self.runtime_dependency_paths = runtime_dependency_paths
# Create command to execute the Kotlin Language Server script
@@ -51,10 +54,13 @@ class KotlinLanguageServer(SolidLanguageServer):
repository_root_path,
ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path),
"kotlin",
solidlsp_settings,
)
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths:
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> KotlinRuntimeDependencyPaths:
"""
Setup runtime dependencies for Kotlin Language Server and return the paths.
"""
@@ -111,7 +117,7 @@ class KotlinLanguageServer(SolidLanguageServer):
java_dependency = runtime_dependencies["java"][platform_id.value]
# Setup paths for dependencies
static_dir = os.path.join(cls.ls_resources_dir(), "kotlin_language_server")
static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "kotlin_language_server")
os.makedirs(static_dir, exist_ok=True)
# Setup Java paths
+16 -10
View File
@@ -6,7 +6,6 @@ import json
import logging
import os
import pathlib
import stat
import threading
from collections.abc import Iterable
@@ -14,11 +13,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import LanguageServerException
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
def breadth_first_file_scan(root) -> Iterable[str]:
@@ -59,16 +59,18 @@ class OmniSharp(SolidLanguageServer):
Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config)
omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
slnfilename = find_least_depth_sln_file(repository_root_path)
if slnfilename is None:
logger.log("No *.sln file found in repository", logging.ERROR)
raise LanguageServerException("No SLN file found in repository")
raise SolidLSPException("No SLN file found in repository")
cmd = " ".join(
[
@@ -102,7 +104,9 @@ class OmniSharp(SolidLanguageServer):
"formattingOptions:indentationSize=4",
]
)
super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp")
super().__init__(
config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings
)
self.server_ready = threading.Event()
self.definition_available = threading.Event()
@@ -138,7 +142,9 @@ class OmniSharp(SolidLanguageServer):
return d
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]:
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> tuple[str, str]:
"""
Setup runtime dependencies for OmniSharp.
"""
@@ -181,15 +187,15 @@ class OmniSharp(SolidLanguageServer):
assert "OmniSharp" in runtime_dependencies
assert "RazorOmnisharp" in runtime_dependencies
omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "OmniSharp")
omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp")
if not os.path.exists(omnisharp_ls_dir):
os.makedirs(omnisharp_ls_dir)
FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip")
omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"])
assert os.path.exists(omnisharp_executable_path)
os.chmod(omnisharp_executable_path, stat.S_IEXEC)
os.chmod(omnisharp_executable_path, 0o755)
razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "RazorOmnisharp")
razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp")
if not os.path.exists(razor_omnisharp_ls_dir):
os.makedirs(razor_omnisharp_ls_dir)
FileUtils.download_and_extract_archive(logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip")
@@ -15,6 +15,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
class PyrightServer(SolidLanguageServer):
@@ -23,7 +24,9 @@ class PyrightServer(SolidLanguageServer):
Contains various configurations and settings specific to Python.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a PyrightServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
@@ -36,6 +39,7 @@ class PyrightServer(SolidLanguageServer):
# Note 2: we can also use `bpyright-langserver --stdio` if we ever are unhappy with pyright
ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path),
"python",
solidlsp_settings,
)
# Event to signal when initial workspace analysis is complete
+11 -6
View File
@@ -5,7 +5,6 @@ Provides Rust specific instantiation of the LanguageServer class. Contains vario
import logging
import os
import pathlib
import stat
import threading
from overrides import override
@@ -15,6 +14,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -24,17 +24,20 @@ class RustAnalyzer(SolidLanguageServer):
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config)
rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path),
"rust",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.service_ready_event = threading.Event()
@@ -46,7 +49,9 @@ class RustAnalyzer(SolidLanguageServer):
return super().is_ignored_dirname(dirname) or dirname in ["target"]
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Setup runtime dependencies for rust_analyzer and return the command to start the server.
"""
@@ -100,13 +105,13 @@ class RustAnalyzer(SolidLanguageServer):
# "win-x64",
# ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment"
rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(), "RustAnalyzer")
rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RustAnalyzer")
rustanalyzer_executable_path = deps.binary_path(rustanalyzer_ls_dir)
if not os.path.exists(rustanalyzer_ls_dir):
os.makedirs(rustanalyzer_ls_dir)
deps.install(logger, rustanalyzer_ls_dir)
assert os.path.exists(rustanalyzer_executable_path)
os.chmod(rustanalyzer_executable_path, stat.S_IEXEC)
os.chmod(rustanalyzer_executable_path, 0o755)
return rustanalyzer_executable_path
+6 -15
View File
@@ -7,7 +7,6 @@ import json
import logging
import os
import pathlib
import stat
import subprocess
import threading
@@ -18,6 +17,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
class Solargraph(SolidLanguageServer):
@@ -26,7 +26,9 @@ class Solargraph(SolidLanguageServer):
Contains various configurations and settings specific to Ruby.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a Solargraph instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
@@ -38,6 +40,7 @@ class Solargraph(SolidLanguageServer):
repository_root_path,
ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path),
"ruby",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.service_ready_event = threading.Event()
@@ -83,19 +86,7 @@ class Solargraph(SolidLanguageServer):
logger.log("Installing Solargraph...", logging.INFO)
subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path)
# Get the gem executable path directly
result = subprocess.run(["gem", "which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path)
gem_path = result.stdout.strip()
bin_dir = os.path.join(os.path.dirname(os.path.dirname(gem_path)), "bin")
executable_path = os.path.join(bin_dir, "solargraph")
if not os.path.exists(executable_path):
raise RuntimeError(f"Solargraph executable not found at {executable_path}")
# Ensure the executable has the right permissions
os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC)
return executable_path
return "gem exec solargraph"
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") from e
+10 -7
View File
@@ -1,7 +1,6 @@
import logging
import os
import shutil
import stat
import threading
from overrides import override
@@ -12,6 +11,7 @@ from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PathUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -57,7 +57,7 @@ class TerraformLS(SolidLanguageServer):
)
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger) -> str:
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for terraform-ls.
Downloads and installs terraform-ls if not already present.
@@ -102,24 +102,26 @@ class TerraformLS(SolidLanguageServer):
)
dependency = deps.single_for_current_platform()
terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir())
terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings))
if not os.path.exists(terraform_ls_executable_path):
logger.log(f"Downloading terraform-ls from {dependency.url}", logging.INFO)
deps.install(logger, cls.ls_resources_dir())
deps.install(logger, cls.ls_resources_dir(solidlsp_settings))
assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}"
# Make the executable file executable on Unix-like systems
if platform_id.value != "win-x64":
os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD)
os.chmod(terraform_ls_executable_path, 0o755)
return terraform_ls_executable_path
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
terraform_ls_executable_path = self._setup_runtime_dependencies(logger)
terraform_ls_executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings)
super().__init__(
config,
@@ -127,6 +129,7 @@ class TerraformLS(SolidLanguageServer):
repository_root_path,
ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path),
"terraform",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.request_id = 0
@@ -18,6 +18,7 @@ from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -42,17 +43,20 @@ class TypeScriptLanguageServer(SolidLanguageServer):
Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config)
ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path),
"typescript",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.initialize_searcher_command_available = threading.Event()
@@ -67,7 +71,9 @@ class TypeScriptLanguageServer(SolidLanguageServer):
]
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> list[str]:
"""
Setup runtime dependencies for TypeScript Language Server and return the command to start the server.
"""
@@ -89,13 +95,13 @@ class TypeScriptLanguageServer(SolidLanguageServer):
RuntimeDependency(
id="typescript",
description="typescript package",
command="npm install --prefix ./ typescript@5.5.4",
command=["npm", "install", "--prefix", "./", "typescript@5.5.4"],
platform_id="any",
),
RuntimeDependency(
id="typescript-language-server",
description="typescript-language-server package",
command="npm install --prefix ./ typescript-language-server@4.3.3",
command=["npm", "install", "--prefix", "./", "typescript-language-server@4.3.3"],
platform_id="any",
),
]
@@ -114,7 +120,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
# Install typescript and typescript-language-server if not already installed
tsserver_ls_dir = os.path.join(cls.ls_resources_dir(), "ts-lsp")
tsserver_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "ts-lsp")
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
if not os.path.exists(tsserver_executable_path):
logger.log(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...", logging.INFO)
@@ -125,7 +131,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
raise FileNotFoundError(
f"typescript-language-server executable not found at {tsserver_executable_path}, something went wrong with the installation."
)
return f"{tsserver_executable_path} --stdio"
return [tsserver_executable_path, "--stdio"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
@@ -19,6 +19,7 @@ from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -29,17 +30,20 @@ class VtsLanguageServer(SolidLanguageServer):
Contains various configurations and settings specific to TypeScript via vtsls wrapper.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a VtsLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
vts_lsp_executable_path = self._setup_runtime_dependencies(logger, config)
vts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=vts_lsp_executable_path, cwd=repository_root_path),
"typescript",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.initialize_searcher_command_available = threading.Event()
@@ -54,7 +58,9 @@ class VtsLanguageServer(SolidLanguageServer):
]
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
"""
Setup runtime dependencies for VTS Language Server and return the command to start the server.
"""
@@ -81,7 +87,7 @@ class VtsLanguageServer(SolidLanguageServer):
),
]
)
vts_ls_dir = os.path.join(cls.ls_resources_dir(), "vts-lsp")
vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vts-lsp")
vts_executable_path = os.path.join(vts_ls_dir, "vtsls")
# Verify both node and npm are installed
+54 -33
View File
@@ -5,6 +5,7 @@ import logging
import os
import pathlib
import pickle
import shutil
import subprocess
import threading
from abc import ABC, abstractmethod
@@ -21,7 +22,7 @@ from serena.text_utils import MatchedConsecutiveLines
from serena.util.file_system import match_path
from solidlsp import ls_types
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_exceptions import LanguageServerException
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_handler import SolidLanguageServerHandler
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils
@@ -30,10 +31,11 @@ from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes
from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind
from solidlsp.lsp_protocol_handler.server import (
Error,
LSPError,
ProcessLaunchInfo,
StringDict,
)
from solidlsp.settings import SolidLSPSettings
GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation]
@@ -89,19 +91,34 @@ class SolidLanguageServer(ABC):
return dirname.startswith(".")
@classmethod
def ls_resources_dir(cls, mkdir: bool = True) -> str:
def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str:
"""
Returns the directory where the language server resources are downloaded.
This is used to store language server binaries, configuration files, etc.
"""
result = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__)
result = os.path.join(solidlsp_settings.ls_resources_dir, cls.__name__)
# Migration of previously downloaded LS resources that were downloaded to a subdir of solidlsp instead of to the user's home
pre_migration_ls_resources_dir = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__)
if os.path.exists(pre_migration_ls_resources_dir):
if os.path.exists(result):
# if the directory already exists, we just remove the old resources
shutil.rmtree(result, ignore_errors=True)
else:
# move old resources to the new location
shutil.move(pre_migration_ls_resources_dir, result)
if mkdir:
os.makedirs(result, exist_ok=True)
return result
@classmethod
def create(
cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, timeout: float | None = None
cls,
config: LanguageServerConfig,
logger: LanguageServerLogger,
repository_root_path: str,
timeout: float | None = None,
solidlsp_settings: SolidLSPSettings | None = None,
) -> "SolidLanguageServer":
"""
Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language.
@@ -116,100 +133,102 @@ class SolidLanguageServer(ABC):
:return LanguageServer: A language specific LanguageServer instance.
"""
ls: SolidLanguageServer
if solidlsp_settings is None:
solidlsp_settings = SolidLSPSettings()
if config.code_language == Language.PYTHON:
from solidlsp.language_servers.pyright_server import (
PyrightServer,
)
ls = PyrightServer(config, logger, repository_root_path)
ls = PyrightServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.PYTHON_JEDI:
from solidlsp.language_servers.jedi_server import JediServer
ls = JediServer(config, logger, repository_root_path)
ls = JediServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.JAVA:
from solidlsp.language_servers.eclipse_jdtls import (
EclipseJDTLS,
)
ls = EclipseJDTLS(config, logger, repository_root_path)
ls = EclipseJDTLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.KOTLIN:
from solidlsp.language_servers.kotlin_language_server import (
KotlinLanguageServer,
)
ls = KotlinLanguageServer(config, logger, repository_root_path)
ls = KotlinLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.RUST:
from solidlsp.language_servers.rust_analyzer import (
RustAnalyzer,
)
ls = RustAnalyzer(config, logger, repository_root_path)
ls = RustAnalyzer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CSHARP:
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
ls = CSharpLanguageServer(config, logger, repository_root_path)
ls = CSharpLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CSHARP_OMNISHARP:
from solidlsp.language_servers.omnisharp import OmniSharp
ls = OmniSharp(config, logger, repository_root_path)
ls = OmniSharp(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.TYPESCRIPT:
from solidlsp.language_servers.typescript_language_server import (
TypeScriptLanguageServer,
)
ls = TypeScriptLanguageServer(config, logger, repository_root_path)
ls = TypeScriptLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.TYPESCRIPT_VTS:
# VTS based Language Server implementation, need to experiment to see if it improves performance
from solidlsp.language_servers.vts_language_server import VtsLanguageServer
ls = VtsLanguageServer(config, logger, repository_root_path)
ls = VtsLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.GO:
from solidlsp.language_servers.gopls import Gopls
ls = Gopls(config, logger, repository_root_path)
ls = Gopls(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.RUBY:
from solidlsp.language_servers.solargraph import Solargraph
ls = Solargraph(config, logger, repository_root_path)
ls = Solargraph(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.DART:
from solidlsp.language_servers.dart_language_server import DartLanguageServer
ls = DartLanguageServer(config, logger, repository_root_path)
ls = DartLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CPP:
from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer
ls = ClangdLanguageServer(config, logger, repository_root_path)
ls = ClangdLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.PHP:
from solidlsp.language_servers.intelephense import Intelephense
ls = Intelephense(config, logger, repository_root_path)
ls = Intelephense(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.CLOJURE:
from solidlsp.language_servers.clojure_lsp import ClojureLSP
ls = ClojureLSP(config, logger, repository_root_path)
ls = ClojureLSP(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.ELIXIR:
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
ls = ElixirTools(config, logger, repository_root_path)
ls = ElixirTools(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.TERRAFORM:
from solidlsp.language_servers.terraform_ls import TerraformLS
ls = TerraformLS(config, logger, repository_root_path)
ls = TerraformLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
raise LanguageServerException(f"Language {config.code_language} is not supported")
raise SolidLSPException(f"Language {config.code_language} is not supported")
ls.set_request_timeout(timeout)
return ls
@@ -221,6 +240,7 @@ class SolidLanguageServer(ABC):
repository_root_path: str,
process_launch_info: ProcessLaunchInfo,
language_id: str,
solidlsp_settings: SolidLSPSettings,
):
"""
Initializes a LanguageServer instance.
@@ -235,6 +255,7 @@ class SolidLanguageServer(ABC):
The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
as opposed to HTTP, TCP modes supported by some language servers.
"""
self._solidlsp_settings = solidlsp_settings
self.logger = logger
self.repository_root_path: str = repository_root_path
self.logger.log(
@@ -260,7 +281,7 @@ class SolidLanguageServer(ABC):
if config.trace_lsp_communication:
def logging_fn(source: str, target: str, msg: StringDict | str):
self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level)
self.logger.log(f"LSP: {source} -> {target}: {msg!s}", self.logger.logger.level)
else:
logging_fn = None
@@ -427,7 +448,7 @@ class SolidLanguageServer(ABC):
"open_file called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
@@ -483,7 +504,7 @@ class SolidLanguageServer(ABC):
"insert_text_at_position called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
@@ -529,7 +550,7 @@ class SolidLanguageServer(ABC):
"insert_text_at_position called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
@@ -573,7 +594,7 @@ class SolidLanguageServer(ABC):
"request_definition called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
with self.open_file(relative_file_path):
# sending request to the language server and waiting for response
@@ -665,14 +686,14 @@ class SolidLanguageServer(ABC):
"request_references called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
with self.open_file(relative_file_path):
try:
response = self._send_references_request(relative_file_path, line=line, column=column)
except Exception as e:
# Catch LSP internal error (-32603) and raise a more informative exception
if isinstance(e, Error) and getattr(e, "code", None) == -32603:
if isinstance(e, LSPError) and getattr(e, "code", None) == -32603:
raise RuntimeError(
f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. "
"This often occurs when requesting references for a symbol not referenced in the expected way. "
@@ -724,7 +745,7 @@ class SolidLanguageServer(ABC):
"request_text_document_diagnostics called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
with self.open_file(relative_file_path):
response = self.server.send.text_document_diagnostic(
@@ -1286,7 +1307,7 @@ class SolidLanguageServer(ABC):
"request_referencing_symbols called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
# First, get all references to the symbol
references = self.request_references(relative_file_path, line, column)
@@ -1558,7 +1579,7 @@ class SolidLanguageServer(ABC):
"request_defining_symbol called before Language Server started",
logging.ERROR,
)
raise LanguageServerException("Language Server not started")
raise SolidLSPException("Language Server not started")
# Get the definition location(s)
definitions = self.request_definition(relative_file_path, line, column)
+32 -7
View File
@@ -1,15 +1,40 @@
"""
This module contains the exceptions raised by the Multilspy framework.
This module contains the exceptions raised by the framework.
"""
class LanguageServerException(Exception):
"""
Exceptions raised by the Multilspy framework.
"""
def __init__(self, message: str):
class SolidLSPException(Exception):
def __init__(self, message: str, cause: Exception | None = None):
"""
Initializes the exception with the given message.
:param message: the message describing the exception
:param cause: the original exception that caused this exception, if any.
For exceptions raised during request handling, this is typically
* an LSPError for errors returned by the LSP server
* LanguageServerTerminatedException for errors due to the language server having terminated.
"""
self.cause = cause
super().__init__(message)
def is_language_server_terminated(self):
"""
:return: True if the exception is caused by the language server having terminated as indicated
by the causing exception being an instance of LanguageServerTerminatedException.
"""
from .ls_handler import LanguageServerTerminatedException
return isinstance(self.cause, LanguageServerTerminatedException)
def __str__(self) -> str:
"""
Returns a string representation of the exception.
"""
s = super().__str__()
if self.cause:
if "\n" in s:
s += "\n"
else:
s += " "
s += f"(caused by {self.cause})"
return s
+101 -71
View File
@@ -12,14 +12,15 @@ from queue import Queue
from typing import Any
import psutil
from sensai.util.string import ToStringMixin
from solidlsp.ls_exceptions import LanguageServerException
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_request import LanguageServerRequest
from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification
from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes
from solidlsp.lsp_protocol_handler.server import (
ENCODING,
Error,
LSPError,
MessageType,
PayloadLike,
ProcessLaunchInfo,
@@ -35,23 +36,50 @@ from solidlsp.lsp_protocol_handler.server import (
log = logging.getLogger(__name__)
class Request:
class LanguageServerTerminatedException(Exception):
"""
Exception raised when the language server process has terminated unexpectedly.
"""
def __init__(self, message: str, cause: Exception | None = None) -> None:
super().__init__(message)
self.message = message
self.cause = cause
def __str__(self) -> str:
return f"LanguageServerTerminatedException: {self.message}" + (f"; Cause: {self.cause}" if self.cause else "")
class Request(ToStringMixin):
@dataclass
class Result:
payload: PayloadLike | None = None
error: Error | None = None
error: Exception | None = None
def is_error(self) -> bool:
return self.error is not None
def __init__(self) -> None:
def __init__(self, request_id: int, method: str) -> None:
self._request_id = request_id
self._method = method
self._status = "pending"
self._result_queue = Queue()
def _tostring_includes(self) -> list[str]:
return ["_request_id", "_status", "_method"]
def on_result(self, params: PayloadLike) -> None:
self._status = "completed"
self._result_queue.put(Request.Result(payload=params))
def on_error(self, err: Error) -> None:
def on_error(self, err: Exception) -> None:
"""
:param err: the error that occurred while processing the request (typically an LSPError
for errors returned by the LS or LanguageServerTerminatedException if the error
is due to the language server process terminating unexpectedly).
"""
self._status = "error"
self._result_queue.put(Request.Result(error=err))
def get_result(self, timeout: float | None = None) -> Result:
@@ -75,10 +103,8 @@ class SolidLanguageServerHandler:
notify: A LspNotification object that can be used to send notifications to the server.
cmd: A string that represents the command to launch the language server process.
process: A subprocess.Popen object that represents the language server process.
_received_shutdown: A boolean flag that indicates whether the client has received
a shutdown request from the server.
request_id: An integer that represents the next available request id for the client.
_response_handlers: A dictionary that maps request ids to Request objects that
_pending_requests: A dictionary that maps request ids to Request objects that
store the results or errors of the requests.
on_request_handlers: A dictionary that maps method names to callback functions
that handle requests from the server.
@@ -104,15 +130,15 @@ class SolidLanguageServerHandler:
start_independent_lsp_process=True,
request_timeout: float | None = None,
) -> None:
self.send = LanguageServerRequest(self.send_request)
self.send = LanguageServerRequest(self)
self.notify = LspNotification(self.send_notification)
self.process_launch_info = process_launch_info
self.process = None
self._received_shutdown = False
self.process: subprocess.Popen | None = None
self._is_shutting_down = False
self.request_id = 1
self._response_handlers: dict[Any, Request] = {}
self._pending_requests: dict[Any, Request] = {}
self.on_request_handlers = {}
self.on_notification_handlers = {}
self.logger = logger
@@ -176,12 +202,12 @@ class SolidLanguageServerHandler:
# start threads to read stdout and stderr of the process
threading.Thread(
target=self.run_forever,
target=self._read_ls_process_stdout,
name="LSP-stdout-reader",
daemon=True,
).start()
threading.Thread(
target=self.run_forever_stderr,
target=self._read_ls_process_stderr,
name="LSP-stderr-reader",
daemon=True,
).start()
@@ -225,21 +251,6 @@ class SolidLanguageServerHandler:
# First try to terminate the process tree gracefully
self._signal_process_tree(process, terminate=True)
# TODO
"""
# Wait for the process to exit (with timeout)
try:
asyncio.wait_for(process.wait(), timeout=10)
except (asyncio.TimeoutError, Exception):
# If termination failed, forcefully kill the process tree
self._signal_process_tree(process, terminate=False)
try:
# Give it one more chance to exit
await asyncio.wait_for(process.wait(), timeout=2)
except Exception:
pass
"""
def _signal_process_tree(self, process, terminate=True):
"""Send signal (terminate or kill) to the process and all its children."""
signal_method = "terminate" if terminate else "kill"
@@ -276,21 +287,13 @@ class SolidLanguageServerHandler:
"""
Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit
"""
self._is_shutting_down = True
self._log("Sending shutdown request to server")
self.send.shutdown()
self._log("Received shutdown response from server")
self._received_shutdown = True
self._log("Sending exit notification to server")
self.notify.exit()
self._log("Sent exit notification to server")
# TODO
"""
if self.process and self.process.stdout:
self.process.stdout.set_exception(StopLoopException())
# This yields the control to the event loop to allow the exception to be handled
# in the run_forever and run_forever_stderr methods
await asyncio.sleep(0)
"""
def _log(self, message: str | StringDict) -> None:
"""
@@ -302,29 +305,30 @@ class SolidLanguageServerHandler:
@staticmethod
def _read_bytes_from_process(process, stream, num_bytes):
"""Read exactly num_bytes from process stdout"""
if process.poll() is not None:
# Process has terminated, check if we can still read
pass
data = b""
while len(data) < num_bytes:
chunk = stream.read(num_bytes - len(data))
if not chunk:
if process.poll() is not None:
raise EOFError(f"Process terminated. Expected {num_bytes} bytes, got {len(data)}")
# Process still running but no data available yet
time.sleep(0.01) # Small delay
raise LanguageServerTerminatedException(
f"Process terminated while trying to read response (read {num_bytes} of {len(data)} bytes before termination)"
)
# Process still running but no data available yet, retry after a short delay
time.sleep(0.01)
continue
data += chunk
return data
def run_forever(self) -> bool:
def _read_ls_process_stdout(self) -> None:
"""
Continuously read from the language server process stdout and handle the messages
invoking the registered response and notification handlers
"""
exception: Exception | None = None
try:
while self.process and self.process.stdout and self.process.stdout.readable():
while self.process and self.process.stdout:
if self.process.poll() is not None: # process has terminated
break
line = self.process.stdout.readline()
if not line:
continue
@@ -341,22 +345,38 @@ class SolidLanguageServerHandler:
body = self._read_bytes_from_process(self.process, self.process.stdout, num_bytes)
self._handle_body(body)
except (BrokenPipeError, ConnectionResetError):
pass
return self._received_shutdown
except LanguageServerTerminatedException as e:
exception = e
except (BrokenPipeError, ConnectionResetError) as e:
exception = LanguageServerTerminatedException("Language server process terminated while reading stdout", cause=e)
except Exception as e:
exception = LanguageServerTerminatedException("Unexpected error while reading stdout from language server process", cause=e)
log.info("Language server stdout reader thread has terminated")
if not self._is_shutting_down:
if exception is None:
exception = LanguageServerTerminatedException("Language server stdout read process terminated unexpectedly")
log.error(str(exception))
self._cancel_pending_requests(exception)
def run_forever_stderr(self) -> None:
def _read_ls_process_stderr(self) -> None:
"""
Continuously read from the language server process stderr and log the messages
"""
try:
while self.process and self.process.stderr and self.process.stderr.readable():
while self.process and self.process.stderr:
if self.process.poll() is not None:
# process has terminated
break
line = self.process.stderr.readline()
if not line:
continue
self._log("LSP stderr: " + line.decode(ENCODING, errors="replace"))
except (BrokenPipeError, ConnectionResetError):
pass
log.error(line.decode(ENCODING, errors="replace"))
except Exception as e:
log.error("Error while reading stderr from language server process: %s", e, exc_info=e)
if not self._is_shutting_down:
log.error("Language server stderr reader thread terminated unexpectedly")
else:
log.info("Language server stderr reader thread has terminated")
def _handle_body(self, body: bytes) -> None:
"""
@@ -402,37 +422,47 @@ class SolidLanguageServerHandler:
"""
self._send_payload(make_response(request_id, params))
def send_error_response(self, request_id: Any, err: Error) -> None:
def send_error_response(self, request_id: Any, err: LSPError) -> None:
"""
Send error response to the given request id to the server with the given error
"""
# Use lock to prevent race conditions on tasks and task_counter
self._send_payload(make_error_response(request_id, err))
def _cancel_pending_requests(self, exception: Exception) -> None:
"""
Cancel all pending requests by setting their results to an error
"""
with self._response_handlers_lock:
log.info("Cancelling %d pending language server requests", len(self._pending_requests))
for request in self._pending_requests.values():
log.info("Cancelling %s", request)
request.on_error(exception)
self._pending_requests.clear()
def send_request(self, method: str, params: dict | None = None) -> PayloadLike:
"""
Send request to the server, register the request id, and wait for the response
"""
request = Request()
# Use lock to prevent race conditions on request_id and _response_handlers
with self._request_id_lock:
request_id = self.request_id
self.request_id += 1
request = Request(request_id=request_id, method=method)
log.debug("Starting: %s", request)
with self._response_handlers_lock:
self._response_handlers[request_id] = request
self._pending_requests[request_id] = request
self._send_payload(make_request(method, request_id, params))
self._log(f"Waiting for response to request {method} with params:\n{params}")
result = request.get_result(timeout=self._request_timeout)
log.debug("Completed: %s", request)
self._log("Processing result")
if result.is_error():
raise LanguageServerException(
f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}"
) from result.error
raise SolidLSPException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error
self._log(f"Returning non-error result, which is:\n{result.payload}")
return result.payload
@@ -474,14 +504,14 @@ class SolidLanguageServerHandler:
Handle the response received from the server for a request, using the id to determine the request
"""
with self._response_handlers_lock:
request = self._response_handlers.pop(response["id"])
request = self._pending_requests.pop(response["id"])
if "result" in response and "error" not in response:
request.on_result(response["result"])
elif "result" not in response and "error" in response:
request.on_error(Error.from_lsp(response["error"]))
request.on_error(LSPError.from_lsp(response["error"]))
else:
request.on_error(Error(ErrorCodes.InvalidRequest, ""))
request.on_error(LSPError(ErrorCodes.InvalidRequest, ""))
def _request_handler(self, response: StringDict) -> None:
"""
@@ -494,7 +524,7 @@ class SolidLanguageServerHandler:
if not handler:
self.send_error_response(
request_id,
Error(
LSPError(
ErrorCodes.MethodNotFound,
f"method '{method}' not handled on client.",
),
@@ -502,10 +532,10 @@ class SolidLanguageServerHandler:
return
try:
self.send_response(request_id, handler(params))
except Error as ex:
except LSPError as ex:
self.send_error_response(request_id, ex)
except Exception as ex:
self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex)))
self.send_error_response(request_id, LSPError(ErrorCodes.InternalError, str(ex)))
def _notification_handler(self, response: StringDict) -> None:
"""
@@ -522,7 +552,7 @@ class SolidLanguageServerHandler:
except asyncio.CancelledError:
return
except Exception as ex:
if (not self._received_shutdown) and self.logger:
if (not self._is_shutting_down) and self.logger:
self.logger(
"client",
"logger",
+60 -54
View File
@@ -1,11 +1,17 @@
from typing import Union
from typing import TYPE_CHECKING, Any, Union
from solidlsp.lsp_protocol_handler import lsp_types
if TYPE_CHECKING:
from .ls_handler import SolidLanguageServerHandler
class LanguageServerRequest:
def __init__(self, send_request):
self.send_request = send_request
def __init__(self, handler: "SolidLanguageServerHandler"):
self.handler = handler
def _send_request(self, method: str, params: Any | None = None) -> Any:
return self.handler.send_request(method, params)
def implementation(self, params: lsp_types.ImplementationParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
"""A request to resolve the implementation locations of a symbol at a given text
@@ -13,7 +19,7 @@ class LanguageServerRequest:
(#TextDocumentPositionParams) the response is of type {@link Definition} or a
Thenable that resolves to such.
"""
return self.send_request("textDocument/implementation", params)
return self._send_request("textDocument/implementation", params)
def type_definition(
self, params: lsp_types.TypeDefinitionParams
@@ -23,7 +29,7 @@ class LanguageServerRequest:
(#TextDocumentPositionParams) the response is of type {@link Definition} or a
Thenable that resolves to such.
"""
return self.send_request("textDocument/typeDefinition", params)
return self._send_request("textDocument/typeDefinition", params)
def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]:
"""A request to list all color symbols found in a given text document. The request's
@@ -31,7 +37,7 @@ class LanguageServerRequest:
response is of type {@link ColorInformation ColorInformation[]} or a Thenable
that resolves to such.
"""
return self.send_request("textDocument/documentColor", params)
return self._send_request("textDocument/documentColor", params)
def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]:
"""A request to list all presentation for a color. The request's
@@ -39,7 +45,7 @@ class LanguageServerRequest:
response is of type {@link ColorInformation ColorInformation[]} or a Thenable
that resolves to such.
"""
return self.send_request("textDocument/colorPresentation", params)
return self._send_request("textDocument/colorPresentation", params)
def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None:
"""A request to provide folding ranges in a document. The request's
@@ -47,7 +53,7 @@ class LanguageServerRequest:
response is of type {@link FoldingRangeList} or a Thenable
that resolves to such.
"""
return self.send_request("textDocument/foldingRange", params)
return self._send_request("textDocument/foldingRange", params)
def declaration(self, params: lsp_types.DeclarationParams) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]:
"""A request to resolve the type definition locations of a symbol at a given text
@@ -56,7 +62,7 @@ class LanguageServerRequest:
or a typed array of {@link DeclarationLink} or a Thenable that resolves
to such.
"""
return self.send_request("textDocument/declaration", params)
return self._send_request("textDocument/declaration", params)
def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None:
"""A request to provide selection ranges in a document. The request's
@@ -64,7 +70,7 @@ class LanguageServerRequest:
response is of type {@link SelectionRange SelectionRange[]} or a Thenable
that resolves to such.
"""
return self.send_request("textDocument/selectionRange", params)
return self._send_request("textDocument/selectionRange", params)
def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None:
"""A request to result a `CallHierarchyItem` in a document at a given position.
@@ -72,42 +78,42 @@ class LanguageServerRequest:
@since 3.16.0
"""
return self.send_request("textDocument/prepareCallHierarchy", params)
return self._send_request("textDocument/prepareCallHierarchy", params)
def incoming_calls(self, params: lsp_types.CallHierarchyIncomingCallsParams) -> list["lsp_types.CallHierarchyIncomingCall"] | None:
"""A request to resolve the incoming calls for a given `CallHierarchyItem`.
@since 3.16.0
"""
return self.send_request("callHierarchy/incomingCalls", params)
return self._send_request("callHierarchy/incomingCalls", params)
def outgoing_calls(self, params: lsp_types.CallHierarchyOutgoingCallsParams) -> list["lsp_types.CallHierarchyOutgoingCall"] | None:
"""A request to resolve the outgoing calls for a given `CallHierarchyItem`.
@since 3.16.0
"""
return self.send_request("callHierarchy/outgoingCalls", params)
return self._send_request("callHierarchy/outgoingCalls", params)
def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]:
"""@since 3.16.0"""
return self.send_request("textDocument/semanticTokens/full", params)
return self._send_request("textDocument/semanticTokens/full", params)
def semantic_tokens_delta(
self, params: lsp_types.SemanticTokensDeltaParams
) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]:
"""@since 3.16.0"""
return self.send_request("textDocument/semanticTokens/full/delta", params)
return self._send_request("textDocument/semanticTokens/full/delta", params)
def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]:
"""@since 3.16.0"""
return self.send_request("textDocument/semanticTokens/range", params)
return self._send_request("textDocument/semanticTokens/range", params)
def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]:
"""A request to provide ranges that can be edited together.
@since 3.16.0
"""
return self.send_request("textDocument/linkedEditingRange", params)
return self._send_request("textDocument/linkedEditingRange", params)
def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
"""The will create files request is sent from the client to the server before files are actually
@@ -115,7 +121,7 @@ class LanguageServerRequest:
@since 3.16.0
"""
return self.send_request("workspace/willCreateFiles", params)
return self._send_request("workspace/willCreateFiles", params)
def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
"""The will rename files request is sent from the client to the server before files are actually
@@ -123,7 +129,7 @@ class LanguageServerRequest:
@since 3.16.0
"""
return self.send_request("workspace/willRenameFiles", params)
return self._send_request("workspace/willRenameFiles", params)
def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
"""The did delete files notification is sent from the client to the server when
@@ -131,14 +137,14 @@ class LanguageServerRequest:
@since 3.16.0
"""
return self.send_request("workspace/willDeleteFiles", params)
return self._send_request("workspace/willDeleteFiles", params)
def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None:
"""A request to get the moniker of a symbol at a given text document position.
The request parameter is of type {@link TextDocumentPositionParams}.
The response is of type {@link Moniker Moniker[]} or `null`.
"""
return self.send_request("textDocument/moniker", params)
return self._send_request("textDocument/moniker", params)
def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None:
"""A request to result a `TypeHierarchyItem` in a document at a given position.
@@ -146,21 +152,21 @@ class LanguageServerRequest:
@since 3.17.0
"""
return self.send_request("textDocument/prepareTypeHierarchy", params)
return self._send_request("textDocument/prepareTypeHierarchy", params)
def type_hierarchy_supertypes(self, params: lsp_types.TypeHierarchySupertypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
"""A request to resolve the supertypes for a given `TypeHierarchyItem`.
@since 3.17.0
"""
return self.send_request("typeHierarchy/supertypes", params)
return self._send_request("typeHierarchy/supertypes", params)
def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
"""A request to resolve the subtypes for a given `TypeHierarchyItem`.
@since 3.17.0
"""
return self.send_request("typeHierarchy/subtypes", params)
return self._send_request("typeHierarchy/subtypes", params)
def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None:
"""A request to provide inline values in a document. The request's parameter is of
@@ -169,7 +175,7 @@ class LanguageServerRequest:
@since 3.17.0
"""
return self.send_request("textDocument/inlineValue", params)
return self._send_request("textDocument/inlineValue", params)
def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None:
"""A request to provide inlay hints in a document. The request's parameter is of
@@ -178,7 +184,7 @@ class LanguageServerRequest:
@since 3.17.0
"""
return self.send_request("textDocument/inlayHint", params)
return self._send_request("textDocument/inlayHint", params)
def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint":
"""A request to resolve additional properties for an inlay hint.
@@ -187,21 +193,21 @@ class LanguageServerRequest:
@since 3.17.0
"""
return self.send_request("inlayHint/resolve", params)
return self._send_request("inlayHint/resolve", params)
def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport":
"""The document diagnostic request definition.
@since 3.17.0
"""
return self.send_request("textDocument/diagnostic", params)
return self._send_request("textDocument/diagnostic", params)
def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport":
"""The workspace diagnostic request definition.
@since 3.17.0
"""
return self.send_request("workspace/diagnostic", params)
return self._send_request("workspace/diagnostic", params)
def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult":
"""The initialize request is sent from the client to the server.
@@ -210,7 +216,7 @@ class LanguageServerRequest:
the response if of type {@link InitializeResult} of a Thenable that
resolves to such.
"""
return self.send_request("initialize", params)
return self._send_request("initialize", params)
def shutdown(self) -> None:
"""A shutdown request is sent from the client to the server.
@@ -218,7 +224,7 @@ class LanguageServerRequest:
server. The only notification that is sent after a shutdown request
is the exit event.
"""
return self.send_request("shutdown")
return self._send_request("shutdown")
def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None:
"""A document will save request is sent from the client to the server before
@@ -228,7 +234,7 @@ class LanguageServerRequest:
server constantly fails on this request. This is done to keep the save fast and
reliable.
"""
return self.send_request("textDocument/willSaveWaitUntil", params)
return self._send_request("textDocument/willSaveWaitUntil", params)
def completion(self, params: lsp_types.CompletionParams) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]:
"""Request to request completion at a given text document position. The request's
@@ -241,24 +247,24 @@ class LanguageServerRequest:
request. However, properties that are needed for the initial sorting and filtering, like `sortText`,
`filterText`, `insertText`, and `textEdit`, must not be changed during resolve.
"""
return self.send_request("textDocument/completion", params)
return self._send_request("textDocument/completion", params)
def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem":
"""Request to resolve additional information for a given completion item.The request's
parameter is of type {@link CompletionItem} the response
is of type {@link CompletionItem} or a Thenable that resolves to such.
"""
return self.send_request("completionItem/resolve", params)
return self._send_request("completionItem/resolve", params)
def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]:
"""Request to request hover information at a given text document position. The request's
parameter is of type {@link TextDocumentPosition} the response is of
type {@link Hover} or a Thenable that resolves to such.
"""
return self.send_request("textDocument/hover", params)
return self._send_request("textDocument/hover", params)
def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]:
return self.send_request("textDocument/signatureHelp", params)
return self._send_request("textDocument/signatureHelp", params)
def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
"""A request to resolve the definition location of a symbol at a given text
@@ -267,7 +273,7 @@ class LanguageServerRequest:
or a typed array of {@link DefinitionLink} or a Thenable that resolves
to such.
"""
return self.send_request("textDocument/definition", params)
return self._send_request("textDocument/definition", params)
def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None:
"""A request to resolve project-wide references for the symbol denoted
@@ -275,7 +281,7 @@ class LanguageServerRequest:
type {@link ReferenceParams} the response is of type
{@link Location Location[]} or a Thenable that resolves to such.
"""
return self.send_request("textDocument/references", params)
return self._send_request("textDocument/references", params)
def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None:
"""Request to resolve a {@link DocumentHighlight} for a given
@@ -283,7 +289,7 @@ class LanguageServerRequest:
(#TextDocumentPosition) the request response is of type [DocumentHighlight[]]
(#DocumentHighlight) or a Thenable that resolves to such.
"""
return self.send_request("textDocument/documentHighlight", params)
return self._send_request("textDocument/documentHighlight", params)
def document_symbol(
self, params: lsp_types.DocumentSymbolParams
@@ -293,18 +299,18 @@ class LanguageServerRequest:
response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable
that resolves to such.
"""
return self.send_request("textDocument/documentSymbol", params)
return self._send_request("textDocument/documentSymbol", params)
def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None:
"""A request to provide commands for the given text document and range."""
return self.send_request("textDocument/codeAction", params)
return self._send_request("textDocument/codeAction", params)
def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction":
"""Request to resolve additional information for a given code action.The request's
parameter is of type {@link CodeAction} the response
is of type {@link CodeAction} or a Thenable that resolves to such.
"""
return self.send_request("codeAction/resolve", params)
return self._send_request("codeAction/resolve", params)
def workspace_symbol(
self, params: lsp_types.WorkspaceSymbolParams
@@ -318,7 +324,7 @@ class LanguageServerRequest:
need to advertise support for WorkspaceSymbols via the client capability
`workspace.symbol.resolveSupport`.
"""
return self.send_request("workspace/symbol", params)
return self._send_request("workspace/symbol", params)
def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol":
"""A request to resolve the range inside the workspace
@@ -326,52 +332,52 @@ class LanguageServerRequest:
@since 3.17.0
"""
return self.send_request("workspaceSymbol/resolve", params)
return self._send_request("workspaceSymbol/resolve", params)
def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None:
"""A request to provide code lens for the given text document."""
return self.send_request("textDocument/codeLens", params)
return self._send_request("textDocument/codeLens", params)
def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens":
"""A request to resolve a command for a given code lens."""
return self.send_request("codeLens/resolve", params)
return self._send_request("codeLens/resolve", params)
def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None:
"""A request to provide document links"""
return self.send_request("textDocument/documentLink", params)
return self._send_request("textDocument/documentLink", params)
def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink":
"""Request to resolve additional information for a given document link. The request's
parameter is of type {@link DocumentLink} the response
is of type {@link DocumentLink} or a Thenable that resolves to such.
"""
return self.send_request("documentLink/resolve", params)
return self._send_request("documentLink/resolve", params)
def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None:
"""A request to to format a whole document."""
return self.send_request("textDocument/formatting", params)
return self._send_request("textDocument/formatting", params)
def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None:
"""A request to to format a range in a document."""
return self.send_request("textDocument/rangeFormatting", params)
return self._send_request("textDocument/rangeFormatting", params)
def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None:
"""A request to format a document on type."""
return self.send_request("textDocument/onTypeFormatting", params)
return self._send_request("textDocument/onTypeFormatting", params)
def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]:
"""A request to rename a symbol."""
return self.send_request("textDocument/rename", params)
return self._send_request("textDocument/rename", params)
def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]:
"""A request to test and perform the setup necessary for a rename.
@since 3.16 - support for default behavior
"""
return self.send_request("textDocument/prepareRename", params)
return self._send_request("textDocument/prepareRename", params)
def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]:
"""A request send from the client to the server to execute a command. The request might return
a workspace edit which the client will apply to the workspace.
"""
return self.send_request("workspace/executeCommand", params)
return self._send_request("workspace/executeCommand", params)
+12 -11
View File
@@ -14,7 +14,7 @@ from pathlib import Path, PurePath
import requests
from solidlsp.ls_exceptions import LanguageServerException
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_types import UnifiedSymbolInformation
@@ -170,29 +170,30 @@ class FileUtils:
"""
if not os.path.exists(file_path):
logger.log(f"File read '{file_path}' failed: File does not exist.", logging.ERROR)
raise LanguageServerException(f"File read '{file_path}' failed: File does not exist.")
raise SolidLSPException(f"File read '{file_path}' failed: File does not exist.")
try:
with open(file_path, encoding="utf-8") as inp_file:
return inp_file.read()
except Exception as exc:
logger.log(f"File read '{file_path}' failed to read with encoding 'utf-8': {exc}", logging.ERROR)
raise LanguageServerException("File read failed.") from None
raise SolidLSPException("File read failed.") from None
@staticmethod
def download_file(logger: LanguageServerLogger, url: str, target_path: str) -> None:
"""
Downloads the file from the given URL to the given {target_path}
"""
os.makedirs(os.path.dirname(target_path), exist_ok=True)
try:
response = requests.get(url, stream=True, timeout=60)
if response.status_code != 200:
logger.log(f"Error downloading file '{url}': {response.status_code} {response.text}", logging.ERROR)
raise LanguageServerException("Error downloading file.")
raise SolidLSPException("Error downloading file.")
with open(target_path, "wb") as f:
shutil.copyfileobj(response.raw, f)
except Exception as exc:
logger.log(f"Error downloading file '{url}': {exc}", logging.ERROR)
raise LanguageServerException("Error downloading file.") from None
raise SolidLSPException("Error downloading file.") from None
@staticmethod
def download_and_extract_archive(logger: LanguageServerLogger, url: str, target_path: str, archive_type: str) -> None:
@@ -220,10 +221,10 @@ class FileUtils:
shutil.copyfileobj(f_in, f_out)
else:
logger.log(f"Unknown archive type '{archive_type}' for extraction", logging.ERROR)
raise LanguageServerException(f"Unknown archive type '{archive_type}'")
raise SolidLSPException(f"Unknown archive type '{archive_type}'")
except Exception as exc:
logger.log(f"Error extracting archive '{tmp_file_name}' obtained from '{url}': {exc}", logging.ERROR)
raise LanguageServerException("Error extracting archive.") from exc
raise SolidLSPException("Error extracting archive.") from exc
finally:
for tmp_file_name in tmp_files:
if os.path.exists(tmp_file_name):
@@ -289,7 +290,7 @@ class PlatformUtils:
platform_id += "-" + libc
return PlatformId(platform_id)
else:
raise LanguageServerException(f"Unknown platform: {system=}, {machine=}, {bitness=}")
raise SolidLSPException(f"Unknown platform: {system=}, {machine=}, {bitness=}")
@staticmethod
def _determine_windows_machine_type():
@@ -345,7 +346,7 @@ class PlatformUtils:
available_version_cmd_output.append(version_cmd_output)
if not available_version_cmd_output:
raise LanguageServerException("dotnet not found on the system")
raise SolidLSPException("dotnet not found on the system")
# Check for supported versions in order of preference (latest first)
for version_cmd_output in available_version_cmd_output:
@@ -361,7 +362,7 @@ class PlatformUtils:
return DotnetVersion.V4
# If no supported version found, raise exception with all available versions
raise LanguageServerException(
raise SolidLSPException(
f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8"
)
except (FileNotFoundError, subprocess.CalledProcessError):
@@ -369,7 +370,7 @@ class PlatformUtils:
result = subprocess.run(["mono", "--version"], capture_output=True, check=True)
return DotnetVersion.VMONO
except (FileNotFoundError, subprocess.CalledProcessError):
raise LanguageServerException("dotnet or mono not found on the system")
raise SolidLSPException("dotnet or mono not found on the system")
class SymbolUtils:
+4 -4
View File
@@ -59,7 +59,7 @@ class ProcessLaunchInfo:
cwd: str = os.getcwd()
class Error(Exception):
class LSPError(Exception):
def __init__(self, code: ErrorCodes, message: str) -> None:
super().__init__(message)
self.code = code
@@ -68,8 +68,8 @@ class Error(Exception):
return {"code": self.code, "message": super().__str__()}
@classmethod
def from_lsp(cls, d: StringDict) -> "Error":
return Error(d["code"], d["message"])
def from_lsp(cls, d: StringDict) -> "LSPError":
return LSPError(d["code"], d["message"])
def __str__(self) -> str:
return f"{super().__str__()} ({self.code})"
@@ -79,7 +79,7 @@ def make_response(request_id: Any, params: PayloadLike) -> StringDict:
return {"jsonrpc": "2.0", "id": request_id, "result": params}
def make_error_response(request_id: Any, err: Error) -> StringDict:
def make_error_response(request_id: Any, err: LSPError) -> StringDict:
return {"jsonrpc": "2.0", "id": request_id, "error": err.to_lsp()}
+10 -14
View File
@@ -4,21 +4,17 @@ Defines settings for Solid-LSP
import os
import pathlib
from dataclasses import dataclass
@dataclass
class SolidLSPSettings:
@staticmethod
def get_language_server_directory() -> str:
"""Returns the directory for language servers"""
user_home = pathlib.Path.home()
multilspy_dir = str(pathlib.PurePath(user_home, ".multilspy"))
lsp_dir = str(pathlib.PurePath(multilspy_dir, "lsp"))
os.makedirs(lsp_dir, exist_ok=True)
return lsp_dir
solidlsp_dir: str = str(pathlib.Path.home() / ".solidlsp")
@staticmethod
def get_global_cache_directory() -> str:
"""Returns the cache directory"""
global_cache_dir = os.path.join(str(pathlib.Path.home()), ".multilspy", "global_cache")
os.makedirs(global_cache_dir, exist_ok=True)
return global_cache_dir
def __post_init__(self):
os.makedirs(str(self.solidlsp_dir), exist_ok=True)
os.makedirs(str(self.ls_resources_dir), exist_ok=True)
@property
def ls_resources_dir(self):
return os.path.join(str(self.solidlsp_dir), "language_servers", "static")
+5 -1
View File
@@ -4,11 +4,13 @@ from pathlib import Path
import pytest
from sensai.util.logging import configure
from serena.constants import SERENA_MANAGED_DIR_IN_HOME
from serena.project import Project
from serena.util.file_system import GitignoreParser
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.settings import SolidLSPSettings
configure(level=logging.ERROR)
@@ -43,7 +45,9 @@ def create_ls(
ignored_paths.extend(spec.patterns)
config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication)
logger = LanguageServerLogger(log_level=log_level)
return SolidLanguageServer.create(config, logger, repo_path)
return SolidLanguageServer.create(
config, logger, repo_path, solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME)
)
def create_default_ls(language: Language) -> SolidLanguageServer:
+14 -9
View File
@@ -4,16 +4,28 @@ import pytest
from mcp.server.fastmcp.tools.base import Tool as MCPTool
from serena.agent import Tool, ToolRegistry
from serena.config.context_mode import SerenaAgentContext
from serena.mcp import SerenaMCPFactory
make_tool = SerenaMCPFactory.make_mcp_tool
# Create a mock agent for tool initialization
class MockAgent:
def __init__(self):
self.project_config = None
self.serena_config = None
@staticmethod
def get_context() -> SerenaAgentContext:
return SerenaAgentContext.load_default()
class BaseMockTool(Tool):
"""A mock Tool class for testing."""
def __init__(self) -> None:
pass
def __init__(self):
super().__init__(MockAgent())
class BasicTool(BaseMockTool):
@@ -274,13 +286,6 @@ def is_test_mock_class(tool_class: type) -> bool:
@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."""
# Create a mock agent for tool initialization
class MockAgent:
def __init__(self):
self.project_config = None
self.serena_config = None
# Create an instance of the tool
tool_instance = tool_class(MockAgent())
+7 -2
View File
@@ -13,6 +13,7 @@ from solidlsp.language_servers.csharp_language_server import (
)
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import SymbolUtils
from solidlsp.settings import SolidLSPSettings
@pytest.mark.csharp
@@ -214,7 +215,9 @@ class TestCSharpSolutionProjectOpening:
mock_config.ignored_paths = []
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
mock_settings = Mock(spec=SolidLSPSettings)
mock_settings.ls_resources_dir = "/tmp/test_ls_resources"
CSharpLanguageServer(mock_config, mock_logger, str(temp_path), mock_settings)
# Verify that logger was called with solution file discovery
mock_logger.log.assert_any_call(f"Found solution/project file: {solution_file}", 20) # logging.INFO
@@ -236,7 +239,9 @@ class TestCSharpSolutionProjectOpening:
mock_config.ignored_paths = []
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
mock_settings = Mock(spec=SolidLSPSettings)
mock_settings.ls_resources_dir = "/tmp/test_ls_resources"
CSharpLanguageServer(mock_config, mock_logger, str(temp_path), mock_settings)
# Verify that logger was called with warning about no solution/project files
mock_logger.log.assert_any_call(
Generated
+70 -70
View File
@@ -4,7 +4,7 @@ requires-python = "==3.11.*"
[[package]]
name = "agno"
version = "1.7.2"
version = "1.7.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docstring-parser" },
@@ -20,9 +20,9 @@ dependencies = [
{ name = "typer" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/64/6a82d2375145fd733009db0ada531b595fc2f3c1712ac91c91b2e5fcf02f/agno-1.7.2.tar.gz", hash = "sha256:f4f97e5ea1f9c0781d9bb8396862c82671381386c1569cdb456d70f6c8d3a108", size = 661788, upload-time = "2025-07-10T15:50:06.217Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/84/d2390249d1b0150971a749df79cf653a631227b748fe10be1ef4f426ed2d/agno-1.7.6.tar.gz", hash = "sha256:8df575f86eac6511996045fb90d281ca61fd19774dccbb82f9dc34bda46effdf", size = 724233, upload-time = "2025-07-24T14:36:27.356Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/24/884cbd1133e3b9985d53733250251098caead365bb8706af8da105813abe/agno-1.7.2-py3-none-any.whl", hash = "sha256:37a1985e67762f0d8016318ba5d8b8697c855827d7c86b21a41cdff088294cc2", size = 888229, upload-time = "2025-07-10T15:50:04.262Z" },
{ url = "https://files.pythonhosted.org/packages/d9/7e/5f85c92a34f41f8ffb1467768bae50506f734320c478ed24c4237dc342ea/agno-1.7.6-py3-none-any.whl", hash = "sha256:c6350e44747b37048d4ac080ffa5fb0e0ce2b3d5bc55d37120711ae8d31f27c6", size = 962755, upload-time = "2025-07-24T14:36:25.648Z" },
]
[[package]]
@@ -36,7 +36,7 @@ wheels = [
[[package]]
name = "anthropic"
version = "0.57.1"
version = "0.59.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -47,9 +47,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d7/75/6261a1a8d92aed47e27d2fcfb3a411af73b1435e6ae1186da02b760565d0/anthropic-0.57.1.tar.gz", hash = "sha256:7815dd92245a70d21f65f356f33fc80c5072eada87fb49437767ea2918b2c4b0", size = 423775, upload-time = "2025-07-03T16:57:35.932Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/52daff015f5a1f24eec891b3041f5f816712fea8b5113dc76638bcbc23d8/anthropic-0.59.0.tar.gz", hash = "sha256:d710d1ef0547ebbb64b03f219e44ba078e83fc83752b96a9b22e9726b523fd8f", size = 425679, upload-time = "2025-07-23T16:23:16.901Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/cf/ca0ba77805aec6171629a8b665c7dc224dab374539c3d27005b5d8c100a0/anthropic-0.57.1-py3-none-any.whl", hash = "sha256:33afc1f395af207d07ff1bffc0a3d1caac53c371793792569c5d2f09283ea306", size = 292779, upload-time = "2025-07-03T16:57:34.636Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b1/03f680393eac04afd8f2be44ee0e39e033c40faf43dbc1c11764b07a2687/anthropic-0.59.0-py3-none-any.whl", hash = "sha256:cbc8b3dccef66ad6435c4fa1d317e5ebb092399a4b88b33a09dc4bf3944c3183", size = 293057, upload-time = "2025-07-23T16:23:14.934Z" },
]
[[package]]
@@ -130,11 +130,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2025.7.9"
version = "2025.7.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
]
[[package]]
@@ -200,11 +200,11 @@ wheels = [
[[package]]
name = "docstring-parser"
version = "0.16"
version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" },
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
]
[[package]]
@@ -267,14 +267,14 @@ wheels = [
[[package]]
name = "gitpython"
version = "3.1.44"
version = "3.1.45"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" },
{ url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" },
]
[[package]]
@@ -293,7 +293,7 @@ wheels = [
[[package]]
name = "google-genai"
version = "1.25.0"
version = "1.27.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -305,9 +305,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/59/c9b9148c8702b60253f5a251f16ae436534c5d4362da193c9db05ac9858c/google_genai-1.25.0.tar.gz", hash = "sha256:a08a79c819a5d949d9948cd372e36e512bf85cd28158994daaa36d0ec4cb2b02", size = 228141, upload-time = "2025-07-09T20:53:47.885Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/37/6c0ececc3a7a629029b5beed2ceb9f28f73292236eb96272355636769b0d/google_genai-1.27.0.tar.gz", hash = "sha256:15a13ffe7b3938da50b9ab77204664d82122617256f55b5ce403d593848ef635", size = 220099, upload-time = "2025-07-23T22:00:46.145Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/ec/149f3d49b56cf848142071772aabb1c290b535bd9b5327a5dfccf1d00332/google_genai-1.25.0-py3-none-any.whl", hash = "sha256:fb5cee79b9a0a1b2afd5cfdf279099ecebd186551eefcaa6ec0c6016244e6138", size = 226847, upload-time = "2025-07-09T20:53:46.532Z" },
{ url = "https://files.pythonhosted.org/packages/5a/12/279afe7357af73f9737a3412b6f0bc1482075b896340eb46a2f9cb0fd791/google_genai-1.27.0-py3-none-any.whl", hash = "sha256:afd6b4efaf8ec1d20a6e6657d768b68d998d60007c6e220e9024e23c913c1833", size = 218489, upload-time = "2025-07-23T22:00:44.879Z" },
]
[[package]]
@@ -489,7 +489,7 @@ wheels = [
[[package]]
name = "jsonschema"
version = "4.24.0"
version = "4.25.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -497,9 +497,9 @@ dependencies = [
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" },
{ url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" },
]
[[package]]
@@ -558,7 +558,7 @@ wheels = [
[[package]]
name = "mcp"
version = "1.11.0"
version = "1.12.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -573,9 +573,9 @@ dependencies = [
{ name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907, upload-time = "2025-07-10T16:41:09.388Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/19/9955e2df5384ff5dd25d38f8e88aaf89d2d3d9d39f27e7383eaf0b293836/mcp-1.12.3.tar.gz", hash = "sha256:ab2e05f5e5c13e1dc90a4a9ef23ac500a6121362a564447855ef0ab643a99fed", size = 427203, upload-time = "2025-07-31T18:36:36.795Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880, upload-time = "2025-07-10T16:41:07.935Z" },
{ url = "https://files.pythonhosted.org/packages/8f/8b/0be74e3308a486f1d127f3f6767de5f9f76454c9b4183210c61cc50999b6/mcp-1.12.3-py3-none-any.whl", hash = "sha256:5483345bf39033b858920a5b6348a303acacf45b23936972160ff152107b850e", size = 158810, upload-time = "2025-07-31T18:36:34.915Z" },
]
[[package]]
@@ -589,22 +589,22 @@ wheels = [
[[package]]
name = "mypy"
version = "1.16.1"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" },
{ url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" },
{ url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" },
{ url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" },
{ url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" },
{ url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" },
{ url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" },
{ url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" },
{ url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" },
{ url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" },
{ url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" },
{ url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" },
]
[[package]]
@@ -913,12 +913,12 @@ wheels = [
[[package]]
name = "pywin32"
version = "310"
version = "311"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" },
{ url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" },
{ url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" },
{ url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
{ url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
{ url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
]
[[package]]
@@ -992,15 +992,15 @@ wheels = [
[[package]]
name = "rich"
version = "14.0.0"
version = "14.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
{ url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" },
]
[[package]]
@@ -1079,39 +1079,39 @@ wheels = [
[[package]]
name = "ruff"
version = "0.12.3"
version = "0.12.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" }
sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" },
{ url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" },
{ url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" },
{ url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" },
{ url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" },
{ url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" },
{ url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" },
{ url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" },
{ url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" },
{ url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" },
{ url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" },
{ url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" },
{ url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" },
{ url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" },
{ url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" },
{ url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" },
{ url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" },
{ url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" },
{ url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" },
{ url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" },
{ url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" },
{ url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" },
{ url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" },
{ url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" },
{ url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" },
{ url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" },
{ url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" },
{ url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" },
{ url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" },
{ url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" },
{ url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" },
]
[[package]]
name = "sensai-utils"
version = "1.4.0"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317, upload-time = "2025-01-27T22:47:51.121Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/dd/faa2e2de71a03af3def212c70777e794dd54ad5ab87927bb5c29f85f24fc/sensai_utils-1.5.0.tar.gz", hash = "sha256:2ca709a0d5807caf1632d665a455c173987b25276ce61693021672e875f0f17b", size = 61677, upload-time = "2025-07-28T22:46:10.148Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214, upload-time = "2025-01-27T22:47:49.606Z" },
{ url = "https://files.pythonhosted.org/packages/ad/d5/62a0e693230bace8e9a767d6d187a4d9421a7c6ee4b48551f8ff7bd1629a/sensai_utils-1.5.0-py3-none-any.whl", hash = "sha256:c23c51d23d353e7a9b77c70aa8ba7f2d0a8fe4e67ee5bc1ef41c69dba5e4befb", size = 68836, upload-time = "2025-07-28T22:46:08.929Z" },
]
[[package]]
@@ -1174,7 +1174,7 @@ requires-dist = [
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "jinja2", marker = "extra == 'dev'" },
{ name = "joblib", specifier = ">=1.5.1" },
{ name = "mcp", specifier = ">=1.5.0" },
{ name = "mcp", specifier = "==1.12.3" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.1" },
{ name = "overrides", specifier = ">=7.7.0,<8" },
{ name = "pathspec", specifier = ">=0.12.1" },
@@ -1189,7 +1189,7 @@ requires-dist = [
{ name = "requests", specifier = ">=2.32.3,<3" },
{ name = "ruamel-yaml", specifier = ">=0.18.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.0.285" },
{ name = "sensai-utils", specifier = ">=1.4.0" },
{ name = "sensai-utils", specifier = ">=1.5.0" },
{ name = "sqlalchemy", marker = "extra == 'agno'", specifier = ">=2.0.40" },
{ name = "syrupy", marker = "extra == 'dev'", specifier = ">=4.9.1" },
{ name = "tiktoken", specifier = ">=0.9.0" },
@@ -1250,14 +1250,14 @@ wheels = [
[[package]]
name = "sse-starlette"
version = "2.4.1"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/07/3e/eae74d8d33e3262bae0a7e023bb43d8bdd27980aa3557333f4632611151f/sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926", size = 18635, upload-time = "2025-07-06T09:41:33.631Z" }
sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/f1/6c7eaa8187ba789a6dd6d74430307478d2a91c23a5452ab339b6fbe15a08/sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a", size = 10824, upload-time = "2025-07-06T09:41:32.321Z" },
{ url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" },
]
[[package]]
@@ -1276,15 +1276,15 @@ wheels = [
[[package]]
name = "starlette"
version = "0.47.1"
version = "0.47.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" }
sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" },
{ url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
]
[[package]]