diff --git a/README.md b/README.md index 1bde986c..063f0971 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,85 @@ -# serena +# Serena -Welcome to the serena library! +> **🚀 A powerful, fully-featured coding agent that you can use for free. No API key and no subscription required!** + + +Serena answers the following important question: +Can I have a state-of-the-art coding agent without paying (enormous) API costs +or constantly purchasing tokens? The answer is: yes, you can! + +We enable developers to harness the full coding power of Claude +(and soon also ChatGPT, see [here](https://x.com/OpenAIDevs/status/1904957755829481737)) +for free by leveraging two key technologies: +**MCP Servers** and **Language Servers**. + +The former allows MCP clients like Claude Desktop to gain access to your codebase and developer +tools. Using the latter, we built the core logic of serena - a set of versatile code querying +and editing functionalities based on symbolic understanding of the code. Serena discovers and +edits code just like a seasoned developer would do it by navigating the codebase inside an IDE. +Because of this, serena can find the right context and do the right thing even in very large and +complex projects! So not only is it free and open source, it is also often better than other existing +solutions. + +Serena stands for *Semantic Retrieval and Editing Noetic Agent*, but we also just liked the name. + + +## Is it Really Free? + +Yes! Even the free tier of Anthropic has support for MCP Servers, so you can use serena there. +Presumably, the same will be true for ChatGPT Desktop once they add support for MCP servers. +But we do recommend to buy the Claude Pro subscription for 20$ per month as this way the rate +limits are much higher. + +Serena is [Oraios AI](https://oraios-ai.de/)'s contribution to the developer community. We use it ourselves every day. + +We got tired of having to pay multiple +subscriptions (Cursor, Windsurf) that forced us to keep purchasing tokens on top of the subscription costs. +We also got tired of paying the huge API costs from Claude Code, Cline, Aider and other API-based tools. +This is why we built serena and cancelled most subscriptions. +See below for a more detailed comparison to existing technologies. + +## What if I Do Want to Pay? + +If you want to use your own API key, connect serena to a custom model, +or use it within a paid tool, you can do so. +We decoupled the tools offered by serena from the MCP server implementation, so they can also +be used in any agent framework and with any model. See the section [Serena as Agent](#serena-as-agent). +This way, you can run serena as a command line tool or within a UI of your choice. + +## Quick Start + +1. Install `uv` if not done yet (instructions [here](https://docs.astral.sh/uv/getting-started/installation/)) +2. Clone the repository to `/path/to/serena`. +3. Create a configuration file for your project, say `myproject.yml`. See `myproject.demo.yml` for the structure. + You can just copy it and edit the entries. +4. Configure the mcp server in your client. For example, for Claude Desktop, you need to go to `Settings->Developer->MCP Servers`, + which will let you open a json file. There add the following to enable serena: + +```json +{ + "mcpServers": { + "serena": { + "command": "/abs/path/to/uv", + "args": ["run", "--directory", "/abs/path/to/serena", "serena", "/abs/path/to/myproject.yml"] + } + } +} +``` + +That's it! Save the config, restart Claude (you will have actually terminate the process on windows, just closing the window is not enough), +and you should see the serena mcp tools in your chat interface (the small hammer icon). + +Note that serena is always configured **for a single project**. To use it for another, you will have to +write a new configuration file and adjust the config in the MCP client (don't forget to restart after). + +For more info on MCP servers with Claude Desktop see [here](https://modelcontextprotocol.io/quickstart/user). + +## Serena as Agent + +In the agent mode, serena can be used with any model, including the currently popular (and SOTA in coding) +Gemini-2.5-pro. + +... ## Getting Started @@ -68,8 +147,8 @@ This is typically done by providing a configuration file in JSON format as follo { "mcpServers": { "serena": { - "command": "uv", - "args": ["run", "--directory", "/path/to/serena", "python", "scripts/mcp_server.py", "myproject.yml"] + "command": "/path/to/uv", + "args": ["run", "--directory", "/path/to/serena", "serena", "/path/to/myproject.yml"] } } } diff --git a/prompts/mcp_prompts.yml b/prompts/mcp_prompts.yml index a76c108e..450aba90 100644 --- a/prompts/mcp_prompts.yml +++ b/prompts/mcp_prompts.yml @@ -65,4 +65,10 @@ prompts: Recommend which tools to use and the next steps to take. Imagine that you are handing over the task to another person who has access to the same tools and memory files as you do, but has not been part of the conversation so far. If you have discovered anything general about the project in the course of the conversation, consider writing a new memory file to store that information. - + + system_prompt: | + You are a professional coding agent who is concerned with one particular codebase. You have tools to explore the codebase + iteratively, to edit files and to execute arbitrary commands. You heavily rely on these tools to solve the tasks give to you, + but you also operate in a frugal and intelligent manner, always + keeping in mind to not load content that is not needed for the task at hand. If you cannot find everything you need in the codebase + and generally when in doubt, you ask the user clarifying questions. diff --git a/pyproject.toml b/pyproject.toml index 7a561269..29dbce7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,13 @@ dependencies = [ "sensai-utils>=1.4.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", + "pyyaml>=6.0.2", + "jinja2>=3.1.6", ] +[project.scripts] +serena = "serena.mcp:start_mcp_server" + [project.license] text = "MIT" @@ -60,6 +65,9 @@ dev = [ "sphinxcontrib-spelling>=8.0.0", "types-pyyaml>=6.0.12.20241230", ] +agno = [ + "agno>=1.2.6", +] [project.urls] Homepage = "https://github.com/oraios/serena" diff --git a/scripts/agno_agent.py b/scripts/agno_agent.py new file mode 100644 index 00000000..dcbb59d0 --- /dev/null +++ b/scripts/agno_agent.py @@ -0,0 +1,13 @@ +from agno.agent import Agent +from agno.models.google import Gemini +from agno.tools.duckduckgo import DuckDuckGoTools + +agent = Agent( + model=Gemini(id="gemini-2.5-pro-exp-03-25"), + description="", + tools=[], # Add serena tools here + show_tool_calls=True, + markdown=True, + context= +) +agent.print_response("Tell me about a breaking news story from New York.", stream=True) \ No newline at end of file diff --git a/scripts/run_sample.py b/scripts/run_sample.py deleted file mode 100644 index 1523ba58..00000000 --- a/scripts/run_sample.py +++ /dev/null @@ -1,5 +0,0 @@ -from serena.sample_module import SampleClass - -if __name__ == "__main__": - print(SampleClass().sample_method("Oraios AI")) - print("Your new library project serena is waiting for you!") diff --git a/src/serena/llm/prompt_factory.py b/src/serena/llm/prompt_factory.py index f37280d9..989aca7e 100644 --- a/src/serena/llm/prompt_factory.py +++ b/src/serena/llm/prompt_factory.py @@ -38,4 +38,9 @@ class PromptFactory: return self._format_prompt("summarize_changes", locals()) def create_prepare_for_new_conversation(self) -> str: - return self._format_prompt("prepare_for_new_conversation", locals()) + return self._format_prompt('prepare_for_new_conversation', locals()) + + def create_system_prompt(self) -> str: + return self._format_prompt('system_prompt', locals()) + + diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 6ecde00b..b795d3e6 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -94,3 +94,7 @@ def create_mcp_server() -> FastMCP: mcp._tool_manager._tools[tool.get_name()] = make_tool(tool) return mcp + + +def start_mcp_server(): + create_mcp_server().run() \ No newline at end of file diff --git a/uv.lock b/uv.lock index 2cb6479a..82554dc8 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903 }, ] +[[package]] +name = "agno" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser" }, + { name = "gitpython" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tomli" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/56/52971b8f6d17af618a3f88aaa55a06b388f1c2871db509efe6f792d1e662/agno-1.2.6.tar.gz", hash = "sha256:0e83cf44e2957823814b47a8321be23e7faba0ff144845e60eea585fb86fdf57", size = 441431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ca/c0bf8045f6ca7cf8e99fe2f60c1162eca7f056c6b447fad27e0fd81e88ec/agno-1.2.6-py3-none-any.whl", hash = "sha256:b008416f01f4a6b2ff106777586ccc0d93d50f114944140dd95e2ff8adf7802a", size = 586946 }, +] + [[package]] name = "alabaster" version = "0.7.16" @@ -449,6 +472,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] +[[package]] +name = "docstring-parser" +version = "0.16" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, +] + [[package]] name = "docutils" version = "0.20.1" @@ -524,6 +556,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121 }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, +] + [[package]] name = "greenlet" version = "3.1.1" @@ -1780,6 +1836,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163 }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + [[package]] name = "pywin32" version = "310" @@ -2008,11 +2073,13 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, + { name = "jinja2" }, { name = "mcp" }, { name = "overrides" }, { name = "pydantic" }, { name = "pyright" }, { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "requests" }, { name = "sensai-utils" }, { name = "types-pyyaml" }, @@ -2046,11 +2113,17 @@ dev = [ { name = "types-pyyaml" }, ] +[package.dev-dependencies] +agno = [ + { name = "agno" }, +] + [package.metadata] requires-dist = [ { name = "black", extras = ["jupyter"], marker = "extra == 'dev'", specifier = ">=23.7.0" }, { name = "docutils", marker = "extra == 'dev'", specifier = ">=0.20.1" }, { name = "fastmcp", specifier = ">=0.4.1" }, + { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, { name = "jupyter", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = "==1.0.0" }, @@ -2066,6 +2139,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.2" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "python-dotenv", specifier = ">=1.0.0,<2" }, + { name = "pyyaml", specifier = ">=6.0.2" }, { name = "requests", specifier = ">=2.32.3,<3" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.0.285" }, { name = "sensai-utils", specifier = ">=1.4.0" }, @@ -2084,6 +2158,9 @@ requires-dist = [ ] provides-extras = ["dev"] +[package.metadata.requires-dev] +agno = [{ name = "agno", specifier = ">=1.2.6" }] + [[package]] name = "setuptools" version = "78.0.1" @@ -2111,6 +2188,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + [[package]] name = "sniffio" version = "1.3.1"