diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a4f0b780 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ + +.PHONY = setup deps compose-up compose-down compose-destroy + +# to check if docker is installed on the machine +DOCKER := $(shell command -v docker) +DOCKER_COMPOSE := $(shell command -v docker-compose) +deps: +ifndef DOCKER + @echo "Docker is not available. Please install docker" + @echo "try running sudo apt-get install docker" + @exit 1 +endif +ifndef DOCKER_COMPOSE + @echo "docker-compose is not available. Please install docker-compose" + @echo "try running sudo apt-get install docker-compose" + @exit 1 +endif + +setup: + sh +x build + +compose-down: deps + docker volume ls + docker-compose ps + docker images + docker-compose down; + +compose-up: deps compose-down + docker-compose up --build + +compose-destroy: deps + docker images | grep -i devika | awk '{print $$3}' | xargs docker rmi -f + docker volume prune \ No newline at end of file diff --git a/README.md b/README.md index bf8e9d7f..cc1f5786 100644 --- a/README.md +++ b/README.md @@ -145,12 +145,16 @@ To start using Devika, follow these steps: Devika requires certain configuration settings and API keys to function properly. Update the `config.toml` file with the following information: -- `OPENAI_API_KEY`: Your OpenAI API key for accessing GPT models. -- `CLAUDE_API_KEY`: Your Anthropic API key for accessing Claude models. -- `BING_API_KEY`: Your Bing Search API key for web searching capabilities. -- `DATABASE_URL`: The URL for your database connection. -- `LOG_DIRECTORY`: The directory where Devika's logs will be stored. -- `PROJECT_DIRECTORY`: The directory where Devika's projects will be stored. +- `SQLITE_DB`: The path to the SQLite database file for storing Devika's data. +- `SCREENSHOTS_DIR`: The directory where screenshots captured by Devika will be stored. +- `PDFS_DIR`: The directory where PDF files processed by Devika will be stored. +- `PROJECTS_DIR`: The directory where Devika's projects will be stored. +- `LOGS_DIR`: The directory where Devika's logs will be stored. +- `REPOS_DIR`: The directory where Git repositories cloned by Devika will be stored. +- `BING`: Your Bing Search API key for web searching capabilities. +- `CLAUDE`: Your Anthropic API key for accessing Claude models. +- `NETLIFY`: Your Netlify API key for deploying and managing web projects. +- `OPENAI`: Your OpenAI API key for accessing GPT models. Make sure to keep your API keys secure and do not share them publicly. @@ -217,7 +221,7 @@ To join the Devika community Discord server, [click here](https://discord.com/in ## Contributing -We welcome contributions to enhance Devika's capabilities and improve its performance. To contribute, please see the `CONTRIBUTING.md` file for steps. +We welcome contributions to enhance Devika's capabilities and improve its performance. To contribute, please see the [`CONTRIBUTING.md`](CONTRIBUTING.md) file for steps. ## License diff --git a/app.dockerfile b/app.dockerfile new file mode 100644 index 00000000..67a99764 --- /dev/null +++ b/app.dockerfile @@ -0,0 +1,29 @@ +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 +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/config.toml b/config.toml index 8e45dbcf..6ed36824 100644 --- a/config.toml +++ b/config.toml @@ -11,6 +11,12 @@ BING = "" CLAUDE = "" NETLIFY = "" OPENAI = "" +GROQ = "" [API_ENDPOINTS] BING = "https://api.bing.microsoft.com/v7.0/search" +OLLAMA = "http://127.0.0.1:11434" + +[LOGGING] +LOG_REST_API = "true" +LOG_PROMPTS = "false" \ No newline at end of file diff --git a/devika.dockerfile b/devika.dockerfile new file mode 100644 index 00000000..6dee5a26 --- /dev/null +++ b/devika.dockerfile @@ -0,0 +1,37 @@ +FROM debian:12 + +# setting up os env +USER root +WORKDIR /home/nonroot/devika +RUN groupadd -r nonroot && useradd -r -g nonroot -d /home/nonroot/devika -s /bin/bash nonroot + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 + +# setting up python3 +RUN apt-get update && apt-get upgrade +RUN apt-get install -y build-essential software-properties-common curl sudo wget git +RUN apt-get install -y python3 python3-pip +RUN curl -fsSL https://astral.sh/uv/install.sh | sudo -E bash - +RUN $HOME/.cargo/bin/uv venv +ENV PATH="/home/nonroot/devika/.venv/bin:$HOME/.cargo/bin:$PATH" +RUN echo $PATH + +# copy devika python engine only +RUN $HOME/.cargo/bin/uv venv +COPY requirements.txt /home/nonroot/devika/ +RUN UV_HTTP_TIMEOUT=100000 $HOME/.cargo/bin/uv pip install -r requirements.txt +RUN playwright install --with-deps + +COPY src /home/nonroot/devika/src +COPY config.toml /home/nonroot/devika/ +COPY devika.py /home/nonroot/devika/ +RUN chown -R nonroot:nonroot /home/nonroot/devika +RUN ls -al + +USER nonroot +WORKDIR /home/nonroot/devika +ENV PATH="/home/nonroot/devika/.venv/bin:$HOME/.cargo/bin:$PATH" +RUN mkdir /home/nonroot/devika/db + +ENTRYPOINT [ "python3", "-m", "devika" ] \ No newline at end of file diff --git a/devika.py b/devika.py index 57b5f93c..1beab9e8 100644 --- a/devika.py +++ b/devika.py @@ -160,8 +160,9 @@ def calculate_tokens(): @app.route("/api/token-usage", methods=["GET"]) @route_logger(logger) def token_usage(): - from src.llm import TOKEN_USAGE - return jsonify({"token_usage": TOKEN_USAGE}) + project_name = request.args.get("project_name") + token_count = AgentState().get_latest_token_usage(project_name) + return jsonify({"token_usage": token_count}) @app.route("/api/real-time-logs", methods=["GET"]) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..cb7d06cb --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,61 @@ +version: "3.9" + +services: + ollama-service: + image: ollama/ollama:latest + expose: + - 11434 + ports: + - 11434:11434 + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:11434/ || exit 1"] + interval: 5s + timeout: 30s + retries: 5 + start_period: 30s + networks: + - devika-subnetwork + + devika-backend-engine: + build: + context: . + dockerfile: devika.dockerfile + depends_on: + - ollama-service + expose: + - 1337 + ports: + - 1337:1337 + environment: + - OLLAMA_HOST=http://ollama-service:11434 + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:1337/ || exit 1"] + interval: 5s + timeout: 30s + retries: 5 + start_period: 30s + volumes: + - devika-backend-dbstore:/home/nonroot/devika/db + networks: + - devika-subnetwork + + devika-frontend-app: + build: + context: . + dockerfile: app.dockerfile + args: + - VITE_API_BASE_URL=http://127.0.0.1:1337 + depends_on: + - devika-backend-engine + expose: + - 3000 + ports: + - 3000:3000 + networks: + - devika-subnetwork + +networks: + devika-subnetwork: + +volumes: + devika-backend-dbstore: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 53496f48..65823cdb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,5 @@ keybert GitPython netlify-py Markdown -xhtml2pdf \ No newline at end of file +xhtml2pdf +groq \ No newline at end of file diff --git a/src/agents/action/action.py b/src/agents/action/action.py index f61f7ba0..5544d60c 100644 --- a/src/agents/action/action.py +++ b/src/agents/action/action.py @@ -39,15 +39,15 @@ def validate_response(self, response: str): else: return response["response"], response["action"] - def execute(self, conversation: list) -> str: + def execute(self, conversation: list, project_name: str) -> str: prompt = self.render(conversation) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) while not valid_response: print("Invalid response from the model, trying again...") - return self.execute(conversation) + return self.execute(conversation, project_name) print("===" * 10) print(valid_response) diff --git a/src/agents/agent.py b/src/agents/agent.py index 204360ca..165bf23e 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -93,7 +93,8 @@ def search_queries(self, queries: list, project_name: str) -> dict: Formatter Agent is invoked to format and learn from the contents """ results[query] = self.formatter.execute( - browser.extract_text() + browser.extract_text(), + project_name ) """ @@ -118,7 +119,7 @@ def update_contextual_keywords(self, sentence: str): Decision making Agent """ def make_decision(self, prompt: str, project_name: str) -> str: - decision = self.decision.execute(prompt) + decision = self.decision.execute(prompt, project_name) for item in decision: function = item["function"] @@ -134,7 +135,7 @@ def make_decision(self, prompt: str, project_name: str) -> str: elif function == "generate_pdf_document": user_prompt = args["user_prompt"] # Call the reporter agent to generate the PDF document - markdown = self.reporter.execute([user_prompt], "") + markdown = self.reporter.execute([user_prompt], "", project_name) _out_pdf_file = PDF().markdown_to_pdf(markdown, project_name) project_name_space_url = project_name.replace(" ", "%20") @@ -154,10 +155,10 @@ def make_decision(self, prompt: str, project_name: str) -> str: elif function == "coding_project": user_prompt = args["user_prompt"] # Call the planner, researcher, coder agents in sequence - plan = self.planner.execute(user_prompt) + plan = self.planner.execute(user_prompt, project_name) planner_response = self.planner.parse_response(plan) - research = self.researcher.execute(plan, self.collected_context_keywords) + research = self.researcher.execute(plan, self.collected_context_keywords, project_name) search_results = self.search_queries(research["queries"], project_name) code = self.coder.execute( @@ -177,7 +178,7 @@ def subsequent_execute(self, prompt: str, project_name: str) -> str: conversation = ProjectManager().get_all_messages_formatted(project_name) code_markdown = ReadCode(project_name).code_set_to_markdown() - response, action = self.action.execute(conversation) + response, action = self.action.execute(conversation, project_name) ProjectManager().add_message_from_devika(project_name, response) @@ -188,7 +189,8 @@ def subsequent_execute(self, prompt: str, project_name: str) -> str: if action == "answer": response = self.answer.execute( conversation=conversation, - code_markdown=code_markdown + code_markdown=code_markdown, + project_name=project_name ) ProjectManager().add_message_from_devika(project_name, response) elif action == "run": @@ -238,7 +240,7 @@ def subsequent_execute(self, prompt: str, project_name: str) -> str: self.patcher.save_code_to_project(code, project_name) elif action == "report": - markdown = self.reporter.execute(conversation, code_markdown) + markdown = self.reporter.execute(conversation, code_markdown, project_name) _out_pdf_file = PDF().markdown_to_pdf(markdown, project_name) @@ -261,7 +263,7 @@ def execute(self, prompt: str, project_name_from_user: str = None) -> str: if project_name_from_user: ProjectManager().add_message_from_user(project_name_from_user, prompt) - plan = self.planner.execute(prompt) + plan = self.planner.execute(prompt, project_name_from_user) print(plan) print("=====" * 10) @@ -288,7 +290,7 @@ def execute(self, prompt: str, project_name_from_user: str = None) -> str: self.update_contextual_keywords(focus) print(self.collected_context_keywords) - internal_monologue = self.internal_monologue.execute(current_prompt=plan) + internal_monologue = self.internal_monologue.execute(current_prompt=plan, project_name=project_name) print(internal_monologue) print("=====" * 10) @@ -296,7 +298,7 @@ def execute(self, prompt: str, project_name_from_user: str = None) -> str: new_state["internal_monologue"] = internal_monologue AgentState().add_to_current_state(project_name, new_state) - research = self.researcher.execute(plan, self.collected_context_keywords) + research = self.researcher.execute(plan, self.collected_context_keywords, project_name) print(research) print("=====" * 10) diff --git a/src/agents/answer/answer.py b/src/agents/answer/answer.py index 6c609516..94e2575b 100644 --- a/src/agents/answer/answer.py +++ b/src/agents/answer/answer.py @@ -40,14 +40,14 @@ def validate_response(self, response: str): else: return response["response"] - def execute(self, conversation: list, code_markdown: str) -> str: + def execute(self, conversation: list, code_markdown: str, project_name: str) -> str: prompt = self.render(conversation, code_markdown) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) while not valid_response: print("Invalid response from the model, trying again...") - return self.execute(conversation, code_markdown) + return self.execute(conversation, code_markdown, project_name) return valid_response diff --git a/src/agents/coder/coder.py b/src/agents/coder/coder.py index cd2b0d75..65846449 100644 --- a/src/agents/coder/coder.py +++ b/src/agents/coder/coder.py @@ -102,13 +102,13 @@ def execute( project_name: str ) -> str: prompt = self.render(step_by_step_plan, user_context, search_results) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) while not valid_response: print("Invalid response from the model, trying again...") - return self.execute(step_by_step_plan, user_context, search_results) + return self.execute(step_by_step_plan, user_context, search_results, project_name) print(valid_response) diff --git a/src/agents/decision/decision.py b/src/agents/decision/decision.py index d11dba39..6a984452 100644 --- a/src/agents/decision/decision.py +++ b/src/agents/decision/decision.py @@ -32,14 +32,14 @@ def validate_response(self, response: str): return response - def execute(self, prompt: str) -> str: + def execute(self, prompt: str, project_name: str) -> str: prompt = self.render(prompt) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) while not valid_response: print("Invalid response from the model, trying again...") - return self.execute(prompt) + return self.execute(prompt, project_name) return valid_response \ No newline at end of file diff --git a/src/agents/feature/feature.py b/src/agents/feature/feature.py index fa4a2738..cf4f88ab 100644 --- a/src/agents/feature/feature.py +++ b/src/agents/feature/feature.py @@ -103,7 +103,7 @@ def execute( project_name: str ) -> str: prompt = self.render(conversation, code_markdown, system_os) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) diff --git a/src/agents/formatter/formatter.py b/src/agents/formatter/formatter.py index 9cd062a8..f6e3e7ae 100644 --- a/src/agents/formatter/formatter.py +++ b/src/agents/formatter/formatter.py @@ -16,7 +16,7 @@ def render(self, raw_text: str) -> str: def validate_response(self, response: str) -> bool: return True - def execute(self, raw_text: str) -> str: + def execute(self, raw_text: str, project_name: str) -> str: raw_text = self.render(raw_text) - response = self.llm.inference(raw_text) + response = self.llm.inference(raw_text, project_name) return response \ No newline at end of file diff --git a/src/agents/internal_monologue/internal_monologue.py b/src/agents/internal_monologue/internal_monologue.py index 48460edf..6a81820b 100644 --- a/src/agents/internal_monologue/internal_monologue.py +++ b/src/agents/internal_monologue/internal_monologue.py @@ -33,15 +33,15 @@ def validate_response(self, response: str): else: return response["internal_monologue"] - def execute(self, current_prompt: str) -> str: + def execute(self, current_prompt: str, project_name: str) -> str: current_prompt = self.render(current_prompt) - response = self.llm.inference(current_prompt) + response = self.llm.inference(current_prompt, project_name) valid_response = self.validate_response(response) while not valid_response: print("Invalid response from the model, trying again...") - return self.execute(current_prompt) + return self.execute(current_prompt, project_name) return valid_response diff --git a/src/agents/patcher/patcher.py b/src/agents/patcher/patcher.py index 3be452f2..b3108dd4 100644 --- a/src/agents/patcher/patcher.py +++ b/src/agents/patcher/patcher.py @@ -115,7 +115,7 @@ def execute( error, system_os ) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) diff --git a/src/agents/planner/planner.py b/src/agents/planner/planner.py index 3b77e654..3f241773 100644 --- a/src/agents/planner/planner.py +++ b/src/agents/planner/planner.py @@ -65,7 +65,7 @@ def parse_response(self, response: str): return result - def execute(self, prompt: str) -> str: + def execute(self, prompt: str, project_name: str) -> str: prompt = self.render(prompt) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) return response diff --git a/src/agents/reporter/reporter.py b/src/agents/reporter/reporter.py index 43790f3a..8cfc98de 100644 --- a/src/agents/reporter/reporter.py +++ b/src/agents/reporter/reporter.py @@ -28,16 +28,17 @@ def validate_response(self, response: str): def execute(self, conversation: list, - code_markdown: str + code_markdown: str, + project_name: str ) -> str: prompt = self.render(conversation, code_markdown) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) while not valid_response: print("Invalid response from the model, trying again...") - return self.execute(conversation, code_markdown) + return self.execute(conversation, code_markdown, project_name) return valid_response diff --git a/src/agents/researcher/researcher.py b/src/agents/researcher/researcher.py index 309a8067..c9c25742 100644 --- a/src/agents/researcher/researcher.py +++ b/src/agents/researcher/researcher.py @@ -42,16 +42,16 @@ def validate_response(self, response: str): "ask_user": response["ask_user"] } - def execute(self, step_by_step_plan: str, contextual_keywords: List[str]) -> str: + def execute(self, step_by_step_plan: str, contextual_keywords: List[str], project_name: str) -> str: contextual_keywords = ", ".join(map(lambda k: k.capitalize(), contextual_keywords)) step_by_step_plan = self.render(step_by_step_plan, contextual_keywords) - response = self.llm.inference(step_by_step_plan) + response = self.llm.inference(step_by_step_plan, project_name) valid_response = self.validate_response(response) while not valid_response: print("Invalid response from the model, trying again...") - return self.execute(step_by_step_plan, contextual_keywords) + return self.execute(step_by_step_plan, contextual_keywords, project_name) return valid_response \ No newline at end of file diff --git a/src/agents/runner/runner.py b/src/agents/runner/runner.py index 8e61c3ad..0ec84d4f 100644 --- a/src/agents/runner/runner.py +++ b/src/agents/runner/runner.py @@ -136,7 +136,7 @@ def run_code( error=command_output ) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_rerunner_response(response) @@ -233,7 +233,7 @@ def execute( project_name: str ) -> str: prompt = self.render(conversation, code_markdown, os_system) - response = self.llm.inference(prompt) + response = self.llm.inference(prompt, project_name) valid_response = self.validate_response(response) diff --git a/src/config.py b/src/config.py index 4f0d06bb..063ae8b0 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,16 @@ import toml +from os import environ + class Config: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.config = toml.load("config.toml") + return cls._instance + def __init__(self): self.config = toml.load("config.toml") @@ -8,38 +18,52 @@ def get_config(self): return self.config def get_bing_api_key(self): - return self.config["API_KEYS"]["BING"] - + return environ.get("BING_API_KEY", self.config["API_KEYS"]["BING"]) + def get_bing_api_endpoint(self): - return self.config["API_ENDPOINTS"]["BING"] - + return environ.get("BING_API_ENDPOINT", self.config["API_ENDPOINTS"]["BING"]) + + def get_ollama_api_endpoint(self): + return environ.get( + "OLLAMA_API_ENDPOINT", self.config["API_ENDPOINTS"]["OLLAMA"] + ) + def get_claude_api_key(self): - return self.config["API_KEYS"]["CLAUDE"] - + return environ.get("CLAUDE_API_KEY", self.config["API_KEYS"]["CLAUDE"]) + def get_openai_api_key(self): - return self.config["API_KEYS"]["OPENAI"] - + return environ.get("OPENAI_API_KEY", self.config["API_KEYS"]["OPENAI"]) + def get_netlify_api_key(self): - return self.config["API_KEYS"]["NETLIFY"] + return environ.get("NETLIFY_API_KEY", self.config["API_KEYS"]["NETLIFY"]) + def get_groq_api_key(self): + return environ.get("GROQ_API_KEY", self.config["API_KEYS"]["GROQ"]) + def get_sqlite_db(self): - return self.config["STORAGE"]["SQLITE_DB"] - + return environ.get("SQLITE_DB_PATH", self.config["STORAGE"]["SQLITE_DB"]) + def get_screenshots_dir(self): - return self.config["STORAGE"]["SCREENSHOTS_DIR"] - + return environ.get("SCREENSHOTS_DIR", self.config["STORAGE"]["SCREENSHOTS_DIR"]) + def get_pdfs_dir(self): - return self.config["STORAGE"]["PDFS_DIR"] - + return environ.get("PDFS_DIR", self.config["STORAGE"]["PDFS_DIR"]) + def get_projects_dir(self): - return self.config["STORAGE"]["PROJECTS_DIR"] - + return environ.get("PROJECTS_DIR", self.config["STORAGE"]["PROJECTS_DIR"]) + def get_logs_dir(self): - return self.config["STORAGE"]["LOGS_DIR"] - + return environ.get("LOGS_DIR", self.config["STORAGE"]["LOGS_DIR"]) + def get_repos_dir(self): - return self.config["STORAGE"]["REPOS_DIR"] - + return environ.get("REPOS_DIR", self.config["STORAGE"]["REPOS_DIR"]) + + 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 set_bing_api_key(self, key): self.config["API_KEYS"]["BING"] = key self.save_config() @@ -47,7 +71,11 @@ def set_bing_api_key(self, key): def set_bing_api_endpoint(self, endpoint): self.config["API_ENDPOINTS"]["BING"] = endpoint self.save_config() - + + def set_ollama_api_endpoint(self, endpoint): + self.config["API_ENDPOINTS"]["OLLAMA"] = endpoint + self.save_config() + def set_claude_api_key(self, key): self.config["API_KEYS"]["CLAUDE"] = key self.save_config() @@ -59,11 +87,11 @@ def set_openai_api_key(self, key): 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() @@ -71,7 +99,7 @@ def set_screenshots_dir(self, dir): 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() @@ -84,6 +112,14 @@ 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() + + def set_logging_prompts(self, value): + self.config["LOGGING"]["LOG_PROMPTS"] = "true" if value else "false" + self.save_config() + def save_config(self): with open("config.toml", "w") as f: toml.dump(self.config, f) diff --git a/src/llm/__init__.py b/src/llm/__init__.py index 6c2bf5c1..5ab9f9c6 100644 --- a/src/llm/__init__.py +++ b/src/llm/__init__.py @@ -1 +1 @@ -from .llm import LLM, TOKEN_USAGE \ No newline at end of file +from .llm import LLM \ No newline at end of file diff --git a/src/llm/groq_client.py b/src/llm/groq_client.py new file mode 100644 index 00000000..92fb8400 --- /dev/null +++ b/src/llm/groq_client.py @@ -0,0 +1,24 @@ +from groq import Groq + +from src.config import Config + +class Groq: + def __init__(self, api_key: str): + config = Config() + api_key = config.get_groq_api_key() + self.client = Groq( + api_key=api_key, + ) + + def inference(self, model_id: str, prompt: str) -> str: + chat_completion = self.client.chat.completions.create( + messages=[ + { + "role": "user", + "content": prompt.strip(), + } + ], + model=model_id, + ) + + return chat_completion.choices[0].message.content diff --git a/src/llm/llm.py b/src/llm/llm.py index 4080b5e8..bf04bb89 100644 --- a/src/llm/llm.py +++ b/src/llm/llm.py @@ -1,11 +1,18 @@ from enum import Enum +from typing import List, Tuple from .ollama_client import Ollama from .claude_client import Claude from .openai_client import OpenAI +from .groq_client import Groq + +from src.state import AgentState import tiktoken +from ..config import Config +from ..logger import Logger + TOKEN_USAGE = 0 TIKTOKEN_ENC = tiktoken.get_encoding("cl100k_base") @@ -22,12 +29,17 @@ class Model(Enum): ) for model in Ollama.list_models() ] + GROQ = ("GROQ Mixtral", "mixtral-8x7b-32768") + + +logger = Logger(filename="devika_prompts.log") class LLM: def __init__(self, model_id: str = None): self.model_id = model_id + self.log_prompts = Config().get_logging_prompts() - def list_models(self) -> list[tuple[str, str]]: + def list_models(self) -> List[Tuple[str, str]]: return [model.value for model in Model if model.name != "OLLAMA_MODELS"] + list( Model.OLLAMA_MODELS.value ) @@ -38,27 +50,34 @@ def model_id_to_enum_mapping(self): models.update(ollama_models) return models - def update_global_token_usage(self, string: str): - global TOKEN_USAGE - TOKEN_USAGE += len(TIKTOKEN_ENC.encode(string)) - print(f"Token usage: {TOKEN_USAGE}") + def update_global_token_usage(self, string: str, project_name: str): + token_usage = len(TIKTOKEN_ENC.encode(string)) + AgentState().update_token_usage(project_name, token_usage) def inference( - self, prompt: str + self, prompt: str, project_name: str ) -> str: - self.update_global_token_usage(prompt) + self.update_global_token_usage(prompt, project_name) model = self.model_id_to_enum_mapping()[self.model_id] + if self.log_prompts: + logger.debug(f"Prompt ({model}): --> {prompt}") + if model == "OLLAMA_MODELS": response = Ollama().inference(self.model_id, prompt).strip() elif "CLAUDE" in str(model): response = Claude().inference(self.model_id, prompt).strip() elif "GPT" in str(model): response = OpenAI().inference(self.model_id, prompt).strip() + elif "GROQ" in str(model): + response = Groq().inference(self.model_id, prompt).strip() else: raise ValueError(f"Model {model} not supported") - self.update_global_token_usage(response) + if self.log_prompts: + logger.debug(f"Response ({model}): --> {response}") + + self.update_global_token_usage(response, project_name) return response diff --git a/src/llm/ollama_client.py b/src/llm/ollama_client.py index b5964212..0977dc10 100644 --- a/src/llm/ollama_client.py +++ b/src/llm/ollama_client.py @@ -1,15 +1,19 @@ import httpx -import ollama +from ollama import Client +from src.config import Config from src.logger import Logger logger = Logger() +client = Client(host=Config().get_ollama_api_endpoint()) + + class Ollama: @staticmethod def list_models(): try: - return ollama.list()["models"] + return client.list()["models"] except httpx.ConnectError: logger.warning("Ollama server not running, please start the server to use models from Ollama.") except Exception as e: @@ -22,5 +26,4 @@ def inference(self, model_id: str, prompt: str) -> str: return response['response'] except Exception as e: logger.error(f"Error during model inference: {e}") - return "" - + return "" \ No newline at end of file diff --git a/src/logger.py b/src/logger.py index def8faae..94ae2b09 100644 --- a/src/logger.py +++ b/src/logger.py @@ -6,10 +6,10 @@ from src.config import Config class Logger: - def __init__(self): + def __init__(self, filename="devika_agent.log"): config = Config() logs_dir = config.get_logs_dir() - self.logger = LogInit(pathName=logs_dir + "/devika_agent.log", console=True, colors=True) + self.logger = LogInit(pathName=logs_dir + "/" + filename, console=True, colors=True) def read_log_file(self) -> str: with open(self.logger.pathName, "r") as file: @@ -43,20 +43,25 @@ def route_logger(logger: Logger): :param logger: The logger instance to use for logging. """ + + log_enabled = Config().get_logging_rest_api() + def decorator(func): @wraps(func) def wrapper(*args, **kwargs): # Log entry point - logger.info(f"{request.path} {request.method}") + if log_enabled: + logger.info(f"{request.path} {request.method}") # Call the actual route function response = func(*args, **kwargs) # Log exit point, including response summary if possible try: - response_summary = response.get_data(as_text=True) - logger.debug(f"{request.path} {request.method} - Response: {response_summary}") + if log_enabled: + response_summary = response.get_data(as_text=True) + logger.debug(f"{request.path} {request.method} - Response: {response_summary}") except Exception as e: logger.exception(f"{request.path} {request.method} - {e})") diff --git a/src/state.py b/src/state.py index 323146f8..412b7add 100644 --- a/src/state.py +++ b/src/state.py @@ -132,4 +132,27 @@ def is_agent_completed(self, project: str): agent_state = session.query(AgentStateModel).filter(AgentStateModel.project == project).first() if agent_state: return json.loads(agent_state.state_stack_json)[-1]["completed"] - return None \ No newline at end of file + return None + + def update_token_usage(self, project: str, token_usage: int): + with Session(self.engine) as session: + agent_state = session.query(AgentStateModel).filter(AgentStateModel.project == project).first() + print(agent_state) + if agent_state: + state_stack = json.loads(agent_state.state_stack_json) + state_stack[-1]["token_usage"] += token_usage + agent_state.state_stack_json = json.dumps(state_stack) + session.commit() + else: + state_stack = [self.new_state()] + state_stack[-1]["token_usage"] = token_usage + agent_state = AgentStateModel(project=project, state_stack_json=json.dumps(state_stack)) + session.add(agent_state) + session.commit() + + def get_latest_token_usage(self, project: str): + with Session(self.engine) as session: + agent_state = session.query(AgentStateModel).filter(AgentStateModel.project == project).first() + if agent_state: + return json.loads(agent_state.state_stack_json)[-1]["token_usage"] + return 0 \ No newline at end of file diff --git a/ui/bun.lockb b/ui/bun.lockb index 1be2284e..9204232c 100755 Binary files a/ui/bun.lockb and b/ui/bun.lockb differ diff --git a/ui/package.json b/ui/package.json index 9bf7a67e..02c17b99 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ }, "type": "module", "dependencies": { + "clsx": "^2.1.0", "xterm": "^5.3.0", "xterm-addon-fit": "^0.8.0" } diff --git a/ui/src/lib/api.js b/ui/src/lib/api.js index 1d09e9f4..c812b7b0 100644 --- a/ui/src/lib/api.js +++ b/ui/src/lib/api.js @@ -1,12 +1,12 @@ import { - messages, - projectList, - modelList, agentState, internet, + messages, + modelList, + projectList, } from "./store"; -export const API_BASE_URL = "http://127.0.0.1:1337"; +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://127.0.0.1:1337"; export async function fetchProjectList() { const response = await fetch(`${API_BASE_URL}/api/project-list`); @@ -128,3 +128,21 @@ export async function checkInternetStatus() { internet.set(false); } } + +export async function getSettings() { + const response = await fetch(`${API_BASE_URL}/api/get-settings`); + const data = await response.json(); + return data.settings; +} + +export async function setSettings(newSettings) { + const response = await fetch(`${API_BASE_URL}/api/set-settings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newSettings), + }); + const data = await response.json(); + return data; +} diff --git a/ui/src/lib/components/Sidebar.svelte b/ui/src/lib/components/Sidebar.svelte index b413a8d5..a73b984e 100644 --- a/ui/src/lib/components/Sidebar.svelte +++ b/ui/src/lib/components/Sidebar.svelte @@ -2,16 +2,20 @@ class="flex flex-col w-20 h-dvh fixed top-0 left-0 z-10 bg-slate-950 p-4 space-y-4 items-center shadow-2xl shadow-indigo-700" >