diff --git a/.gitignore b/.gitignore index 5a34e1c9..0521c154 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Application specific +.devon.config + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -191,3 +194,6 @@ dist .yarn build + +package-lock.json +poetry.lock \ No newline at end of file diff --git a/README.md b/README.md index e570b41c..057b8a2d 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Configuring Devon CLI... ? Select the model name: claude-opus gpt4-o + gemini-pro llama-3-70b ❯ ollama/deepseek-coder:6.7b ``` diff --git a/devon-tui/new b/devon-tui/new deleted file mode 100644 index 9a875884..00000000 --- a/devon-tui/new +++ /dev/null @@ -1 +0,0 @@ -ello, this is a new file. diff --git a/devon-tui/source/cli.tsx b/devon-tui/source/cli.tsx index 19775dfb..7708ea8b 100644 --- a/devon-tui/source/cli.tsx +++ b/devon-tui/source/cli.tsx @@ -46,6 +46,7 @@ const cli = meow( $ devon start --api_key=YOUR_API_KEY $ devon start --port 8080 --api_key=YOUR_API_KEY $ devon start --model=gpt4-o --api_key=YOUR_API_KEY + $ devon start --model=gemini-pro --api_key=YOUR_API_KEY $ devon start --model=claude-opus --api_key=YOUR_API_KEY $ devon start --model=llama-3-70b --api_key=YOUR_API_KEY $ devon start --model=custom --api_base=https://api.example.com --prompt_type=anthropic --api_key=YOUR_API_KEY @@ -67,7 +68,8 @@ const cli = meow( type: 'string', }, debug: { - type: 'boolean' + type: 'boolean', + default: false }, }, }, @@ -85,14 +87,14 @@ const { input } = cli; if (input[0] === 'configure') { // Handle the configure subcommand console.log('Configuring Devon CLI...'); - + inquirer .prompt([ { type: 'list', name: 'modelName', message: 'Select the model name:', - choices: ['claude-opus', 'gpt4-o', 'llama-3-70b', 'ollama/deepseek-coder:6.7b', 'custom'], + choices: ['claude-opus', 'gpt4-o', 'gemini-pro', 'llama-3-70b', 'ollama/deepseek-coder:6.7b', 'custom'], }, ]) .then((answers) => { @@ -169,19 +171,31 @@ if (input[0] === 'configure') { let api_base: string | undefined = undefined let prompt_type: string | undefined = undefined - if (cli.flags.apiKey){ - api_key = cli.flags['apiKey']; - } else if (process.env['OPENAI_API_KEY']){ + if (process.env['OPENAI_API_KEY']){ api_key = process.env['OPENAI_API_KEY']; modelName = "gpt4-o" + } else if (process.env['GEMINI_API_KEY']){ + api_key = process.env['GEMINI_API_KEY']; + modelName = "gemini-pro" } else if (process.env['ANTHROPIC_API_KEY']){ api_key = process.env['ANTHROPIC_API_KEY']; modelName = "claude-opus" } else if (process.env['GROQ_API_KEY']){ api_key = process.env['GROQ_API_KEY']; modelName = "llama-3-70b" + } else if (cli.flags['apiKey']){ + api_key = cli.flags['apiKey']; + + if(api_key != "FOSS") { + if(cli.flags['model']) { + modelName = cli.flags['model'] as string; + } else { + console.log('Please provide a model name. Allowed values are gpt4-o, gemini-pro, claude-opus, llama-3-70b or ollama.'); + process.exit(1); + } + } } else { - console.log('Please provide an API key using the --api_key option or by setting OPENAI_API_KEY or ANTHROPIC_API_KEY.'); + console.log('Please provide an API key using the --api_key option or by setting OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GROQ_API_KEY.'); process.exit(1); } @@ -218,7 +232,7 @@ if (input[0] === 'configure') { ); process.exit(1); } - console.log( ['server', '--port', port.toString(), '--model', modelName as string, '--api_key', api_key as string, '--api_base', api_base as string, '--prompt_type', prompt_type as string]) + console.log( ['server', '--port', port.toString(), '--model', modelName as string, '--api_key', api_key as string]) let reset = false @@ -236,7 +250,7 @@ if (input[0] === 'configure') { const subProcess = childProcess.spawn( 'devon_agent', - ['server', '--port', port.toString(), '--model', modelName as string, '--api_key', api_key as string, '--api_base', api_base as string, '--prompt_type', prompt_type as string], + ['server', '--port', port.toString(), '--model', modelName as string, '--api_key', api_key as string], { signal: controller.signal }, diff --git a/devon_agent/__main__.py b/devon_agent/__main__.py index acf39565..307f7522 100644 --- a/devon_agent/__main__.py +++ b/devon_agent/__main__.py @@ -35,9 +35,11 @@ def server(port, model, api_key, prompt_type, api_base): app.prompt_type = prompt_type app.model = model - with open(os.path.join(os.getcwd(), ".devon.config"), "r") as f: - config = f.read() - app.config = json.loads(config) + config_path = os.path.join(os.getcwd(), ".devon.config") + if os.path.exists(config_path): + with open(config_path, "r") as f: + config = f.read() + app.config = json.loads(config) uvicorn.run(app, host="0.0.0.0", port=port) @@ -63,9 +65,11 @@ def headless(model, api_key, prompt_type, api_base, headless): app.model = model app.headless = headless - with open(os.path.join(os.getcwd(), ".devon.config"), "r") as f: - config = f.read() - app.config = json.loads(config) + config_path = os.path.join(os.getcwd(), ".devon.config") + if os.path.exists(config_path): + with open(config_path, "r") as f: + config = f.read() + app.config = json.loads(config) agent = TaskAgent( name="Devon", diff --git a/devon_agent/agents/default/agent.py b/devon_agent/agents/default/agent.py index c2c04419..98d095fd 100644 --- a/devon_agent/agents/default/agent.py +++ b/devon_agent/agents/default/agent.py @@ -5,15 +5,14 @@ import traceback from typing import Optional, Tuple -from devon_agent.agents.model import AnthropicModel, GroqModel, ModelArguments, OllamaModel, OpenAiModel +from devon_agent.agents.model import AnthropicModel, GroqModel, ModelArguments, OllamaModel, OpenAiModel, GeminiModel from devon_agent.agents.default.anthropic_prompts import anthropic_history_to_bash_history, anthropic_last_user_prompt_template_v3, anthropic_system_prompt_template_v3, anthropic_commands_to_command_docs from devon_agent.agents.default.openai_prompts import openai_last_user_prompt_template_v3, openai_system_prompt_template_v3, openai_commands_to_command_docs -from devon_agent.agents.default.anthropic_prompts import ( - parse_response -) from devon_agent.agents.default.llama3_prompts import llama3_commands_to_command_docs, llama3_history_to_bash_history, llama3_last_user_prompt_template_v1, llama3_parse_response, llama3_system_prompt_template_v1 +from devon_agent.agents.default.gemini_prompts import gemini_commands_to_command_docs, gemini_history_to_bash_history, gemini_last_user_prompt_template_v1, gemini_parse_response, gemini_system_prompt_template_v1 from devon_agent.agents.default.codegemma_prompts import llama3_7b_commands_to_command_docs, llama3_7b_history_to_bash_history, llama3_7b_last_user_prompt_template_v1, llama3_7b_parse_response, llama3_7b_system_prompt_template_v1 + from devon_agent.tools.utils import get_cwd from devon_agent.udiff import Hallucination @@ -47,6 +46,7 @@ def run(self, session: "Session", observation: str = None): ... class TaskAgent(Agent): default_models = { "gpt4-o": OpenAiModel, + "gemini-pro": GeminiModel, "claude-opus": AnthropicModel, "llama-3-70b": GroqModel, "ollama/deepseek-coder:6.7b": OllamaModel @@ -56,6 +56,9 @@ class TaskAgent(Agent): "gpt4-o": { "prompt_type": "openai", }, + "gemini-pro": { + "prompt_type": "gemini", + }, "claude-opus": { "prompt_type": "anthropic", }, @@ -204,6 +207,32 @@ def _prepare_llama3(self, task, editor, session): messages = [{"role": "user", "content": last_user_prompt}] return messages, system_prompt + + def _prepare_gemini(self, task, editor, session): + time.sleep(3) + + command_docs = ( + "Custom Commands Documentation:\n" + + gemini_commands_to_command_docs( + list(session.generate_command_docs().values()) + ) + + "\n" + ) + + history = gemini_history_to_bash_history(self.chat_history) + system_prompt = gemini_system_prompt_template_v1(command_docs) + last_user_prompt = gemini_last_user_prompt_template_v1( + task, history, editor, get_cwd( + { + "session": session, + "environment": session.default_environment, + "state": session.state + } + ), session.base_path, self.scratchpad + ) + + messages = [{"role": "user", "content": last_user_prompt}] + return messages, system_prompt def _prepare_ollama(self, task, editor, session): time.sleep(3) @@ -262,7 +291,8 @@ def predict( "anthropic": self._prepare_anthropic, "openai": self._prepare_openai, "llama3": self._prepare_llama3, - "ollama": self._prepare_ollama + "ollama": self._prepare_ollama, + "gemini": self._prepare_gemini, } if not self.prompt_type: diff --git a/devon_agent/agents/default/gemini_prompts.py b/devon_agent/agents/default/gemini_prompts.py new file mode 100644 index 00000000..c1200448 --- /dev/null +++ b/devon_agent/agents/default/gemini_prompts.py @@ -0,0 +1,159 @@ +from typing import Dict, List, Union + +def gemini_commands_to_command_docs(commands: List[Dict]): + doc = "" + for command in commands: + doc += f"{command['signature']}\n{command['docstring']}\n" + return doc + +def editor_repr(editor): + return "\n\n".join(f"{file}:\n{editor[file]}" for file in editor) + +def gemini_history_to_bash_history(history): + # self.history.append( + # { + # "role": "assistant", + # "content": output, + # "thought": thought, + # "action": action, + # "agent": self.name, + + bash_history = "" + for entry in history: + if entry["role"] == "user": + result = entry["content"].strip() if entry["content"] else "" + "\n" + bash_history += f"\n{result}\n" + elif entry["role"] == "assistant": + bash_history += f""" + +{entry['thought']} + +{entry['action'][1:]} + + +""" + return bash_history + +def object_to_xml(data: Union[dict, bool], root="object"): + xml = f"<{root}>" + if isinstance(data, dict): + xml += "".join(object_to_xml(value, key) for key, value in data.items()) + elif isinstance(data, (list, tuple, set)): + xml += "".join(object_to_xml(item, "item") for item in data) + else: + xml += str(data) + xml += f"" + return xml + +def print_tree(directory, level=0, indent=""): + return "".join(f"\n{indent}├── {name}/" + print_tree(content, level + 1, indent + "│ ") if isinstance(content, dict) else f"\n{indent}├── {name}" for name, content in directory.items()) + +def gemini_system_prompt_template_v1(command_docs: str): + return f""" + + You are an autonomous programmer, and you're working directly in the command line with a special interface. + + Environment: +- Editor (): Open, edit, and auto-save code files. Focus on relevant files for each bug fix. +- Terminal: Execute commands to perform actions. Modify failed commands before retrying. +- History (): Log of previous thoughts and actions. Act as if you've had these thoughts and performed these actions. + +Constraints: +- Maintain proper formatting and adhere to the project's coding conventions. +- Keep only relevant files open. Close inactive files. +- Modify failed commands before retrying. +- Use efficient search techniques to locate relevant code elements. +- Verify fixes resolve the original issue before submitting. +- Prioritize general fixes over specific ones. +- Ask for user input when needed for feedback, clarification, or guidance. + + + +{command_docs} + + +Shell prompt format: $ +Required fields for each response: + +Your reflection, planning, and justification + + +Information you want to write down + + +A single executable command (no interactive commands) + + +""" + +def gemini_last_user_prompt_template_v1(issue, history, editor, cwd, root_dir, scratchpad): + return f""" + +Objective: {issue} + +Instructions: +- Edit files and run checks/tests +- Submit with 'submit' when done +- No interactive commands, write scripts instead + + +- One command at a time +- Wait for feedback after each command +- Locate classes/functions over files +- Use 'no_op' for thinking time +- Issue title/first line describes it succinctly + + +- Write unit tests to verify fixes +- Run tests frequently to catch regressions +- Test edge cases and error handling +- Manually verify UI and integration tests +- Ensure tests pass before submitting + + +- Identify root cause and failure case +- Fix underlying logic bug generally +- Trace error to source +- Identify flawed logic or edge case handling +- Devise robust solution for core problem +- Test fix thoroughly for potential impacts + + +- Use 'no_op' to pause and think +- Match source lines precisely +- Scroll to lines before changing +- Make one change at a time +- Finish edits before testing +- Access limited to {root_dir} +- Current directory: {cwd} + + +{history} + + +{editor} + + +{scratchpad} + + +{root_dir} + +{cwd} $ +""" + +def gemini_parse_response(response): + if "" in response: + thought = response.split("")[1].split("")[0] + action = response.split("")[1].split("")[0] + scratchpad = None + if "" in response: + scratchpad = response.split("")[1].split("")[0] + else: + thought = response.split("")[1].split("")[0] + action = response.split("")[1].split("")[0] + scratchpad = None + if "" in response: + scratchpad = response.split("")[1].split("")[0] + + return thought, action, scratchpad diff --git a/devon_agent/agents/model.py b/devon_agent/agents/model.py index 8f73c7aa..feefdf5d 100644 --- a/devon_agent/agents/model.py +++ b/devon_agent/agents/model.py @@ -158,6 +158,59 @@ def query(self, messages: list[dict[str, str]], system_message: str = "") -> str response = model_completion.choices[0].message.content.rstrip("") return response + "" +class GeminiModel: + MODELS = { + "gemini/gemini-1.5-pro": { + "max_tokens": 4096, + } + } + + SHORTCUTS = { + "gemini-pro": "gemini/gemini-1.5-pro" + } + def __init__(self, args: ModelArguments): + self.args = args + self.api_model = self.SHORTCUTS.get(args.model_name, args.model_name) + self.model_metadata = self.MODELS[self.api_model] + self.prompt_type = 'gemini' + if args.api_key is not None: + self.api_key = args.api_key + else: + self.api_key = os.getenv("GEMINI_API_KEY") + + def query(self, messages: list[dict[str, str]], system_message: str = "") -> str: + + print(self.api_model); + + model_completion = completion( + messages=[{"role": "system", "content": system_message}] + messages, + max_tokens=self.model_metadata["max_tokens"], + model=self.api_model, + temperature=self.args.temperature, + stop=[""], + safety_settings=[ + { + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "BLOCK_NONE", + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "threshold": "BLOCK_NONE", + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold": "BLOCK_NONE", + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "BLOCK_NONE", + }, + ] + ) + + response = model_completion.choices[0].message.content.rstrip("") + return response + "" + class OllamaModel: def __init__(self, args: ModelArguments): diff --git a/devon_agent/server.py b/devon_agent/server.py index 216b35c1..2b6b9bcb 100644 --- a/devon_agent/server.py +++ b/devon_agent/server.py @@ -275,6 +275,9 @@ async def event_generator(): if os.environ.get("OPENAI_API_KEY"): app.api_key = os.environ.get("OPENAI_API_KEY") app.model = "gpt4-o" + if os.environ.get("GEMINI_API_KEY"): + app.api_key = os.environ.get("GEMINI_API_KEY") + app.model = "gemini-pro" elif os.environ.get("ANTHROPIC_API_KEY"): app.api_key = os.environ.get("ANTHROPIC_API_KEY") app.model = "claude-opus" diff --git a/devon_agent/session.py b/devon_agent/session.py index 5623f724..1858e9da 100644 --- a/devon_agent/session.py +++ b/devon_agent/session.py @@ -110,7 +110,7 @@ def __init__(self, args: SessionArguments, agent): self.name = args.name self.agent_branch = "devon_agent_" + self.name self.global_config = args.config - self.excludes = self.global_config["excludes"] if self.global_config else [] + self.excludes = self.global_config["excludes"] if "excludes" in self.global_config else [] local_environment = LocalEnvironment(args.path) local_environment.register_tools({ diff --git a/devon_agent/tools/codeindex.py b/devon_agent/tools/codeindex.py index c9ada720..a8e189d6 100644 --- a/devon_agent/tools/codeindex.py +++ b/devon_agent/tools/codeindex.py @@ -36,7 +36,7 @@ class FindFunctionTool(Tool): @property def name(self): - return "create_file" + return "find_function" def setup(self, ctx, **kwargs): diff --git a/devon_agent/tools/editortools.py b/devon_agent/tools/editortools.py index d8f0fb53..219fe59f 100644 --- a/devon_agent/tools/editortools.py +++ b/devon_agent/tools/editortools.py @@ -405,7 +405,7 @@ def supported_formats(self): @property def name(self): - return "scroll_up_in_editor" + return "scroll_up" def setup(self, ctx: ToolContext): ctx["state"].editor.files = {} diff --git a/pyproject.toml b/pyproject.toml index 723b74fb..4423cea0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dspy-ai = {version = "^2.4.9", optional = true} aiosqlite = "^0.20.0" greenlet = "^3.0.3" +google-generativeai = "^0.5.4" [tool.poetry.extras] swebench = ["swebench", "datasets", "gymnasium"] experimental = ["dspy-ai"]