diff --git a/scripts/agno_agent.py b/scripts/agno_agent.py index 19e94d59..47f4fed4 100644 --- a/scripts/agno_agent.py +++ b/scripts/agno_agent.py @@ -1,82 +1,21 @@ -import argparse -import os -from logging import Logger -from pathlib import Path - -from agno.agent.agent import Agent -from agno.memory.agent import AgentMemory from agno.models.anthropic.claude import Claude from agno.models.google.gemini import Gemini from agno.playground.playground import Playground from agno.playground.serve import serve_playground_app -from agno.storage.sqlite import SqliteStorage -from dotenv import load_dotenv from sensai.util import logging from sensai.util.helper import mark_used -from serena.agent import SerenaAgent -from serena.agno import SerenaAgnoToolkit +from serena.agno import SerenaAgnoAgentProvider mark_used(Gemini, Claude) -os.chdir(Path(__file__).parent) -Logger.root.setLevel(logging.INFO) +logging.configure(level=logging.INFO) -load_dotenv() - - -parser = argparse.ArgumentParser(description="Serena coding assistant") -parser.add_argument("--project-file", required=True, help="Path to the project file, either absolute or relative to the root directory") -args = parser.parse_args() - -project_file = Path(args.project_file).resolve() -# If project file path is relative, make it absolute by joining with project root -if not project_file.is_absolute(): - # Get the project root directory (parent of scripts directory) - project_root = Path(__file__).parent.parent - project_file = project_root / args.project_file - -# Ensure the path is normalized and absolute -project_file = project_file.resolve() - - -serena_agent = SerenaAgent(str(project_file), start_language_server=True) - -# Even though we don't want to keep history between sessions, -# for the agno ui to work as a conversation, we a persistent storage on disk -# This storage should be deleted between sessions. -# Note that this might collide with custom options for the agent, like adding vector-search based tools. -# See here for an explanation: https://www.reddit.com/r/agno/comments/1jk6qea/regarding_the_built_in_memory/ -sql_db_path = Path("tmp") / "agent_storage.db" -sql_db_path.parent.mkdir(exist_ok=True) -# delete the db file if it exists -if sql_db_path.exists(): - sql_db_path.unlink() - -# model = Claude(id="claude-3-7-sonnet-20250219") -# Or use any other model supported by agno, e.g. the newest Gemini: -model = Gemini(id="gemini-2.5-pro-exp-03-25") - -agno_agent = Agent( - name="Serena", - model=model, - # See explanation above on why storage is needed - storage=SqliteStorage(table_name="serena_agent_sessions", db_file=str(sql_db_path)), - description="A fully-featured coding assistant", - tools=[SerenaAgnoToolkit(serena_agent)], # type: ignore - # The tool calls will be shown in the UI anyway since whether to show them is configurable per tool - # To see detailed logs, you should use the serena logger (configure it in the project file path) - show_tool_calls=False, - markdown=True, - system_message=serena_agent.prompt_factory.create_system_prompt(), - telemetry=False, - memory=AgentMemory(), - add_history_to_messages=True, - num_history_responses=100, # you might want to adjust this (expense vs. history awareness) -) - -app = Playground(agents=[agno_agent]).get_app() +# Define the model to use (see Agno documentation for supported models; these are just examples) +model = Claude(id="claude-3-7-sonnet-20250219") +# model = Gemini(id="gemini-2.5-pro-exp-03-25") +app = Playground(agents=[SerenaAgnoAgentProvider.get_agent(model)]).get_app() if __name__ == "__main__": serve_playground_app("agno_agent:app", reload=False) diff --git a/src/serena/__init__.py b/src/serena/__init__.py index f1acdf73..aa295c0d 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1 +1,7 @@ __version__ = "0.1.0-dev1" + + +def serena_root_path() -> str: + from pathlib import Path + + return str(Path(__file__).parent.parent.absolute()) diff --git a/src/serena/agno.py b/src/serena/agno.py index 54ccd166..80f33168 100644 --- a/src/serena/agno.py +++ b/src/serena/agno.py @@ -1,9 +1,20 @@ +import argparse import logging +import os +import threading +from pathlib import Path from typing import Any +from agno.agent import Agent +from agno.memory import AgentMemory +from agno.models.base import Model +from agno.storage.sqlite import SqliteStorage from agno.tools.function import Function from agno.tools.toolkit import Toolkit +from dotenv import load_dotenv +from sensai.util.logging import LogTime +from serena import serena_root_path from serena.agent import SerenaAgent, Tool log = logging.getLogger(__name__) @@ -89,3 +100,81 @@ class SerenaAgnoToolkit(Toolkit): function.entrypoint = entrypoint function.skip_entrypoint_processing = True return function + + +class SerenaAgnoAgentProvider: + _agent: Agent | None = None + _lock = threading.Lock() + + @classmethod + def get_agent(cls, model: Model) -> Agent: + """ + Returns the singleton instance of the Serena agent or creates it with the given parameters if it doesn't exist. + + NOTE: This is very ugly with poor separation of concerns, but the way in which the Agno UI works (reloading the + module that defines the `app` variable) essentially forces us to do something like this. + + :param model: the large language model to use for the agent + :return: the agent instance + """ + with cls._lock: + if cls._agent is not None: + return cls._agent + + # change to Serena root + os.chdir(serena_root_path()) + + load_dotenv() + + parser = argparse.ArgumentParser(description="Serena coding assistant") + parser.add_argument( + "--project-file", required=True, help="Path to the project file, either absolute or relative to the root directory" + ) + args = parser.parse_args() + + project_file = Path(args.project_file).resolve() + # If project file path is relative, make it absolute by joining with project root + if not project_file.is_absolute(): + # Get the project root directory (parent of scripts directory) + project_root = Path(serena_root_path()) + project_file = project_root / args.project_file + + # Ensure the path is normalized and absolute + project_file = project_file.resolve() + + with LogTime("Loading Serena agent"): + serena_agent = SerenaAgent(str(project_file), start_language_server=True) + + # Even though we don't want to keep history between sessions, + # for agno-ui to work as a conversation, we use a persistent storage on disk. + # This storage should be deleted between sessions. + # Note that this might collide with custom options for the agent, like adding vector-search based tools. + # See here for an explanation: https://www.reddit.com/r/agno/comments/1jk6qea/regarding_the_built_in_memory/ + sql_db_path = (Path("temp") / "agno_agent_storage.db").absolute() + sql_db_path.parent.mkdir(exist_ok=True) + # delete the db file if it exists + log.info(f"Deleting DB from PID {os.getpid()}") + if sql_db_path.exists(): + sql_db_path.unlink() + + agno_agent = Agent( + name="Serena", + model=model, + # See explanation above on why storage is needed + storage=SqliteStorage(table_name="serena_agent_sessions", db_file=str(sql_db_path)), + description="A fully-featured coding assistant", + tools=[SerenaAgnoToolkit(serena_agent)], + # The tool calls will be shown in the UI anyway since whether to show them is configurable per tool + # To see detailed logs, you should use the serena logger (configure it in the project file path) + show_tool_calls=False, + markdown=True, + system_message=serena_agent.prompt_factory.create_system_prompt(), + telemetry=False, + memory=AgentMemory(), + add_history_to_messages=True, + num_history_responses=100, # you might want to adjust this (expense vs. history awareness) + ) + cls._agent = agno_agent + log.info(f"Agent instantiated: {agno_agent}") + + return agno_agent