From 408d384c1e173ecbf0a19ed1db9b5dcfda5f6242 Mon Sep 17 00:00:00 2001 From: Reiko Date: Mon, 6 May 2024 00:51:48 +0200 Subject: [PATCH 1/8] Rebase with new Devika Versions Idk what the f*ck did i do, i somehow deleted every commit that i've done --- app.dockerfile | 29 --- devika.py | 17 +- requirements.txt | 2 + sample.config.toml | 4 + src/agents/action/action.py | 13 +- src/agents/answer/answer.py | 13 +- src/agents/coder/coder.py | 20 +- src/agents/decision/decision.py | 13 +- src/agents/feature/feature.py | 10 + .../internal_monologue/internal_monologue.py | 17 +- src/agents/patcher/patcher.py | 10 + src/agents/researcher/prompt.jinja2 | 25 +-- src/agents/researcher/researcher.py | 13 +- src/agents/runner/runner.py | 30 +-- src/config.py | 56 +++--- src/filesystem/read_code.py | 6 + src/llm/claude_client.py | 1 + src/llm/g4f_client.py | 24 +++ src/llm/gemini_client.py | 3 +- src/llm/groq_client.py | 1 + src/llm/llm.py | 55 +++++- src/llm/mistral_client.py | 3 +- src/llm/ollama_client.py | 3 +- src/llm/openai_client.py | 1 + src/services/utils.py | 67 +++++++ src/state.py | 26 +++ start.cmd | 58 ++++++ ui/bun.lockb | Bin 88169 -> 89311 bytes ui/package.json | 2 + ui/src/app.pcss | 1 + ui/src/lib/api.js | 9 + ui/src/lib/components/ControlPanel.svelte | 10 +- ui/src/lib/components/EditorWidget.svelte | 99 ++++++++++ ui/src/lib/components/MessageContainer.svelte | 12 +- ui/src/lib/components/MessageInput.svelte | 19 +- ui/src/lib/components/MonacoEditor.js | 142 ++++++++++++++ ui/src/lib/icons.js | 2 + ui/src/lib/store.js | 3 +- ui/src/routes/+layout.svelte | 2 +- ui/src/routes/+page.svelte | 43 ++--- ui/src/routes/settings/+page.svelte | 175 +++++++++++++++++- 41 files changed, 805 insertions(+), 234 deletions(-) delete mode 100644 app.dockerfile create mode 100644 src/llm/g4f_client.py create mode 100644 start.cmd create mode 100644 ui/src/lib/components/EditorWidget.svelte create mode 100644 ui/src/lib/components/MonacoEditor.js diff --git a/app.dockerfile b/app.dockerfile deleted file mode 100644 index 693addb6..00000000 --- a/app.dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM debian:12 - -# setting up build variable -ARG VITE_API_BASE_URL -ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} - -# setting up os env -USER root -WORKDIR /home/nonroot/client -RUN groupadd -r nonroot && useradd -r -g nonroot -d /home/nonroot/client -s /bin/bash nonroot - -# install node js -RUN apt-get update && apt-get upgrade -y -RUN apt-get install -y build-essential software-properties-common curl sudo wget git -RUN curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - -RUN apt-get install nodejs - -# copying devika app client only -COPY ui /home/nonroot/client/ui -COPY src /home/nonroot/client/src -COPY config.toml /home/nonroot/client/ - -RUN cd ui && npm install && npm install -g npm && npm install -g bun -RUN chown -R nonroot:nonroot /home/nonroot/client - -USER nonroot -WORKDIR /home/nonroot/client/ui - -ENTRYPOINT [ "npx", "bun", "run", "dev", "--", "--host" ] \ No newline at end of file diff --git a/devika.py b/devika.py index eef55428..900a4873 100644 --- a/devika.py +++ b/devika.py @@ -92,8 +92,6 @@ def handle_message(data): emit_agent("info", {"type": "warning", "message": "previous agent doesn't completed it's task."}) last_state = AgentState.get_latest_state(project_name) if last_state["agent_is_active"] or not last_state["completed"]: - # emit_agent("info", {"type": "info", "message": "I'm trying to complete the previous task again."}) - # message = manager.get_latest_message_from_user(project_name) thread = Thread(target=lambda: agent.execute(message, project_name)) thread.start() else: @@ -118,6 +116,14 @@ def get_agent_state(): return jsonify({"state": agent_state}) +@app.route("/api/get-project-files/", methods=["GET"]) +@route_logger(logger) +def project_files(): + project_name = request.args.get("project_name") + files = AgentState.get_project_files(project_name) + return jsonify({"files": files}) + + @app.route("/api/get-browser-snapshot", methods=["GET"]) @route_logger(logger) def browser_snapshot(): @@ -186,9 +192,7 @@ def real_time_logs(): @route_logger(logger) def set_settings(): data = request.json - print("Data: ", data) - config.config.update(data) - config.save_config() + config.update_config(data) return jsonify({"message": "Settings updated"}) @@ -200,8 +204,9 @@ def get_settings(): @app.route("/api/status", methods=["GET"]) +@route_logger(logger) def status(): - return jsonify({"status": "server is running!"}), 200 + return jsonify({"status": "server is running!"}) if __name__ == "__main__": logger.info("Devika is up and running!") diff --git a/requirements.txt b/requirements.txt index d2bdc164..41325ee3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,5 @@ duckduckgo-search orjson gevent gevent-websocket +g4f[all] +nodriver diff --git a/sample.config.toml b/sample.config.toml index 81b50da3..d123297d 100644 --- a/sample.config.toml +++ b/sample.config.toml @@ -26,3 +26,7 @@ OPENAI = "https://api.openai.com/v1" [LOGGING] LOG_REST_API = "true" LOG_PROMPTS = "false" + +[CUSTOM] +BLACKLIST_FOLDER = "node_modules, libs, package-lock.json, bun.lockb" +TIMEOUT_INFERENCE = 60 diff --git a/src/agents/action/action.py b/src/agents/action/action.py index ec1bd603..f7563ec4 100644 --- a/src/agents/action/action.py +++ b/src/agents/action/action.py @@ -2,7 +2,7 @@ from jinja2 import Environment, BaseLoader -from src.services.utils import retry_wrapper +from src.services.utils import retry_wrapper, validate_responses from src.config import Config from src.llm import LLM @@ -24,17 +24,8 @@ def render( conversation=conversation ) + @validate_responses def validate_response(self, response: str): - response = response.strip().replace("```json", "```") - - if response.startswith("```") and response.endswith("```"): - response = response[3:-3].strip() - - try: - response = json.loads(response) - except Exception as _: - return False - if "response" not in response and "action" not in response: return False else: diff --git a/src/agents/answer/answer.py b/src/agents/answer/answer.py index 6e9186ac..c6bcbed2 100644 --- a/src/agents/answer/answer.py +++ b/src/agents/answer/answer.py @@ -2,7 +2,7 @@ from jinja2 import Environment, BaseLoader -from src.services.utils import retry_wrapper +from src.services.utils import retry_wrapper, validate_responses from src.config import Config from src.llm import LLM @@ -25,17 +25,8 @@ def render( code_markdown=code_markdown ) + @validate_responses def validate_response(self, response: str): - response = response.strip().replace("```json", "```") - - if response.startswith("```") and response.endswith("```"): - response = response[3:-3].strip() - - try: - response = json.loads(response) - except Exception as _: - return False - if "response" not in response: return False else: diff --git a/src/agents/coder/coder.py b/src/agents/coder/coder.py index b6c6ee04..310a399d 100644 --- a/src/agents/coder/coder.py +++ b/src/agents/coder/coder.py @@ -9,6 +9,7 @@ from src.state import AgentState from src.logger import Logger from src.services.utils import retry_wrapper +from src.socket_instance import emit_agent PROMPT = open("src/agents/coder/prompt.jinja2", "r").read().strip() @@ -69,13 +70,13 @@ def save_code_to_project(self, response: List[Dict[str, str]], project_name: str project_name = project_name.lower().replace(" ", "-") for file in response: - file_path = f"{self.project_dir}/{project_name}/{file['file']}" - file_path_dir = file_path[:file_path.rfind("/")] + file_path = os.path.join(self.project_dir, project_name, file['file']) + file_path_dir = os.path.dirname(file_path) os.makedirs(file_path_dir, exist_ok=True) - - with open(file_path, "w") as f: - f.write(file["code"]) + with open(file_path, "w", encoding="utf-8") as f: + f.write(file["code"]) + return file_path_dir def get_project_path(self, project_name: str): @@ -87,6 +88,7 @@ def response_to_markdown_prompt(self, response: List[Dict[str, str]]) -> str: return f"~~~\n{response}\n~~~" def emulate_code_writing(self, code_set: list, project_name: str): + files = [] for current_file in code_set: file = current_file["file"] code = current_file["code"] @@ -98,8 +100,16 @@ def emulate_code_writing(self, code_set: list, project_name: str): new_state["terminal_session"]["title"] = f"Editing {file}" new_state["terminal_session"]["command"] = f"vim {file}" new_state["terminal_session"]["output"] = code + files.append({ + "file": file, + "code": code + }) AgentState().add_to_current_state(project_name, new_state) time.sleep(2) + emit_agent("code", { + "files": files, + "from": "coder" + }) @retry_wrapper def execute( diff --git a/src/agents/decision/decision.py b/src/agents/decision/decision.py index 923c93d0..304d2ffe 100644 --- a/src/agents/decision/decision.py +++ b/src/agents/decision/decision.py @@ -2,7 +2,7 @@ from jinja2 import Environment, BaseLoader -from src.services.utils import retry_wrapper +from src.services.utils import retry_wrapper, validate_responses from src.llm import LLM PROMPT = open("src/agents/decision/prompt.jinja2").read().strip() @@ -16,17 +16,8 @@ def render(self, prompt: str) -> str: template = env.from_string(PROMPT) return template.render(prompt=prompt) + @validate_responses def validate_response(self, response: str): - response = response.strip().replace("```json", "```") - - if response.startswith("```") and response.endswith("```"): - response = response[3:-3].strip() - - try: - response = json.loads(response) - except Exception as _: - return False - for item in response: if "function" not in item or "args" not in item or "reply" not in item: return False diff --git a/src/agents/feature/feature.py b/src/agents/feature/feature.py index c410b800..99bcf30b 100644 --- a/src/agents/feature/feature.py +++ b/src/agents/feature/feature.py @@ -8,6 +8,7 @@ from src.llm import LLM from src.state import AgentState from src.services.utils import retry_wrapper +from src.socket_instance import emit_agent PROMPT = open("src/agents/feature/prompt.jinja2", "r").read().strip() @@ -85,6 +86,7 @@ def response_to_markdown_prompt(self, response: List[Dict[str, str]]) -> str: return f"~~~\n{response}\n~~~" def emulate_code_writing(self, code_set: list, project_name: str): + files = [] for file in code_set: filename = file["file"] code = file["code"] @@ -94,8 +96,16 @@ def emulate_code_writing(self, code_set: list, project_name: str): new_state["terminal_session"]["title"] = f"Editing {filename}" new_state["terminal_session"]["command"] = f"vim {filename}" new_state["terminal_session"]["output"] = code + files.append({ + "file": filename, + "code": code, + }) AgentState().add_to_current_state(project_name, new_state) time.sleep(1) + emit_agent("code", { + "files": files, + "from": "feature" + }) @retry_wrapper def execute( diff --git a/src/agents/internal_monologue/internal_monologue.py b/src/agents/internal_monologue/internal_monologue.py index e1d95030..5faf1ad2 100644 --- a/src/agents/internal_monologue/internal_monologue.py +++ b/src/agents/internal_monologue/internal_monologue.py @@ -3,7 +3,7 @@ from jinja2 import Environment, BaseLoader from src.llm import LLM -from src.services.utils import retry_wrapper +from src.services.utils import retry_wrapper, validate_responses PROMPT = open("src/agents/internal_monologue/prompt.jinja2").read().strip() @@ -16,19 +16,10 @@ def render(self, current_prompt: str) -> str: template = env.from_string(PROMPT) return template.render(current_prompt=current_prompt) + @validate_responses def validate_response(self, response: str): - response = response.strip().replace("```json", "```") - - if response.startswith("```") and response.endswith("```"): - response = response[3:-3].strip() - - try: - response = json.loads(response) - except Exception as _: - return False - - response = {k.replace("\\", ""): v for k, v in response.items()} - + print('-------------------> ', response) + print("####", type(response)) if "internal_monologue" not in response: return False else: diff --git a/src/agents/patcher/patcher.py b/src/agents/patcher/patcher.py index c6553938..06d2e9e3 100644 --- a/src/agents/patcher/patcher.py +++ b/src/agents/patcher/patcher.py @@ -3,6 +3,7 @@ from jinja2 import Environment, BaseLoader from typing import List, Dict, Union +from src.socket_instance import emit_agent from src.config import Config from src.llm import LLM @@ -87,6 +88,7 @@ def response_to_markdown_prompt(self, response: List[Dict[str, str]]) -> str: return f"~~~\n{response}\n~~~" def emulate_code_writing(self, code_set: list, project_name: str): + files = [] for current_file in code_set: file = current_file["file"] code = current_file["code"] @@ -96,8 +98,16 @@ def emulate_code_writing(self, code_set: list, project_name: str): new_state["terminal_session"]["title"] = f"Editing {file}" new_state["terminal_session"]["command"] = f"vim {file}" new_state["terminal_session"]["output"] = code + files.append({ + "file": file, + "code": code + }) AgentState().add_to_current_state(project_name, new_state) time.sleep(1) + emit_agent("code", { + "files": files, + "from": "patcher" + }) @retry_wrapper def execute( diff --git a/src/agents/researcher/prompt.jinja2 b/src/agents/researcher/prompt.jinja2 index 19ff7189..7da77c0a 100644 --- a/src/agents/researcher/prompt.jinja2 +++ b/src/agents/researcher/prompt.jinja2 @@ -11,18 +11,20 @@ Only respond in the following JSON format: ``` { - "queries": [ - "", - "" - ], - "ask_user": "" + "queries": ["", "", "", ... ], + "ask_user": "" +} +``` +Example => +``` +{ + "queries": ["How to do Bing Search via API in Python", "Claude API Documentation Python"], + "ask_user": "Can you please provide API Keys for Claude, OpenAI, and Firebase?" } ``` Keywords for Search Query: {{ contextual_keywords }} -Example "queries": ["How to do Bing Search via API in Python", "Claude API Documentation Python"] -Example "ask_user": "Can you please provide API Keys for Claude, OpenAI, and Firebase?" Rules: - Only search for a maximum of 3 queries. @@ -33,13 +35,6 @@ Rules: - Do not search for basic queries, only search for advanced and specific queries. You are allowed to leave the "queries" field empty if no search queries are needed for the step. - DO NOT EVER SEARCH FOR BASIC QUERIES. ONLY SEARCH FOR ADVANCED QUERIES. - YOU ARE ALLOWED TO LEAVE THE "queries" FIELD EMPTY IF NO SEARCH QUERIES ARE NEEDED FOR THE STEP. - -Remember to only make search queries for resources that might require external information (like Documentation or a Blog or an Article). If the information is already known to you or commonly known, there is no need to search for it. - -The `queries` key and the `ask_user` key can be empty list and string respectively if no search queries or user input are needed for the step. Try to keep the number of search queries to a minimum to save context window. One query per subject. - -Only search for documentation or articles that are relevant to the task at hand. Do not search for general information. - -Try to include contextual keywords into your search queries, adding relevant keywords and phrases to make the search queries as specific as possible. +- you only have to return one JSON object with the queries and ask_user fields. You can't return multiple JSON objects. Only the provided JSON response format is accepted. Any other response format will be rejected. \ No newline at end of file diff --git a/src/agents/researcher/researcher.py b/src/agents/researcher/researcher.py index 529b01ed..cec2b2ff 100644 --- a/src/agents/researcher/researcher.py +++ b/src/agents/researcher/researcher.py @@ -4,7 +4,7 @@ from jinja2 import Environment, BaseLoader from src.llm import LLM -from src.services.utils import retry_wrapper +from src.services.utils import retry_wrapper, validate_responses from src.browser.search import BingSearch PROMPT = open("src/agents/researcher/prompt.jinja2").read().strip() @@ -23,17 +23,8 @@ def render(self, step_by_step_plan: str, contextual_keywords: str) -> str: contextual_keywords=contextual_keywords ) + @validate_responses def validate_response(self, response: str) -> dict | bool: - response = response.strip().replace("```json", "```") - - if response.startswith("```") and response.endswith("```"): - response = response[3:-3].strip() - try: - response = json.loads(response) - except Exception as _: - return False - - response = {k.replace("\\", ""): v for k, v in response.items()} if "queries" not in response and "ask_user" not in response: return False diff --git a/src/agents/runner/runner.py b/src/agents/runner/runner.py index 291ea609..9a594eb0 100644 --- a/src/agents/runner/runner.py +++ b/src/agents/runner/runner.py @@ -10,7 +10,7 @@ from src.llm import LLM from src.state import AgentState from src.project import ProjectManager -from src.services.utils import retry_wrapper +from src.services.utils import retry_wrapper, validate_responses PROMPT = open("src/agents/runner/prompt.jinja2", "r").read().strip() RERUNNER_PROMPT = open("src/agents/runner/rerunner.jinja2", "r").read().strip() @@ -52,37 +52,15 @@ def render_rerunner( error=error ) + @validate_responses def validate_response(self, response: str): - response = response.strip().replace("```json", "```") - - if response.startswith("```") and response.endswith("```"): - response = response[3:-3].strip() - - try: - response = json.loads(response) - except Exception as _: - return False - if "commands" not in response: return False else: return response["commands"] - + + @validate_responses def validate_rerunner_response(self, response: str): - response = response.strip().replace("```json", "```") - - if response.startswith("```") and response.endswith("```"): - response = response[3:-3].strip() - - print(response) - - try: - response = json.loads(response) - except Exception as _: - return False - - print(response) - if "action" not in response and "response" not in response: return False else: diff --git a/src/config.py b/src/config.py index 9e1d37be..ba1c76e7 100644 --- a/src/config.py +++ b/src/config.py @@ -14,8 +14,10 @@ def __new__(cls): def _load_config(self): # If the config file doesn't exist, copy from the sample if not os.path.exists("config.toml"): - with open("sample.config.toml", "r") as f_in, open("config.toml", "w") as f_out: + with open("sample.config.toml", "r") as f_in, open("config.toml", "w+") as f_out: f_out.write(f_in.read()) + f_out.seek(0) + self.config = toml.load(f_out) else: # check if all the keys are present in the config file with open("sample.config.toml", "r") as f: @@ -35,7 +37,8 @@ def _load_config(self): toml.dump(config, f) f.truncate() - self.config = config + self.config = config + def get_config(self): return self.config @@ -96,6 +99,12 @@ def get_logs_dir(self): def get_repos_dir(self): return self.config["STORAGE"]["REPOS_DIR"] + def get_blacklist_dir(self): + return self.config["CUSTOM"]["BLACKLIST_FOLDER"] + + def get_timeout_inference(self): + return self.config["CUSTOM"]["TIMEOUT_INFERENCE"] + def get_logging_rest_api(self): return self.config["LOGGING"]["LOG_REST_API"] == "true" @@ -154,30 +163,6 @@ def set_netlify_api_key(self, key): self.config["API_KEYS"]["NETLIFY"] = key self.save_config() - def set_sqlite_db(self, db): - self.config["STORAGE"]["SQLITE_DB"] = db - self.save_config() - - def set_screenshots_dir(self, dir): - self.config["STORAGE"]["SCREENSHOTS_DIR"] = dir - self.save_config() - - def set_pdfs_dir(self, dir): - self.config["STORAGE"]["PDFS_DIR"] = dir - self.save_config() - - def set_projects_dir(self, dir): - self.config["STORAGE"]["PROJECTS_DIR"] = dir - self.save_config() - - def set_logs_dir(self, dir): - self.config["STORAGE"]["LOGS_DIR"] = dir - self.save_config() - - def set_repos_dir(self, dir): - self.config["STORAGE"]["REPOS_DIR"] = dir - self.save_config() - def set_logging_rest_api(self, value): self.config["LOGGING"]["LOG_REST_API"] = "true" if value else "false" self.save_config() @@ -186,6 +171,25 @@ def set_logging_prompts(self, value): self.config["LOGGING"]["LOG_PROMPTS"] = "true" if value else "false" self.save_config() + def set_blacklist_folder(self, dir): + self.config["CUSTOM"]["BLACKLIST_FOLDER"] = dir + self.save_config() + + def set_timeout_inference(self, value): + self.config["CUSTOM"]["TIMEOUT_INFERENCE"] = value + self.save_config() + def save_config(self): with open("config.toml", "w") as f: toml.dump(self.config, f) + + def update_config(self, data): + for key, value in data.items(): + if key in self.config: + with open("config.toml", "r+") as f: + config = toml.load(f) + for sub_key, sub_value in value.items(): + self.config[key][sub_key] = sub_value + config[key][sub_key] = sub_value + f.seek(0) + toml.dump(config, f) diff --git a/src/filesystem/read_code.py b/src/filesystem/read_code.py index 71b76f7f..e3fd587d 100644 --- a/src/filesystem/read_code.py +++ b/src/filesystem/read_code.py @@ -10,14 +10,20 @@ class ReadCode: def __init__(self, project_name: str): config = Config() project_path = config.get_projects_dir() + blacklist_dir = config.get_blacklist_dir() self.directory_path = os.path.join(project_path, project_name.lower().replace(" ", "-")) + self.blacklist_dirs = [dir.strip() for dir in blacklist_dir.split(', ')] def read_directory(self): files_list = [] + for root, _dirs, files in os.walk(self.directory_path): for file in files: try: file_path = os.path.join(root, file) + if any(blacklist_dir in file_path for blacklist_dir in self.blacklist_dirs): + print(f"SKIPPED FILE: {file_path}") + continue with open(file_path, 'r') as file_content: files_list.append({"filename": file_path, "code": file_content.read()}) except: diff --git a/src/llm/claude_client.py b/src/llm/claude_client.py index 61bdd98a..68fa6eba 100644 --- a/src/llm/claude_client.py +++ b/src/llm/claude_client.py @@ -20,6 +20,7 @@ def inference(self, model_id: str, prompt: str) -> str: } ], model=model_id, + temperature=0 ) return message.content[0].text diff --git a/src/llm/g4f_client.py b/src/llm/g4f_client.py new file mode 100644 index 00000000..e2b757d3 --- /dev/null +++ b/src/llm/g4f_client.py @@ -0,0 +1,24 @@ +from g4f.client import Client as g4f +import asyncio + +from src.config import Config + +# adding g4f- in and removing it while calling inference is needed because if i put the same model name in llm.py as an other model it won't work +class GPT4FREE: + def __init__(self): + config = Config() + self.client = g4f() + + def inference(self, model_id: str, prompt: str) -> str: + model_id = model_id.replace("g4f-", "") + chat_completion = self.client.chat.completions.create( + model=model_id, + messages=[ + { + "role": "user", + "content": prompt.strip(), + } + ], + temperature=0 + ) + return chat_completion.choices[0].message.content diff --git a/src/llm/gemini_client.py b/src/llm/gemini_client.py index dd2b220e..0d566673 100644 --- a/src/llm/gemini_client.py +++ b/src/llm/gemini_client.py @@ -10,7 +10,8 @@ def __init__(self): genai.configure(api_key=api_key) def inference(self, model_id: str, prompt: str) -> str: - model = genai.GenerativeModel(model_id) + config = genai.GenerationConfig(temperature=0) + model = genai.GenerativeModel(model_id, generation_config=config) # Set safety settings for the request safety_settings = { HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, diff --git a/src/llm/groq_client.py b/src/llm/groq_client.py index cd2ea5ea..828c9cc6 100644 --- a/src/llm/groq_client.py +++ b/src/llm/groq_client.py @@ -18,6 +18,7 @@ def inference(self, model_id: str, prompt: str) -> str: } ], model=model_id, + temperature=0 ) return chat_completion.choices[0].message.content diff --git a/src/llm/llm.py b/src/llm/llm.py index 06b7ac95..1d55f6a5 100644 --- a/src/llm/llm.py +++ b/src/llm/llm.py @@ -1,11 +1,14 @@ import sys import tiktoken +import asyncio +from asyncio import WindowsSelectorEventLoopPolicy from typing import List, Tuple from src.socket_instance import emit_agent from .ollama_client import Ollama from .claude_client import Claude +from .g4f_client import GPT4FREE from .openai_client import OpenAi from .gemini_client import Gemini from .mistral_client import MistralAi @@ -19,15 +22,35 @@ TIKTOKEN_ENC = tiktoken.get_encoding("cl100k_base") ollama = Ollama() +gpt4f = GPT4FREE() logger = Logger() agentState = AgentState() +config = Config() +# asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) class LLM: def __init__(self, model_id: str = None): self.model_id = model_id - self.log_prompts = Config().get_logging_prompts() + self.log_prompts = config.get_logging_prompts() + self.timeout_inference = config.get_timeout_inference() self.models = { + "GPT4FREE": [ + ("Free GPT-4 Turbo", "g4f-gpt-4-turbo"), + ("Free GPT-4", "g4f-gpt-4"), + ("Free GPT-3.5 Turbo", "g4f-gpt-3.5-turbo-16k"), + ("Free GPT-3.5", "g4f-gpt-3.5-long"), + ("Free Llama3 70b", "g4f-llama3-70b"), + ("Free Llama3 8b", "g4f-llama3-8b"), + ("Free Llama3 70b Instruct", "g4f-llama3-70b-instruct"), + ("Free Llama3 8b Instruct", "g4f-llama3-8b-instruct"), + ("Free Mixtral 8x7B", "g4f-mixtral-8x7b"), + ("Free Gemini", "g4f-gemini"), + ("Free Gemini Pro", "g4f-gemini-pro"), + ("Free Claude 3 Sonnet", "g4f-claude-3-sonnet"), + ("Free Claude 3 Opus", "g4f-claude-3-opus"), + ("Free Openchat 3.5", "g4f-openchat_3.5"), + ], "CLAUDE": [ ("Claude 3 Opus", "claude-3-opus-20240229"), ("Claude 3 Sonnet", "claude-3-sonnet-20240229"), @@ -80,6 +103,14 @@ def update_global_token_usage(string: str, project_name: str): def inference(self, prompt: str, project_name: str) -> str: self.update_global_token_usage(prompt, project_name) + if sys.platform == 'win32': + try: + asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + except ImportError: + asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) + print("WindowsSelectorEventLoopPolicy not available, using default event loop policy.") + else: + asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) model_enum, model_name = self.model_enum(self.model_id) @@ -89,6 +120,7 @@ def inference(self, prompt: str, project_name: str) -> str: model_mapping = { "OLLAMA": ollama, + "GPT4FREE": gpt4f, "CLAUDE": Claude(), "OPENAI": OpenAi(), "GOOGLE": Gemini(), @@ -106,22 +138,24 @@ def inference(self, prompt: str, project_name: str) -> str: with concurrent.futures.ThreadPoolExecutor() as executor: future = executor.submit(model.inference, model_name, prompt) try: - while future.running(): + while True: elapsed_time = time.time() - start_time - emit_agent("inference", {"type": "time", "elapsed_time": format(elapsed_time, ".2f")}) - if int(elapsed_time) == 30: + elapsed_seconds = format(elapsed_time, ".2f") + emit_agent("inference", {"type": "time", "elapsed_time": elapsed_seconds}) + if int(elapsed_time) == 5: emit_agent("inference", {"type": "warning", "message": "Inference is taking longer than expected"}) - if elapsed_time > 60: + if elapsed_time > self.timeout_inference: raise concurrent.futures.TimeoutError - time.sleep(1) - - response = future.result(timeout=60).strip() + if future.done(): + break + time.sleep(0.5) + + response = future.result(timeout=self.timeout_inference).strip() except concurrent.futures.TimeoutError: - logger.error(f"Inference took too long. Model: {model_enum}, Model ID: {self.model_id}") + logger.error(f"Inference failed. took too long. Model: {model_enum}, Model ID: {self.model_id}") emit_agent("inference", {"type": "error", "message": "Inference took too long. Please try again."}) response = False - logger.warning("Inference failed") sys.exit() except Exception as e: @@ -138,5 +172,6 @@ def inference(self, prompt: str, project_name: str) -> str: logger.debug(f"Response ({model}): --> {response}") self.update_global_token_usage(response, project_name) + asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) return response diff --git a/src/llm/mistral_client.py b/src/llm/mistral_client.py index 8ccbdf0d..ee019c42 100644 --- a/src/llm/mistral_client.py +++ b/src/llm/mistral_client.py @@ -16,6 +16,7 @@ def inference(self, model_id: str, prompt: str) -> str: model=model_id, messages=[ ChatMessage(role="user", content=prompt.strip()) - ] + ], + temperature=0 ) return chat_completion.choices[0].message.content diff --git a/src/llm/ollama_client.py b/src/llm/ollama_client.py index 95602036..95b1e365 100644 --- a/src/llm/ollama_client.py +++ b/src/llm/ollama_client.py @@ -19,6 +19,7 @@ def __init__(self): def inference(self, model_id: str, prompt: str) -> str: response = self.client.generate( model=model_id, - prompt=prompt.strip() + prompt=prompt.strip(), + options={"temperature": 0} ) return response['response'] diff --git a/src/llm/openai_client.py b/src/llm/openai_client.py index b6cfe203..17344c05 100644 --- a/src/llm/openai_client.py +++ b/src/llm/openai_client.py @@ -19,5 +19,6 @@ def inference(self, model_id: str, prompt: str) -> str: } ], model=model_id, + temperature=0 ) return chat_completion.choices[0].message.content diff --git a/src/services/utils.py b/src/services/utils.py index 6a85c82a..6678168e 100644 --- a/src/services/utils.py +++ b/src/services/utils.py @@ -1,6 +1,9 @@ # create wrapper function that will has retry logic of 5 times import sys import time +from functools import wraps +import json + from src.socket_instance import emit_agent def retry_wrapper(func): @@ -21,3 +24,67 @@ def wrapper(*args, **kwargs): return False return wrapper + + +class InvalidResponseError(Exception): + pass + +def validate_responses(func): + @wraps(func) + def wrapper(*args, **kwargs): + args = list(args) + response = args[1] + response = response.strip() + + try: + response = json.loads(response) + print("first", type(response)) + args[1] = response + return func(*args, **kwargs) + + except json.JSONDecodeError: + pass + + try: + response = response.split("```")[1] + if response: + response = json.loads(response.strip()) + print("second", type(response)) + args[1] = response + return func(*args, **kwargs) + + except (IndexError, json.JSONDecodeError): + pass + + try: + start_index = response.find('{') + end_index = response.rfind('}') + if start_index != -1 and end_index != -1: + json_str = response[start_index:end_index+1] + try: + response = json.loads(json_str) + print("third", type(response)) + args[1] = response + return func(*args, **kwargs) + + except json.JSONDecodeError: + pass + except json.JSONDecodeError: + pass + + for line in response.splitlines(): + try: + response = json.loads(line) + print("fourth", type(response)) + args[1] = response + return func(*args, **kwargs) + + except json.JSONDecodeError: + pass + + # If all else fails, raise an exception + emit_agent("info", {"type": "error", "message": "Failed to parse response as JSON"}) + # raise InvalidResponseError("Failed to parse response as JSON") + return False + + return wrapper \ No newline at end of file diff --git a/src/state.py b/src/state.py index 44677fc1..5aefdecd 100644 --- a/src/state.py +++ b/src/state.py @@ -1,4 +1,5 @@ import json +import os from datetime import datetime from typing import Optional from sqlmodel import Field, Session, SQLModel, create_engine @@ -173,3 +174,28 @@ def get_latest_token_usage(self, project: str): if agent_state: return json.loads(agent_state.state_stack_json)[-1]["token_usage"] return 0 + + def get_project_files(self, project_name: str): + if not project_name: + return [] + project_directory = "-".join(project_name.split(" ")) + directory = os.path.join(os.getcwd(), 'data', 'projects', project_directory) + if(not os.path.exists(directory)): + return [] + files = [] + for root, _, filenames in os.walk(directory): + for filename in filenames: + file_relative_path = os.path.relpath(root, directory) + if file_relative_path == '.': file_relative_path = '' + file_path = os.path.join(file_relative_path, filename) + print("file_path",file_path) + try: + with open(os.path.join(root, filename), 'r') as file: + print("File:", filename) + files.append({ + "file": file_path, + "code": file.read() + }) + except Exception as e: + print(f"Error reading file {filename}: {e}") + return files \ No newline at end of file diff --git a/start.cmd b/start.cmd new file mode 100644 index 00000000..d6756737 --- /dev/null +++ b/start.cmd @@ -0,0 +1,58 @@ +@echo off + +rem Check if python is installed +python --version | findstr /R "Python 3\.[1-9][0-9]*\." >nul +if %errorlevel% neq 0 ( + echo Python is not installed, downloading Python 3.10.11... + PowerShell.exe -Command "irm https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe -OutFile python-3.10.11-amd64.exe" + echo Download of Python 3.10.11 completed. + + echo Installing Python 3.10.11... + python-3.10.11-amd64.exe /quiet InstallAllUsers=1 InstallLauncherAllUsers=1 PrependPath=1 Include_test=0 + echo Python 3.10.11 has been installed successfully. +) else ( + echo Python already installed. +) + +where bun >nul 2>nul +if %errorlevel% neq 0 ( + echo Installing Bun. Accept Administrator request + PowerShell.exe -Command "Start-Process PowerShell -Verb RunAs -ArgumentList '-Command', 'irm bun.sh/install.ps1 | iex' -Wait" + echo Bun is installed. +) else ( + echo Bun is already installed. +) + +where uv >nul 2>nul +if %errorlevel% neq 0 ( + echo Installing Uv. Accept Administrator request + PowerShell.exe -Command "Start-Process PowerShell -Verb RunAs -ArgumentList '-Command', 'irm https://astral.sh/uv/install.ps1 | iex' -Wait" + echo Uv is installed. +) else ( + echo Uv is already installed. +) + +rem Check if the virtual environment exists +if not exist .venv ( + echo Creating virtual environment... + uv venv +) + +rem Activate the virtual environment +echo Activating virtual environment... +start cmd /k ".venv\Scripts\activate & echo Installing Python dependencies... & uv pip install -r requirements.txt & playwright install & echo Starting AI server... & python devika.py" + +rem Navigate to the UI directory +cd ui/ + +rem Install frontend dependencies +echo Installing frontend dependencies... +bun install + +rem Launch the UI +echo Launching UI... +bun run start + +rem Deactivate the virtual environment +echo Deactivating virtual environment... +deactivate \ No newline at end of file diff --git a/ui/bun.lockb b/ui/bun.lockb index 38bfc46404f64b01a00550def664563ce41c662d..b15943b04f71c41f4811e708bc03f7af10557cc2 100755 GIT binary patch delta 17762 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 D&Q&ug diff --git a/ui/package.json b/ui/package.json index f3b046d8..45373478 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,10 +9,12 @@ "preview": "vite preview" }, "devDependencies": { + "@monaco-editor/loader": "^1.4.0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.2", "autoprefixer": "^10.4.16", + "monaco-editor": "^0.48.0", "postcss": "^8.4.32", "postcss-load-config": "^5.0.2", "svelte": "^4.2.7", diff --git a/ui/src/app.pcss b/ui/src/app.pcss index b7bae3a6..7ff61672 100644 --- a/ui/src/app.pcss +++ b/ui/src/app.pcss @@ -92,6 +92,7 @@ /* WebKit (Chrome, Safari) */ *::-webkit-scrollbar { width: 5px; + height: 2px } *::-webkit-scrollbar-thumb { background: #999797; diff --git a/ui/src/lib/api.js b/ui/src/lib/api.js index ed75fe75..6472be2e 100644 --- a/ui/src/lib/api.js +++ b/ui/src/lib/api.js @@ -4,6 +4,7 @@ import { modelList, projectList, messages, + projectFiles, searchEngineList, } from "./store"; import { io } from "socket.io-client"; @@ -125,6 +126,14 @@ export async function getBrowserSnapshot(snapshotPath) { return data.snapshot; } +export async function fetchProjectFiles() { + const projectName = localStorage.getItem("selectedProject"); + const response = await fetch(`${API_BASE_URL}/api/get-project-files?project_name=${projectName}`) + const data = await response.json(); + projectFiles.set(data.files); + return data.files; +} + export async function checkInternetStatus() { if (navigator.onLine) { internet.set(true); diff --git a/ui/src/lib/components/ControlPanel.svelte b/ui/src/lib/components/ControlPanel.svelte index 529e597b..751f8ee7 100644 --- a/ui/src/lib/components/ControlPanel.svelte +++ b/ui/src/lib/components/ControlPanel.svelte @@ -1,13 +1,14 @@ + + +
+
+
+
+
+
+
+
+ {#if Object.keys(models).length == 0} +
Code viewer
+ {/if} +
+
+
+
+
+
\ No newline at end of file diff --git a/ui/src/lib/components/MessageContainer.svelte b/ui/src/lib/components/MessageContainer.svelte index 90249f5b..1cdb4f4b 100644 --- a/ui/src/lib/components/MessageContainer.svelte +++ b/ui/src/lib/components/MessageContainer.svelte @@ -6,11 +6,13 @@ let previousMessageCount = 0; afterUpdate(() => { - if ($messages && $messages.length > previousMessageCount) { - messageContainer.scrollTop = messageContainer.scrollHeight; - previousMessageCount = $messages.length; - } - }); + if ($messages && $messages.length > 0) { + messageContainer.scrollTo({ + top: messageContainer.scrollHeight, + behavior: "smooth" + }); + } +}); diff --git a/ui/src/lib/components/MessageInput.svelte b/ui/src/lib/components/MessageInput.svelte index d3839d64..2b541c3f 100644 --- a/ui/src/lib/components/MessageInput.svelte +++ b/ui/src/lib/components/MessageInput.svelte @@ -5,12 +5,17 @@ import { onMount } from "svelte"; import { Icons } from "../icons"; - let isAgentActive = false; let inference_time = 0; - if ($agentState !== null) { - isAgentActive = $agentState.agent_is_active; - } + agentState.subscribe((value) => { + if (value !== null && value.agent_is_active == false) { + isSending.set(false); + } + if (value == null){ + inference_time = 0; + } + }); + let messageInput = ""; async function handleSendMessage() { const projectName = localStorage.getItem("selectedProject"); @@ -34,12 +39,6 @@ project_name: projectName, search_engine: serachEngine, }); - console.log({ - message: messageInput, - base_model: selectedModel, - project_name: projectName, - search_engine: serachEngine, - }); messageInput = ""; } diff --git a/ui/src/lib/components/MonacoEditor.js b/ui/src/lib/components/MonacoEditor.js new file mode 100644 index 00000000..b1530b25 --- /dev/null +++ b/ui/src/lib/components/MonacoEditor.js @@ -0,0 +1,142 @@ +import loader from "@monaco-editor/loader"; +import { Icons } from "../icons"; + +function getFileLanguage(fileType) { + const fileTypeToLanguage = { + js: "javascript", + jsx: "javascript", + ts: "typescript", + tsx: "typescript", + html: "html", + css: "css", + py: "python", + java: "java", + rb: "ruby", + php: "php", + cpp: "c++", + c: "c", + swift: "swift", + kt: "kotlin", + json: "json", + xml: "xml", + sql: "sql", + sh: "shell", + }; + const language = fileTypeToLanguage[fileType.toLowerCase()]; + return language; +} + +const getTheme = () => { + const theme = localStorage.getItem("mode-watcher-mode"); + return theme === "light" ? "vs-light" : "vs-dark"; +}; + +export async function initializeMonaco() { + const monacoEditor = await import("monaco-editor"); + loader.config({ monaco: monacoEditor.default }); + return loader.init(); +} + +export async function initializeEditorRef(monaco, container) { + const editor = monaco.editor.create(container, { + theme: getTheme(), + readOnly: false, + automaticLayout: true, + }); + return editor; +} + +export function createModel(monaco, file) { + const model = monaco.editor.createModel( + file.code, + getFileLanguage(file.file.split(".").pop()) + ); + return model; +} + +export function disposeEditor(editor) { + if(editor) editor.dispose(); +} + +export function enableTabSwitching(editor, models, tabContainer) { + tabContainer.innerHTML = ""; + Object.keys(models).forEach((filename, index) => { + const tabElement = document.createElement("div"); + tabElement.textContent = filename.split("/").pop(); + tabElement.className = "tab p-2 me-2 rounded-lg text-sm cursor-pointer hover:bg-secondary text-primary whitespace-nowrap"; + tabElement.setAttribute("data-filename", filename); + tabElement.addEventListener("click", () => + switchTab(editor, models, filename, tabElement) + ); + if (index === Object.keys(models).length - 1) { + tabElement.classList.add("bg-secondary"); + } + tabContainer.appendChild(tabElement); + }); +} + +function switchTab(editor, models, filename, tabElement) { + Object.entries(models).forEach(([file, model]) => { + if (file === filename) { + editor.setModel(model); + } + }); + + const allTabElements = tabElement?.parentElement?.children; + for (let i = 0; i < allTabElements?.length; i++) { + allTabElements[i].classList.remove("bg-secondary"); + } + + tabElement.classList.add("bg-secondary"); +} + +export function sidebar(editor, models, sidebarContainer) { + sidebarContainer.innerHTML = ""; + const createSidebarElement = (filename, isFolder) => { + const sidebarElement = document.createElement("div"); + sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer"); + if (isFolder) { + sidebarElement.innerHTML = `

${Icons.Folder}${" "}${filename}

`; + // TODO implement folder collapse/expand to the element sidebarElement + } else { + sidebarElement.innerHTML = `

${Icons.File}${" "}${filename}

`; + } + return sidebarElement; + }; + + const changeTabColor = (index) => { + const allTabElements = document.querySelectorAll("#tabContainer")[0].children; + for (let i = 0; i < allTabElements?.length; i++) { + allTabElements[i].classList.remove("bg-secondary"); + } + allTabElements[index].classList.add("bg-secondary"); + } + + const folders = {}; + + Object.entries(models).forEach(([filename, model], modelIndex) => { + const parts = filename.split('/'); + let currentFolder = sidebarContainer; + + parts.forEach((part, index) => { + if (index === parts.length - 1) { + const fileElement = createSidebarElement(part, false); + fileElement.addEventListener("click", () => { + editor.setModel(model); + changeTabColor(modelIndex); + }); + currentFolder.appendChild(fileElement); + } else { + const folderName = part; + if (!folders[folderName]) { + const folderElement = createSidebarElement(part, true); + currentFolder.appendChild(folderElement); + folders[folderName] = folderElement; + currentFolder = folderElement; + } else { + currentFolder = folders[folderName]; + } + } + }); + }); +} \ No newline at end of file diff --git a/ui/src/lib/icons.js b/ui/src/lib/icons.js index 2f638446..fefc01bf 100644 --- a/ui/src/lib/icons.js +++ b/ui/src/lib/icons.js @@ -6,4 +6,6 @@ export const Icons = { ChevronDown : '', Check : '', CornerDownLeft : '', + Folder: '', + File: '', }; \ No newline at end of file diff --git a/ui/src/lib/store.js b/ui/src/lib/store.js index ded66c10..1df56112 100644 --- a/ui/src/lib/store.js +++ b/ui/src/lib/store.js @@ -24,6 +24,7 @@ export const internet = writable(true); // Message related stores export const messages = writable([]); +export const projectFiles = writable(null); // Selection related stores export const selectedProject = writable(''); @@ -44,4 +45,4 @@ export const agentState = writable(null); export const isSending = writable(false); // Token usage store -export const tokenUsage = writable(0); \ No newline at end of file +export const tokenUsage = writable(0); diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index a8b64d08..bf7e24ac 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -7,7 +7,7 @@
- + diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte index 5c402b28..0793500a 100644 --- a/ui/src/routes/+page.svelte +++ b/ui/src/routes/+page.svelte @@ -7,13 +7,13 @@ import MessageInput from "$lib/components/MessageInput.svelte"; import BrowserWidget from "$lib/components/BrowserWidget.svelte"; import TerminalWidget from "$lib/components/TerminalWidget.svelte"; + import EditorWidget from "../lib/components/EditorWidget.svelte"; import * as Resizable from "$lib/components/ui/resizable/index.js"; import { serverStatus } from "$lib/store"; import { initializeSockets, destroySockets } from "$lib/sockets"; import { checkInternetStatus, checkServerStatus } from "$lib/api"; - let resizeEnabled = localStorage.getItem("resize") && localStorage.getItem("resize") === "enable"; @@ -22,7 +22,7 @@ const load = async () => { await checkInternetStatus(); - if(!await checkServerStatus()) { + if(!(await checkServerStatus())) { toast.error("Failed to connect to server"); return; } @@ -36,35 +36,22 @@ }); -
+
- - +
+
- - {#if resizeEnabled} - - {/if} - - - -
- -
-
- - -
- -
-
-
-
- -
+
+ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/ui/src/routes/settings/+page.svelte b/ui/src/routes/settings/+page.svelte index 45faa626..c243a46e 100644 --- a/ui/src/routes/settings/+page.svelte +++ b/ui/src/routes/settings/+page.svelte @@ -4,7 +4,8 @@ import * as Tabs from "$lib/components/ui/tabs"; import { setMode } from "mode-watcher"; import * as Select from "$lib/components/ui/select/index.js"; - import Seperator from "../../lib/components/ui/Seperator.svelte"; + import Seperator from "../../lib/components/ui/Seperator.svelte"; + import { toast } from "svelte-sonner"; let settings = {}; let editMode = false; @@ -55,15 +56,22 @@ }; // make a copy of the original settings original = JSON.parse(JSON.stringify(settings)); + }); const save = async () => { let updated = {}; for (let key in settings) { - if (settings[key] !== original[key]) { - updated[key] = settings[key]; + for (let subkey in settings[key]) { + if (settings[key][subkey] !== original[key][subkey]) { + if (!updated[key]) { + updated[key] = {}; + } + updated[key][subkey] = settings[key][subkey]; + } } } + await updateSettings(updated); editMode = !editMode; @@ -84,9 +92,16 @@ API Keys API Endpoints + Config Appearance + + + {#if !settings["API_KEYS"] && !settings["API_ENDPOINTS"] && !settings["CUSTOM"]} + An error occured, Devika couldn't fetch the config from server, please make sure you are running Devika Server! + {/if} + {#if settings["API_KEYS"]}
@@ -96,8 +111,9 @@

{key.toLowerCase()}

settings["API_KEYS"][key] = e.target.value} name={key} class="p-2 border-2 w-1/2 rounded-lg {editMode ? '' @@ -114,7 +130,7 @@ {#if !editMode}
- {#if settings["API_KEYS"]} + {#if settings["API_ENDPOINTS"]}
{#each Object.entries(settings["API_ENDPOINTS"]) as [key, value]} @@ -141,7 +157,8 @@

{key.toLowerCase()}

settings["API_ENDPOINTS"][key] = e.target.value} name={key} class="p-2 border-2 w-1/2 rounded-lg {editMode ? '' @@ -153,6 +170,67 @@
{/if} +
+ {#if !editMode} + + {:else} + + {/if} +
+
+ + {#if settings["CUSTOM"]} +
+
+
+
+

+ Blacklist files and folders context +

+ +
+
+

+ Timeout Inference +

+ +
+
+
+
+ {/if}
{#if !editMode}
+
+
+ Reset layout +
+
+ +
+
+
+ + {#if settings["CUSTOM"]} +
+
+
+
+

+ Blacklist files and folders context +

+ +
+
+

+ Timeout Inference +

+ +
+
+
+
+ {/if} +
+ {#if !editMode} + + {:else} + + {/if} +
+
From bf0ebab63d495dc6add149361f2cdfe789e89da7 Mon Sep 17 00:00:00 2001 From: Reiko Date: Mon, 6 May 2024 00:58:10 +0200 Subject: [PATCH 2/8] Fix duplicate Rebase --- ui/src/routes/settings/+page.svelte | 138 ---------------------------- 1 file changed, 138 deletions(-) diff --git a/ui/src/routes/settings/+page.svelte b/ui/src/routes/settings/+page.svelte index 83a214bb..dbc50da1 100644 --- a/ui/src/routes/settings/+page.svelte +++ b/ui/src/routes/settings/+page.svelte @@ -192,144 +192,6 @@ {/if}
- - {#if settings["CUSTOM"]} -
-
-
-
-

- Blacklist files and folders context -

- -
-
-

- Timeout Inference -

- -
-
-
-
- {/if} -
- {#if !editMode} - - {:else} - - {/if} -
-
- - {#if settings["TIMEOUT"]} -
- -
-
- Timouts -
-
- {#each Object.entries(settings["TIMEOUT"]) as [key, value]} -
-

{key.toLowerCase()}

- -
- {/each} -
-
- -
-
- Logging -
-
- {#each Object.entries(settings["LOGGING"]) as [key, value]} -
-

{key.toLowerCase()}

- {settings["LOGGING"][key] = v.value}} - disabled={!editMode}> - - - - - - true - false - - - - -
- {/each} -
-
- -
- {/if} -
- {#if !editMode} - - {:else} - - {/if} -
-
From d37efc170f9f122009a9f40089589b84e153eb52 Mon Sep 17 00:00:00 2001 From: Reiko Date: Mon, 6 May 2024 01:45:54 +0200 Subject: [PATCH 3/8] Update config.py --- src/config.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/config.py b/src/config.py index 4efd0193..ba1c76e7 100644 --- a/src/config.py +++ b/src/config.py @@ -110,9 +110,6 @@ def get_logging_rest_api(self): def get_logging_prompts(self): return self.config["LOGGING"]["LOG_PROMPTS"] == "true" - - def get_timeout_inference(self): - return self.config["TIMEOUT"]["INFERENCE"] def set_bing_api_key(self, key): self.config["API_KEYS"]["BING"] = key From 1fee07b033302bbcc9459dc2036e1bfc508557f9 Mon Sep 17 00:00:00 2001 From: Reiko Date: Wed, 8 May 2024 02:22:47 +0200 Subject: [PATCH 4/8] Folder collapse/expanding + Easy disable context On the Monaco Editor there is now folder that are collapsed and you can expand them! And now you can click on the little eye in the monacoEditor to block the context of the file for the AI --- src/config.py | 4 +- ui/src/app.pcss | 5 ++ ui/src/lib/components/MonacoEditor.js | 121 ++++++++++++++++++++++++-- ui/src/lib/icons.js | 3 + 4 files changed, 122 insertions(+), 11 deletions(-) diff --git a/src/config.py b/src/config.py index ba1c76e7..7ad7f604 100644 --- a/src/config.py +++ b/src/config.py @@ -171,8 +171,8 @@ def set_logging_prompts(self, value): self.config["LOGGING"]["LOG_PROMPTS"] = "true" if value else "false" self.save_config() - def set_blacklist_folder(self, dir): - self.config["CUSTOM"]["BLACKLIST_FOLDER"] = dir + def set_blacklist_folder(self, value): + self.config["CUSTOM"]["BLACKLIST_FOLDER"] = value self.save_config() def set_timeout_inference(self, value): diff --git a/ui/src/app.pcss b/ui/src/app.pcss index 7ff61672..87ffb397 100644 --- a/ui/src/app.pcss +++ b/ui/src/app.pcss @@ -86,6 +86,11 @@ body { @apply bg-background text-foreground; } + + .align-container { + display: flex; + justify-content: space-between; + } /* Styling for scrollbar */ diff --git a/ui/src/lib/components/MonacoEditor.js b/ui/src/lib/components/MonacoEditor.js index b1530b25..e1a63e3b 100644 --- a/ui/src/lib/components/MonacoEditor.js +++ b/ui/src/lib/components/MonacoEditor.js @@ -1,5 +1,32 @@ import loader from "@monaco-editor/loader"; import { Icons } from "../icons"; +import { updateSettings, fetchSettings } from "$lib/api"; + +let settings = {}; +let original = {}; + +const getSettings = async () => { + settings = await fetchSettings(); + original = JSON.parse(JSON.stringify(settings)); +} + +await getSettings(); + +const save = async () => { + let updated = {}; + for (let key in settings) { + for (let subkey in settings[key]) { + if (settings[key][subkey] !== original[key][subkey]) { + if (!updated[key]) { + updated[key] = {}; + } + updated[key][subkey] = settings[key][subkey]; + } + } + } + + await updateSettings(updated); +}; function getFileLanguage(fileType) { const fileTypeToLanguage = { @@ -92,15 +119,21 @@ function switchTab(editor, models, filename, tabElement) { export function sidebar(editor, models, sidebarContainer) { sidebarContainer.innerHTML = ""; - const createSidebarElement = (filename, isFolder) => { + + const createSidebarElement = (filename, isFolder, isAIContext) => { const sidebarElement = document.createElement("div"); - sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer"); + const icon = isAIContext ? Icons.ContextOff : Icons.ContextOn; + const state = isAIContext ? "inactive" : "active"; + if (isFolder) { - sidebarElement.innerHTML = `

${Icons.Folder}${" "}${filename}

`; + sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer"); + sidebarElement.innerHTML = `

${Icons.FolderEditor}${" "}${filename}

${icon}

`; // TODO implement folder collapse/expand to the element sidebarElement } else { - sidebarElement.innerHTML = `

${Icons.File}${" "}${filename}

`; + sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer", "align-container"); + sidebarElement.innerHTML = `

${Icons.File}${" "}${filename}

${icon}

`; } + return sidebarElement; }; @@ -112,24 +145,94 @@ export function sidebar(editor, models, sidebarContainer) { allTabElements[index].classList.add("bg-secondary"); } + const contextToggle = (elementParagraph, name) => { + const state = elementParagraph.getAttribute('data-state'); + + if (state === "inactive") { + elementParagraph.setAttribute('data-state', 'active'); + elementParagraph.innerHTML = `${Icons.ContextOn}`; + + const nameIndex = settings["CUSTOM"]["BLACKLIST_FOLDER"].indexOf(name); + if (nameIndex !== -1) { + settings["CUSTOM"]["BLACKLIST_FOLDER"] = settings["CUSTOM"]["BLACKLIST_FOLDER"].split(', ').filter(item => item !== name).join(', '); + } + } else { + elementParagraph.setAttribute('data-state', 'inactive'); + elementParagraph.innerHTML = `${Icons.ContextOff}`; + + settings["CUSTOM"]["BLACKLIST_FOLDER"] += `, ${name}`; + } + + console.log(settings["CUSTOM"]["BLACKLIST_FOLDER"]); + save(); + } + + const expandFolder = (folder, expand) => { + const elements = document.querySelectorAll(`[id="${folder}"]`); + elements.forEach(element => { + element.style.display = (element.style.display === "none") ? "" : "none"; + }); + } + const folders = {}; + const blacklistDir = settings["CUSTOM"]["BLACKLIST_FOLDER"]; + const blacklistDirs = blacklistDir.split(', ').map(dir => dir.trim()); Object.entries(models).forEach(([filename, model], modelIndex) => { - const parts = filename.split('/'); + const parts = filename.split(/[\/\\]/); let currentFolder = sidebarContainer; + let folderID = "" parts.forEach((part, index) => { + const contextEnable = blacklistDirs.some(dir => part.includes(dir)); + const parentFolder = index !== 0 ? `FOLDER::${parts.slice(0, index).join("/")}` : "" + const actualFile = `FOLDER::${parts.slice(0, index + 1).join("/")}` + if (index === parts.length - 1) { - const fileElement = createSidebarElement(part, false); - fileElement.addEventListener("click", () => { + const fileElement = createSidebarElement(part, false, contextEnable); + const fileElementParagraphs = fileElement.querySelectorAll('p'); + + // What folder is the parent of this file + fileElement.setAttribute("id", parentFolder); + + // Collapse every folder/file + if (index !== 0) { + fileElement.style.display = "none" + } + + fileElementParagraphs[0].addEventListener("click", () => { editor.setModel(model); changeTabColor(modelIndex); }); + + fileElementParagraphs[1].addEventListener("click", () => { + contextToggle(fileElementParagraphs[1], part); + }); currentFolder.appendChild(fileElement); } else { - const folderName = part; + // We get the path of the actual folder with the parent directory, otherwise duplicate folder name will glitch out + const folderName = actualFile; + if (!folders[folderName]) { - const folderElement = createSidebarElement(part, true); + const folderElement = createSidebarElement(part, true, contextEnable); + const folderElementParagraphs = folderElement.querySelectorAll('p'); + + // If it's a sub-directory (Not the first index), we set the id to the previous folder name + folderElement.setAttribute("id", parentFolder); + + // Collapse every folder/file + if (index !== 0) { + folderElement.style.display = "none" + } + + folderElementParagraphs[0].addEventListener("click", () => { + expandFolder(actualFile); + }); + + folderElementParagraphs[1].addEventListener("click", () => { + contextToggle(folderElementParagraphs[1], part); + }); + currentFolder.appendChild(folderElement); folders[folderName] = folderElement; currentFolder = folderElement; diff --git a/ui/src/lib/icons.js b/ui/src/lib/icons.js index fefc01bf..d1136d3d 100644 --- a/ui/src/lib/icons.js +++ b/ui/src/lib/icons.js @@ -8,4 +8,7 @@ export const Icons = { CornerDownLeft : '', Folder: '', File: '', + ContextOn: '', + ContextOff: '', + FolderEditor: '', }; \ No newline at end of file From 66c22509453c40278c2d6ccc5c6744252f057ccb Mon Sep 17 00:00:00 2001 From: Reiko Date: Wed, 8 May 2024 06:28:29 +0200 Subject: [PATCH 5/8] Fixed update_config, optimization, animation Fixed an issue with update_config where Python will glitch out and write random stuff in the config file because the code keeps opening, read, add value, write, close very fast... Now it just write the update in the self.config and THEN save_config Made some optimization on the save func in MonacoEditor Added some smooth animation in the folder collapse/expanding~ --- src/config.py | 10 +--- ui/src/app.pcss | 6 ++ ui/src/lib/components/MonacoEditor.js | 80 +++++++++++++++++---------- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/config.py b/src/config.py index 7ad7f604..3aa682dc 100644 --- a/src/config.py +++ b/src/config.py @@ -186,10 +186,6 @@ def save_config(self): def update_config(self, data): for key, value in data.items(): if key in self.config: - with open("config.toml", "r+") as f: - config = toml.load(f) - for sub_key, sub_value in value.items(): - self.config[key][sub_key] = sub_value - config[key][sub_key] = sub_value - f.seek(0) - toml.dump(config, f) + for sub_key, sub_value in value.items(): + self.config[key][sub_key] = sub_value + self.save_config() diff --git a/ui/src/app.pcss b/ui/src/app.pcss index 87ffb397..c2489e3c 100644 --- a/ui/src/app.pcss +++ b/ui/src/app.pcss @@ -91,6 +91,12 @@ display: flex; justify-content: space-between; } + + .smooth-anim { + transition-duration: 0.2s; + opacity: 1; + transform: translateY(0px); + } /* Styling for scrollbar */ diff --git a/ui/src/lib/components/MonacoEditor.js b/ui/src/lib/components/MonacoEditor.js index e1a63e3b..cb74af8e 100644 --- a/ui/src/lib/components/MonacoEditor.js +++ b/ui/src/lib/components/MonacoEditor.js @@ -2,28 +2,19 @@ import loader from "@monaco-editor/loader"; import { Icons } from "../icons"; import { updateSettings, fetchSettings } from "$lib/api"; -let settings = {}; -let original = {}; +let setting = ""; const getSettings = async () => { - settings = await fetchSettings(); - original = JSON.parse(JSON.stringify(settings)); + const settings = await fetchSettings(); + setting = settings["CUSTOM"]["BLACKLIST_FOLDER"] } await getSettings(); -const save = async () => { +const saveBCKST = async () => { let updated = {}; - for (let key in settings) { - for (let subkey in settings[key]) { - if (settings[key][subkey] !== original[key][subkey]) { - if (!updated[key]) { - updated[key] = {}; - } - updated[key][subkey] = settings[key][subkey]; - } - } - } + updated["CUSTOM"] = {}; + updated["CUSTOM"]["BLACKLIST_FOLDER"] = setting; await updateSettings(updated); }; @@ -126,11 +117,11 @@ export function sidebar(editor, models, sidebarContainer) { const state = isAIContext ? "inactive" : "active"; if (isFolder) { - sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer"); + sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer", "smooth-anim"); sidebarElement.innerHTML = `

${Icons.FolderEditor}${" "}${filename}

${icon}

`; // TODO implement folder collapse/expand to the element sidebarElement } else { - sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer", "align-container"); + sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer", "smooth-anim", "align-container"); sidebarElement.innerHTML = `

${Icons.File}${" "}${filename}

${icon}

`; } @@ -152,30 +143,54 @@ export function sidebar(editor, models, sidebarContainer) { elementParagraph.setAttribute('data-state', 'active'); elementParagraph.innerHTML = `${Icons.ContextOn}`; - const nameIndex = settings["CUSTOM"]["BLACKLIST_FOLDER"].indexOf(name); + const nameIndex = setting.indexOf(name); if (nameIndex !== -1) { - settings["CUSTOM"]["BLACKLIST_FOLDER"] = settings["CUSTOM"]["BLACKLIST_FOLDER"].split(', ').filter(item => item !== name).join(', '); + setting = setting.split(', ').filter(item => item !== name).join(', '); } } else { elementParagraph.setAttribute('data-state', 'inactive'); elementParagraph.innerHTML = `${Icons.ContextOff}`; - settings["CUSTOM"]["BLACKLIST_FOLDER"] += `, ${name}`; + setting += `, ${name}`; } - console.log(settings["CUSTOM"]["BLACKLIST_FOLDER"]); - save(); + saveBCKST(); } - const expandFolder = (folder, expand) => { + // I have put a lot of Timeout and all just for some fancy animation + const expandFolder = (folder) => { const elements = document.querySelectorAll(`[id="${folder}"]`); - elements.forEach(element => { - element.style.display = (element.style.display === "none") ? "" : "none"; + const lengths = elements.length; + + elements.forEach((element, key) => { + if (element.style.display === "none") { + setTimeout(() => { + + element.style.display = ""; + + setTimeout(() => { + element.style.opacity = "1"; + element.style.transform = "translateY(0px)"; + }, 5); + + }, 20 * key); + } else { + setTimeout(() => { + + element.style.opacity = "0"; + element.style.transform = "translateY(-15px)"; + + setTimeout(() => { + element.style.display = "none"; + }, 250); + + }, lengths * 20 - (20 * key)); + } }); } const folders = {}; - const blacklistDir = settings["CUSTOM"]["BLACKLIST_FOLDER"]; + const blacklistDir = setting; const blacklistDirs = blacklistDir.split(', ').map(dir => dir.trim()); Object.entries(models).forEach(([filename, model], modelIndex) => { @@ -185,9 +200,12 @@ export function sidebar(editor, models, sidebarContainer) { parts.forEach((part, index) => { const contextEnable = blacklistDirs.some(dir => part.includes(dir)); + + // Get the entire parent folder and actual path of the file/folder const parentFolder = index !== 0 ? `FOLDER::${parts.slice(0, index).join("/")}` : "" const actualFile = `FOLDER::${parts.slice(0, index + 1).join("/")}` + // If it's the last index, then it's a file, otherwise it's a folder if (index === parts.length - 1) { const fileElement = createSidebarElement(part, false, contextEnable); const fileElementParagraphs = fileElement.querySelectorAll('p'); @@ -195,9 +213,11 @@ export function sidebar(editor, models, sidebarContainer) { // What folder is the parent of this file fileElement.setAttribute("id", parentFolder); - // Collapse every folder/file + // Collapse every folder/file by default if (index !== 0) { fileElement.style.display = "none" + fileElement.style.opacity = "0"; + fileElement.style.transform = "translateY(-15px)"; } fileElementParagraphs[0].addEventListener("click", () => { @@ -220,9 +240,11 @@ export function sidebar(editor, models, sidebarContainer) { // If it's a sub-directory (Not the first index), we set the id to the previous folder name folderElement.setAttribute("id", parentFolder); - // Collapse every folder/file + // Collapse every folder/file by default if (index !== 0) { - folderElement.style.display = "none" + folderElement.style.display = "none"; + folderElement.style.opacity = "0"; + folderElement.style.transform = "translateY(-15px)"; } folderElementParagraphs[0].addEventListener("click", () => { From ccdd6b2ed408e1f2575667b3282b3dd6a3967105 Mon Sep 17 00:00:00 2001 From: kgott Date: Fri, 10 May 2024 15:40:55 +0700 Subject: [PATCH 6/8] Fix list index out of range error (https://github.com/stitionai/devika/issues/546) It seems that the line needed to be split on ":" instead of "`", because there will always be a colon when the line starts with "File: ". --- src/agents/coder/coder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/coder/coder.py b/src/agents/coder/coder.py index 310a399d..3e021d61 100644 --- a/src/agents/coder/coder.py +++ b/src/agents/coder/coder.py @@ -52,7 +52,7 @@ def validate_response(self, response: str) -> Union[List[Dict[str, str]], bool]: if line.startswith("File: "): if current_file and current_code: result.append({"file": current_file, "code": "\n".join(current_code)}) - current_file = line.split("`")[1].strip() + current_file = line.split(":")[1].strip() current_code = [] code_block = False elif line.startswith("```"): From d0c38908daf7a6cea235995e7b455e7c8c3479e8 Mon Sep 17 00:00:00 2001 From: Reiko <34484331+Reiko69420@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:33:03 +0200 Subject: [PATCH 7/8] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 41325ee3..2c10a91f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,5 +30,6 @@ duckduckgo-search orjson gevent gevent-websocket +numpy g4f[all] nodriver From 543e76bbeb1681ed55e69080061de5795ed1f72c Mon Sep 17 00:00:00 2001 From: Reiko <34484331+Reiko69420@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:17:56 +0200 Subject: [PATCH 8/8] Update requirements.txt --- requirements.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2c10a91f..30f799c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,10 @@ flask flask-cors toml +torch=2.3.1 +numpy=1.26.4 +transformers=4.41.2 +sentence-transformers=2.7.0 urllib3 requests colorama @@ -17,7 +21,7 @@ openai anthropic google-generativeai sqlmodel -keybert +keybert=0.8.4 GitPython netlify-py Markdown @@ -30,6 +34,5 @@ duckduckgo-search orjson gevent gevent-websocket -numpy g4f[all] nodriver