diff --git a/requirements.txt b/requirements.txt index d2bdc164..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,3 +34,5 @@ duckduckgo-search orjson gevent gevent-websocket +g4f[all] +nodriver diff --git a/sample.config.toml b/sample.config.toml index cd796133..d123297d 100644 --- a/sample.config.toml +++ b/sample.config.toml @@ -27,5 +27,6 @@ OPENAI = "https://api.openai.com/v1" LOG_REST_API = "true" LOG_PROMPTS = "false" -[TIMEOUT] -INFERENCE = 60 \ No newline at end of file +[CUSTOM] +BLACKLIST_FOLDER = "node_modules, libs, package-lock.json, bun.lockb" +TIMEOUT_INFERENCE = 60 diff --git a/src/config.py b/src/config.py index 0e6c2249..3aa682dc 100644 --- a/src/config.py +++ b/src/config.py @@ -99,14 +99,17 @@ 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" 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 @@ -168,8 +171,12 @@ def set_logging_prompts(self, value): self.config["LOGGING"]["LOG_PROMPTS"] = "true" if value else "false" self.save_config() + def set_blacklist_folder(self, value): + self.config["CUSTOM"]["BLACKLIST_FOLDER"] = value + self.save_config() + def set_timeout_inference(self, value): - self.config["TIMEOUT"]["INFERENCE"] = value + self.config["CUSTOM"]["TIMEOUT_INFERENCE"] = value self.save_config() def save_config(self): @@ -179,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/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/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/llm.py b/src/llm/llm.py index b7ad6a73..c5cc94cb 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,10 +22,12 @@ 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): @@ -30,6 +35,22 @@ def __init__(self, model_id: str = None): 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"), @@ -83,6 +104,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) @@ -92,6 +121,7 @@ def inference(self, prompt: str, project_name: str) -> str: model_mapping = { "OLLAMA": ollama, + "GPT4FREE": gpt4f, "CLAUDE": Claude(), "OPENAI": OpenAi(), "GOOGLE": Gemini(), @@ -143,5 +173,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/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/src/app.pcss b/ui/src/app.pcss index 7ff61672..c2489e3c 100644 --- a/ui/src/app.pcss +++ b/ui/src/app.pcss @@ -86,6 +86,17 @@ body { @apply bg-background text-foreground; } + + .align-container { + 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 b1530b25..cb74af8e 100644 --- a/ui/src/lib/components/MonacoEditor.js +++ b/ui/src/lib/components/MonacoEditor.js @@ -1,5 +1,23 @@ import loader from "@monaco-editor/loader"; import { Icons } from "../icons"; +import { updateSettings, fetchSettings } from "$lib/api"; + +let setting = ""; + +const getSettings = async () => { + const settings = await fetchSettings(); + setting = settings["CUSTOM"]["BLACKLIST_FOLDER"] +} + +await getSettings(); + +const saveBCKST = async () => { + let updated = {}; + updated["CUSTOM"] = {}; + updated["CUSTOM"]["BLACKLIST_FOLDER"] = setting; + + await updateSettings(updated); +}; function getFileLanguage(fileType) { const fileTypeToLanguage = { @@ -92,15 +110,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", "smooth-anim"); + 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", "smooth-anim", "align-container"); + sidebarElement.innerHTML = `

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

${icon}

`; } + return sidebarElement; }; @@ -112,24 +136,125 @@ 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 = setting.indexOf(name); + if (nameIndex !== -1) { + setting = setting.split(', ').filter(item => item !== name).join(', '); + } + } else { + elementParagraph.setAttribute('data-state', 'inactive'); + elementParagraph.innerHTML = `${Icons.ContextOff}`; + + setting += `, ${name}`; + } + + saveBCKST(); + } + + // I have put a lot of Timeout and all just for some fancy animation + const expandFolder = (folder) => { + const elements = document.querySelectorAll(`[id="${folder}"]`); + 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 = setting; + 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)); + + // 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); - 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 by default + if (index !== 0) { + fileElement.style.display = "none" + fileElement.style.opacity = "0"; + fileElement.style.transform = "translateY(-15px)"; + } + + 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 by default + if (index !== 0) { + folderElement.style.display = "none"; + folderElement.style.opacity = "0"; + folderElement.style.transform = "translateY(-15px)"; + } + + 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 diff --git a/ui/src/routes/settings/+page.svelte b/ui/src/routes/settings/+page.svelte index 3db5ecde..dbc50da1 100644 --- a/ui/src/routes/settings/+page.svelte +++ b/ui/src/routes/settings/+page.svelte @@ -5,7 +5,7 @@ import { setMode } from "mode-watcher"; import * as Select from "$lib/components/ui/select/index.js"; import Seperator from "../../lib/components/ui/Seperator.svelte"; - import { toast } from "svelte-sonner"; + import { toast } from "svelte-sonner"; let settings = {}; let editMode = false; @@ -56,6 +56,7 @@ }; // make a copy of the original settings original = JSON.parse(JSON.stringify(settings)); + }); const save = async () => { @@ -96,6 +97,10 @@ + + {#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"]} @@ -187,83 +192,6 @@ {/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} -
-
@@ -322,6 +250,68 @@
+ + {#if settings["CUSTOM"]} +
+
+
+
+

+ Blacklist files and folders context +

+ +
+
+

+ Timeout Inference +

+ +
+
+
+
+ {/if} +
+ {#if !editMode} + + {:else} + + {/if} +
+
+