/r /tmp/posthog.html' /usr/local/lib/code-server/lib/vscode/out/vs/code/browser/workbench/workbench.html && \
- rm /tmp/posthog.html
-
-RUN chmod +x /entrypoint.sh && \
- chmod +x /var/init_data/on-event-extension-install.sh && \
- chown -R devuser:devusergroup /pythagora && \
- chown -R devuser: /var/init_data/
-
-# Create workspace directory
-RUN mkdir -p ${PYTH_INSTALL_DIR}/pythagora-core/workspace && \
- chown -R devuser:devusergroup ${PYTH_INSTALL_DIR}/pythagora-core/workspace
-
-# Set up git config
-RUN su -c "git config --global user.email 'devuser@pythagora.ai'" devuser && \
- su -c "git config --global user.name 'pythagora'" devuser
-
-# Remove the USER directive to keep root as the running user
-ENTRYPOINT ["/entrypoint.sh"]
\ No newline at end of file
diff --git a/README.md b/README.md
index b97572b1f..24df917f9 100644
--- a/README.md
+++ b/README.md
@@ -44,16 +44,6 @@
---
-
-
-
-
-
-
-GPT Pilot is the core technology for the [Pythagora VS Code extension](https://bit.ly/3IeZxp6) that aims to provide **the first real AI developer companion**. Not just an autocomplete or a helper for PR messages but rather a real AI developer that can write full features, debug them, talk to you about issues, ask for review, etc.
-
----
-
📫 If you would like to get updates on future releases or just get in touch, join our [Discord server](https://discord.gg/HaqXugmxr9) or you [can add your email here](http://eepurl.com/iD6Mpo). 📬
---
@@ -97,9 +87,6 @@ If you are interested in our learnings during this project, you can check [our l
- **Python 3.9+**
# 🚦How to start using gpt-pilot?
-👉 If you are using VS Code as your IDE, the easiest way to start is by downloading [GPT Pilot VS Code extension](https://bit.ly/3IeZxp6). 👈
-
-Otherwise, you can use the CLI tool.
### If you're new to GPT Pilot:
@@ -120,10 +107,37 @@ After you have Python and (optionally) PostgreSQL installed, follow these steps:
All generated code will be stored in the folder `workspace` inside the folder named after the app name you enter upon starting the pilot.
+### If you're upgrading from GPT Pilot v0.1
+
+Assuming you already have the git repository with an earlier version:
+
+1. `git pull` (update the repo)
+2. `source pilot-env/bin/activate` (or on Windows `pilot-env\Scripts\activate`) (activate the virtual environment)
+3. `pip install -r requirements.txt` (install the new dependencies)
+4. `python main.py --import-v0 pilot/gpt-pilot` (this should import your settings and existing projects)
+
+This will create a new database `pythagora.db` and import all apps from the old database. For each app,
+it will import the start of the latest task you were working on.
+
+To verify that the import was successful, you can run `python main.py --list` to see all the apps you have created,
+and check `config.json` to check the settings were correctly converted to the new config file format (and make
+any adjustments if needed).
+
# 🔎 [Examples](https://github.com/Pythagora-io/gpt-pilot/wiki/Apps-created-with-GPT-Pilot)
[Click here](https://github.com/Pythagora-io/gpt-pilot/wiki/Apps-created-with-GPT-Pilot) to see all example apps created with GPT Pilot.
+## 🐳 How to start gpt-pilot in docker?
+1. `git clone https://github.com/Pythagora-io/gpt-pilot.git` (clone the repo)
+2. Update the `docker-compose.yml` environment variables, which can be done via `docker compose config`. If you wish to use a local model, please go to [https://localai.io/basics/getting_started/](https://localai.io/basics/getting_started/).
+3. By default, GPT Pilot will read & write to `~/gpt-pilot-workspace` on your machine, you can also edit this in `docker-compose.yml`
+4. run `docker compose build`. this will build a gpt-pilot container for you.
+5. run `docker compose up`.
+6. access the web terminal on `port 7681`
+7. `python main.py` (start GPT Pilot)
+
+This will start two containers, one being a new image built by the `Dockerfile` and a Postgres database. The new image also has [ttyd](https://github.com/tsl0922/ttyd) installed so that you can easily interact with gpt-pilot. Node is also installed on the image and port 3000 is exposed.
+
### PostgreSQL support
GPT Pilot uses built-in SQLite database by default. If you want to use the PostgreSQL database, you need to additional install `asyncpg` and `psycopg2` packages:
@@ -166,6 +180,14 @@ python main.py --delete
Delete project with the specified `app_id`. Warning: this cannot be undone!
+### Import projects from v0.1
+
+```bash
+python main.py --import-v0
+```
+
+This will import projects from the old GPT Pilot v0.1 database. The path should be the path to the old GPT Pilot v0.1 database. For each project, it will import the start of the latest task you were working on. If the project was already imported, the import procedure will skip it (won't overwrite the project in the database).
+
### Other command-line options
There are several other command-line options that mostly support calling GPT Pilot from our VSCode extension. To see all the available options, use the `--help` flag:
diff --git a/cloud/config-docker.json b/cloud/config-docker.json
deleted file mode 100644
index a6fa608e5..000000000
--- a/cloud/config-docker.json
+++ /dev/null
@@ -1,154 +0,0 @@
-{
- "llm": {
- "openai": {
- "base_url": null,
- "api_key": null,
- "connect_timeout": 60.0,
- "read_timeout": 60.0,
- "extra": null
- },
- "anthropic": {
- "base_url": null,
- "api_key": null,
- "connect_timeout": 60.0,
- "read_timeout": 60.0,
- "extra": null
- },
- "relace": {
- "base_url": null,
- "api_key": null,
- "connect_timeout": 60.0,
- "read_timeout": 60.0,
- "extra": null
- }
- },
- "agent": {
- "default": {
- "provider": "openai",
- "model": "gpt-4o-2024-05-13",
- "temperature": 0.5
- },
- "BugHunter.check_logs": {
- "provider": "openai",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.5
- },
- "CodeMonkey": {
- "provider": "openai",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.0
- },
- "CodeMonkey.code_review": {
- "provider": "openai",
- "model": "claude-3-5-sonnet-20240620",
- "temperature": 0.0
- },
- "CodeMonkey.implement_changes": {
- "provider": "relace",
- "model": "relace-code-merge",
- "temperature": 0.0
- },
- "CodeMonkey.describe_files": {
- "provider": "openai",
- "model": "gpt-4o-mini-2024-07-18",
- "temperature": 0.0
- },
- "Frontend": {
- "provider": "openai",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.0
- },
- "get_relevant_files": {
- "provider": "openai",
- "model": "gpt-4o-2024-05-13",
- "temperature": 0.5
- },
- "Developer.parse_task": {
- "provider": "openai",
- "model": "claude-3-5-sonnet-20241022",
- "temperature": 0.0
- },
- "SpecWriter": {
- "provider": "openai",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.0
- },
- "Developer.breakdown_current_task": {
- "provider": "openai",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.5
- },
- "TechLead.plan_epic": {
- "provider": "openai",
- "model": "claude-3-5-sonnet-20240620",
- "temperature": 0.5
- },
- "TechLead.epic_breakdown": {
- "provider": "openai",
- "model": "claude-3-5-sonnet-20241022",
- "temperature": 0.5
- },
- "Troubleshooter.generate_bug_report": {
- "provider": "openai",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.5
- },
- "Troubleshooter.get_run_command": {
- "provider": "openai",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.0
- },
- "Troubleshooter.define_user_review_goal": {
- "provider": "anthropic",
- "model": "claude-sonnet-4-20250514",
- "temperature": 0.0
- }
- },
- "prompt": {
- "paths": [
- "/pythagora/pythagora-core/core/prompts"
- ]
- },
- "log": {
- "level": "DEBUG",
- "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s",
- "output": "data/pythagora.log"
- },
- "db": {
- "url": "sqlite+aiosqlite:///data/database/pythagora.db",
- "debug_sql": false,
- "save_llm_requests": false
- },
- "ui": {
- "type": "plain"
- },
- "fs": {
- "type": "local",
- "workspace_root": "/pythagora/pythagora-core/workspace",
- "ignore_paths": [
- ".git",
- ".gpt-pilot",
- ".idea",
- ".vscode",
- ".next",
- ".DS_Store",
- "__pycache__",
- "site-packages",
- "node_modules",
- "package-lock.json",
- "venv",
- ".venv",
- "dist",
- "build",
- "target",
- "*.min.js",
- "*.min.css",
- "*.svg",
- "*.csv",
- "*.log",
- "go.sum",
- "migration_lock.toml"
- ],
- "ignore_size_threshold": 50000
- }
-}
diff --git a/cloud/entrypoint.sh b/cloud/entrypoint.sh
deleted file mode 100644
index 091cf7c17..000000000
--- a/cloud/entrypoint.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-
-set -e
-# Production instances are slow with date command and stderr
-# export PS4='+ $(date "+%Y-%m-%d %H:%M:%S") '
-# set -x
-
-echo "TASK: Entrypoint script started"
-# export MONGO_DB_DATA=$PYTHAGORA_DATA_DIR/mongodata
-# mkdir -p $MONGO_DB_DATA
-
-# # Start MongoDB in the background
-# mongod --dbpath "$MONGO_DB_DATA" --bind_ip_all >> $MONGO_DB_DATA/mongo_logs.txt 2>&1 &
-
-# # Loop until MongoDB is running (use pgrep for speed)
-# for ((i=0; i<10*5; i++)); do
-# if pgrep -x mongod > /dev/null; then
-# echo "TASK: MongoDB started"
-# break
-# fi
-# sleep 0.2
-# done
-
-export DB_DIR=$PYTHAGORA_DATA_DIR/database
-
-chown -R devuser: $PYTHAGORA_DATA_DIR
-su -c "mkdir -p $DB_DIR" devuser
-
-# Start the VS Code extension installer/HTTP server script in the background
-su -c "cd /var/init_data/ && ./on-event-extension-install.sh" devuser
-
-# Keep container running
-echo "FINISH: Entrypoint script finished"
-tail -f /dev/null
diff --git a/cloud/favicon.ico b/cloud/favicon.ico
deleted file mode 100644
index 2c8f3fc87..000000000
Binary files a/cloud/favicon.ico and /dev/null differ
diff --git a/cloud/favicon.svg b/cloud/favicon.svg
deleted file mode 100644
index 8c5a9b8a2..000000000
--- a/cloud/favicon.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/cloud/on-event-extension-install.sh b/cloud/on-event-extension-install.sh
deleted file mode 100644
index 51e65d7c4..000000000
--- a/cloud/on-event-extension-install.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-
-set -e
-
-VSCODE_SERVER_PORT=8080
-
-# Create workspace directory and settings
-mkdir -p /pythagora/pythagora-core/workspace/.vscode
-printf '{\n "gptPilot.isRemoteWs": true,\n "gptPilot.useRemoteWs": false,\n "workbench.colorTheme": "Default Dark+",\n "remote.autoForwardPorts": false\n}' > /pythagora/pythagora-core/workspace/.vscode/settings.json
-
-# Start code-server and direct to our workspace
-echo "Starting code-server..."
-code-server --disable-proxy --disable-workspace-trust --config /etc/code-server/config.yaml /pythagora/pythagora-core/workspace &
-CODE_SERVER_PID=$!
-echo $CODE_SERVER_PID > /tmp/vscode-http-server.pid
-
-# Wait for code-server to open the port (e.g., 8080)
-for ((i=0; i<15*2; i++)); do
- if curl -s "http://localhost:$VSCODE_SERVER_PORT/healthz" > /dev/null; then
- echo "TASK: VS Code server started"
- echo "VS Code HTTP server started with PID $CODE_SERVER_PID. Access at http://localhost:$VSCODE_SERVER_PORT"
- break
- fi
- sleep 0.5
-done
diff --git a/cloud/posthog.html b/cloud/posthog.html
deleted file mode 100644
index 30d2ebd5b..000000000
--- a/cloud/posthog.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
\ No newline at end of file
diff --git a/cloud/settings.json b/cloud/settings.json
deleted file mode 100644
index b25fbb226..000000000
--- a/cloud/settings.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "workbench.startupEditor": "none",
- "workbench.statusBar.visible": false,
- "workbench.editor.showTabs": "none",
- "chat.commandCenter.enabled": false,
- "window.commandCenter": false,
- "workbench.colorCustomizations": {
- "commandCenter.background": "#0B0912",
- "activityBar.foreground": "#F7F8F8",
- "activityBar.background": "#0B0912",
- "commandCenter.foreground": "#F7F8F8",
- "commandCenter.activeBackground": "#0B0912",
- "titleBar.activeBackground": "#0B0912",
- "titleBar.inactiveBackground": "#0B0912",
- "sideBarSectionHeader.background": "#0B0912",
- "sideBarSectionHeader.foreground": "#F7F8F8",
- "editorGroupHeader.tabsBackground": "#0B0912",
- "tab.activeBackground": "#0B0912",
- "tab.inactiveBackground": "#0B0912",
- "tab.activeForeground": "#F7F8F8",
- "tab.inactiveForeground": "#F7F8F8",
- "editorGroup.emptyBackground": "#0B0912",
- "editor.background": "#0B0912",
- "sideBar.background": "#0B0912",
- "editorGroup.border": "#0B0912"
- }
-}
diff --git a/cloud/setup-dependencies.sh b/cloud/setup-dependencies.sh
deleted file mode 100644
index 92348810c..000000000
--- a/cloud/setup-dependencies.sh
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/bin/bash
-set -e
-
-# Set environment variables
-export DEBIAN_FRONTEND=noninteractive
-export TZ=Etc/UTC
-
-# IMPORTANT: Create a dummy update-ca-certificates script that does nothing
-# This completely bypasses the problematic ca-certificates update process
-echo "Creating dummy update-ca-certificates script"
-if [ -f /usr/sbin/update-ca-certificates ]; then
- mv /usr/sbin/update-ca-certificates /usr/sbin/update-ca-certificates.orig
-fi
-cat > /usr/sbin/update-ca-certificates << 'EOF'
-#!/bin/sh
-echo "Dummy update-ca-certificates called, doing nothing"
-exit 0
-EOF
-chmod +x /usr/sbin/update-ca-certificates
-
-# Update package list and install prerequisites
-echo "Installing basic packages"
-apt-get update && apt-get install -y --no-install-recommends \
- software-properties-common \
- build-essential \
- curl \
- git \
- gnupg \
- tzdata \
- inotify-tools \
- vim \
- nano \
- lsof \
- procps \
- ca-certificates
-echo "Basic packages installed successfully"
-rm -rf /var/lib/apt/lists/*
-
-# Install Python 3.12
-echo "Adding Python 3.12 PPA"
-add-apt-repository ppa:deadsnakes/ppa -y && apt-get update
-echo "Installing Python 3.12"
-apt-get install -y --no-install-recommends \
- python3.12 \
- python3.12-venv \
- python3.12-dev \
- python3-pip
-echo "Python 3.12 installed successfully"
-rm -rf /var/lib/apt/lists/*
-
-# Set Python 3.12 as default
-echo "Setting Python 3.12 as default"
-update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1
-update-alternatives --install /usr/bin/python python /usr/bin/python3 1
-python --version
-
-# Install Node.js
-echo "Installing Node.js"
-curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
-apt-get install -y nodejs
-node --version && npm --version
-
-# Install MongoDB based on platform architecture
-echo "Installing MongoDB"
-case "$TARGETPLATFORM" in
- "linux/amd64")
- MONGO_ARCH="amd64"
- ;;
- "linux/arm64"|"linux/arm64/v8")
- MONGO_ARCH="arm64"
- ;;
- *)
- echo "Using default architecture amd64 for MongoDB"
- MONGO_ARCH="amd64"
- ;;
-esac
-
-curl -fsSL https://www.mongodb.org/static/pgp/server-6.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg
-echo "deb [arch=$MONGO_ARCH signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list
-apt-get update && apt-get install -y mongodb-org
-echo "MongoDB installed successfully"
-rm -rf /var/lib/apt/lists/*
-
-# Install code-server
-echo "Installing code-server"
-VERSION="4.97.2"
-case "$TARGETPLATFORM" in
- "linux/amd64")
- PLATFORM="amd64"
- ;;
- "linux/arm64"|"linux/arm64/v8")
- PLATFORM="arm64"
- ;;
- *)
- echo "Using default platform amd64 for code-server"
- PLATFORM="amd64"
- ;;
-esac
-
-DOWNLOAD_URL="https://github.com/coder/code-server/releases/download/v${VERSION}/code-server-${VERSION}-linux-${PLATFORM}.tar.gz"
-echo "Downloading code-server from $DOWNLOAD_URL"
-curl -L "$DOWNLOAD_URL" -o /tmp/code-server.tar.gz
-
-# Create directories and install
-mkdir -p /usr/local/lib/code-server
-mkdir -p /usr/local/bin
-mkdir -p /usr/local/share/code-server/extensions
-mkdir -p /usr/local/share/code-server/data
-mkdir -p /etc/code-server
-
-# Install code-server
-tar -xzf /tmp/code-server.tar.gz -C /usr/local/lib/code-server --strip-components=1
-ln -s /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server
-rm /tmp/code-server.tar.gz
-echo "code-server installed successfully"
-
-# Create default config
-cat > /etc/code-server/config.yaml << EOF
-bind-addr: 0.0.0.0:8080
-auth: none
-extensions-dir: /usr/local/share/code-server/extensions
-user-data-dir: /usr/local/share/code-server/data
-EOF
-
-# Pre-install extension
-echo "Installing VS Code extension..."
-code-server --config /etc/code-server/config.yaml --install-extension /var/init_data/pythagora-vs-code.vsix || {
- echo "Extension installation failed but continuing build process..."
-}
-
-# Restore original update-ca-certificates if it exists
-if [ -f /usr/sbin/update-ca-certificates.orig ]; then
- mv /usr/sbin/update-ca-certificates.orig /usr/sbin/update-ca-certificates
- echo "Restored original update-ca-certificates"
-fi
-
-echo "Setup completed successfully"
\ No newline at end of file
diff --git a/core/agents/architect.py b/core/agents/architect.py
index 196c3c7e0..c1691d4ca 100644
--- a/core/agents/architect.py
+++ b/core/agents/architect.py
@@ -1,4 +1,3 @@
-import json
from enum import Enum
from typing import Any, Optional
@@ -17,6 +16,7 @@
PROJECT_TEMPLATES,
ProjectTemplateEnum,
)
+from core.ui.base import ProjectStage
ARCHITECTURE_STEP_NAME = "Project architecture"
WARN_SYSTEM_DEPS = ["docker", "kubernetes", "microservices"]
@@ -34,8 +34,8 @@ class AppType(str, Enum):
CLI = "cli-tool"
-# FIXME: all the response pydantic models should be strict (see config._StrictModel), also check if we
-# can disallow adding custom Python attributes to the model
+# FIXME: all the reponse pydantic models should be strict (see config._StrictModel), also check if we
+# can disallow adding custom Python attributes to the model
class SystemDependency(BaseModel):
name: str = Field(
None,
@@ -97,6 +97,8 @@ class Architect(BaseAgent):
display_name = "Architect"
async def run(self) -> AgentResponse:
+ await self.ui.send_project_stage(ProjectStage.ARCHITECTURE)
+
spec = self.current_state.specification.clone()
if spec.example_project:
@@ -109,16 +111,6 @@ async def run(self) -> AgentResponse:
self.next_state.specification = spec
telemetry.set("templates", spec.templates)
self.next_state.action = ARCHITECTURE_STEP_NAME
- await self.ui.send_back_logs(
- [
- {
- "title": "Setting up backend",
- "project_state_id": "be_0",
- "disallow_reload": True,
- "labels": ["E2 / T2", "Backend setup", "done"],
- }
- ]
- )
return AgentResponse.done(self)
async def select_templates(self, spec: Specification) -> tuple[str, dict[ProjectTemplateEnum, Any]]:
@@ -145,28 +137,28 @@ async def select_templates(self, spec: Specification) -> tuple[str, dict[Project
)
tpl: TemplateSelection = await llm(convo, parser=JSONParser(TemplateSelection))
templates = {}
- # if tpl.template:
- # answer = await self.ask_question(
- # f"Do you want to use the '{tpl.template.name}' template?",
- # buttons={"yes": "Yes", "no": "No"},
- # default="yes",
- # buttons_only=True,
- # hint="Project templates are here to speed up start of your app development and save tokens and time.\n"
- # "Choose 'Yes' to use suggested template for your app.\n"
- # "If you choose 'No', project will be created from scratch.",
- # )
- #
- # if answer.button == "no":
- # return tpl.architecture, templates
- #
- # template_class = PROJECT_TEMPLATES.get(tpl.template)
- # if template_class:
- # options = await self.configure_template(spec, template_class)
- # templates[tpl.template] = template_class(
- # options,
- # self.state_manager,
- # self.process_manager,
- # )
+ if tpl.template:
+ answer = await self.ask_question(
+ f"Do you want to use the '{tpl.template.name}' template?",
+ buttons={"yes": "Yes", "no": "No"},
+ default="yes",
+ buttons_only=True,
+ hint="Project templates are here to speed up start of your app development and save tokens and time.\n"
+ "Choose 'Yes' to use suggested template for your app.\n"
+ "If you choose 'No', project will be created from scratch.",
+ )
+
+ if answer.button == "no":
+ return tpl.architecture, templates
+
+ template_class = PROJECT_TEMPLATES.get(tpl.template)
+ if template_class:
+ options = await self.configure_template(spec, template_class)
+ templates[tpl.template] = template_class(
+ options,
+ self.state_manager,
+ self.process_manager,
+ )
return tpl.architecture, templates
@@ -194,7 +186,6 @@ async def plan_architecture(self, spec: Specification):
spec.templates = {t.name: t.options_dict for t in templates.values()}
spec.system_dependencies = [d.model_dump() for d in arch.system_dependencies]
spec.package_dependencies = [d.model_dump() for d in arch.package_dependencies]
- telemetry.set("architecture", json.loads(arch.model_dump_json()))
async def check_compatibility(self, arch: Architecture) -> bool:
warn_system_deps = [dep.name for dep in arch.system_dependencies if dep.name.lower() in WARN_SYSTEM_DEPS]
@@ -256,7 +247,7 @@ async def check_system_dependencies(self, spec: Specification):
remedy = "If you would like to use it locally, please install it before proceeding."
await self.send_message(f"❌ {dep['name']} is not available. {remedy}")
await self.ask_question(
- f"Have you installed {dep['name']}?",
+ "",
buttons={"continue": f"I've installed {dep['name']}"},
buttons_only=True,
default="continue",
diff --git a/core/agents/base.py b/core/agents/base.py
index 26c80e16b..f2e5ac541 100644
--- a/core/agents/base.py
+++ b/core/agents/base.py
@@ -29,7 +29,6 @@ def __init__(
prev_response: Optional["AgentResponse"] = None,
process_manager: Optional["ProcessManager"] = None,
data: Optional[Any] = None,
- args: Optional[Any] = None,
):
"""
Create a new agent.
@@ -41,7 +40,6 @@ def __init__(
self.prev_response = prev_response
self.step = step
self.data = data
- self.args = args
@property
def current_state(self) -> ProjectState:
@@ -53,7 +51,7 @@ def next_state(self) -> ProjectState:
"""Next state of the project (write-only)."""
return self.state_manager.next_state
- async def send_message(self, message: str, extra_info: Optional[dict] = None):
+ async def send_message(self, message: str):
"""
Send a message to the user.
@@ -61,11 +59,8 @@ async def send_message(self, message: str, extra_info: Optional[dict] = None):
setting the correct source and project state ID.
:param message: Message to send.
- :param extra_info: Extra information to indicate special functionality in extension
"""
- await self.ui.send_message(
- message + "\n", source=self.ui_source, project_state_id=str(self.current_state.id), extra_info=extra_info
- )
+ await self.ui.send_message(message + "\n", source=self.ui_source, project_state_id=str(self.current_state.id))
async def ask_question(
self,
@@ -74,13 +69,9 @@ async def ask_question(
buttons: Optional[dict[str, str]] = None,
default: Optional[str] = None,
buttons_only: bool = False,
+ initial_text: Optional[str] = None,
allow_empty: bool = False,
- full_screen: Optional[bool] = False,
hint: Optional[str] = None,
- verbose: bool = True,
- initial_text: Optional[str] = None,
- extra_info: Optional[dict] = None,
- placeholder: Optional[str] = None,
) -> UserInput:
"""
Ask a question to the user and return the response.
@@ -94,12 +85,8 @@ async def ask_question(
:param default: Default button to select.
:param buttons_only: Only display buttons, no text input.
:param allow_empty: Allow empty input.
- :param full_screen: Show question full screen in extension.
:param hint: Text to display in a popup as a hint to the question.
- :param verbose: Whether to log the question and response.
:param initial_text: Initial text input.
- :param extra_info: Extra information to indicate special functionality in extension.
- :param placeholder: Placeholder text for the input field.
:return: User response.
"""
response = await self.ui.ask_question(
@@ -108,18 +95,11 @@ async def ask_question(
default=default,
buttons_only=buttons_only,
allow_empty=allow_empty,
- full_screen=full_screen,
hint=hint,
- verbose=verbose,
initial_text=initial_text,
source=self.ui_source,
- project_state_id=str(self.current_state.id) if self.current_state.prev_state_id is not None else None,
- extra_info=extra_info,
- placeholder=placeholder,
+ project_state_id=str(self.current_state.id),
)
- # Store the access token in the state manager
- if hasattr(response, "access_token") and response.access_token:
- self.state_manager.update_access_token(response.access_token)
await self.state_manager.log_user_input(question, response)
return response
@@ -131,10 +111,8 @@ async def stream_handler(self, content: str):
:param content: Response content.
"""
- route = getattr(self, "_current_route", None)
- await self.ui.send_stream_chunk(
- content, source=self.ui_source, project_state_id=str(self.current_state.id), route=route
- )
+
+ await self.ui.send_stream_chunk(content, source=self.ui_source, project_state_id=str(self.current_state.id))
if content is None:
await self.ui.send_message("", source=self.ui_source, project_state_id=str(self.current_state.id))
@@ -172,7 +150,7 @@ async def error_handler(self, error: LLMError, message: Optional[str] = None) ->
return False
- def get_llm(self, name=None, stream_output=False, route=None) -> Callable:
+ def get_llm(self, name=None, stream_output=False) -> Callable:
"""
Get a new instance of the agent-specific LLM client.
@@ -182,8 +160,6 @@ def get_llm(self, name=None, stream_output=False, route=None) -> Callable:
model configuration.
:param name: Name of the agent for configuration (default: class name).
- :param stream_output: Whether to enable streaming output.
- :param route: Route information for message routing.
:return: LLM client for the agent.
"""
@@ -195,13 +171,7 @@ def get_llm(self, name=None, stream_output=False, route=None) -> Callable:
llm_config = config.llm_for_agent(name)
client_class = BaseLLMClient.for_provider(llm_config.provider)
stream_handler = self.stream_handler if stream_output else None
- llm_client = client_class(
- llm_config,
- stream_handler=stream_handler,
- error_handler=self.error_handler,
- ui=self.ui,
- state_manager=self.state_manager,
- )
+ llm_client = client_class(llm_config, stream_handler=stream_handler, error_handler=self.error_handler)
async def client(convo, **kwargs) -> Any:
"""
@@ -210,15 +180,9 @@ async def client(convo, **kwargs) -> Any:
For details on optional arguments to pass to the LLM client,
see `pythagora.llm.openai_client.OpenAIClient()`.
"""
- # Set the route for this LLM request
- self._current_route = route
- try:
- response, request_log = await llm_client(convo, **kwargs)
- await self.state_manager.log_llm_request(request_log, agent=self)
- return response
- finally:
- # Clear the route after the request
- self._current_route = None
+ response, request_log = await llm_client(convo, **kwargs)
+ await self.state_manager.log_llm_request(request_log, agent=self)
+ return response
return client
diff --git a/core/agents/bug_hunter.py b/core/agents/bug_hunter.py
index bbadf6bc0..550d12b9f 100644
--- a/core/agents/bug_hunter.py
+++ b/core/agents/bug_hunter.py
@@ -1,29 +1,15 @@
-import asyncio
-import json
from enum import Enum
from pydantic import BaseModel, Field
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
-from core.agents.mixins import ChatWithBreakdownMixin, TestSteps
from core.agents.response import AgentResponse
from core.config import CHECK_LOGS_AGENT_NAME, magic_words
-from core.config.actions import (
- BH_ADDITIONAL_FEEDBACK,
- BH_HUMAN_TEST_AGAIN,
- BH_IS_BUG_FIXED,
- BH_START_BUG_HUNT,
- BH_START_USER_TEST,
- BH_STARTING_PAIR_PROGRAMMING,
- BH_WAIT_BUG_REP_INSTRUCTIONS,
-)
-from core.config.constants import CONVO_ITERATIONS_LIMIT
from core.db.models.project_state import IterationStatus
from core.llm.parser import JSONParser
from core.log import get_logger
from core.telemetry import telemetry
-from core.ui.base import ProjectStage, pythagora_source
log = get_logger(__name__)
@@ -54,7 +40,7 @@ class ImportantLogsForDebugging(BaseModel):
logs: list[ImportantLog] = Field(description="Important logs that will help the human debug the current bug.")
-class BugHunter(ChatWithBreakdownMixin, BaseAgent):
+class BugHunter(BaseAgent):
agent_type = "bug-hunter"
display_name = "Bug Hunter"
@@ -62,10 +48,7 @@ async def run(self) -> AgentResponse:
current_iteration = self.current_state.current_iteration
if "bug_reproduction_description" not in current_iteration:
- if not self.state_manager.async_tasks:
- self.state_manager.async_tasks = []
- self.state_manager.async_tasks.append(asyncio.create_task(self.get_bug_reproduction_instructions()))
-
+ await self.get_bug_reproduction_instructions()
if current_iteration["status"] == IterationStatus.HUNTING_FOR_BUG:
# TODO determine how to find a bug (eg. check in db, ask user a question, etc.)
return await self.check_logs()
@@ -80,39 +63,23 @@ async def run(self) -> AgentResponse:
return await self.start_pair_programming()
async def get_bug_reproduction_instructions(self):
- await self.send_message("Finding a way to reproduce the bug ...")
- await self.ui.set_important_stream()
- llm = self.get_llm()
- convo = (
- AgentConvo(self)
- .template(
- "get_bug_reproduction_instructions",
- current_task=self.current_state.current_task,
- user_feedback=self.current_state.current_iteration["user_feedback"],
- user_feedback_qa=self.current_state.current_iteration["user_feedback_qa"],
- docs=self.current_state.docs,
- next_solution_to_try=None,
- )
- .require_schema(TestSteps)
- )
- bug_reproduction_instructions: TestSteps = await llm(convo, parser=JSONParser(TestSteps), temperature=0)
- self.next_state.current_iteration["bug_reproduction_description"] = json.dumps(
- [test.dict() for test in bug_reproduction_instructions.steps]
+ llm = self.get_llm(stream_output=True)
+ convo = AgentConvo(self).template(
+ "get_bug_reproduction_instructions",
+ current_task=self.current_state.current_task,
+ user_feedback=self.current_state.current_iteration["user_feedback"],
+ user_feedback_qa=self.current_state.current_iteration["user_feedback_qa"],
+ docs=self.current_state.docs,
+ next_solution_to_try=None,
)
+ bug_reproduction_instructions = await llm(convo, temperature=0)
+ self.next_state.current_iteration["bug_reproduction_description"] = bug_reproduction_instructions
async def check_logs(self, logs_message: str = None):
- self.next_state.action = BH_START_BUG_HUNT.format(
- self.current_state.tasks.index(self.current_state.current_task) + 1
- )
llm = self.get_llm(CHECK_LOGS_AGENT_NAME, stream_output=True)
convo = self.generate_iteration_convo_so_far()
- await self.ui.start_breakdown_stream()
human_readable_instructions = await llm(convo, temperature=0.5)
- convo.assistant(human_readable_instructions)
-
- human_readable_instructions = await self.chat_with_breakdown(convo, human_readable_instructions)
-
convo = (
AgentConvo(self)
.template(
@@ -138,116 +105,95 @@ async def check_logs(self, logs_message: str = None):
await self.ui.send_bug_hunter_status("adding_logs", num_bug_hunting_cycles)
self.next_state.flag_iterations_as_modified()
- await self.async_task_finish()
return AgentResponse.done(self)
async def ask_user_to_test(self, awaiting_bug_reproduction: bool = False, awaiting_user_test: bool = False):
- if awaiting_user_test:
- self.next_state.action = BH_START_USER_TEST.format(
- self.current_state.tasks.index(self.current_state.current_task) + 1
- )
- elif awaiting_bug_reproduction:
- self.next_state.action = BH_WAIT_BUG_REP_INSTRUCTIONS.format(
- self.current_state.tasks.index(self.current_state.current_task) + 1
- )
-
- await self.async_task_finish()
-
+ await self.ui.stop_app()
test_instructions = self.current_state.current_iteration["bug_reproduction_description"]
- await self.ui.send_message(
- "Start the app and test it by following these instructions:\n\n", source=pythagora_source
- )
- await self.ui.send_test_instructions(test_instructions, project_state_id=str(self.current_state.id))
+ await self.send_message("You can reproduce the bug like this:\n\n" + test_instructions)
+ await self.ui.send_test_instructions(test_instructions)
if self.current_state.run_command:
await self.ui.send_run_command(self.current_state.run_command)
- user_feedback = await self.ask_question(
- BH_HUMAN_TEST_AGAIN,
- buttons={"done": "I am done testing"},
- buttons_only=True,
- default="continue",
- extra_info={"restart_app": True},
- hint="Instructions for testing:\n\n" + test_instructions,
- )
-
if awaiting_user_test:
- self.next_state.current_iteration["bug_hunting_cycles"][-1]["fix_attempted"] = True
-
- if awaiting_user_test and not user_feedback.text:
buttons = {"yes": "Yes, the issue is fixed", "no": "No", "start_pair_programming": "Start Pair Programming"}
user_feedback = await self.ask_question(
- BH_IS_BUG_FIXED,
+ "Is the bug you reported fixed now?",
buttons=buttons,
default="yes",
buttons_only=True,
- hint="Instructions for testing:\n\n" + test_instructions,
+ hint="Instructions for testing:\n\n"
+ + self.current_state.current_iteration["bug_reproduction_description"],
)
- # self.next_state.current_iteration["bug_hunting_cycles"][-1]["fix_attempted"] = True
+ self.next_state.current_iteration["bug_hunting_cycles"][-1]["fix_attempted"] = True
if user_feedback.button == "yes":
self.next_state.complete_iteration()
- return AgentResponse.done(self)
elif user_feedback.button == "start_pair_programming":
self.next_state.current_iteration["status"] = IterationStatus.START_PAIR_PROGRAMMING
self.next_state.flag_iterations_as_modified()
- return AgentResponse.done(self)
else:
awaiting_bug_reproduction = True
- if awaiting_bug_reproduction and not user_feedback.text:
+ if awaiting_bug_reproduction:
+ # TODO how can we get FE and BE logs automatically?
buttons = {
+ "copy_backend_logs": "Copy Backend Logs",
+ "continue": "Continue without logs",
"done": "Bug is fixed",
- "continue": "Continue without feedback", # DO NOT CHANGE THIS TEXT without changing it in the extension (it is hardcoded)
"start_pair_programming": "Start Pair Programming",
}
- await self.ui.send_project_stage(
- {
- "stage": ProjectStage.ADDITIONAL_FEEDBACK,
- }
- )
- user_feedback = await self.ask_question(
- BH_ADDITIONAL_FEEDBACK,
+ backend_logs = await self.ask_question(
+ "Please share the relevant Backend logs",
buttons=buttons,
default="continue",
- extra_info={"collect_logs": True},
- hint="Instructions for testing:\n\n" + test_instructions,
+ hint="Instructions for testing:\n\n"
+ + self.current_state.current_iteration["bug_reproduction_description"],
)
- if user_feedback.button == "done":
+ if backend_logs.button == "done":
self.next_state.complete_iteration()
- return AgentResponse.done(self)
- elif user_feedback.button == "start_pair_programming":
+ elif backend_logs.button == "start_pair_programming":
self.next_state.current_iteration["status"] = IterationStatus.START_PAIR_PROGRAMMING
self.next_state.flag_iterations_as_modified()
- return AgentResponse.done(self)
-
- # TODO select only the logs that are new (with PYTHAGORA_DEBUGGING_LOG)
- self.next_state.current_iteration["bug_hunting_cycles"][-1]["backend_logs"] = None
- self.next_state.current_iteration["bug_hunting_cycles"][-1]["frontend_logs"] = None
- self.next_state.current_iteration["bug_hunting_cycles"][-1]["user_feedback"] = user_feedback.text
- self.next_state.current_iteration["status"] = IterationStatus.HUNTING_FOR_BUG
- self.next_state.current_iteration["attempts"] += 1
- self.next_state.flag_iterations_as_modified()
-
- await self.ui.send_project_stage(
- {
- "bug_fix_attempt": self.next_state.current_iteration["attempts"],
- }
- )
+ else:
+ buttons = {
+ "copy_frontend_logs": "Copy Frontend Logs",
+ "continue": "Continue without logs",
+ }
+ frontend_logs = await self.ask_question(
+ "Please share the relevant Frontend logs",
+ buttons=buttons,
+ default="continue",
+ hint="Instructions for testing:\n\n"
+ + self.current_state.current_iteration["bug_reproduction_description"],
+ )
+
+ buttons = {"continue": "Continue without feedback"}
+ user_feedback = await self.ask_question(
+ "Please add any additional feedback that could help Pythagora solve this bug",
+ buttons=buttons,
+ default="continue",
+ hint="Instructions for testing:\n\n"
+ + self.current_state.current_iteration["bug_reproduction_description"],
+ )
+
+ # TODO select only the logs that are new (with PYTHAGORA_DEBUGGING_LOG)
+ self.next_state.current_iteration["bug_hunting_cycles"][-1]["backend_logs"] = backend_logs.text
+ self.next_state.current_iteration["bug_hunting_cycles"][-1]["frontend_logs"] = frontend_logs.text
+ self.next_state.current_iteration["bug_hunting_cycles"][-1]["user_feedback"] = user_feedback.text
+ self.next_state.current_iteration["status"] = IterationStatus.HUNTING_FOR_BUG
return AgentResponse.done(self)
async def start_pair_programming(self):
- self.next_state.action = BH_STARTING_PAIR_PROGRAMMING.format(
- self.current_state.tasks.index(self.current_state.current_task) + 1
- )
llm = self.get_llm(stream_output=True)
convo = self.generate_iteration_convo_so_far(True)
if len(convo.messages) > 1:
convo.remove_last_x_messages(1)
convo = convo.template("problem_explanation")
- await self.ui.set_important_stream()
+ await self.ui.start_important_stream()
initial_explanation = await llm(convo, temperature=0.5)
llm = self.get_llm()
@@ -270,8 +216,6 @@ async def start_pair_programming(self):
}
)
- await self.async_task_finish()
-
while True:
self.next_state.current_iteration["initial_explanation"] = initial_explanation
next_step = await self.ask_question(
@@ -305,8 +249,8 @@ async def start_pair_programming(self):
# TODO: remove when Leon checks
convo.remove_last_x_messages(2)
- if len(convo.messages) > CONVO_ITERATIONS_LIMIT:
- convo.slice(1, CONVO_ITERATIONS_LIMIT)
+ if len(convo.messages) > 10:
+ convo.trim(1, 2)
# TODO: in the future improve with a separate conversation that parses the user info and goes into an appropriate if statement
if next_step.button == "done":
@@ -315,19 +259,19 @@ async def start_pair_programming(self):
elif next_step.button == "question":
user_response = await self.ask_question("Oh, cool, what would you like to know?")
convo = convo.template("ask_a_question", question=user_response.text)
- await self.ui.set_important_stream()
+ await self.ui.start_important_stream()
llm_answer = await llm(convo, temperature=0.5)
await self.send_message(llm_answer)
elif next_step.button == "tell_me_more":
convo.template("tell_me_more")
- await self.ui.set_important_stream()
+ await self.ui.start_important_stream()
response = await llm(convo, temperature=0.5)
await self.send_message(response)
elif next_step.button == "other":
# this is the same as "question" - we want to keep an option for users to click to understand if we're missing something with other options
user_response = await self.ask_question("Let me know what you think ...")
convo = convo.template("ask_a_question", question=user_response.text)
- await self.ui.set_important_stream()
+ await self.ui.start_important_stream()
llm_answer = await llm(convo, temperature=0.5)
await self.send_message(llm_answer)
elif next_step.button == "solution_hint":
@@ -335,7 +279,7 @@ async def start_pair_programming(self):
while True:
human_hint = await self.ask_question(human_hint_label)
convo = convo.template("instructions_from_human_hint", human_hint=human_hint.text)
- await self.ui.set_important_stream()
+ await self.ui.start_important_stream()
llm = self.get_llm(CHECK_LOGS_AGENT_NAME, stream_output=True)
human_readable_instructions = await llm(convo, temperature=0.5)
human_approval = await self.ask_question(
@@ -353,7 +297,7 @@ async def start_pair_programming(self):
break
elif next_step.button == "tell_me_more":
convo.template("tell_me_more")
- await self.ui.set_important_stream()
+ await self.ui.start_important_stream()
response = await llm(convo, temperature=0.5)
await self.send_message(response)
continue
@@ -369,7 +313,6 @@ def generate_iteration_convo_so_far(self, omit_last_cycle=False):
docs=self.current_state.docs,
magic_words=magic_words,
next_solution_to_try=None,
- test_instructions=json.loads(self.current_state.current_task.get("test_instructions") or "[]"),
)
hunting_cycles = self.current_state.current_iteration.get("bug_hunting_cycles", [])[
@@ -385,18 +328,8 @@ def generate_iteration_convo_so_far(self, omit_last_cycle=False):
user_feedback=hunting_cycle.get("user_feedback"),
)
- if len(convo.messages) > CONVO_ITERATIONS_LIMIT:
- convo.slice(1, CONVO_ITERATIONS_LIMIT)
-
return convo
- async def async_task_finish(self):
- if self.state_manager.async_tasks:
- if not self.state_manager.async_tasks[-1].done():
- await self.send_message("Waiting for the bug reproduction instructions...")
- await self.state_manager.async_tasks[-1]
- self.state_manager.async_tasks = []
-
def set_data_for_next_hunting_cycle(self, human_readable_instructions, new_status):
self.next_state.current_iteration["description"] = human_readable_instructions
self.next_state.current_iteration["bug_hunting_cycles"] += [
@@ -409,3 +342,9 @@ def set_data_for_next_hunting_cycle(self, human_readable_instructions, new_statu
]
self.next_state.current_iteration["status"] = new_status
+
+ async def continue_on(self, convo, button_value, user_response):
+ llm = self.get_llm(stream_output=True)
+ convo = convo.template("continue_on")
+ continue_on = await llm(convo, temperature=0.5)
+ return continue_on
diff --git a/core/agents/code_monkey.py b/core/agents/code_monkey.py
index 41756d2dc..3141d9ef4 100644
--- a/core/agents/code_monkey.py
+++ b/core/agents/code_monkey.py
@@ -1,18 +1,14 @@
-import asyncio
import re
+from difflib import unified_diff
from enum import Enum
-from typing import Optional
+from typing import Optional, Union
from pydantic import BaseModel, Field
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
-from core.agents.mixins import FileDiffMixin
from core.agents.response import AgentResponse, ResponseType
-from core.config import CODE_MONKEY_AGENT_NAME, DESCRIBE_FILES_AGENT_NAME, IMPLEMENT_CHANGES_AGENT_NAME
-from core.config.actions import CM_UPDATE_FILES
-from core.db.models import File
-from core.llm.convo import Convo
+from core.config import CODE_MONKEY_AGENT_NAME, CODE_REVIEW_AGENT_NAME, DESCRIBE_FILES_AGENT_NAME
from core.llm.parser import JSONParser, OptionalCodeBlockParser
from core.log import get_logger
@@ -28,7 +24,7 @@
# Maximum number of attempts to ask for review if it can't be parsed
MAX_REVIEW_RETRIES = 2
-# Maximum number of code implementation attempts after which we accept the changes unconditionally
+# Maximum number of code implementation attempts after which we accept the changes unconditionaly
MAX_CODING_ATTEMPTS = 3
@@ -58,14 +54,7 @@ class FileDescription(BaseModel):
)
-def extract_code_blocks(content):
- # Use regex to find all blocks with file attribute and their content
- code_blocks = re.findall(r'(.*?)', content, re.DOTALL)
- # Convert matches into a list of dictionaries
- return [{"file_name": file_name, "file_content": file_content} for file_name, file_content in code_blocks]
-
-
-class CodeMonkey(FileDiffMixin, BaseAgent):
+class CodeMonkey(BaseAgent):
agent_type = "code-monkey"
display_name = "Code Monkey"
@@ -74,9 +63,12 @@ async def run(self) -> AgentResponse:
return await self.describe_files()
else:
data = await self.implement_changes()
- if not data:
- return AgentResponse.done(self)
- return await self.accept_changes(data["path"], data["old_content"], data["new_content"])
+ code_review_done = False
+ while not code_review_done:
+ review_response = await self.run_code_review(data)
+ if isinstance(review_response, AgentResponse):
+ return review_response
+ data = await self.implement_changes(review_response)
async def implement_changes(self, data: Optional[dict] = None) -> dict:
file_name = self.step["save_file"]["path"]
@@ -84,25 +76,27 @@ async def implement_changes(self, data: Optional[dict] = None) -> dict:
current_file = await self.state_manager.get_file_by_path(file_name)
file_content = current_file.content.content if current_file else ""
+ task = self.current_state.current_task
+
if data is not None:
attempt = data["attempt"] + 1
feedback = data["feedback"]
log.debug(f"Fixing file {file_name} after review feedback: {feedback} ({attempt}. attempt)")
- await self.ui.send_file_status(file_name, "reworking", source=self.ui_source)
+ await self.ui.send_file_status(file_name, "reworking")
else:
log.debug(f"Implementing file {file_name}")
if data is None:
- await self.ui.send_file_status(
- file_name, "updating" if file_content else "creating", source=self.ui_source
- )
+ await self.ui.send_file_status(file_name, "updating" if file_content else "creating")
else:
- await self.ui.send_file_status(file_name, "reworking", source=self.ui_source)
- self.next_state.action = CM_UPDATE_FILES
+ await self.ui.send_file_status(file_name, "reworking")
+ self.next_state.action = "Updating files"
+ attempt = 1
feedback = None
iterations = self.current_state.iterations
user_feedback = None
user_feedback_qa = None
+ llm = self.get_llm(CODE_MONKEY_AGENT_NAME)
if iterations:
last_iteration = iterations[-1]
@@ -112,57 +106,36 @@ async def implement_changes(self, data: Optional[dict] = None) -> dict:
else:
instructions = self.current_state.current_task["instructions"]
- blocks = extract_code_blocks(instructions)
- response = None
-
- if blocks and self.state_manager.get_access_token():
- try:
- # Try Relace first
- block = next((item for item in blocks if item["file_name"] == file_name), None)
- if block:
- llm = self.get_llm(IMPLEMENT_CHANGES_AGENT_NAME)
- convo = Convo().user(
- {
- "initialCode": file_content,
- "editSnippet": block["file_content"],
- }
- )
- response = await llm(convo, temperature=0, parser=OptionalCodeBlockParser())
- except Exception:
- response = None
-
- # Fall back to OpenAI if Relace wasn't used or returned empty response
- if not response or response is None:
- llm = self.get_llm(CODE_MONKEY_AGENT_NAME)
- convo = AgentConvo(self).template(
- "implement_changes",
- file_name=file_name,
- file_content=file_content,
- instructions=instructions,
- user_feedback=user_feedback,
- user_feedback_qa=user_feedback_qa,
+ convo = AgentConvo(self).template(
+ "implement_changes",
+ file_name=file_name,
+ file_content=file_content,
+ instructions=instructions,
+ user_feedback=user_feedback,
+ user_feedback_qa=user_feedback_qa,
+ )
+ if feedback:
+ convo.assistant(f"```\n{data['new_content']}\n```\n").template(
+ "review_feedback",
+ content=data["approved_content"],
+ original_content=file_content,
+ rework_feedback=feedback,
)
- if feedback:
- convo.assistant(f"```\n{data['new_content']}\n```\n").template(
- "review_feedback",
- content=data["approved_content"],
- original_content=file_content,
- rework_feedback=feedback,
- )
- response = await llm(convo, temperature=0, parser=OptionalCodeBlockParser())
+ response: str = await llm(convo, temperature=0, parser=OptionalCodeBlockParser())
+ # FIXME: provide a counter here so that we don't have an endless loop here
return {
"path": file_name,
+ "instructions": task["instructions"],
"old_content": file_content,
"new_content": response,
+ "attempt": attempt,
}
async def describe_files(self) -> AgentResponse:
- tasks = []
+ llm = self.get_llm(DESCRIBE_FILES_AGENT_NAME)
to_describe = {
- file.path: file.content.content
- for file in self.current_state.files
- if not file.content.meta.get("description")
+ file.path: file.content.content for file in self.current_state.files if not file.meta.get("description")
}
for file in self.next_state.files:
@@ -171,58 +144,73 @@ async def describe_files(self) -> AgentResponse:
continue
if content == "":
- file.content.meta = {
- **file.content.meta,
+ file.meta = {
+ **file.meta,
"description": "Empty file",
"references": [],
}
continue
- tasks.append(self.describe_file(file, content))
-
- await asyncio.gather(*tasks)
- return AgentResponse.done(self)
-
- async def describe_file(self, file: File, content: str):
- """
- Describes a file by sending it to the LLM agent and then updating the file's metadata in the database.
- """
- llm = self.get_llm(DESCRIBE_FILES_AGENT_NAME)
- log.debug(f"Describing file {file.path}")
- convo = (
- AgentConvo(self)
- .template(
- "describe_file",
- path=file.path,
- content=content,
+ log.debug(f"Describing file {file.path}")
+ convo = (
+ AgentConvo(self)
+ .template(
+ "describe_file",
+ path=file.path,
+ content=content,
+ )
+ .require_schema(FileDescription)
)
- .require_schema(FileDescription)
- )
- llm_response: FileDescription = await llm(convo, parser=JSONParser(spec=FileDescription))
+ llm_response: FileDescription = await llm(convo, parser=JSONParser(spec=FileDescription))
- file.content.meta = {
- **file.content.meta,
- "description": llm_response.summary,
- "references": llm_response.references,
- }
+ file.meta = {
+ **file.meta,
+ "description": llm_response.summary,
+ "references": llm_response.references,
+ }
+ return AgentResponse.done(self)
# ------------------------------
# CODE REVIEW
# ------------------------------
+ async def run_code_review(self, data: Optional[dict]) -> Union[AgentResponse, dict]:
+ await self.ui.send_file_status(data["path"], "reviewing")
+ if (
+ data is not None
+ and not data["old_content"]
+ or data["new_content"] == data["old_content"]
+ or data["attempt"] >= MAX_CODING_ATTEMPTS
+ ):
+ # we always auto-accept new files and unchanged files, or if we've tried too many times
+ return await self.accept_changes(data["path"], data["old_content"], data["new_content"])
+
+ approved_content, feedback = await self.review_change(
+ data["path"],
+ data["instructions"],
+ data["old_content"],
+ data["new_content"],
+ )
+ if feedback:
+ return {
+ "new_content": data["new_content"],
+ "approved_content": approved_content,
+ "feedback": feedback,
+ "attempt": data["attempt"],
+ }
+ else:
+ return await self.accept_changes(data["path"], data["old_content"], approved_content)
+
async def accept_changes(self, file_path: str, old_content: str, new_content: str) -> AgentResponse:
- await self.ui.send_file_status(file_path, "done", source=self.ui_source)
+ await self.ui.send_file_status(file_path, "done")
n_new_lines, n_del_lines = self.get_line_changes(old_content, new_content)
- await self.ui.generate_diff(
- file_path, old_content, new_content, n_new_lines, n_del_lines, source=self.ui_source
- )
+ await self.ui.generate_diff(file_path, old_content, new_content, n_new_lines, n_del_lines)
await self.state_manager.save_file(file_path, new_content)
- self.step["save_file"]["content"] = new_content
- self.next_state.complete_step("save_file")
+ self.next_state.complete_step()
- input_required = self.state_manager.get_input_required(new_content, file_path)
+ input_required = self.state_manager.get_input_required(new_content)
if input_required:
return AgentResponse.input_required(
self,
@@ -230,3 +218,266 @@ async def accept_changes(self, file_path: str, old_content: str, new_content: st
)
else:
return AgentResponse.done(self)
+
+ def _get_task_convo(self) -> AgentConvo:
+ # FIXME: Current prompts reuse conversation from the developer so we have to resort to this
+ task = self.current_state.current_task
+ current_task_index = self.current_state.tasks.index(task)
+
+ convo = AgentConvo(self).template(
+ "breakdown",
+ task=task,
+ iteration=None,
+ current_task_index=current_task_index,
+ )
+ # TODO: We currently show last iteration to the code monkey; we might need to show the task
+ # breakdown and all the iterations instead? To think about when refactoring prompts
+ if self.current_state.iterations:
+ convo.assistant(self.current_state.iterations[-1]["description"])
+ else:
+ convo.assistant(self.current_state.current_task["instructions"])
+ return convo
+
+ async def review_change(
+ self, file_name: str, instructions: str, old_content: str, new_content: str
+ ) -> tuple[str, str]:
+ """
+ Review changes that were applied to the file.
+
+ This asks the LLM to act as a PR reviewer and for each part (hunk) of the
+ diff, decide if it should be applied (kept) or ignored (removed from the PR).
+
+ :param file_name: name of the file being modified
+ :param instructions: instructions for the reviewer
+ :param old_content: old file content
+ :param new_content: new file content (with proposed changes)
+ :return: tuple with file content update with approved changes, and review feedback
+
+ Diff hunk explanation: https://www.gnu.org/software/diffutils/manual/html_node/Hunks.html
+ """
+
+ hunks = self.get_diff_hunks(file_name, old_content, new_content)
+
+ llm = self.get_llm(CODE_REVIEW_AGENT_NAME)
+ convo = (
+ self._get_task_convo()
+ .template(
+ "review_changes",
+ instructions=instructions,
+ file_name=file_name,
+ old_content=old_content,
+ hunks=hunks,
+ )
+ .require_schema(ReviewChanges)
+ )
+ llm_response: ReviewChanges = await llm(convo, temperature=0, parser=JSONParser(ReviewChanges))
+
+ for i in range(MAX_REVIEW_RETRIES):
+ reasons = {}
+ ids_to_apply = set()
+ ids_to_ignore = set()
+ ids_to_rework = set()
+ for hunk in llm_response.hunks:
+ reasons[hunk.number - 1] = hunk.reason
+ if hunk.decision == "apply":
+ ids_to_apply.add(hunk.number - 1)
+ elif hunk.decision == "ignore":
+ ids_to_ignore.add(hunk.number - 1)
+ elif hunk.decision == "rework":
+ ids_to_rework.add(hunk.number - 1)
+
+ n_hunks = len(hunks)
+ n_review_hunks = len(reasons)
+ if n_review_hunks == n_hunks:
+ break
+ elif n_review_hunks < n_hunks:
+ error = "Not all hunks have been reviewed. Please review all hunks and add 'apply', 'ignore' or 'rework' decision for each."
+ elif n_review_hunks > n_hunks:
+ error = f"Your review contains more hunks ({n_review_hunks}) than in the original diff ({n_hunks}). Note that one hunk may have multiple changed lines."
+
+ # Max two retries; if the reviewer still hasn't reviewed all hunks, we'll just use the entire new content
+ convo.assistant(llm_response.model_dump_json()).user(error)
+ llm_response = await llm(convo, parser=JSONParser(ReviewChanges))
+ else:
+ return new_content, None
+
+ hunks_to_apply = [h for i, h in enumerate(hunks) if i in ids_to_apply]
+ diff_log = f"--- {file_name}\n+++ {file_name}\n" + "\n".join(hunks_to_apply)
+
+ hunks_to_rework = [(i, h) for i, h in enumerate(hunks) if i in ids_to_rework]
+ review_log = (
+ "\n\n".join([f"## Change\n```{hunk}```\nReviewer feedback:\n{reasons[i]}" for (i, hunk) in hunks_to_rework])
+ + "\n\nReview notes:\n"
+ + llm_response.review_notes
+ )
+
+ if len(hunks_to_apply) == len(hunks):
+ log.info(f"Applying entire change to {file_name}")
+ return new_content, None
+
+ elif len(hunks_to_apply) == 0:
+ if hunks_to_rework:
+ log.info(f"Requesting rework for {len(hunks_to_rework)} changes to {file_name} (0 hunks to apply)")
+ return old_content, review_log
+ else:
+ # If everything can be safely ignored, it's probably because the files already implement the changes
+ # from previous tasks (which can happen often). Insisting on a change here is likely to cause problems.
+ log.info(f"Rejecting entire change to {file_name} with reason: {llm_response.review_notes}")
+ return old_content, None
+
+ log.debug(f"Applying code change to {file_name}:\n{diff_log}")
+ new_content = self.apply_diff(file_name, old_content, hunks_to_apply, new_content)
+ if hunks_to_rework:
+ log.info(f"Requesting further rework for {len(hunks_to_rework)} changes to {file_name}")
+ return new_content, review_log
+ else:
+ return new_content, None
+
+ @staticmethod
+ def get_line_changes(old_content: str, new_content: str) -> tuple[int, int]:
+ """
+ Get the number of added and deleted lines between two files.
+
+ This uses Python difflib to produce a unified diff, then counts
+ the number of added and deleted lines.
+
+ :param old_content: old file content
+ :param new_content: new file content
+ :return: a tuple (added_lines, deleted_lines)
+ """
+
+ from_lines = old_content.splitlines(keepends=True)
+ to_lines = new_content.splitlines(keepends=True)
+
+ diff_gen = unified_diff(from_lines, to_lines)
+
+ added_lines = 0
+ deleted_lines = 0
+
+ for line in diff_gen:
+ if line.startswith("+") and not line.startswith("+++"): # Exclude the file headers
+ added_lines += 1
+ elif line.startswith("-") and not line.startswith("---"): # Exclude the file headers
+ deleted_lines += 1
+
+ return added_lines, deleted_lines
+
+ @staticmethod
+ def get_diff_hunks(file_name: str, old_content: str, new_content: str) -> list[str]:
+ """
+ Get the diff between two files.
+
+ This uses Python difflib to produce an unified diff, then splits
+ it into hunks that will be separately reviewed by the reviewer.
+
+ :param file_name: name of the file being modified
+ :param old_content: old file content
+ :param new_content: new file content
+ :return: change hunks from the unified diff
+ """
+ from_name = "old_" + file_name
+ to_name = "to_" + file_name
+ from_lines = old_content.splitlines(keepends=True)
+ to_lines = new_content.splitlines(keepends=True)
+ diff_gen = unified_diff(from_lines, to_lines, fromfile=from_name, tofile=to_name)
+ diff_txt = "".join(diff_gen)
+
+ hunks = re.split(r"\n@@", diff_txt, re.MULTILINE)
+ result = []
+ for i, h in enumerate(hunks):
+ # Skip the prologue (file names)
+ if i == 0:
+ continue
+ txt = h.splitlines()
+ txt[0] = "@@" + txt[0]
+ result.append("\n".join(txt))
+ return result
+
+ def apply_diff(self, file_name: str, old_content: str, hunks: list[str], fallback: str):
+ """
+ Apply the diff to the original file content.
+
+ This uses the internal `_apply_patch` method to apply the
+ approved diff hunks to the original file content.
+
+ If patch apply fails, the fallback is the full new file content
+ with all the changes applied (as if the reviewer approved everythng).
+
+ :param file_name: name of the file being modified
+ :param old_content: old file content
+ :param hunks: change hunks from the unified diff
+ :param fallback: proposed new file content (with all the changes applied)
+ """
+ diff = (
+ "\n".join(
+ [
+ f"--- {file_name}",
+ f"+++ {file_name}",
+ ]
+ + hunks
+ )
+ + "\n"
+ )
+ try:
+ fixed_content = self._apply_patch(old_content, diff)
+ except Exception as e:
+ # This should never happen but if it does, just use the new version from
+ # the LLM and hope for the best
+ print(f"Error applying diff: {e}; hoping all changes are valid")
+ return fallback
+
+ return fixed_content
+
+ # Adapted from https://gist.github.com/noporpoise/16e731849eb1231e86d78f9dfeca3abc (Public Domain)
+ @staticmethod
+ def _apply_patch(original: str, patch: str, revert: bool = False):
+ """
+ Apply a patch to a string to recover a newer version of the string.
+
+ :param original: The original string.
+ :param patch: The patch to apply.
+ :param revert: If True, treat the original string as the newer version and recover the older string.
+ :return: The updated string after applying the patch.
+ """
+ original_lines = original.splitlines(True)
+ patch_lines = patch.splitlines(True)
+
+ updated_text = ""
+ index_original = start_line = 0
+
+ # Choose which group of the regex to use based on the revert flag
+ match_index, line_sign = (1, "+") if not revert else (3, "-")
+
+ # Skip header lines of the patch
+ while index_original < len(patch_lines) and patch_lines[index_original].startswith(("---", "+++")):
+ index_original += 1
+
+ while index_original < len(patch_lines):
+ match = PATCH_HEADER_PATTERN.match(patch_lines[index_original])
+ if not match:
+ raise Exception("Bad patch -- regex mismatch [line " + str(index_original) + "]")
+
+ line_number = int(match.group(match_index)) - 1 + (match.group(match_index + 1) == "0")
+
+ if start_line > line_number or line_number > len(original_lines):
+ raise Exception("Bad patch -- bad line number [line " + str(index_original) + "]")
+
+ updated_text += "".join(original_lines[start_line:line_number])
+ start_line = line_number
+ index_original += 1
+
+ while index_original < len(patch_lines) and patch_lines[index_original][0] != "@":
+ if index_original + 1 < len(patch_lines) and patch_lines[index_original + 1][0] == "\\":
+ line_content = patch_lines[index_original][:-1]
+ index_original += 2
+ else:
+ line_content = patch_lines[index_original]
+ index_original += 1
+
+ if line_content:
+ if line_content[0] == line_sign or line_content[0] == " ":
+ updated_text += line_content[1:]
+ start_line += line_content[0] != line_sign
+
+ updated_text += "".join(original_lines[start_line:])
+ return updated_text
diff --git a/core/agents/convo.py b/core/agents/convo.py
index d36cd0cc4..60c05f240 100644
--- a/core/agents/convo.py
+++ b/core/agents/convo.py
@@ -97,18 +97,6 @@ def trim(self, trim_index: int, trim_count: int) -> "AgentConvo":
self.messages = self.messages[:trim_index] + self.messages[trim_index + trim_count :]
return self
- def slice(self, slice_index: int, slice_count: int) -> "AgentConvo":
- """
- Create a new conversation containing messages from slice_index to the end, excluding slice_count messages.
-
- :param slice_index: Starting index to slice from
- :param slice_count: Number of messages to exclude from the end
- :return: self for method chaining
- """
- end_index = max(slice_index, len(self.messages) - slice_count)
- self.messages = self.messages[:slice_index] + self.messages[end_index:]
- return self
-
def require_schema(self, model: BaseModel) -> "AgentConvo":
def remove_defs(d):
if isinstance(d, dict):
diff --git a/core/agents/developer.py b/core/agents/developer.py
index ee2586cbf..dbc31078c 100644
--- a/core/agents/developer.py
+++ b/core/agents/developer.py
@@ -1,30 +1,20 @@
import json
from enum import Enum
from typing import Annotated, Literal, Union
-from uuid import UUID, uuid4
+from uuid import uuid4
from pydantic import BaseModel, Field
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
-from core.agents.mixins import ChatWithBreakdownMixin, RelevantFilesMixin
+from core.agents.mixins import RelevantFilesMixin
from core.agents.response import AgentResponse
-from core.cli.helpers import get_epic_task_number
from core.config import PARSE_TASK_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME
-from core.config.actions import (
- DEV_EXECUTE_TASK,
- DEV_TASK_BREAKDOWN,
- DEV_TASK_REVIEW_FEEDBACK,
- DEV_TASK_START,
- DEV_TROUBLESHOOT,
- DEV_WAIT_TEST,
-)
from core.db.models.project_state import IterationStatus, TaskStatus
from core.db.models.specification import Complexity
from core.llm.parser import JSONParser
from core.log import get_logger
from core.telemetry import telemetry
-from core.ui.base import ProjectStage, pythagora_source
log = get_logger(__name__)
@@ -33,7 +23,6 @@ class StepType(str, Enum):
COMMAND = "command"
SAVE_FILE = "save_file"
HUMAN_INTERVENTION = "human_intervention"
- UTILITY_FUNCTION = "utility_function"
class CommandOptions(BaseModel):
@@ -61,18 +50,8 @@ class HumanInterventionStep(BaseModel):
human_intervention_description: str
-class UtilityFunction(BaseModel):
- type: Literal[StepType.UTILITY_FUNCTION] = StepType.UTILITY_FUNCTION
- file: str
- function_name: str
- description: str
- return_value: str
- input_value: str
- status: Literal["mocked", "implemented"]
-
-
Step = Annotated[
- Union[SaveFileStep, CommandStep, HumanInterventionStep, UtilityFunction],
+ Union[SaveFileStep, CommandStep, HumanInterventionStep],
Field(discriminator="type"),
]
@@ -81,21 +60,11 @@ class TaskSteps(BaseModel):
steps: list[Step]
-def has_correct_num_of_tags(response: str) -> bool:
- """
- Checks if the response has the correct number of opening and closing tags.
- """
- return response.count("")
-
-
-class Developer(ChatWithBreakdownMixin, RelevantFilesMixin, BaseAgent):
+class Developer(RelevantFilesMixin, BaseAgent):
agent_type = "developer"
display_name = "Developer"
async def run(self) -> AgentResponse:
- if self.current_state.current_step and self.current_state.current_step.get("type") == "utility_function":
- return await self.update_knowledge_base()
-
if not self.current_state.unfinished_tasks:
log.warning("No unfinished tasks found, nothing to do (why am I called? is this a bug?)")
return AgentResponse.done(self)
@@ -150,8 +119,9 @@ async def breakdown_current_iteration(self) -> AgentResponse:
log.debug(f"Breaking down the iteration {description}")
if self.current_state.files and self.current_state.relevant_files is None:
- await self.get_relevant_files_parallel(user_feedback, description)
+ return await self.get_relevant_files(user_feedback, description)
+ await self.send_message("Breaking down the task into steps ...")
await self.ui.send_task_progress(
n_tasks, # iterations and reviews can be created only one at a time, so we are always on last one
n_tasks,
@@ -162,12 +132,20 @@ async def breakdown_current_iteration(self) -> AgentResponse:
self.current_state.tasks,
)
llm = self.get_llm(PARSE_TASK_AGENT_NAME)
-
# FIXME: In case of iteration, parse_task depends on the context (files, tasks, etc) set there.
- # Ideally this prompt would be self-contained.
-
+ # Ideally this prompt would be self-contained.
convo = (
- AgentConvo(self).template("parse_task", implementation_instructions=description).require_schema(TaskSteps)
+ AgentConvo(self)
+ .template(
+ "iteration",
+ user_feedback=user_feedback,
+ user_feedback_qa=None,
+ next_solution_to_try=None,
+ docs=self.current_state.docs,
+ )
+ .assistant(description)
+ .template("parse_task")
+ .require_schema(TaskSteps)
)
response: TaskSteps = await llm(convo, parser=JSONParser(TaskSteps), temperature=0)
@@ -179,20 +157,19 @@ async def breakdown_current_iteration(self) -> AgentResponse:
):
# This is just a support for old iterations that don't have status
self.next_state.complete_iteration()
- self.next_state.action = DEV_TROUBLESHOOT.format(len(self.current_state.iterations))
+ self.next_state.action = f"Troubleshooting #{len(self.current_state.iterations)}"
elif iteration["status"] == IterationStatus.IMPLEMENT_SOLUTION:
# If the user requested a change, then, we'll implement it and go straight back to testing
self.next_state.complete_iteration()
- self.next_state.action = DEV_TROUBLESHOOT.format(len(self.current_state.iterations))
+ self.next_state.action = f"Troubleshooting #{len(self.current_state.iterations)}"
elif iteration["status"] == IterationStatus.AWAITING_BUG_FIX:
# If bug fixing is done, ask user to test again
- self.next_state.action = DEV_WAIT_TEST
self.next_state.current_iteration["status"] = IterationStatus.AWAITING_USER_TEST
elif iteration["status"] == IterationStatus.AWAITING_LOGGING:
# If logging is done, ask user to reproduce the bug
self.next_state.current_iteration["status"] = IterationStatus.AWAITING_BUG_REPRODUCTION
else:
- self.next_state.action = DEV_TASK_REVIEW_FEEDBACK
+ self.next_state.action = "Task review feedback"
current_task_index = self.current_state.tasks.index(current_task)
self.next_state.tasks[current_task_index] = {
@@ -203,9 +180,6 @@ async def breakdown_current_iteration(self) -> AgentResponse:
async def breakdown_current_task(self) -> AgentResponse:
current_task = self.current_state.current_task
- current_task_index = self.current_state.tasks.index(current_task)
- self.next_state.action = DEV_TASK_BREAKDOWN.format(current_task_index + 1)
-
source = self.current_state.current_epic.get("source", "app")
await self.ui.send_task_progress(
self.current_state.tasks.index(current_task) + 1,
@@ -221,67 +195,24 @@ async def breakdown_current_task(self) -> AgentResponse:
log.debug(f"Current state files: {len(self.current_state.files)}, relevant {self.current_state.relevant_files}")
# Check which files are relevant to the current task
- await self.get_relevant_files_parallel()
+ if self.current_state.files and self.current_state.relevant_files is None:
+ return await self.get_relevant_files()
current_task_index = self.current_state.tasks.index(current_task)
- await self.send_message("### Thinking about how to implement this task ...")
+ await self.send_message("Thinking about how to implement this task ...")
- await self.ui.start_breakdown_stream()
- await self.ui.set_important_stream()
- related_api_endpoints = current_task.get("related_api_endpoints", [])
llm = self.get_llm(TASK_BREAKDOWN_AGENT_NAME, stream_output=True)
- # TODO: Temp fix for old projects
- if not (
- related_api_endpoints
- and len(related_api_endpoints) > 0
- and all(isinstance(api, dict) and "endpoint" in api for api in related_api_endpoints)
- ):
- related_api_endpoints = []
-
- redo_task_user_feedback = None
-
- if (
- self.next_state
- and self.next_state.current_task
- and self.next_state.current_task.get("redo_human_instructions", None) is not None
- ):
- redo_task_user_feedback = self.next_state.current_task["redo_human_instructions"]
-
convo = AgentConvo(self).template(
"breakdown",
task=current_task,
iteration=None,
current_task_index=current_task_index,
docs=self.current_state.docs,
- related_api_endpoints=related_api_endpoints,
- redo_task_user_feedback=redo_task_user_feedback,
)
-
response: str = await llm(convo)
- convo.assistant(response)
-
- max_retries = 2
- retry_count = 0
-
- while retry_count < max_retries:
- if has_correct_num_of_tags(response):
- break
-
- convo.user(
- "Ok, now think carefully about your previous response. If the response ends by mentioning something about continuing with the implementation, continue but don't implement any files that have already been implemented. If your last response finishes with an incomplete file, implement that file and any other that needs implementation. Finally, if your last response doesn't end by mentioning continuing and if there isn't an unfinished file implementation, respond only with `DONE` and with nothing else."
- )
- continue_response: str = await llm(convo)
-
- last_open_tag_index = response.rfind(" AgentResponse:
self.next_state.flag_tasks_as_modified()
llm = self.get_llm(PARSE_TASK_AGENT_NAME)
-
- convo = AgentConvo(self).template("parse_task", implementation_instructions=response).require_schema(TaskSteps)
-
+ await self.send_message("Breaking down the task into steps ...")
+ convo.assistant(response).template("parse_task").require_schema(TaskSteps)
response: TaskSteps = await llm(convo, parser=JSONParser(TaskSteps), temperature=0)
# There might be state leftovers from previous tasks that we need to clean here
self.next_state.modified_files = {}
self.set_next_steps(response, source)
- self.next_state.current_task["status"] = TaskStatus.IN_PROGRESS
- self.next_state.action = DEV_TASK_START.format(current_task_index + 1)
+ self.next_state.action = f"Task #{current_task_index + 1} start"
await telemetry.trace_code_event(
"task-start",
{
@@ -360,83 +289,9 @@ async def ask_to_execute_task(self) -> bool:
buttons["skip"] = "Skip Task"
description = self.current_state.current_task["description"]
- epic_index, task_index = get_epic_task_number(self.current_state, self.current_state.current_task)
-
- await self.ui.send_project_stage(
- {
- "stage": ProjectStage.STARTING_TASK,
- "task_index": task_index,
- }
- )
-
- # find latest finished task, send back logs for it being finished
- tasks_done = [task for task in self.current_state.tasks if task not in self.current_state.unfinished_tasks]
- previous_task = tasks_done[-1] if tasks_done else None
- if previous_task:
- e_i, t_i = get_epic_task_number(self.current_state, previous_task)
- task_convo = await self.state_manager.get_task_conversation_project_states(
- UUID(previous_task["id"]), first_last_only=True
- )
- await self.ui.send_back_logs(
- [
- {
- "title": previous_task["description"],
- "project_state_id": str(task_convo[0].id) if task_convo else "be_0",
- "start_id": str(task_convo[0].id) if task_convo else "be_0",
- "end_id": str(task_convo[-1].prev_state_id) if task_convo else "be_0",
- "labels": [f"E{e_i} / T{t_i}", "Backend", "done"],
- }
- ]
- )
- await self.ui.send_front_logs_headers(
- str(task_convo[0].id) if task_convo else "be_0",
- [f"E{e_i} / T{t_i}", "Backend", "done"],
- previous_task["description"],
- self.current_state.current_task.get("id"),
- )
-
- await self.ui.send_front_logs_headers(
- str(self.current_state.id),
- [f"E{epic_index} / T{task_index}", "Backend", "working"],
- description,
- self.current_state.current_task.get("id"),
- )
-
- await self.ui.send_back_logs(
- [
- {
- "title": description,
- "project_state_id": str(self.current_state.id),
- "labels": [f"E{epic_index} / T{task_index}", "working"],
- }
- ]
- )
- await self.ui.clear_main_logs()
- await self.send_message(f"Starting task #{task_index} with the description:\n\n## {description}")
- if self.current_state.run_command:
- await self.ui.send_run_command(self.current_state.run_command)
-
- if self.next_state.current_task.get("redo_human_instructions", None) is not None:
- await self.send_message(f"Additional feedback: {self.next_state.current_task['redo_human_instructions']}")
- return True
-
- if self.current_state.current_task.get("quick_implementation", False):
- return True
-
- if self.current_state.current_task.get("user_added_subsequently", False):
- return True
-
- if self.current_state.current_task.get("hardcoded", False):
- return True
-
- if self.current_state.current_task and self.current_state.current_task.get("hardcoded", False):
- await self.ui.send_message(
- "Ok, great, you're now starting to build the backend and the first task is to test how the authentication works. You can now register and login. Your data will be saved into the database.",
- source=pythagora_source,
- )
-
+ await self.send_message("Starting new task with description:\n\n" + description)
user_response = await self.ask_question(
- DEV_EXECUTE_TASK,
+ "Do you want to execute the above task?",
buttons=buttons,
default="yes",
buttons_only=True,
@@ -474,11 +329,3 @@ async def ask_to_execute_task(self) -> bool:
log.info(f"Task description updated to: {user_response.text}")
# Orchestrator will rerun us with the new task description
return False
-
- async def update_knowledge_base(self):
- """
- Update the knowledge base with the current task and steps.
- """
- await self.state_manager.update_utility_functions(self.current_state.current_step)
- self.next_state.complete_step("utility_function")
- return AgentResponse.done(self)
diff --git a/core/agents/executor.py b/core/agents/executor.py
index 8b56cbb1c..e29c8e9be 100644
--- a/core/agents/executor.py
+++ b/core/agents/executor.py
@@ -6,7 +6,6 @@
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
from core.agents.response import AgentResponse
-from core.config.actions import EX_RUN_COMMAND, EX_SKIP_COMMAND, RUN_COMMAND
from core.llm.parser import JSONParser
from core.log import get_logger
from core.proc.exec_log import ExecLog
@@ -79,28 +78,23 @@ async def run(self) -> AgentResponse:
timeout = options.get("timeout")
if timeout:
- q = f"{RUN_COMMAND} {cmd} with {timeout}s timeout?"
+ q = f"Can I run command: {cmd} with {timeout}s timeout?"
else:
- q = f"{RUN_COMMAND} {cmd}?"
+ q = f"Can I run command: {cmd}?"
confirm = await self.ask_question(
q,
buttons={"yes": "Yes", "no": "No"},
default="yes",
- buttons_only=False,
- initial_text=cmd,
- extra_info={"remove_button": "yes"},
+ buttons_only=True,
)
if confirm.button == "no":
log.info(f"Skipping command execution of `{cmd}` (requested by user)")
await self.send_message(f"Skipping command {cmd}")
self.complete()
- self.next_state.action = EX_SKIP_COMMAND.format(cmd_name)
+ self.next_state.action = f'Skip "{cmd_name}"'
return AgentResponse.done(self)
- if confirm.button != "yes":
- cmd = confirm.text
-
started_at = datetime.now(timezone.utc)
log.info(f"Running command `{cmd}` with timeout {timeout}s")
@@ -111,7 +105,7 @@ async def run(self) -> AgentResponse:
duration = (datetime.now(timezone.utc) - started_at).total_seconds()
self.complete()
- self.next_state.action = EX_RUN_COMMAND.format(cmd_name)
+ self.next_state.action = f'Run "{cmd_name}"'
exec_log = ExecLog(
started_at=started_at,
@@ -178,4 +172,4 @@ def complete(self):
information we give it.
"""
self.step = None
- self.next_state.complete_step("command")
+ self.next_state.complete_step()
diff --git a/core/agents/frontend.py b/core/agents/frontend.py
deleted file mode 100644
index ff76eb505..000000000
--- a/core/agents/frontend.py
+++ /dev/null
@@ -1,602 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-from urllib.parse import urljoin
-
-import httpx
-
-from core.agents.base import BaseAgent
-from core.agents.convo import AgentConvo
-from core.agents.git import GitMixin
-from core.agents.mixins import FileDiffMixin
-from core.agents.response import AgentResponse
-from core.cli.helpers import capture_exception
-from core.config import FRONTEND_AGENT_NAME, IMPLEMENT_CHANGES_AGENT_NAME, PYTHAGORA_API
-from core.config.actions import (
- FE_CHANGE_REQ,
- FE_CONTINUE,
- FE_DONE_WITH_UI,
- FE_ITERATION,
- FE_ITERATION_DONE,
- FE_START,
-)
-from core.llm.convo import Convo
-from core.llm.parser import DescriptiveCodeBlockParser, OptionalCodeBlockParser
-from core.log import get_logger
-from core.telemetry import telemetry
-from core.ui.base import ProjectStage
-
-log = get_logger(__name__)
-
-
-def has_correct_num_of_backticks(response: str) -> bool:
- """
- Checks if the response has the correct number of backticks.
- """
- return response.count("```") % 2 == 0 and response.count("```") > 0
-
-
-class Frontend(FileDiffMixin, GitMixin, BaseAgent):
- agent_type = "frontend"
- display_name = "Frontend"
-
- async def run(self) -> AgentResponse:
- if not self.current_state.epics[-1]["messages"]:
- finished = await self.start_frontend()
- elif self.next_state.epics[-1].get("file_paths_to_remove_mock"):
- finished = await self.remove_mock()
- if finished is None:
- return AgentResponse.exit(self)
- elif not self.next_state.epics[-1].get("fe_iteration_done"):
- finished = await self.continue_frontend()
- else:
- await self.set_app_details()
- finished = await self.iterate_frontend()
- if finished is None:
- return AgentResponse.exit(self)
-
- return await self.end_frontend_iteration(finished)
-
- async def start_frontend(self):
- """
- Starts the frontend of the app.
- """
- self.state_manager.fe_auto_debug = True
- await self.ui.clear_main_logs()
- await self.ui.send_front_logs_headers(str(self.next_state.id), ["E2 / T1", "working"], "Building frontend")
- await self.ui.send_back_logs(
- [
- {
- "title": "Building frontend",
- "project_state_id": str(self.next_state.id),
- "labels": ["E2 / T1", "Frontend", "working"],
- }
- ]
- )
- self.next_state.action = FE_START
- await self.send_message("## Building the frontend\n\nThis may take a couple of minutes.")
- await self.ui.send_project_stage({"stage": ProjectStage.FRONTEND_STARTED})
-
- await self.ui.set_important_stream(False)
- llm = self.get_llm(FRONTEND_AGENT_NAME, stream_output=True)
- convo = AgentConvo(self).template(
- "build_frontend",
- summary=self.state_manager.template["template"].get_summary()
- if self.state_manager.template is not None
- else self.current_state.specification.template_summary,
- description=self.next_state.epics[-1]["description"],
- user_feedback=None,
- first_time_build=True,
- )
- response = await llm(convo, parser=DescriptiveCodeBlockParser())
- response_blocks = response.blocks
- convo.assistant(response.original_response)
-
- # Await the template task if it's not done yet
- if self.state_manager.async_tasks:
- if not self.state_manager.async_tasks[-1].done():
- await self.state_manager.async_tasks[-1]
- self.state_manager.async_tasks = []
-
- await self.process_response(response_blocks)
-
- self.next_state.epics[-1]["messages"] = convo.messages
- self.next_state.epics[-1]["fe_iteration_done"] = (
- "done" in response.original_response[-20:].lower().strip() or len(convo.messages) > 11
- )
- self.next_state.flag_epics_as_modified()
-
- return False
-
- async def continue_frontend(self):
- """
- Continues building the frontend of the app after the initial user input.
- """
- self.state_manager.fe_auto_debug = True
- self.next_state.action = FE_CONTINUE
- await self.ui.send_project_stage({"stage": ProjectStage.CONTINUE_FRONTEND})
- await self.send_message("### Continuing to build UI... This may take a couple of minutes")
-
- llm = self.get_llm(FRONTEND_AGENT_NAME, stream_output=True)
- convo = AgentConvo(self)
- convo.messages = self.current_state.epics[-1]["messages"]
- convo.user(
- "Ok, now think carefully about your previous response. If the response ends by mentioning something about continuing with the implementation, continue but don't implement any files that have already been implemented. If your last response finishes with an incomplete file, implement that file and any other that needs implementation. Finally, if your last response doesn't end by mentioning continuing and if there isn't an unfinished file implementation, respond only with `DONE` and with nothing else."
- )
-
- response = await llm(convo, parser=DescriptiveCodeBlockParser())
- response_blocks = response.blocks
- convo.assistant(response.original_response)
-
- use_relace = self.current_state.epics[-1].get("use_relace", False)
- await self.process_response(response_blocks, relace=use_relace)
-
- if self.next_state.epics[-1].get("manual_iteration", False):
- self.next_state.epics[-1]["fe_iteration_done"] = (
- has_correct_num_of_backticks(response.original_response)
- or self.current_state.epics[-1].get("retry_count", 0) >= 2
- )
- self.next_state.epics[-1]["retry_count"] = self.current_state.epics[-1].get("retry_count", 0) + 1
- else:
- self.next_state.epics[-1]["fe_iteration_done"] = (
- "done" in response.original_response[-20:].lower().strip() or len(convo.messages) > 15
- )
-
- self.next_state.epics[-1]["messages"] = convo.messages
- self.next_state.flag_epics_as_modified()
-
- return False
-
- async def iterate_frontend(self):
- """
- Iterates over the frontend.
-
- :return: True if the frontend is fully built, False otherwise.
- """
- self.next_state.epics[-1]["auto_debug_attempts"] = 0
- self.next_state.epics[-1]["retry_count"] = 0
- user_input = await self.try_auto_debug()
-
- frontend_only = self.current_state.branch.project.project_type == "swagger"
- self.next_state.action = FE_ITERATION
- # update the pages in the knowledge base
- await self.state_manager.update_implemented_pages_and_apis()
-
- await self.ui.send_project_stage({"stage": ProjectStage.ITERATE_FRONTEND, "iteration_index": 1})
-
- if user_input:
- await self.send_message("Errors detected, fixing...")
- else:
- answer = await self.ask_question(
- "Do you want to change anything or report a bug?" if frontend_only else FE_CHANGE_REQ,
- buttons={"yes": "I'm done building the UI"} if not frontend_only else None,
- default="yes",
- extra_info={"restart_app": True, "collect_logs": True},
- placeholder='For example, "I don\'t see anything when I open http://localhost:5173/" or "Nothing happens when I click on the NEW PROJECT button"',
- )
-
- if answer.button == "yes":
- answer = await self.ask_question(
- FE_DONE_WITH_UI,
- buttons={
- "yes": "Yes, let's build the backend",
- "no": "No, continue working on the UI",
- },
- buttons_only=True,
- default="yes",
- )
-
- if answer.button == "yes":
- fe_states = await self.state_manager.get_fe_states()
- first_fe_state_id = fe_states[0].id if fe_states else None
- last_fe_state_id = fe_states[-1].id if fe_states else None
-
- await self.ui.clear_main_logs()
- await self.ui.send_front_logs_headers(
- str(first_fe_state_id) if first_fe_state_id else "fe_0",
- ["E2 / T1", "done"],
- "Building frontend",
- )
- await self.ui.send_back_logs(
- [
- {
- "title": "Building frontend",
- "project_state_id": str(first_fe_state_id) if first_fe_state_id else "fe_0",
- "start_id": str(first_fe_state_id) if first_fe_state_id else "fe_0",
- "end_id": str(last_fe_state_id) if last_fe_state_id else "fe_0",
- "labels": ["E2 / T1", "Frontend", "done"],
- }
- ]
- )
- await self.ui.send_back_logs(
- [
- {
- "title": "Setting up backend",
- "disallow_reload": True,
- "project_state_id": "be_0",
- "labels": ["E2 / T2", "Backend setup", "working"],
- }
- ]
- )
- await self.ui.send_front_logs_headers("", ["E2 / T2", "working"], "Setting up backend")
- return True
- elif answer.button == "no":
- return False
-
- if answer.text:
- user_input = answer.text
- await self.send_message("Implementing the changes you suggested...")
-
- llm = self.get_llm(FRONTEND_AGENT_NAME)
-
- relevant_api_documentation = None
-
- if frontend_only:
- convo = AgentConvo(self).template(
- "is_relevant_for_docs_search",
- user_feedback=user_input,
- )
-
- response = await llm(convo)
- if str(response).lower() == "yes":
- error = None
- for attempt in range(3):
- try:
- url = urljoin(PYTHAGORA_API, "rag/search")
- async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport()) as client:
- resp = await client.post(
- url,
- json={"text": user_input, "project_id": str(self.state_manager.project.id)},
- headers={"Authorization": f"Bearer {self.state_manager.get_access_token()}"},
- )
-
- if resp.status_code in [200]:
- relevant_api_documentation = "\n".join(item["content"] for item in resp.json())
- break
- elif resp.status_code in [401, 403]:
- access_token = await self.ui.send_token_expired()
- self.state_manager.update_access_token(access_token)
- else:
- try:
- error = resp.json()["error"]
- except Exception as e:
- error = e
- log.warning(f"Failed to fetch from RAG service: {error}")
- await self.send_message(
- f"Couldn't find any relevant API documentation. Retrying... \nError: {error}"
- )
-
- except Exception as e:
- error = e
- capture_exception(e)
- log.warning(f"Failed to fetch from RAG service: {e}", exc_info=True)
- if error:
- await self.send_message(f"Please try reloading the project. \nError: {error}")
- return None
-
- llm = self.get_llm(FRONTEND_AGENT_NAME, stream_output=True)
-
- # try relace first
- convo = AgentConvo(self).template(
- "iterate_frontend",
- description=self.current_state.epics[-1]["description"],
- user_feedback=user_input,
- relevant_api_documentation=relevant_api_documentation,
- first_time_build=False,
- )
-
- # replace system prompt because of relace
- convo.messages[0]["content"] = AgentConvo(self).render("system_relace")
-
- response = await llm(convo, parser=DescriptiveCodeBlockParser())
-
- relace_finished = await self.process_response(response.blocks, relace=True)
-
- if not relace_finished:
- log.debug("Relace didn't finish, reverting to build_frontend")
- convo = AgentConvo(self).template(
- "build_frontend",
- description=self.current_state.epics[-1]["description"],
- user_feedback=user_input,
- relevant_api_documentation=relevant_api_documentation,
- first_time_build=False,
- )
-
- response = await llm(convo, parser=DescriptiveCodeBlockParser())
-
- await self.process_response(response.blocks)
-
- convo.assistant(response.original_response)
-
- self.next_state.epics[-1]["messages"] = convo.messages
- self.next_state.epics[-1]["use_relace"] = relace_finished
- self.next_state.epics[-1]["fe_iteration_done"] = has_correct_num_of_backticks(response.original_response)
- self.next_state.epics[-1]["manual_iteration"] = True
- self.next_state.flag_epics_as_modified()
-
- return False
-
- async def end_frontend_iteration(self, finished: bool) -> AgentResponse:
- """
- Ends the frontend iteration.
-
- :param finished: Whether the frontend is fully built.
- :return: AgentResponse.done(self)
- """
- if finished:
- # TODO Add question if user app is fully finished
- self.next_state.action = FE_ITERATION_DONE
-
- self.next_state.complete_epic()
- await telemetry.trace_code_event(
- "frontend-finished",
- {
- "description": self.current_state.epics[-1]["description"],
- "messages": self.current_state.epics[-1]["messages"],
- },
- )
-
- if self.state_manager.git_available and self.state_manager.git_used:
- await self.git_commit(commit_message="Frontend finished")
-
- inputs = []
- for file in self.current_state.files:
- if not file.content:
- continue
- input_required = self.state_manager.get_input_required(file.content.content, file.path)
- if input_required:
- inputs += [{"file": file.path, "line": line} for line in input_required]
-
- if inputs:
- return AgentResponse.input_required(self, inputs)
-
- return AgentResponse.done(self)
-
- async def process_response(self, response_blocks: list, removed_mock: bool = False, relace: bool = False) -> bool:
- """
- Processes the response blocks from the LLM.
-
- :param response_blocks: The response blocks from the LLM.
- :return: AgentResponse.done(self)
- """
- for block in response_blocks:
- description = block.description.strip()
- content = block.content.strip()
-
- # Split description into lines and check the last line for file path
- description_lines = description.split("\n")
- last_line = description_lines[-1].strip()
-
- if "file:" in last_line:
- # Extract file path from the last line - get everything after "file:"
- file_path = last_line[last_line.index("file:") + 5 :].strip()
- file_path = file_path.strip("\"'`")
- # Skip empty file paths
- if file_path.strip() == "":
- continue
- new_content = content
- old_content = self.current_state.get_file_content_by_path(file_path)
-
- if relace:
- llm = self.get_llm(IMPLEMENT_CHANGES_AGENT_NAME)
- convo = Convo().user(
- {
- "initialCode": old_content,
- "editSnippet": new_content,
- }
- )
-
- new_content = await llm(convo, temperature=0, parser=OptionalCodeBlockParser())
-
- if not new_content or new_content == ("", 0, 0):
- return False
-
- n_new_lines, n_del_lines = self.get_line_changes(old_content, new_content)
- await self.ui.send_file_status(file_path, "done", source=self.ui_source)
- await self.ui.generate_diff(
- file_path, old_content, new_content, n_new_lines, n_del_lines, source=self.ui_source
- )
- if not removed_mock and self.current_state.branch.project.project_type == "swagger":
- if "client/src/api" in file_path:
- if not self.next_state.epics[-1].get("file_paths_to_remove_mock"):
- self.next_state.epics[-1]["file_paths_to_remove_mock"] = []
- self.next_state.epics[-1]["file_paths_to_remove_mock"].append(file_path)
-
- await self.state_manager.save_file(file_path, new_content)
-
- elif "command:" in last_line:
- # Split multiple commands and execute them sequentially
- commands = content.strip().split("\n")
- for command in commands:
- command = command.strip()
- if command:
- # Add "cd client" prefix if not already present
- if not command.startswith("cd "):
- command = f"cd client && {command}"
- if "run start" in command or "run dev" in command:
- continue
-
- # if command is cd client && some_command client/ -> won't work, we need to remove client/ after &&
- prefix, cmd_part = command.split("&&", 1)
- cmd_part = cmd_part.strip().replace("client/", "")
- command = f"{prefix} && {cmd_part}"
-
- # check if cmd_part contains npm run something, if that something is not in scripts, then skip it
- if "npm run" in cmd_part:
- npm_script = cmd_part.split("npm run")[1].strip()
-
- absolute_path = os.path.join(
- self.state_manager.get_full_project_root(),
- os.path.join(
- "client" if "client" in prefix else "server" if "server" in prefix else "",
- "package.json",
- ),
- )
- with open(absolute_path, "r") as file:
- package_json = json.load(file)
- if npm_script not in package_json.get("scripts", {}):
- log.warning(
- f"Skipping command: {command} as npm script {npm_script} not found, command is {command}"
- )
- continue
-
- await self.send_message(f"Running command: `{command}`...")
- await self.process_manager.run_command(command)
- else:
- log.info(f"Unknown block description: {description}")
-
- return True
-
- async def remove_mock(self):
- """
- Remove mock API from the backend and replace it with api endpoints defined in the external documentation
- """
- new_file_paths = self.current_state.epics[-1]["file_paths_to_remove_mock"]
- llm = self.get_llm(FRONTEND_AGENT_NAME)
-
- for file_path in new_file_paths:
- old_content = self.current_state.get_file_content_by_path(file_path)
-
- convo = AgentConvo(self).template("create_rag_query", file_content=old_content)
- topics = await llm(convo)
-
- if topics != "None":
- error = None
- for attempt in range(3):
- try:
- url = urljoin(PYTHAGORA_API, "rag/search")
- async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport()) as client:
- resp = await client.post(
- url,
- json={"text": topics, "project_id": str(self.state_manager.project.id)},
- headers={"Authorization": f"Bearer {self.state_manager.get_access_token()}"},
- )
- if resp.status_code == 200:
- resp_json = resp.json()
- relevant_api_documentation = "\n".join(item["content"] for item in resp_json)
-
- referencing_files = await self.state_manager.get_referencing_files(
- self.current_state, file_path
- )
-
- convo = AgentConvo(self).template(
- "remove_mock",
- relevant_api_documentation=relevant_api_documentation,
- file_content=old_content,
- file_path=file_path,
- referencing_files=referencing_files,
- lines=len(old_content.splitlines()),
- )
-
- response = await llm(convo, parser=DescriptiveCodeBlockParser())
- response_blocks = response.blocks
- convo.assistant(response.original_response)
- await self.process_response(response_blocks, removed_mock=True)
- self.next_state.epics[-1]["file_paths_to_remove_mock"].remove(file_path)
- break
- elif resp.status_code in [401, 403]:
- access_token = await self.ui.send_token_expired()
- self.state_manager.update_access_token(access_token)
- else:
- try:
- error = resp.json()["error"]
- except Exception as e:
- error = e
- log.warning(f"Failed to fetch from RAG service: {error}")
- await self.send_message(
- f"I couldn't find any relevant API documentation. Retrying... \nError: {error}"
- )
- except Exception as e:
- capture_exception(e)
- log.warning(f"Failed to fetch from RAG service: {e}", exc_info=True)
- if error:
- await self.send_message(f"Please try reloading the project. \nError: {error}")
- return None
-
- return False
-
- async def set_app_details(self):
- """
- Sets the app details.
- """
- command = "npm run start"
- app_link = "http://localhost:5173"
-
- self.next_state.run_command = command
- # todo store app link and send whenever we are sending run_command
- # self.next_state.app_link = app_link
- await self.ui.send_run_command(command)
- await self.ui.send_app_link(app_link)
-
- async def kill_app(self):
- is_win = sys.platform.lower().startswith("win")
- # TODO make ports configurable
- # kill frontend - both swagger and node
- if is_win:
- await self.process_manager.run_command(
- """for /f "tokens=5" %a in ('netstat -ano ^| findstr :5173 ^| findstr LISTENING') do taskkill /F /PID %a""",
- show_output=False,
- )
- else:
- await self.process_manager.run_command("lsof -ti:5173 | xargs -r kill", show_output=False)
-
- # if node project, kill backend as well
- if self.state_manager.project.project_type == "node":
- if is_win:
- await self.process_manager.run_command(
- """for /f "tokens=5" %a in ('netstat -ano ^| findstr :3000 ^| findstr LISTENING') do taskkill /F /PID %a""",
- show_output=False,
- )
- else:
- await self.process_manager.run_command("lsof -ti:3000 | xargs -r kill", show_output=False)
-
- async def try_auto_debug(self) -> str:
- if not self.state_manager.fe_auto_debug:
- self.state_manager.fe_auto_debug = True
- return ""
- if self.next_state.epics[-1].get("auto_debug_attempts", 0) >= 3:
- return ""
-
- count = 3
-
- try:
- await self.send_message(
- f"### Auto-debugging the frontend #{self.next_state.epics[-1]['auto_debug_attempts']+1}"
- )
- self.next_state.epics[-1]["auto_debug_attempts"] = (
- self.current_state.epics[-1].get("auto_debug_attempts", 0) + 1
- )
- # kill app
- await self.kill_app()
-
- npm_proc = await self.process_manager.start_process("npm run start &", show_output=False)
-
- while True:
- if count == 3:
- await asyncio.sleep(5)
- else:
- await asyncio.sleep(2)
- diff_stdout, diff_stderr = await npm_proc.read_output()
- if (diff_stdout == "" and diff_stderr == "") or count <= 0:
- break
- count -= 1
-
- await self.process_manager.run_command("curl http://localhost:5173", show_output=False)
- await asyncio.sleep(1)
-
- diff_stdout, diff_stderr = await npm_proc.read_output()
-
- # kill app again
- await self.kill_app()
-
- if diff_stdout or diff_stderr:
- await self.send_message(f"### Auto-debugging found an error: \n{diff_stdout}\n{diff_stderr}")
- log.debug(f"Auto-debugging output:\n{diff_stdout}\n{diff_stderr}")
- return f"I got an error. Here are the logs:\n{diff_stdout}\n{diff_stderr}"
- except Exception as e:
- capture_exception(e)
- log.error(f"Error during auto-debugging: {e}", exc_info=True)
-
- await self.send_message("### All good, no errors found.")
- return ""
diff --git a/core/agents/git.py b/core/agents/git.py
deleted file mode 100644
index 027b117a4..000000000
--- a/core/agents/git.py
+++ /dev/null
@@ -1,167 +0,0 @@
-import os
-from typing import Optional
-
-from core.agents.convo import AgentConvo
-from core.config.magic_words import GITIGNORE_CONTENT
-from core.ui.base import pythagora_source
-
-
-class GitMixin:
- """
- Mixin class for git commands
- """
-
- async def check_git_installed(self) -> bool:
- """Check if git is installed on the system."""
- status_code, _, _ = await self.process_manager.run_command("git --version", show_output=False)
- git_available = status_code == 0
- self.state_manager.git_available = git_available
- return git_available
-
- async def is_git_initialized(self) -> bool:
- """Check if git is initialized in the workspace."""
- workspace_path = self.state_manager.get_full_project_root()
-
- status_code, _, _ = await self.process_manager.run_command(
- "git rev-parse --git-dir --is-inside-git-dir",
- cwd=workspace_path,
- show_output=False,
- )
- # Will return status code 0 only if .git exists in the current directory
- git_used = status_code == 0 and os.path.exists(os.path.join(workspace_path, ".git"))
- self.state_manager.git_used = git_used
- return git_used
-
- async def init_git_if_needed(self) -> bool:
- """
- Initialize git repository if it hasn't been initialized yet.
- Returns True if initialization was needed and successful.
- """
-
- workspace_path = self.state_manager.get_full_project_root()
- if await self.is_git_initialized():
- return False
-
- answer = await self.ui.ask_question(
- "Git is not initialized for this project. Do you want to initialize it now?",
- buttons={"yes": "Yes", "no": "No"},
- default="yes",
- buttons_only=True,
- source=pythagora_source,
- )
-
- if answer.button == "no":
- return False
- else:
- status_code, _, stderr = await self.process_manager.run_command("git init", cwd=workspace_path)
- if status_code != 0:
- raise RuntimeError(f"Failed to initialize git repository: {stderr}")
-
- gitignore_path = os.path.join(workspace_path, ".gitignore")
- try:
- with open(gitignore_path, "w") as f:
- f.write(GITIGNORE_CONTENT)
- except Exception as e:
- raise RuntimeError(f"Failed to create .gitignore file: {str(e)}")
-
- # First check if there are any changes to commit
- status_code, stdout, stderr = await self.process_manager.run_command(
- "git status --porcelain",
- cwd=workspace_path,
- )
-
- if status_code == 0 and stdout.strip(): # If there are changes (stdout is not empty)
- # Stage all files
- status_code, _, stderr = await self.process_manager.run_command(
- "git add .",
- cwd=workspace_path,
- )
- if status_code != 0:
- raise RuntimeError(f"Failed to stage files: {stderr}")
-
- # Create initial commit
- status_code, _, stderr = await self.process_manager.run_command(
- 'git commit -m "initial commit"', cwd=workspace_path
- )
- if status_code != 0:
- raise RuntimeError(f"Failed to create initial commit: {stderr}")
-
- self.state_manager.git_used = True
- return True
-
- async def git_commit(self, commit_message: Optional[str] = None) -> None:
- """
- Create a git commit with the specified message. Commit message is optional.
- Raises RuntimeError if the commit fails.
- """
- workspace_path = self.state_manager.get_full_project_root()
-
- # Check if there are any changes to commit
- status_code, git_status, stderr = await self.process_manager.run_command(
- "git status --porcelain",
- cwd=workspace_path,
- show_output=False,
- )
- if status_code != 0:
- raise RuntimeError(f"Failed to get git status: {stderr}")
-
- if not git_status.strip():
- return
-
- answer = await self.ui.ask_question(
- "Do you want to create new git commit?",
- buttons={"yes": "Yes", "no": "No"},
- default="yes",
- buttons_only=True,
- source=pythagora_source,
- )
-
- if answer.button == "no":
- return
-
- # Stage all changes
- status_code, _, stderr = await self.process_manager.run_command("git add .", cwd=workspace_path)
- if status_code != 0:
- raise RuntimeError(f"Failed to stage changes: {stderr}")
-
- # Get git diff
- status_code, git_diff, stderr = await self.process_manager.run_command(
- "git diff --cached || git diff",
- cwd=workspace_path,
- show_output=False,
- )
- if status_code != 0:
- raise RuntimeError(f"Failed to create initial commit: {stderr}")
-
- if not commit_message:
- llm = self.get_llm()
- convo = AgentConvo(self).template(
- "commit",
- git_diff=git_diff,
- )
- commit_message: str = await llm(convo)
-
- answer = await self.ui.ask_question(
- f"Do you accept this 'git commit' message? Here is suggested message: '{commit_message}'",
- buttons={"yes": "Yes", "edit": "Edit", "no": "No, I don't want to commit changes."},
- default="yes",
- buttons_only=True,
- source=pythagora_source,
- )
-
- if answer.button == "no":
- return
- elif answer.button == "edit":
- user_message = await self.ui.ask_question(
- "Please enter the commit message",
- source=pythagora_source,
- initial_text=commit_message,
- )
- commit_message = user_message.text
-
- # Create commit
- status_code, _, stderr = await self.process_manager.run_command(
- f'git commit -m "{commit_message}"', cwd=workspace_path
- )
- if status_code != 0:
- raise RuntimeError(f"Failed to create commit: {stderr}")
diff --git a/core/agents/human_input.py b/core/agents/human_input.py
index 066c7e785..5bd62d295 100644
--- a/core/agents/human_input.py
+++ b/core/agents/human_input.py
@@ -1,6 +1,5 @@
from core.agents.base import BaseAgent
from core.agents.response import AgentResponse, ResponseType
-from core.config.actions import CONTINUE_WHEN_DONE, HUMAN_INTERVENTION_QUESTION
class HumanInput(BaseAgent):
@@ -16,14 +15,13 @@ async def run(self) -> AgentResponse:
async def human_intervention(self, step) -> AgentResponse:
description = step["human_intervention_description"]
- await self.send_message(f"## {HUMAN_INTERVENTION_QUESTION}\n\n{description}")
await self.ask_question(
- CONTINUE_WHEN_DONE,
+ f"I need human intervention: {description}",
buttons={"continue": "Continue"},
default="continue",
buttons_only=True,
)
- self.next_state.complete_step("human_intervention")
+ self.next_state.complete_step()
return AgentResponse.done(self)
async def input_required(self, files: list[dict]) -> AgentResponse:
@@ -37,5 +35,12 @@ async def input_required(self, files: list[dict]) -> AgentResponse:
# figure out where its local files are and how to open it.
full_path = self.state_manager.file_system.get_full_path(file)
- await self.ui.open_editor(full_path, line, True)
+ await self.send_message(f"Input required on {file}:{line}")
+ await self.ui.open_editor(full_path, line)
+ await self.ask_question(
+ f"Please open {file} and modify line {line} according to the instructions.",
+ buttons={"continue": "Continue"},
+ default="continue",
+ buttons_only=True,
+ )
return AgentResponse.done(self)
diff --git a/core/agents/legacy_handler.py b/core/agents/legacy_handler.py
index d046f0e2e..f675152ec 100644
--- a/core/agents/legacy_handler.py
+++ b/core/agents/legacy_handler.py
@@ -8,7 +8,7 @@ class LegacyHandler(BaseAgent):
async def run(self) -> AgentResponse:
if self.data["type"] == "review_task":
- self.next_state.complete_step("review_task")
+ self.next_state.complete_step()
return AgentResponse.done(self)
raise ValueError(f"Unknown reason for calling Legacy Handler with data: {self.data}")
diff --git a/core/agents/mixins.py b/core/agents/mixins.py
index 3b490b4e1..907a5c786 100644
--- a/core/agents/mixins.py
+++ b/core/agents/mixins.py
@@ -1,82 +1,40 @@
-import asyncio
-import json
-from typing import List, Optional
+from typing import List, Optional, Union
from pydantic import BaseModel, Field
from core.agents.convo import AgentConvo
from core.agents.response import AgentResponse
-from core.cli.helpers import get_line_changes
-from core.config import GET_RELEVANT_FILES_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME, TROUBLESHOOTER_BUG_REPORT
-from core.config.actions import MIX_BREAKDOWN_CHAT_PROMPT
-from core.config.constants import CONVO_ITERATIONS_LIMIT
-from core.config.magic_words import ALWAYS_RELEVANT_FILES
+from core.config import GET_RELEVANT_FILES_AGENT_NAME, TROUBLESHOOTER_BUG_REPORT
from core.llm.parser import JSONParser
from core.log import get_logger
-from core.ui.base import ProjectStage
log = get_logger(__name__)
-class RelevantFiles(BaseModel):
- relevant_files: Optional[List[str]] = Field(
- description="List of files you want to add to the list of relevant files."
+class ReadFilesAction(BaseModel):
+ read_files: Optional[List[str]] = Field(
+ description="List of files you want to read. All listed files must be in the project."
)
-class Test(BaseModel):
- title: str = Field(description="Very short title of the test.")
- action: str = Field(description="More detailed description of what actions have to be taken to test the app.")
- result: str = Field(description="Expected result that verifies successful test.")
-
-
-class TestSteps(BaseModel):
- steps: List[Test]
-
-
-class ChatWithBreakdownMixin:
- """
- Provides a method to chat with the user and provide a breakdown of the conversation.
- """
-
- async def chat_with_breakdown(self, convo: AgentConvo, breakdown: str) -> AgentConvo:
- """
- Chat with the user and provide a breakdown of the conversation.
-
- :param convo: The conversation object.
- :param breakdown: The breakdown of the conversation.
- :return: The breakdown.
- """
+class AddFilesAction(BaseModel):
+ add_files: Optional[List[str]] = Field(
+ description="List of files you want to add to the list of relevant files. All listed files must be in the project. You must read files before adding them."
+ )
- llm = self.get_llm(TASK_BREAKDOWN_AGENT_NAME, stream_output=True)
- while True:
- await self.ui.send_project_stage(
- {
- "stage": ProjectStage.BREAKDOWN_CHAT,
- "agent": self.agent_type,
- }
- )
- if self.state_manager.auto_confirm_breakdown:
- break
+class RemoveFilesAction(BaseModel):
+ remove_files: Optional[List[str]] = Field(
+ description="List of files you want to remove from the list of relevant files. All listed files must be in the relevant files list."
+ )
- chat = await self.ask_question(
- MIX_BREAKDOWN_CHAT_PROMPT,
- buttons={"yes": "Yes, looks good!"},
- default="yes",
- verbose=False,
- )
- if chat.button == "yes":
- break
- if len(convo.messages) > CONVO_ITERATIONS_LIMIT:
- convo.slice(3, CONVO_ITERATIONS_LIMIT)
+class DoneBooleanAction(BaseModel):
+ done: Optional[bool] = Field(description="Boolean flag to indicate that you are done creating breakdown.")
- convo.user(chat.text)
- breakdown: str = await llm(convo)
- convo.assistant(breakdown)
- return breakdown
+class RelevantFiles(BaseModel):
+ action: Union[ReadFilesAction, AddFilesAction, RemoveFilesAction, DoneBooleanAction]
class IterationPromptMixin:
@@ -110,55 +68,21 @@ async def find_solution(
user_feedback_qa=user_feedback_qa,
next_solution_to_try=next_solution_to_try,
bug_hunting_cycles=bug_hunting_cycles,
- test_instructions=json.loads(self.current_state.current_task.get("test_instructions", "[]")),
)
llm_solution: str = await llm(convo)
-
- llm_solution = await self.chat_with_breakdown(convo, llm_solution)
-
return llm_solution
class RelevantFilesMixin:
"""
- Asynchronously retrieves relevant files for the current task by separating front-end and back-end files, and processing them in parallel.
-
- This method initiates two asynchronous tasks to fetch relevant files for the front-end (client) and back-end (server) respectively.
- It then combines the results, filters out any non-existing files, and updates the current and next state with the relevant files.
+ Provides a method to get relevant files for the current task.
"""
- async def get_relevant_files_parallel(
+ async def get_relevant_files(
self, user_feedback: Optional[str] = None, solution_description: Optional[str] = None
) -> AgentResponse:
- tasks = [
- self.get_relevant_files(
- user_feedback=user_feedback, solution_description=solution_description, dir_type="client"
- ),
- self.get_relevant_files(
- user_feedback=user_feedback, solution_description=solution_description, dir_type="server"
- ),
- ]
-
- responses = await asyncio.gather(*tasks)
-
- relevant_files = [item for sublist in responses for item in sublist]
-
- existing_files = {file.path for file in self.current_state.files}
- relevant_files = [path for path in relevant_files if path in existing_files]
- self.current_state.relevant_files = relevant_files
- self.next_state.relevant_files = relevant_files
-
- return AgentResponse.done(self)
-
- async def get_relevant_files(
- self,
- user_feedback: Optional[str] = None,
- solution_description: Optional[str] = None,
- dir_type: Optional[str] = None,
- ) -> list[str]:
- log.debug(
- "Getting relevant files for the current task for: " + ("frontend" if dir_type == "client" else "backend")
- )
+ log.debug("Getting relevant files for the current task")
+ done = False
relevant_files = set()
llm = self.get_llm(GET_RELEVANT_FILES_AGENT_NAME)
convo = (
@@ -168,41 +92,35 @@ async def get_relevant_files(
user_feedback=user_feedback,
solution_description=solution_description,
relevant_files=relevant_files,
- dir_type=dir_type,
)
.require_schema(RelevantFiles)
)
- llm_response: RelevantFiles = await llm(convo, parser=JSONParser(RelevantFiles), temperature=0)
- existing_files = {file.path for file in self.current_state.files}
- if not llm_response.relevant_files:
- return []
- paths_list = [path for path in llm_response.relevant_files if path in existing_files]
- try:
- for file_path in ALWAYS_RELEVANT_FILES:
- if file_path not in paths_list and file_path in existing_files:
- paths_list.append(file_path)
- except Exception as e:
- log.error(f"Error while getting most important files: {e}")
+ while not done and len(convo.messages) < 13:
+ llm_response: RelevantFiles = await llm(convo, parser=JSONParser(RelevantFiles), temperature=0)
+ action = llm_response.action
- return paths_list
+ # Check if there are files to add to the list
+ if getattr(action, "add_files", None):
+ # Add only the files from add_files that are not already in relevant_files
+ relevant_files.update(file for file in action.add_files if file not in relevant_files)
+ # Check if there are files to remove from the list
+ if getattr(action, "remove_files", None):
+ # Remove files from relevant_files that are in remove_files
+ relevant_files.difference_update(action.remove_files)
-class FileDiffMixin:
- """
- Provides a method to generate a diff between two files.
- """
+ read_files = [file for file in self.current_state.files if file.path in getattr(action, "read_files", [])]
- def get_line_changes(self, old_content: str, new_content: str) -> tuple[int, int]:
- """
- Get the number of added and deleted lines between two files.
-
- This uses Python difflib to produce a unified diff, then counts
- the number of added and deleted lines.
+ convo.remove_last_x_messages(1)
+ convo.assistant(llm_response.original_response)
+ convo.template("filter_files_loop", read_files=read_files, relevant_files=relevant_files).require_schema(
+ RelevantFiles
+ )
+ done = getattr(action, "done", False)
- :param old_content: old file content
- :param new_content: new file content
- :return: a tuple (n_new_lines, n_del_lines)
- """
+ existing_files = {file.path for file in self.current_state.files}
+ relevant_files = [path for path in relevant_files if path in existing_files]
+ self.next_state.relevant_files = relevant_files
- return get_line_changes(old_content, new_content)
+ return AgentResponse.done(self)
diff --git a/core/agents/orchestrator.py b/core/agents/orchestrator.py
index e3c462d95..756cb5915 100644
--- a/core/agents/orchestrator.py
+++ b/core/agents/orchestrator.py
@@ -1,7 +1,4 @@
import asyncio
-import json
-import os
-import re
from typing import List, Optional, Union
from core.agents.architect import Architect
@@ -12,8 +9,6 @@
from core.agents.error_handler import ErrorHandler
from core.agents.executor import Executor
from core.agents.external_docs import ExternalDocumentation
-from core.agents.frontend import Frontend
-from core.agents.git import GitMixin
from core.agents.human_input import HumanInput
from core.agents.importer import Importer
from core.agents.legacy_handler import LegacyHandler
@@ -24,16 +19,15 @@
from core.agents.tech_lead import TechLead
from core.agents.tech_writer import TechnicalWriter
from core.agents.troubleshooter import Troubleshooter
-from core.agents.wizard import Wizard
from core.db.models.project_state import IterationStatus, TaskStatus
from core.log import get_logger
from core.telemetry import telemetry
-from core.ui.base import UserInterruptError
+from core.ui.base import ProjectStage
log = get_logger(__name__)
-class Orchestrator(BaseAgent, GitMixin):
+class Orchestrator(BaseAgent):
"""
Main agent that controls the flow of the process.
@@ -62,48 +56,14 @@ async def run(self) -> bool:
await self.init_ui()
await self.offline_changes_check()
- await self.install_dependencies()
-
- if self.args.use_git and await self.check_git_installed():
- await self.init_git_if_needed()
-
- await self.set_frontend_script()
- await self.set_package_json()
- await self.set_vite_config()
- await self.set_favicon()
- await self.enable_debugger()
- await self.ui.knowledge_base_update(
- {
- "pages": self.current_state.knowledge_base.pages,
- "apis": self.current_state.knowledge_base.apis,
- "user_options": self.current_state.knowledge_base.user_options,
- "utility_functions": self.current_state.knowledge_base.utility_functions,
- }
- )
-
- # TODO: consider refactoring this into two loop; the outer with one iteration per committed step,
+ # TODO: consider refactoring this into two loop; the outer with one iteration per comitted step,
# and the inner which runs the agents for the current step until they're done. This would simplify
# handle_done() and let us do other per-step processing (eg. describing files) in between agent runs.
while True:
- # If the task is marked as "redo_human_instructions", we need to reload the project at the state before the current task breakdown
- if (
- self.current_state.current_task
- and self.current_state.current_task.get("redo_human_instructions", None) is not None
- ):
- redo_human_instructions = self.current_state.current_task["redo_human_instructions"]
- project_state = await self.state_manager.get_project_state_for_redo_task(self.current_state)
-
- if project_state is not None:
- await self.state_manager.load_project(
- branch_id=project_state.branch_id, step_index=project_state.step_index
- )
- await self.state_manager.restore_files()
- self.current_state.epics[-1]["completed"] = False
- self.next_state.epics[-1]["completed"] = False
- self.next_state.current_task["redo_human_instructions"] = redo_human_instructions
-
await self.update_stats()
+
agent = self.create_agent(response)
+
# In case where agent is a list, run all agents in parallel.
# Only one agent type can be run in parallel at a time (for now). See handle_parallel_responses().
if isinstance(agent, list):
@@ -113,38 +73,9 @@ async def run(self) -> bool:
)
responses = await asyncio.gather(*tasks)
response = self.handle_parallel_responses(agent[0], responses)
-
- should_update_knowledge_base = any(
- "src/pages/" in single_agent.step.get("save_file", {}).get("path", "")
- or "src/api/" in single_agent.step.get("save_file", {}).get("path", "")
- or single_agent.current_state.current_task.get("related_api_endpoints")
- for single_agent in agent
- )
-
- if should_update_knowledge_base:
- files_with_implemented_apis = [
- {
- "path": single_agent.step.get("save_file", {}).get("path", None),
- "content": single_agent.step.get("save_file", {}).get("content", None),
- "related_api_endpoints": single_agent.current_state.current_task.get(
- "related_api_endpoints"
- ),
- "line": 0,
- }
- for single_agent in agent
- if single_agent.current_state.current_task.get("related_api_endpoints")
- ]
-
- await self.state_manager.update_apis(files_with_implemented_apis)
- await self.state_manager.update_implemented_pages_and_apis()
-
else:
log.debug(f"Running agent {agent.__class__.__name__} (step {self.current_state.step_index})")
- try:
- response = await agent.run()
- except UserInterruptError:
- log.debug("User interrupted the agent!")
- response = AgentResponse.done(self)
+ response = await agent.run()
if response.type == ResponseType.EXIT:
log.debug(f"Agent {agent.__class__.__name__} requested exit")
@@ -152,196 +83,11 @@ async def run(self) -> bool:
if response.type == ResponseType.DONE:
response = await self.handle_done(agent, response)
- log.debug(f"Agent {agent.__class__.__name__} returned")
- if not isinstance(agent, list) and agent.agent_type == "spec-writer":
- project_details = self.state_manager.get_project_info()
- await self.ui.send_project_info(
- project_details["name"],
- project_details["id"],
- project_details["folderName"],
- project_details["createdAt"],
- )
continue
# TODO: rollback changes to "next" so they aren't accidentally committed?
return True
- async def install_dependencies(self):
- # First check if package.json exists
- package_json_path = os.path.join(self.state_manager.get_full_project_root(), "package.json")
- if not os.path.exists(package_json_path):
- # Skip if no package.json found
- return
-
- # Then check if node_modules directory exists
- node_modules_path = os.path.join(self.state_manager.get_full_project_root(), "node_modules")
- if not os.path.exists(node_modules_path):
- await self.send_message("Installing project dependencies...")
- await self.process_manager.run_command("npm install", show_output=False, timeout=600)
-
- async def set_frontend_script(self):
- file_path = os.path.join("client", "index.html")
- absolute_path = os.path.join(self.state_manager.get_full_project_root(), file_path)
- script_tag = ''
-
- # Check if file exists
- if not os.path.exists(absolute_path):
- return
-
- try:
- # Read the HTML file
- with open(absolute_path, "r", encoding="utf-8") as file:
- content = file.read()
-
- # Check if script already exists
- if script_tag in content:
- return
-
- # Find the head tag and title tag
- head_match = re.search(r"]*>(.*?)", content, re.DOTALL | re.IGNORECASE)
-
- if head_match:
- head_content = head_match.group(1)
- title_match = re.search(r"(]*>.*?)", head_content, re.DOTALL | re.IGNORECASE)
-
- if title_match:
- # Insert after title
- new_head = head_content.replace(title_match.group(1), f"{title_match.group(1)}\n {script_tag}")
- else:
- # Insert at the beginning of head
- new_head = f"\n {script_tag}{head_content}"
-
- # Replace old head content with new one
- new_content = content.replace(head_content, new_head)
-
- await self.state_manager.save_file(file_path, new_content)
-
- except Exception as e:
- log.error(f"An error occurred: {str(e)}")
-
- async def enable_debugger(self):
- absolute_path = os.path.join(self.state_manager.get_full_project_root(), "package.json")
-
- if not os.path.exists(absolute_path):
- return
-
- try:
- with open(absolute_path, "r") as file:
- package_json = json.load(file)
-
- if "debug" not in package_json["scripts"]:
- package_json["scripts"]["debug"] = (
- 'concurrently -n "client,server" "npm run client" "cross-env NODE_OPTIONS=--inspect-brk=9229 npm run server"'
- )
-
- if "devDependencies" not in package_json:
- package_json["devDependencies"] = {}
-
- if "cross-env" not in package_json["devDependencies"]:
- package_json["devDependencies"]["cross-env"] = "^7.0.3"
- else:
- return
- await self.state_manager.save_file(absolute_path, json.dumps(package_json))
- await self.process_manager.run_command("npm install", show_output=True, timeout=600)
-
- log.debug("Debugger support added.")
-
- except Exception as e:
- log.debug(f"An error occurred: {e}")
-
- async def set_favicon(self):
- """
- Set up favicon link in the client/index.html file.
- """
- try:
- client_dir = os.path.join(self.state_manager.get_full_project_root(), "client")
- index_path = os.path.join(client_dir, "index.html")
-
- if not os.path.exists(index_path):
- return
-
- # Read the HTML file
- with open(index_path, "r", encoding="utf-8") as file:
- content = file.read()
-
- favicon_link = ''
-
- # Check if favicon link already exists
- if favicon_link in content:
- return
-
- # Find the position where to insert the favicon link
- # Look for tag and insert before it
- updated_content = content.replace("", f" {favicon_link}\n ")
-
- await self.state_manager.save_file(index_path, updated_content)
- log.debug("Favicon link added to index.html")
-
- except Exception as e:
- log.debug(f"An error occurred while setting favicon: {e}")
-
- async def set_package_json(self):
- file_path = os.path.join("client", "package.json")
- absolute_path = os.path.join(self.state_manager.get_full_project_root(), file_path)
-
- if not os.path.exists(absolute_path):
- return
-
- try:
- script = "vite build"
- with open(absolute_path, "r") as file:
- package_json = json.load(file)
-
- if package_json["scripts"].get("build") == script:
- return
-
- package_json["scripts"]["build"] = script
-
- await self.state_manager.save_file(absolute_path, json.dumps(package_json, indent=4))
- log.debug(f"Build script changed to {script}.")
-
- except Exception as e:
- log.debug(f"An error occurred: {e}")
-
- async def set_vite_config(self):
- file_path = os.path.join("client", "vite.config.ts")
- absolute_path = os.path.join(self.state_manager.get_full_project_root(), file_path)
-
- if not os.path.exists(absolute_path):
- return
-
- try:
- # Read the current file
- with open(absolute_path, "r", encoding="utf-8") as file:
- current_content = file.read()
-
- # Check if required configs already exist
- has_host_true = "host: true" in current_content
- has_watch_config = "watch: {" in current_content and "ignored:" in current_content
-
- # If both required configs exist, no need to change anything
- if has_host_true and has_watch_config:
- log.debug("Vite config already has host:true and watch configuration. No changes needed.")
- return
-
- # Get the template path
- project_root = self.state_manager.get_full_project_root()
- base_path = project_root.split("/pythagora-core")[0] + "/pythagora-core"
- template_path = os.path.join(
- base_path, "core", "templates", "tree", "vite_react", "client", "vite.config.ts"
- )
-
- # Read the template file
- with open(template_path, "r", encoding="utf-8") as file:
- template_content = file.read()
-
- # Save the template content to the target file
- await self.state_manager.save_file(file_path, template_content)
- log.debug("Updated vite.config.ts with the template configuration.")
-
- except Exception as e:
- log.debug(f"An error occurred while updating vite.config.ts: {e}")
-
def handle_parallel_responses(self, agent: BaseAgent, responses: List[AgentResponse]) -> AgentResponse:
"""
Handle responses from agents that were run in parallel.
@@ -375,45 +121,40 @@ async def offline_changes_check(self):
import if needed.
"""
- try:
- log.info("Checking for offline changes.")
- modified_files = await self.state_manager.get_modified_files_with_content()
+ log.info("Checking for offline changes.")
+ modified_files = await self.state_manager.get_modified_files_with_content()
- if self.state_manager.workspace_is_empty():
- # NOTE: this will currently get triggered on a new project, but will do
- # nothing as there's no files in the database.
- log.info("Detected empty workspace, restoring state from the database.")
- await self.state_manager.restore_files()
- elif modified_files:
- await self.send_message(f"We found {len(modified_files)} new and/or modified files.")
- await self.ui.send_modified_files(modified_files)
- hint = "".join(
- [
- "If you would like Pythagora to import those changes, click 'Yes'.\n",
- "Clicking 'No' means Pythagora will restore (overwrite) all files to the last stored state.\n",
- ]
- )
- use_changes = await self.ask_question(
- question="Would you like to keep your changes?",
- buttons={
- "yes": "Yes, keep my changes",
- "no": "No, restore last Pythagora state",
- },
- buttons_only=True,
- hint=hint,
- )
- if use_changes.button == "yes":
- log.debug("Importing offline changes into Pythagora.")
- await self.import_files()
- else:
- log.debug("Restoring last stored state.")
- await self.state_manager.restore_files()
-
- log.info("Offline changes check done.")
- except UserInterruptError:
+ if self.state_manager.workspace_is_empty():
+ # NOTE: this will currently get triggered on a new project, but will do
+ # nothing as there's no files in the database.
+ log.info("Detected empty workspace, restoring state from the database.")
await self.state_manager.restore_files()
- log.debug("User interrupted the offline changes check, restoring files.")
- return
+ elif modified_files:
+ await self.send_message(f"We found {len(modified_files)} new and/or modified files.")
+ await self.ui.send_modified_files(modified_files)
+ hint = "".join(
+ [
+ "If you would like Pythagora to import those changes, click 'Yes'.\n",
+ "Clicking 'No' means Pythagora will restore (overwrite) all files to the last stored state.\n",
+ ]
+ )
+ use_changes = await self.ask_question(
+ question="Would you like to keep your changes?",
+ buttons={
+ "yes": "Yes, keep my changes",
+ "no": "No, restore last Pythagora state",
+ },
+ buttons_only=True,
+ hint=hint,
+ )
+ if use_changes.button == "yes":
+ log.debug("Importing offline changes into Pythagora.")
+ await self.import_files()
+ else:
+ log.debug("Restoring last stored state.")
+ await self.state_manager.restore_files()
+
+ log.info("Offline changes check done.")
async def handle_done(self, agent: BaseAgent, response: AgentResponse) -> AgentResponse:
"""
@@ -424,25 +165,23 @@ async def handle_done(self, agent: BaseAgent, response: AgentResponse) -> AgentR
will trigger the HumanInput agent to ask the user to provide the required input.
"""
- if self.next_state and self.next_state.tasks:
- n_epics = len(self.next_state.epics)
- n_finished_epics = n_epics - len(self.next_state.unfinished_epics)
- n_tasks = len(self.next_state.tasks)
- n_finished_tasks = n_tasks - len(self.next_state.unfinished_tasks)
- n_iterations = len(self.next_state.iterations)
- n_finished_iterations = n_iterations - len(self.next_state.unfinished_iterations)
- n_steps = len(self.next_state.steps)
- n_finished_steps = n_steps - len(self.next_state.unfinished_steps)
-
- log.debug(
- f"Agent {agent.__class__.__name__} is done, "
- f"committing state for step {self.current_state.step_index}: "
- f"{n_finished_epics}/{n_epics} epics, "
- f"{n_finished_tasks}/{n_tasks} tasks, "
- f"{n_finished_iterations}/{n_iterations} iterations, "
- f"{n_finished_steps}/{n_steps} dev steps."
- )
-
+ n_epics = len(self.next_state.epics)
+ n_finished_epics = n_epics - len(self.next_state.unfinished_epics)
+ n_tasks = len(self.next_state.tasks)
+ n_finished_tasks = n_tasks - len(self.next_state.unfinished_tasks)
+ n_iterations = len(self.next_state.iterations)
+ n_finished_iterations = n_iterations - len(self.next_state.unfinished_iterations)
+ n_steps = len(self.next_state.steps)
+ n_finished_steps = n_steps - len(self.next_state.unfinished_steps)
+
+ log.debug(
+ f"Agent {agent.__class__.__name__} is done, "
+ f"committing state for step {self.current_state.step_index}: "
+ f"{n_finished_epics}/{n_epics} epics, "
+ f"{n_finished_tasks}/{n_tasks} tasks, "
+ f"{n_finished_iterations}/{n_iterations} iterations, "
+ f"{n_finished_steps}/{n_steps} dev steps."
+ )
await self.state_manager.commit()
# If there are any new or modified files changed outside Pythagora,
@@ -451,9 +190,7 @@ async def handle_done(self, agent: BaseAgent, response: AgentResponse) -> AgentR
import_files_response = await self.import_files()
# If any of the files are missing metadata/descriptions, those need to be filled-in
- missing_descriptions = [
- file.path for file in self.current_state.files if not file.content.meta.get("description")
- ]
+ missing_descriptions = [file.path for file in self.current_state.files if not file.meta.get("description")]
if missing_descriptions:
log.debug(f"Some files are missing descriptions: {', '.join(missing_descriptions)}, requesting analysis")
return AgentResponse.describe_files(self)
@@ -476,20 +213,23 @@ def create_agent(self, prev_response: Optional[AgentResponse]) -> Union[List[Bas
if prev_response.type == ResponseType.EXTERNAL_DOCS_REQUIRED:
return ExternalDocumentation(self.state_manager, self.ui, prev_response=prev_response)
if prev_response.type == ResponseType.UPDATE_SPECIFICATION:
- return SpecWriter(self.state_manager, self.ui, prev_response=prev_response, args=self.args)
-
- if not state.epics:
- return Wizard(self.state_manager, self.ui, process_manager=self.process_manager)
- elif state.epics and not state.epics[0].get("description"):
- # New project: ask the Spec Writer to refine and save the project specification
- return SpecWriter(self.state_manager, self.ui, process_manager=self.process_manager, args=self.args)
- elif state.current_epic and state.current_epic.get("source") == "frontend":
- # Build frontend
- return Frontend(self.state_manager, self.ui, process_manager=self.process_manager)
+ return SpecWriter(self.state_manager, self.ui, prev_response=prev_response)
+
+ if not state.specification.description:
+ if state.files:
+ # The project has been imported, but not analyzed yet
+ return Importer(self.state_manager, self.ui)
+ else:
+ # New project: ask the Spec Writer to refine and save the project specification
+ return SpecWriter(self.state_manager, self.ui, process_manager=self.process_manager)
elif not state.specification.architecture:
# Ask the Architect to design the project architecture and determine dependencies
return Architect(self.state_manager, self.ui, process_manager=self.process_manager)
- elif not self.current_state.unfinished_tasks or (state.specification.templates and not state.files):
+ elif (
+ not state.epics
+ or not self.current_state.unfinished_tasks
+ or (state.specification.templates and not state.files)
+ ):
# Ask the Tech Lead to break down the initial project or feature into tasks and apply project templates
return TechLead(self.state_manager, self.ui, process_manager=self.process_manager)
@@ -504,7 +244,7 @@ def create_agent(self, prev_response: Optional[AgentResponse]) -> Union[List[Bas
return TechnicalWriter(self.state_manager, self.ui)
elif current_task_status in [TaskStatus.DOCUMENTED, TaskStatus.SKIPPED]:
# Task is fully done or skipped, call TaskCompleter to mark it as completed
- return TaskCompleter(self.state_manager, self.ui, process_manager=self.process_manager)
+ return TaskCompleter(self.state_manager, self.ui)
if not state.steps and not state.iterations:
# Ask the Developer to break down current task into actionable steps
@@ -546,7 +286,7 @@ def create_agent(self, prev_response: Optional[AgentResponse]) -> Union[List[Bas
return ProblemSolver(self.state_manager, self.ui)
elif current_iteration_status == IterationStatus.NEW_FEATURE_REQUESTED:
# Call Spec Writer to add the "change" requested by the user to project specification
- return SpecWriter(self.state_manager, self.ui, args=self.args)
+ return SpecWriter(self.state_manager, self.ui)
# We have just finished the task, call Troubleshooter to ask the user to review
return Troubleshooter(self.state_manager, self.ui)
@@ -567,8 +307,6 @@ def create_agent_for_step(self, step: dict) -> Union[List[BaseAgent], BaseAgent]
return LegacyHandler(self.state_manager, self.ui, data={"type": "review_task"})
elif step_type == "create_readme":
return TechnicalWriter(self.state_manager, self.ui)
- elif step_type == "utility_function":
- return Developer(self.state_manager, self.ui)
else:
raise ValueError(f"Unknown step type: {step_type}")
@@ -584,7 +322,7 @@ async def import_files(self) -> Optional[AgentResponse]:
input_required_files: list[dict[str, int]] = []
for file in imported_files:
- for line in self.state_manager.get_input_required(file.content.content, file.path):
+ for line in self.state_manager.get_input_required(file.content.content):
input_required_files.append({"file": file.path, "line": line})
if input_required_files:
@@ -598,24 +336,22 @@ async def import_files(self) -> Optional[AgentResponse]:
return None
async def init_ui(self):
- project_details = self.state_manager.get_project_info()
- await self.ui.send_project_info(
- project_details["name"], project_details["id"], project_details["folderName"], project_details["createdAt"]
- )
+ await self.ui.send_project_root(self.state_manager.get_full_project_root())
await self.ui.loading_finished()
if self.current_state.epics:
- if len(self.current_state.epics) > 3:
+ await self.ui.send_project_stage(ProjectStage.CODING)
+ if len(self.current_state.epics) > 2:
# We only want to send previous features, ie. exclude current one and the initial project (first epic)
- await self.ui.send_features_list([e["description"] for e in self.current_state.epics[2:-1]])
+ await self.ui.send_features_list([e["description"] for e in self.current_state.epics[1:-1]])
+
+ elif self.current_state.specification.description:
+ await self.ui.send_project_stage(ProjectStage.ARCHITECTURE)
+ else:
+ await self.ui.send_project_stage(ProjectStage.DESCRIPTION)
if self.current_state.specification.description:
- await self.ui.send_project_description(
- {
- "project_description": self.current_state.specification.description,
- "project_type": self.current_state.branch.project.project_type,
- }
- )
+ await self.ui.send_project_description(self.current_state.specification.description)
async def update_stats(self):
if self.current_state.steps and self.current_state.current_step:
diff --git a/core/agents/problem_solver.py b/core/agents/problem_solver.py
index 509746165..e7b8de059 100644
--- a/core/agents/problem_solver.py
+++ b/core/agents/problem_solver.py
@@ -23,7 +23,6 @@ class AlternativeSolutions(BaseModel):
)
-# TODO: add next state actions whenever this agent is reactivated
class ProblemSolver(IterationPromptMixin, BaseAgent):
agent_type = "problem-solver"
display_name = "Problem Solver"
diff --git a/core/agents/response.py b/core/agents/response.py
index 7b3a54798..7a40a8be6 100644
--- a/core/agents/response.py
+++ b/core/agents/response.py
@@ -39,9 +39,6 @@ class ResponseType(str, Enum):
UPDATE_SPECIFICATION = "update-specification"
"""We need to update the project specification."""
- CREATE_SPECIFICATION = "create-specification"
- """We need to create the project specification."""
-
class AgentResponse:
type: ResponseType = ResponseType.DONE
@@ -101,10 +98,3 @@ def update_specification(agent: "BaseAgent", description: str) -> "AgentResponse
"description": description,
},
)
-
- @staticmethod
- def create_specification(agent: "BaseAgent") -> "AgentResponse":
- return AgentResponse(
- type=ResponseType.CREATE_SPECIFICATION,
- agent=agent,
- )
diff --git a/core/agents/spec_writer.py b/core/agents/spec_writer.py
index 2ca25c0d0..d1cdb98ff 100644
--- a/core/agents/spec_writer.py
+++ b/core/agents/spec_writer.py
@@ -1,17 +1,24 @@
-import secrets
-
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
from core.agents.response import AgentResponse, ResponseType
-from core.config import DEFAULT_AGENT_NAME, SPEC_WRITER_AGENT_NAME
-from core.config.actions import SPEC_CHANGE_FEATURE_STEP_NAME, SPEC_CHANGE_STEP_NAME, SPEC_CREATE_STEP_NAME
+from core.config import SPEC_WRITER_AGENT_NAME
from core.db.models import Complexity
from core.db.models.project_state import IterationStatus
from core.llm.parser import StringParser
from core.log import get_logger
from core.telemetry import telemetry
-from core.templates.registry import PROJECT_TEMPLATES
-from core.ui.base import ProjectStage
+from core.templates.example_project import (
+ DEFAULT_EXAMPLE_PROJECT,
+ EXAMPLE_PROJECTS,
+)
+
+# If the project description is less than this, perform an analysis using LLM
+ANALYZE_THRESHOLD = 1500
+# URL to the wiki page with tips on how to write a good project description
+INITIAL_PROJECT_HOWTO_URL = (
+ "https://github.com/Pythagora-io/gpt-pilot/wiki/How-to-write-a-good-initial-project-description"
+)
+SPEC_STEP_NAME = "Create specification"
log = get_logger(__name__)
@@ -26,300 +33,69 @@ async def run(self) -> AgentResponse:
return await self.update_spec(iteration_mode=True)
elif self.prev_response and self.prev_response.type == ResponseType.UPDATE_SPECIFICATION:
return await self.update_spec(iteration_mode=False)
- elif not self.current_state.specification.description:
- return await self.initialize_spec_and_project()
- else:
- return await self.change_spec()
-
- async def apply_template(self):
- """
- Applies a template to the frontend.
- """
- options = self.current_state.knowledge_base.user_options
- if options["auth_type"] == "api_key" or options["auth_type"] == "none":
- template_name = "vite_react_swagger"
- else:
- template_name = "vite_react"
- template_class = PROJECT_TEMPLATES.get(template_name)
- if not template_class:
- log.error(f"Project template not found: {template_name}")
- return
-
- template = template_class(
- options,
- self.state_manager,
- self.process_manager,
- )
- if not self.state_manager.template:
- self.state_manager.template = {}
- self.state_manager.template["template"] = template
- log.info(f"Applying project template: {template.name}")
- summary = await template.apply()
-
- self.next_state.relevant_files = template.relevant_files
- self.next_state.modified_files = {}
- self.next_state.specification.template_summary = summary
-
- async def initialize_spec_and_project(self) -> AgentResponse:
- self.next_state.action = SPEC_CREATE_STEP_NAME
-
- await self.ui.send_project_stage({"stage": ProjectStage.PROJECT_DESCRIPTION})
-
- await self.ui.clear_main_logs()
-
- # Check if initial_prompt is provided in command line arguments
- if self.args and self.args.initial_prompt:
- description = self.args.initial_prompt.strip()
- await self.ui.send_back_logs(
- [
- {
- "title": "",
- "project_state_id": "spec",
- "labels": [""],
- "convo": [
- {"role": "assistant", "content": "Please describe the app you want to build."},
- {"role": "user", "content": description},
- ],
- }
- ]
- )
else:
- user_description = await self.ask_question(
- "Please describe the app you want to build.",
- allow_empty=False,
- full_screen=True,
- verbose=True,
- extra_info={
- "chat_section_tip": "\"Some text link text on how to build apps with Pythagora.\""
- },
- )
- description = user_description.text.strip()
-
- await self.ui.send_back_logs(
- [
- {
- "title": "",
- "project_state_id": self.current_state.id,
- "labels": [""],
- "convo": [
- {"role": "assistant", "content": "Please describe the app you want to build."},
- {"role": "user", "content": description},
- ],
- }
- ]
- )
-
- await self.ui.send_back_logs(
- [
- {
- "title": "Writing Specification",
- "project_state_id": "spec",
- "labels": ["E1 / T1", "Specs", "working"],
- "disallow_reload": True,
- }
- ]
- )
-
- await self.ui.send_front_logs_headers("specs_0", ["E1 / T1", "Writing Specification", "working"], "")
-
- await self.send_message(
- "## Write specification\n\nPythagora is generating a detailed specification for app based on your input.",
- # project_state_id="setup",
- )
-
- llm = self.get_llm(SPEC_WRITER_AGENT_NAME, stream_output=True, route="forwardToCenter")
- convo = AgentConvo(self).template(
- "build_full_specification",
- initial_prompt=description,
- )
-
- llm_assisted_description = await llm(convo)
-
- await self.ui.send_project_stage({"stage": ProjectStage.PROJECT_NAME})
-
- llm = self.get_llm(DEFAULT_AGENT_NAME)
- convo = AgentConvo(self).template(
- "project_name",
- description=llm_assisted_description,
- )
- llm_response: str = await llm(convo, temperature=0)
- project_name = llm_response.strip()
-
- self.state_manager.project.name = project_name
- self.state_manager.project.folder_name = project_name.replace(" ", "_").replace("-", "_")
-
- self.state_manager.file_system = await self.state_manager.init_file_system(load_existing=False)
-
- self.process_manager.root_dir = self.state_manager.file_system.root
-
- self.next_state.knowledge_base.user_options["original_description"] = description
- self.next_state.knowledge_base.user_options["project_description"] = llm_assisted_description
-
- self.next_state.specification = self.current_state.specification.clone()
- self.next_state.specification.description = llm_assisted_description
- self.next_state.specification.original_description = description
- return AgentResponse.done(self)
-
- async def change_spec(self) -> AgentResponse:
- llm = self.get_llm(SPEC_WRITER_AGENT_NAME, stream_output=True, route="forwardToCenter")
-
- description = self.current_state.specification.original_description
-
- current_description = self.current_state.specification.description
- convo = AgentConvo(self).template(
- "build_full_specification",
- initial_prompt=self.current_state.specification.description.strip(),
- )
-
- while True:
- user_done_with_description = await self.ask_question(
- "Are you satisfied with the project description?",
- buttons={
- "yes": "Yes",
- "no": "No, I want to add more details",
- },
- default="yes",
- buttons_only=True,
- )
-
- if user_done_with_description.button == "yes":
- await self.ui.send_project_stage({"stage": ProjectStage.SPECS_FINISHED})
- break
- elif user_done_with_description.button == "no":
- await self.send_message("## What would you like to add?")
- user_add_to_spec = await self.ask_question(
- "What would you like to add?",
- allow_empty=False,
- )
- else:
- user_add_to_spec = user_done_with_description
-
- await self.send_message("## Refining specification\n\nPythagora is refining the specs based on your input.")
- # if user edits the spec with extension, it will be commited to db immediately, so we have to check if the description has changed
- if current_description != self.current_state.specification.description:
- convo = AgentConvo(self).template(
- "build_full_specification",
- initial_prompt=self.current_state.specification.description.strip(),
- )
-
- convo = convo.template("add_to_specification", user_message=user_add_to_spec.text.strip())
-
- if len(convo.messages) > 6:
- convo.slice(1, 4)
-
- # await self.ui.set_important_stream()
- llm_assisted_description = await llm(convo)
-
- # when llm generates a new spec - make it the new default spec, even if user edited it before - because it will be shown in the extension
- self.current_state.specification.description = llm_assisted_description
- convo = convo.assistant(llm_assisted_description)
-
- await self.ui.clear_main_logs()
- await self.ui.send_back_logs(
- [
- {
- "title": "Writing Specification",
- "project_state_id": "spec", # self.current_state.id,
- "labels": ["E1 / T1", "Specs", "done"],
- "convo": [
- {
- "role": "assistant",
- "content": "What do you want to build?",
- },
- {
- "role": "user",
- "content": self.current_state.specification.original_description,
- },
- ],
- "disallow_reload": True,
- }
- ]
+ return await self.initialize_spec()
+
+ async def initialize_spec(self) -> AgentResponse:
+ response = await self.ask_question(
+ "Describe your app in as much detail as possible",
+ allow_empty=False,
+ buttons={
+ "example": "Start an example project",
+ "import": "Import an existing project",
+ },
)
+ if response.cancelled:
+ return AgentResponse.error(self, "No project description")
- llm = self.get_llm(SPEC_WRITER_AGENT_NAME)
- convo = AgentConvo(self).template(
- "need_auth",
- description=self.current_state.specification.description,
- )
- llm_response: str = await llm(convo, temperature=0)
- auth = llm_response.strip().lower() == "yes"
+ if response.button == "import":
+ return AgentResponse.import_project(self)
- if auth:
- self.next_state.knowledge_base.user_options["auth"] = auth
- self.next_state.knowledge_base.user_options["jwt_secret"] = secrets.token_hex(32)
- self.next_state.knowledge_base.user_options["refresh_token_secret"] = secrets.token_hex(32)
- self.next_state.flag_knowledge_base_as_modified()
+ if response.button == "example":
+ await self.prepare_example_project(DEFAULT_EXAMPLE_PROJECT)
+ return AgentResponse.done(self)
- # if we reload the project from the 1st project state, state_manager.template will be None
- if self.state_manager.template:
- self.state_manager.template["description"] = self.current_state.specification.description
- else:
- # if we do not set this and reload the project, we will load the "old" project description we entered before reload
- self.next_state.epics[0]["description"] = self.current_state.specification.description
-
- self.next_state.specification = self.current_state.specification.clone()
- self.next_state.specification.original_description = description
- self.next_state.specification.description = self.current_state.specification.description
-
- complexity = await self.check_prompt_complexity(self.current_state.specification.description)
- self.next_state.specification.complexity = complexity
-
- telemetry.set("initial_prompt", description)
- telemetry.set("updated_prompt", self.current_state.specification.description)
- telemetry.set("is_complex_app", complexity != Complexity.SIMPLE)
-
- await self.ui.send_project_description(
- {
- "project_description": self.current_state.specification.description,
- "project_type": self.current_state.branch.project.project_type,
- }
- )
+ user_description = response.text.strip()
+ complexity = await self.check_prompt_complexity(user_description)
await telemetry.trace_code_event(
"project-description",
{
+ "initial_prompt": user_description,
"complexity": complexity,
- "initial_prompt": description,
- "llm_assisted_prompt": self.current_state.specification.description,
},
)
- self.next_state.epics = [
- {
- "id": self.current_state.epics[0]["id"],
- "name": "Build frontend",
- "source": "frontend",
- "description": self.current_state.specification.description,
- "messages": [],
- "summary": None,
- "completed": False,
- }
- ]
+ reviewed_spec = user_description
+ if len(user_description) < ANALYZE_THRESHOLD and complexity != Complexity.SIMPLE:
+ initial_spec = await self.analyze_spec(user_description)
+ reviewed_spec = await self.review_spec(desc=user_description, spec=initial_spec)
- if not self.state_manager.async_tasks:
- self.state_manager.async_tasks = []
- await self.apply_template()
+ self.next_state.specification = self.current_state.specification.clone()
+ self.next_state.specification.original_description = user_description
+ self.next_state.specification.description = reviewed_spec
+ self.next_state.specification.complexity = complexity
+ telemetry.set("initial_prompt", user_description)
+ telemetry.set("updated_prompt", reviewed_spec)
+ telemetry.set("is_complex_app", complexity != Complexity.SIMPLE)
+ self.next_state.action = SPEC_STEP_NAME
return AgentResponse.done(self)
async def update_spec(self, iteration_mode) -> AgentResponse:
if iteration_mode:
- self.next_state.action = SPEC_CHANGE_FEATURE_STEP_NAME
feature_description = self.current_state.current_iteration["user_feedback"]
else:
- self.next_state.action = SPEC_CHANGE_STEP_NAME
feature_description = self.prev_response.data["description"]
await self.send_message(
f"Making the following changes to project specification:\n\n{feature_description}\n\nUpdated project specification:"
)
- llm = self.get_llm(SPEC_WRITER_AGENT_NAME, stream_output=True, route="forwardToCenter")
+ llm = self.get_llm(SPEC_WRITER_AGENT_NAME, stream_output=True)
convo = AgentConvo(self).template("add_new_feature", feature_description=feature_description)
llm_response: str = await llm(convo, temperature=0, parser=StringParser())
updated_spec = llm_response.strip()
- await self.ui.generate_diff(
- "project_specification", self.current_state.specification.description, updated_spec, source=self.ui_source
- )
+ await self.ui.generate_diff("project_specification", self.current_state.specification.description, updated_spec)
user_response = await self.ask_question(
"Do you accept these changes to the project specification?",
buttons={"yes": "Yes", "no": "No"},
@@ -337,20 +113,99 @@ async def update_spec(self, iteration_mode) -> AgentResponse:
self.next_state.current_iteration["status"] = IterationStatus.FIND_SOLUTION
self.next_state.flag_iterations_as_modified()
else:
- complexity = await self.check_prompt_complexity(feature_description)
+ complexity = await self.check_prompt_complexity(user_response.text)
self.next_state.current_epic["complexity"] = complexity
return AgentResponse.done(self)
async def check_prompt_complexity(self, prompt: str) -> str:
- is_feature = self.current_state.epics and len(self.current_state.epics) > 2
- await self.send_message("Checking the complexity of the prompt...\n")
+ await self.send_message("Checking the complexity of the prompt ...")
llm = self.get_llm(SPEC_WRITER_AGENT_NAME)
- convo = AgentConvo(self).template(
- "prompt_complexity",
- prompt=prompt,
- is_feature=is_feature,
- )
+ convo = AgentConvo(self).template("prompt_complexity", prompt=prompt)
llm_response: str = await llm(convo, temperature=0, parser=StringParser())
- log.info(f"Complexity check response: {llm_response}")
return llm_response.lower()
+
+ async def prepare_example_project(self, example_name: str):
+ example_description = EXAMPLE_PROJECTS[example_name]["description"].strip()
+
+ log.debug(f"Starting example project: {example_name}")
+ await self.send_message(f"Starting example project with description:\n\n{example_description}")
+
+ spec = self.current_state.specification.clone()
+ spec.example_project = example_name
+ spec.description = example_description
+ spec.complexity = EXAMPLE_PROJECTS[example_name]["complexity"]
+ self.next_state.specification = spec
+
+ telemetry.set("initial_prompt", spec.description)
+ telemetry.set("example_project", example_name)
+ telemetry.set("is_complex_app", spec.complexity != Complexity.SIMPLE)
+
+ async def analyze_spec(self, spec: str) -> str:
+ msg = (
+ "Your project description seems a bit short. "
+ "The better you can describe the project, the better GPT Pilot will understand what you'd like to build.\n\n"
+ f"Here are some tips on how to better describe the project: {INITIAL_PROJECT_HOWTO_URL}\n\n"
+ "Let's start by refining your project idea:"
+ )
+ await self.send_message(msg)
+
+ llm = self.get_llm(SPEC_WRITER_AGENT_NAME, stream_output=True)
+ convo = AgentConvo(self).template("ask_questions").user(spec)
+ n_questions = 0
+ n_answers = 0
+
+ while True:
+ response: str = await llm(convo)
+ if len(response) > 500:
+ # The response is too long for it to be a question, assume it's the updated spec
+ confirm = await self.ask_question(
+ (
+ "Can we proceed with this project description? If so, just press Continue. "
+ "Otherwise, please tell me what's missing or what you'd like to add."
+ ),
+ allow_empty=True,
+ buttons={"continue": "Continue"},
+ )
+ if confirm.cancelled or confirm.button == "continue" or confirm.text == "":
+ updated_spec = response.strip()
+ await telemetry.trace_code_event(
+ "spec-writer-questions",
+ {
+ "num_questions": n_questions,
+ "num_answers": n_answers,
+ "new_spec": updated_spec,
+ },
+ )
+ return updated_spec
+ convo.user(confirm.text)
+
+ else:
+ convo.assistant(response)
+
+ n_questions += 1
+ user_response = await self.ask_question(
+ response,
+ buttons={"skip": "Skip questions"},
+ )
+ if user_response.cancelled or user_response.button == "skip":
+ convo.user(
+ "This is enough clarification, you have all the information. "
+ "Please output the spec now, without additional comments or questions."
+ )
+ response: str = await llm(convo)
+ return response.strip()
+
+ n_answers += 1
+ convo.user(user_response.text)
+
+ async def review_spec(self, desc: str, spec: str) -> str:
+ convo = AgentConvo(self).template("review_spec", desc=desc, spec=spec)
+ llm = self.get_llm(SPEC_WRITER_AGENT_NAME)
+ llm_response: str = await llm(convo, temperature=0)
+ additional_info = llm_response.strip()
+ if additional_info and len(additional_info) > 6:
+ spec += "\n\nAdditional info/examples:\n\n" + additional_info
+ await self.send_message(f"\n\nAdditional info/examples:\n\n {additional_info}")
+
+ return spec
diff --git a/core/agents/task_completer.py b/core/agents/task_completer.py
index 88c01955a..98094968f 100644
--- a/core/agents/task_completer.py
+++ b/core/agents/task_completer.py
@@ -1,23 +1,18 @@
from core.agents.base import BaseAgent
-from core.agents.git import GitMixin
from core.agents.response import AgentResponse
-from core.config.actions import TC_TASK_DONE
from core.log import get_logger
from core.telemetry import telemetry
log = get_logger(__name__)
-class TaskCompleter(BaseAgent, GitMixin):
+class TaskCompleter(BaseAgent):
agent_type = "pythagora"
display_name = "Pythagora"
async def run(self) -> AgentResponse:
- if self.state_manager.git_available and self.state_manager.git_used:
- await self.git_commit()
-
current_task_index1 = self.current_state.tasks.index(self.current_state.current_task) + 1
- self.next_state.action = TC_TASK_DONE.format(current_task_index1)
+ self.next_state.action = f"Task #{current_task_index1} complete"
self.next_state.complete_task()
await self.state_manager.log_task_completed()
tasks = self.current_state.tasks
diff --git a/core/agents/tech_lead.py b/core/agents/tech_lead.py
index 7b84964db..3484582b0 100644
--- a/core/agents/tech_lead.py
+++ b/core/agents/tech_lead.py
@@ -1,46 +1,28 @@
-import asyncio
from uuid import uuid4
from pydantic import BaseModel, Field
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
-from core.agents.mixins import RelevantFilesMixin
from core.agents.response import AgentResponse
-from core.config import TECH_LEAD_EPIC_BREAKDOWN, TECH_LEAD_PLANNING
-from core.config.actions import (
- TL_CREATE_INITIAL_EPIC,
- TL_CREATE_PLAN,
- TL_INITIAL_PROJECT_NAME,
- TL_START_FEATURE,
-)
-from core.db.models import Complexity
+from core.config import TECH_LEAD_PLANNING
from core.db.models.project_state import TaskStatus
from core.llm.parser import JSONParser
from core.log import get_logger
from core.telemetry import telemetry
+from core.templates.example_project import EXAMPLE_PROJECTS
from core.templates.registry import PROJECT_TEMPLATES
-from core.ui.base import ProjectStage, pythagora_source, success_source
-from core.utils.text import trim_logs
+from core.ui.base import ProjectStage, success_source
log = get_logger(__name__)
-class APIEndpoint(BaseModel):
- description: str = Field(description="Description of an API endpoint.")
- method: str = Field(description="HTTP method of the API endpoint.")
- endpoint: str = Field(description="URL of the API endpoint.")
- request_body: dict = Field(description="Request body of the API endpoint.")
- response_body: dict = Field(description="Response body of the API endpoint.")
-
-
class Epic(BaseModel):
- description: str = Field(description="Description of an epic.")
+ description: str = Field(description=("Description of an epic."))
class Task(BaseModel):
description: str = Field(description="Description of a task.")
- related_api_endpoints: list[APIEndpoint] = Field(description="API endpoints that will be implemented in this task.")
testing_instructions: str = Field(description="Instructions for testing the task.")
@@ -52,50 +34,51 @@ class EpicPlan(BaseModel):
plan: list[Task] = Field(description="List of tasks that need to be done to implement the entire epic.")
-class TechLead(RelevantFilesMixin, BaseAgent):
+class TechLead(BaseAgent):
agent_type = "tech-lead"
display_name = "Tech Lead"
async def run(self) -> AgentResponse:
- # Building frontend is the first epic
- if len(self.current_state.epics) == 1:
- await self.remove_mocked_data()
- self.create_initial_project_epic()
+ if len(self.current_state.epics) == 0:
+ if self.current_state.specification.example_project:
+ self.plan_example_project()
+ else:
+ self.create_initial_project_epic()
return AgentResponse.done(self)
- # if self.current_state.specification.templates and len(self.current_state.files) < 2:
- # await self.apply_project_templates()
- # self.next_state.action = "Apply project templates"
- # await self.ui.send_epics_and_tasks(
- # self.next_state.current_epic["sub_epics"],
- # self.next_state.tasks,
- # )
- #
- # inputs = []
- # for file in self.next_state.files:
- # input_required = self.state_manager.get_input_required(file.content.content)
- # if input_required:
- # inputs += [{"file": file.path, "line": line} for line in input_required]
- #
- # if inputs:
- # return AgentResponse.input_required(self, inputs)
- # else:
- # return AgentResponse.done(self)
+ await self.ui.send_project_stage(ProjectStage.CODING)
+
+ if self.current_state.specification.templates and not self.current_state.files:
+ await self.apply_project_templates()
+ self.next_state.action = "Apply project templates"
+ await self.ui.send_epics_and_tasks(
+ self.next_state.current_epic["sub_epics"],
+ self.next_state.tasks,
+ )
+
+ inputs = []
+ for file in self.next_state.files:
+ input_required = self.state_manager.get_input_required(file.content.content)
+ if input_required:
+ inputs += [{"file": file.path, "line": line} for line in input_required]
+
+ if inputs:
+ return AgentResponse.input_required(self, inputs)
+ else:
+ return AgentResponse.done(self)
if self.current_state.current_epic:
- await self.remove_mocked_data()
self.next_state.action = "Create a development plan"
return await self.plan_epic(self.current_state.current_epic)
else:
return await self.ask_for_new_feature()
def create_initial_project_epic(self):
- self.next_state.action = TL_CREATE_INITIAL_EPIC
log.debug("Creating initial project Epic")
- self.next_state.epics = self.current_state.epics + [
+ self.next_state.epics = [
{
"id": uuid4().hex,
- "name": TL_INITIAL_PROJECT_NAME,
+ "name": "Initial Project",
"source": "app",
"description": self.current_state.specification.description,
"test_instructions": None,
@@ -105,8 +88,6 @@ def create_initial_project_epic(self):
"sub_epics": [],
}
]
- self.next_state.relevant_files = None
- self.next_state.modified_files = {}
async def apply_project_templates(self):
state = self.current_state
@@ -146,161 +127,44 @@ async def apply_project_templates(self):
async def ask_for_new_feature(self) -> AgentResponse:
if len(self.current_state.epics) > 2:
await self.ui.send_message("Your new feature is complete!", source=success_source)
- await self.ui.send_project_stage(
- {
- "stage": ProjectStage.FEATURE_FINISHED,
- "feature_number": len(self.current_state.epics),
- }
- )
else:
await self.ui.send_message("Your app is DONE! You can start using it right now!", source=success_source)
- await self.ui.send_project_stage(
- {
- "stage": ProjectStage.INITIAL_APP_FINISHED,
- }
- )
if self.current_state.run_command:
await self.ui.send_run_command(self.current_state.run_command)
log.debug("Asking for new feature")
-
- feature, user_desc = None, None
-
- while True:
- response = await self.ask_question(
- "Do you want to add a new feature or implement something quickly?",
- buttons={
- # "feature": "Feature",
- "task": "Implement new feature",
- # "end": "No, I'm done",
- },
- buttons_only=True,
- )
-
- if response.button == "end" or response.cancelled:
- await self.ui.send_message("Thank you for using Pythagora!", source=pythagora_source)
- return AgentResponse.exit(self)
-
- if not response.text:
- feature = response.button == "feature"
-
- response = await self.ask_question(
- "What do you want to implement?",
- buttons={"back": "Back"},
- allow_empty=False,
- )
-
- if response.text:
- user_desc = response.text
- break
-
- if feature:
- await self.ui.send_project_stage(
- {
- "stage": ProjectStage.STARTING_NEW_FEATURE,
- "feature_number": len(self.current_state.epics),
- }
- )
- self.next_state.epics = self.current_state.epics + [
- {
- "id": uuid4().hex,
- "name": f"Feature #{len(self.current_state.epics)}",
- "test_instructions": None,
- "source": "feature",
- "description": user_desc,
- "summary": None,
- "completed": False,
- "complexity": None, # Determined and defined in SpecWriter
- "sub_epics": [],
- }
- ]
- # Orchestrator will rerun us to break down the new feature epic
- self.next_state.action = TL_START_FEATURE.format(len(self.current_state.epics))
- return AgentResponse.update_specification(self, user_desc)
- else:
- # Quick implementation
- # TODO send project stage?
-
- # load the previous state, because in this state we have deleted tasks due to epic being completed!
- wanted_project_state = await self.state_manager.get_project_state_by_id(self.current_state.prev_state_id)
-
- wanted_project_state.epics[-1]["completed"] = False
- self.next_state.epics = wanted_project_state.epics
-
- # Trim logs from existing tasks before adding the new task
- if wanted_project_state.tasks:
- # Trim logs from all existing tasks
- for task in wanted_project_state.tasks:
- if task.get("description"):
- task["description"] = trim_logs(task["description"])
-
- # Create tasks list with new task (after trimming logs from existing tasks)
- self.next_state.tasks = wanted_project_state.tasks + [
- {
- "id": uuid4().hex,
- "description": user_desc,
- "instructions": None,
- "pre_breakdown_testing_instructions": None,
- "status": TaskStatus.TODO,
- "sub_epic_id": self.next_state.epics[-1]["sub_epics"][-1]["id"],
- "quick_implementation": True,
- }
- ]
-
- # Flag tasks as modified so SQLAlchemy knows to save the changes
- self.next_state.flag_epics_as_modified()
- self.next_state.flag_tasks_as_modified()
-
- await self.ui.send_epics_and_tasks(
- self.next_state.epics[-1].get("sub_epics", []),
- self.next_state.tasks,
- )
-
- return AgentResponse.done(self)
-
- async def process_epic(self, sub_epic_number, sub_epic):
- epic_convo = (
- AgentConvo(self)
- .template(
- "epic_breakdown",
- epic_number=sub_epic_number,
- epic_description=sub_epic.description,
- get_only_api_files=True,
- )
- .require_schema(EpicPlan)
+ response = await self.ask_question(
+ "Do you have a new feature to add to the project? Just write it here:",
+ buttons={"continue": "continue", "end": "No, I'm done"},
+ allow_empty=False,
)
- llm = self.get_llm(TECH_LEAD_EPIC_BREAKDOWN)
- epic_plan: EpicPlan = await llm(epic_convo, parser=JSONParser(EpicPlan))
-
- task = {
- "id": uuid4().hex,
- "description": "",
- "instructions": None,
- "pre_breakdown_testing_instructions": "",
- "status": TaskStatus.TODO,
- "sub_epic_id": sub_epic_number,
- "related_api_endpoints": [],
- }
-
- for epic_task in epic_plan.plan:
- task["description"] += (
- epic_task.description + " " if epic_task.description.endswith(".") else epic_task.description + ". "
- )
- task["related_api_endpoints"] += [rae.model_dump() for rae in (epic_task.related_api_endpoints or [])]
- task["pre_breakdown_testing_instructions"] += f"{epic_task.description}\n{epic_task.testing_instructions}\n"
+ if response.button == "end" or response.cancelled or not response.text:
+ await self.ui.send_message("Thanks for using Pythagora!")
+ return AgentResponse.exit(self)
- return task
+ self.next_state.epics = self.current_state.epics + [
+ {
+ "id": uuid4().hex,
+ "name": f"Feature #{len(self.current_state.epics)}",
+ "test_instructions": None,
+ "source": "feature",
+ "description": response.text,
+ "summary": None,
+ "completed": False,
+ "complexity": None, # Determined and defined in SpecWriter
+ "sub_epics": [],
+ }
+ ]
+ # Orchestrator will rerun us to break down the new feature epic
+ self.next_state.action = f"Start of feature #{len(self.current_state.epics)}"
+ return AgentResponse.update_specification(self, response.text)
async def plan_epic(self, epic) -> AgentResponse:
- self.next_state.action = TL_CREATE_PLAN.format(epic["name"])
log.debug(f"Planning tasks for the epic: {epic['name']}")
await self.send_message("Creating the development plan ...")
- if epic.get("source") == "feature":
- await self.get_relevant_files_parallel(user_feedback=epic.get("description"))
-
llm = self.get_llm(TECH_LEAD_PLANNING)
convo = (
AgentConvo(self)
@@ -310,7 +174,6 @@ async def plan_epic(self, epic) -> AgentResponse:
task_type=self.current_state.current_epic.get("source", "app"),
# FIXME: we're injecting summaries to initial description
existing_summary=None,
- get_only_api_files=True,
)
.require_schema(DevelopmentPlan)
)
@@ -318,45 +181,67 @@ async def plan_epic(self, epic) -> AgentResponse:
response: DevelopmentPlan = await llm(convo, parser=JSONParser(DevelopmentPlan))
convo.remove_last_x_messages(1)
+ formatted_tasks = [f"Epic #{index}: {task.description}" for index, task in enumerate(response.plan, start=1)]
+ tasks_string = "\n\n".join(formatted_tasks)
+ convo = convo.assistant(tasks_string)
+ llm = self.get_llm(TECH_LEAD_PLANNING)
- await self.send_message("Creating tasks ...")
- if epic.get("source") == "feature" or epic.get("complexity") == Complexity.SIMPLE:
+ if epic.get("source") == "feature" or epic.get("complexity") == "simple":
+ await self.send_message(f"Epic 1: {epic['name']}")
self.next_state.current_epic["sub_epics"] = [
{
"id": 1,
"description": epic["name"],
}
]
+ await self.send_message("Creating tasks for this epic ...")
+ self.next_state.tasks = self.next_state.tasks + [
+ {
+ "id": uuid4().hex,
+ "description": task.description,
+ "instructions": None,
+ "pre_breakdown_testing_instructions": None,
+ "status": TaskStatus.TODO,
+ "sub_epic_id": 1,
+ }
+ for task in response.plan
+ ]
+ await self.ui.send_epics_and_tasks(
+ self.next_state.current_epic["sub_epics"],
+ self.next_state.tasks,
+ )
else:
- self.next_state.current_epic["sub_epics"] = [
+ self.next_state.current_epic["sub_epics"] = self.next_state.current_epic["sub_epics"] + [
{
"id": sub_epic_number,
"description": sub_epic.description,
}
for sub_epic_number, sub_epic in enumerate(response.plan, start=1)
]
+ for sub_epic_number, sub_epic in enumerate(response.plan, start=1):
+ await self.send_message(f"Epic {sub_epic_number}: {sub_epic.description}")
+ convo = convo.template(
+ "epic_breakdown", epic_number=sub_epic_number, epic_description=sub_epic.description
+ ).require_schema(EpicPlan)
+ await self.send_message("Creating tasks for this epic ...")
+ epic_plan: EpicPlan = await llm(convo, parser=JSONParser(EpicPlan))
+ self.next_state.tasks = self.next_state.tasks + [
+ {
+ "id": uuid4().hex,
+ "description": task.description,
+ "instructions": None,
+ "pre_breakdown_testing_instructions": task.testing_instructions,
+ "status": TaskStatus.TODO,
+ "sub_epic_id": sub_epic_number,
+ }
+ for task in epic_plan.plan
+ ]
+ convo.remove_last_x_messages(2)
- # Create and gather all epic processing tasks
- epic_tasks = []
- for sub_epic_number, sub_epic in enumerate(response.plan, start=1):
- epic_tasks.append(self.process_epic(sub_epic_number, sub_epic))
-
- all_tasks_results = await asyncio.gather(*epic_tasks)
-
- for tasks_result in all_tasks_results:
- self.next_state.tasks.append(tasks_result)
-
- await self.ui.send_epics_and_tasks(
- self.next_state.current_epic["sub_epics"],
- self.next_state.tasks,
- )
-
- await self.update_epics_and_tasks()
-
- await self.ui.send_epics_and_tasks(
- self.next_state.current_epic["sub_epics"],
- self.next_state.tasks,
- )
+ await self.ui.send_epics_and_tasks(
+ self.next_state.current_epic["sub_epics"],
+ self.next_state.tasks,
+ )
await telemetry.trace_code_event(
"development-plan",
@@ -367,97 +252,23 @@ async def plan_epic(self, epic) -> AgentResponse:
)
return AgentResponse.done(self)
- # TODO - Move to a separate agent for removing mocked data
- async def remove_mocked_data(self):
- files = self.current_state.files
- for file in files:
- file_content = file.content.content
- if "pythagora_mocked_data" in file_content:
- for line in file_content.split("\n"):
- if "pythagora_mocked_data" in line:
- file_content = file_content.replace(line + "\n", "")
- await self.state_manager.save_file(file.path, file_content)
-
- async def update_epics_and_tasks(self):
- if (
- self.current_state.current_epic
- and self.current_state.current_epic.get("source", "") == "app"
- and self.current_state.knowledge_base.user_options.get("auth", False)
- ):
- log.debug("Adding auth task to the beginning of the task list")
- self.next_state.tasks.insert(
- 0,
- {
- "id": uuid4().hex,
- "hardcoded": True,
- "description": "Implement and test Login and Register pages",
- "instructions": """Open /register page, add your data and click on the "Register" button\nExpected result: You should see a success message in the bottom right corner and you should be redirected to the /login page\n2. On the /login page, add your data and click on the "Login" button\nExpected result: You should see a success message in the bottom right corner and you should be redirected to the home page""",
- "test_instructions": """[
- {
- "title": "Open Register Page",
- "action": "Open your web browser and visit 'http://localhost:5173/register'.",
- "result": "You should see a success message in the bottom right corner and you should be redirected to the /login page"
- },
- {
- "title": "Open Login Page",
- "action": "Open your web browser and visit 'http://localhost:5173/login'.",
- "result": "You should see a success message in the bottom right corner and you should be redirected to the home page"
- }
-]""",
- "pre_breakdown_testing_instructions": """Open /register page, add your data and click on the "Register" button\nExpected result: You should see a success message in the bottom right corner and you should be redirected to the /login page\n2. On the /login page, add your data and click on the "Login" button\nExpected result: You should see a success message in the bottom right corner and you should be redirected to the home page""",
- "status": TaskStatus.TODO,
- "sub_epic_id": 1,
- "related_api_endpoints": [
- {
- "description": "Register a new user",
- "method": "POST",
- "endpoint": "/api/auth/register",
- "request_body": {"email": "string", "password": "string"},
- "response_body": {
- "id": "integer",
- "email": "string",
- },
- },
- {
- "description": "Login user",
- "method": "POST",
- "endpoint": "/api/auth/login",
- "request_body": {"username": "string", "password": "string"},
- "response_body": {"token": "string"},
- },
- ],
- },
- )
-
- self.next_state.steps = [
- {
- "completed": True,
- "iteration_index": 0,
- }
- ]
- self.next_state.flag_tasks_as_modified()
- self.next_state.flag_epics_as_modified()
+ def plan_example_project(self):
+ example_name = self.current_state.specification.example_project
+ log.debug(f"Planning example project: {example_name}")
- await self.ui.clear_main_logs()
- await self.ui.send_project_stage(
+ example = EXAMPLE_PROJECTS[example_name]
+ self.next_state.epics = [
{
- "stage": ProjectStage.STARTING_TASK,
- "task_index": 0,
+ "name": "Initial Project",
+ "description": example["description"],
+ "completed": False,
+ "complexity": example["complexity"],
+ "sub_epics": [
+ {
+ "id": 1,
+ "description": "Single Epic Example",
+ }
+ ],
}
- )
- await self.ui.send_front_logs_headers(
- str(self.current_state.id),
- ["E3 / T1", "Backend", "working"],
- self.next_state.tasks[0]["description"],
- self.next_state.tasks[0]["id"],
- )
-
- await self.ui.send_back_logs(
- [
- {
- "title": self.next_state.tasks[0]["description"],
- "project_state_id": str(self.next_state.id),
- "labels": ["E3 / T1", "Backend", "working"],
- }
- ]
- )
+ ]
+ self.next_state.tasks = example["plan"]
diff --git a/core/agents/tech_writer.py b/core/agents/tech_writer.py
index f951ccb51..90d6f5ef1 100644
--- a/core/agents/tech_writer.py
+++ b/core/agents/tech_writer.py
@@ -1,7 +1,6 @@
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
from core.agents.response import AgentResponse
-from core.config.actions import TW_WRITE
from core.db.models.project_state import TaskStatus
from core.log import get_logger
from core.ui.base import success_source
@@ -24,7 +23,7 @@ async def run(self) -> AgentResponse:
await self.send_congratulations()
await self.create_readme()
- self.next_state.action = TW_WRITE
+ self.next_state.action = "Create README.md"
self.next_state.set_current_task_status(TaskStatus.DOCUMENTED)
return AgentResponse.done(self)
diff --git a/core/agents/troubleshooter.py b/core/agents/troubleshooter.py
index 63328ed48..25fd575a7 100644
--- a/core/agents/troubleshooter.py
+++ b/core/agents/troubleshooter.py
@@ -1,4 +1,3 @@
-import json
from typing import Optional
from uuid import uuid4
@@ -6,16 +5,14 @@
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
-from core.agents.mixins import ChatWithBreakdownMixin, IterationPromptMixin, RelevantFilesMixin, TestSteps
+from core.agents.mixins import IterationPromptMixin, RelevantFilesMixin
from core.agents.response import AgentResponse
from core.config import TROUBLESHOOTER_GET_RUN_COMMAND
-from core.config.actions import TS_ALT_SOLUTION, TS_APP_WORKING, TS_DESCRIBE_ISSUE, TS_TASK_REVIEWED
from core.db.models.file import File
from core.db.models.project_state import IterationStatus, TaskStatus
from core.llm.parser import JSONParser, OptionalCodeBlockParser
from core.log import get_logger
from core.telemetry import telemetry
-from core.ui.base import ProjectStage, pythagora_source
log = get_logger(__name__)
@@ -32,7 +29,7 @@ class RouteFilePaths(BaseModel):
files: list[str] = Field(description="List of paths for files that contain routes")
-class Troubleshooter(ChatWithBreakdownMixin, IterationPromptMixin, RelevantFilesMixin, BaseAgent):
+class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
agent_type = "troubleshooter"
display_name = "Troubleshooter"
@@ -75,33 +72,20 @@ async def create_iteration(self) -> AgentResponse:
self.next_state.flag_tasks_as_modified()
return AgentResponse.done(self)
else:
- await self.ui.send_project_stage({"stage": ProjectStage.TEST_APP})
- await self.ui.send_message("Test the app by following these steps:", source=pythagora_source)
+ await self.send_message("Here are instructions on how to test the app:\n\n" + user_instructions)
- await self.ui.send_test_instructions(user_instructions, project_state_id=str(self.current_state.id))
+ await self.ui.stop_app()
+ await self.ui.send_test_instructions(user_instructions)
# Developer sets iteration as "completed" when it generates the step breakdown, so we can't
# use "current_iteration" here
last_iteration = self.current_state.iterations[-1] if len(self.current_state.iterations) >= 3 else None
- should_iterate, is_loop, should_redo, bug_report, change_description = await self.get_user_feedback(
+ should_iterate, is_loop, bug_report, change_description = await self.get_user_feedback(
run_command,
user_instructions,
last_iteration is not None,
)
-
- if should_redo:
- # ask user to provide more info
- task_redo_info_question = await self.ask_question(
- "Please provide more information about the task you want to redo",
- buttons_only=False,
- )
-
- if task_redo_info_question.text:
- self.next_state.current_task["redo_human_instructions"] = task_redo_info_question.text
-
- return AgentResponse.done(self)
-
if not should_iterate:
# User tested and reported no problems, we're done with the task
return await self.complete_task()
@@ -122,6 +106,7 @@ async def create_iteration(self) -> AgentResponse:
else:
# should be - elif change_description is not None: - but to prevent bugs with the extension
# this might be caused if we show the input field instead of buttons
+ await self.get_relevant_files(user_feedback)
iteration_status = IterationStatus.NEW_FEATURE_REQUESTED
self.next_state.iterations = self.current_state.iterations + [
@@ -154,7 +139,7 @@ async def complete_task(self) -> AgentResponse:
await self.trace_loop("loop-end")
current_task_index1 = self.current_state.tasks.index(self.current_state.current_task) + 1
- self.next_state.action = TS_TASK_REVIEWED.format(current_task_index1)
+ self.next_state.action = f"Task #{current_task_index1} reviewed"
self.next_state.set_current_task_status(TaskStatus.REVIEWED)
return AgentResponse.done(self)
@@ -163,15 +148,6 @@ def _get_task_convo(self) -> AgentConvo:
task = self.current_state.current_task
current_task_index = self.current_state.tasks.index(task)
- related_api_endpoints = task.get("related_api_endpoints", [])
- # TODO: Temp fix for old projects
- if not (
- related_api_endpoints
- and len(related_api_endpoints) > 0
- and all(isinstance(api, dict) and "endpoint" in api for api in related_api_endpoints)
- ):
- related_api_endpoints = []
-
return (
AgentConvo(self)
.template(
@@ -179,7 +155,6 @@ def _get_task_convo(self) -> AgentConvo:
task=task,
iteration=None,
current_task_index=current_task_index,
- related_api_endpoints=related_api_endpoints,
)
.assistant(self.current_state.current_task["instructions"])
)
@@ -201,33 +176,21 @@ async def get_run_command(self) -> Optional[str]:
return llm_response
async def get_user_instructions(self) -> Optional[str]:
- await self.send_message("### Determining how to test the app ...")
+ await self.send_message("Determining how to test the app ...")
route_files = await self._get_route_files()
- current_task = self.current_state.current_task
llm = self.get_llm()
- convo = (
- self._get_task_convo()
- .template(
- "define_user_review_goal",
- task=current_task,
- route_files=route_files,
- current_task_index=self.current_state.tasks.index(current_task),
- )
- .require_schema(TestSteps)
+ convo = self._get_task_convo().template(
+ "define_user_review_goal", task=self.current_state.current_task, route_files=route_files
)
- user_instructions: TestSteps = await llm(convo, parser=JSONParser(TestSteps))
+ user_instructions: str = await llm(convo)
- if len(user_instructions.steps) == 0:
- await self.ui.send_message(
- "No testing required for this task, moving on to the next one.", source=pythagora_source
- )
+ user_instructions = user_instructions.strip()
+ if user_instructions.lower() == "done":
log.debug(f"Nothing to do for user testing for task {self.current_state.current_task['description']}")
return None
- user_instructions = json.dumps([test.dict() for test in user_instructions.steps])
-
return user_instructions
async def _get_route_files(self) -> list[File]:
@@ -246,7 +209,7 @@ async def get_user_feedback(
run_command: str,
user_instructions: str,
last_iteration: Optional[dict],
- ) -> tuple[bool, bool, bool, str, str]:
+ ) -> tuple[bool, bool, str, str]:
"""
Ask the user to test the app and provide feedback.
@@ -258,8 +221,6 @@ async def get_user_feedback(
If "is_loop" is True, Pythagora is stuck in a loop and needs to consider alternative solutions.
- If "should_redo" is True, the user wants to redo the task and we need to reset the task and start over.
-
The last element in the tuple is the user feedback, which may be empty if the user provided no
feedback (eg. if they just clicked on "Continue" or "Start Pair Programming").
"""
@@ -270,80 +231,40 @@ async def get_user_feedback(
is_loop = False
should_iterate = True
- should_redo = False
- extra_info = {"restart_app": True} if not self.current_state.iterations else None
- while True:
- await self.ui.send_project_stage({"stage": ProjectStage.GET_USER_FEEDBACK})
+ test_message = "Please check if the app is working"
+ if user_instructions:
+ hint = " Here is a description of what should be working:\n\n" + user_instructions
- test_message = TS_APP_WORKING
- if user_instructions:
- hint = " Here is a description of what should be working:\n\n" + user_instructions
+ if run_command:
+ await self.ui.send_run_command(run_command)
- if run_command:
- await self.ui.send_run_command(run_command)
+ buttons = {
+ "continue": "Everything works",
+ "change": "I want to make a change",
+ "bug": "There is an issue",
+ }
- buttons = {
- "continue": "Everything works",
- "bug": "There is an issue",
- "change": "I want to make a change",
- }
- if not self.current_state.current_task.get("hardcoded", False):
- buttons["redo"] = "Redo task"
+ user_response = await self.ask_question(
+ test_message, buttons=buttons, default="continue", buttons_only=True, hint=hint
+ )
+ if user_response.button == "continue" or user_response.cancelled:
+ should_iterate = False
- user_response = await self.ask_question(
- test_message,
- buttons=buttons,
- default="continue",
- buttons_only=True,
- hint=hint,
- extra_info=extra_info,
+ elif user_response.button == "change":
+ user_description = await self.ask_question(
+ "Please describe the change you want to make to the project specification (one at a time)"
)
- extra_info = None
-
- if user_response.button == "redo":
- should_redo = True
- break
+ change_description = user_description.text
- if user_response.button == "continue" or user_response.cancelled:
- should_iterate = False
- break
-
- elif user_response.button == "change":
- await self.ui.send_project_stage({"stage": ProjectStage.DESCRIBE_CHANGE})
- user_description = await self.ask_question(
- "Please describe the change you want to make to the project specification (one at a time)",
- buttons={"back": "Back"},
- )
- if user_description.button == "back":
- continue
- change_description = user_description.text
- await self.get_relevant_files_parallel(user_feedback=change_description)
- break
-
- elif user_response.button == "bug":
- await self.ui.send_project_stage({"stage": ProjectStage.DESCRIBE_ISSUE})
- user_description = await self.ask_question(
- TS_DESCRIBE_ISSUE,
- extra_info={"collect_logs": True},
- buttons={"back": "Back"},
- )
- if user_description.button == "back":
- continue
- bug_report = user_description.text
- await self.ui.send_project_stage(
- {
- "bug_fix_attempt": 1,
- }
- )
- await self.get_relevant_files_parallel(user_feedback=bug_report)
- break
- elif user_response.text and isinstance(user_response.text, str):
- bug_report = user_response.text
- await self.get_relevant_files_parallel(user_feedback=bug_report)
- break
+ elif user_response.button == "bug":
+ user_description = await self.ask_question(
+ "Please describe the issue you found (one at a time) and share any relevant server logs",
+ buttons={"copy_server_logs": "Copy Server Logs"},
+ )
+ bug_report = user_description.text
- return should_iterate, is_loop, should_redo, bug_report, change_description
+ return should_iterate, is_loop, bug_report, change_description
def try_next_alternative_solution(self, user_feedback: str, user_feedback_qa: list[str]) -> AgentResponse:
"""
@@ -362,7 +283,7 @@ def try_next_alternative_solution(self, user_feedback: str, user_feedback_qa: li
next_state_iteration["attempts"] += 1
next_state_iteration["status"] = IterationStatus.PROBLEM_SOLVER
self.next_state.flag_iterations_as_modified()
- self.next_state.action = TS_ALT_SOLUTION.format(next_state_iteration["attempts"])
+ self.next_state.action = f"Alternative solution (attempt #{next_state_iteration['attempts']})"
return AgentResponse.done(self)
async def generate_bug_report(
diff --git a/core/agents/wizard.py b/core/agents/wizard.py
deleted file mode 100644
index f38afa0c5..000000000
--- a/core/agents/wizard.py
+++ /dev/null
@@ -1,174 +0,0 @@
-import json
-from urllib.parse import urljoin
-from uuid import uuid4
-
-import httpx
-from sqlalchemy import inspect
-
-from core.agents.base import BaseAgent
-from core.agents.response import AgentResponse
-from core.cli.helpers import capture_exception
-from core.config import PYTHAGORA_API
-from core.config.actions import FE_INIT
-from core.db.models import KnowledgeBase
-from core.log import get_logger
-from core.telemetry import telemetry
-
-log = get_logger(__name__)
-
-
-class Wizard(BaseAgent):
- agent_type = "wizard"
- display_name = "Wizard"
-
- async def run(self) -> AgentResponse:
- success = await self.init_template()
- if not success:
- return AgentResponse.exit(self)
- return AgentResponse.create_specification(self)
-
- async def init_template(self) -> bool:
- """
- Sets up the frontend
-
- :return: AgentResponse.done(self)
- """
- self.next_state.action = FE_INIT
- self.state_manager.template = {}
- options = {}
- auth_data = {}
-
- if self.state_manager.project.project_type == "swagger":
- while True:
- try:
- docs = await self.ask_question(
- "Paste the OpenAPI/Swagger JSON or YAML docs here",
- allow_empty=False,
- verbose=True,
- )
- success, options["external_api_url"], options["types"] = await self.upload_docs(docs.text)
- if not success:
- await self.send_message("Please try creating a new project.")
- return False
- else:
- break
- except Exception as e:
- log.debug(f"An error occurred: {str(e)}")
- await self.send_message("Please provide a valid input.")
- continue
-
- while True:
- auth_type_question = await self.ask_question(
- "Which authentication method does your backend use?",
- buttons={
- "none": "No authentication",
- "api_key": "API Key",
- "bearer": "HTTP Bearer (coming soon)",
- "open_id_connect": "OpenID Connect (coming soon)",
- "oauth2": "OAuth2 (coming soon)",
- },
- buttons_only=True,
- default="api_key",
- full_screen=True,
- )
-
- if auth_type_question.button == "api_key":
- if auth_data.get("types") is None or "apiKey" not in auth_data["types"]:
- addit_question = await self.ask_question(
- "The API key authentication method is not supported by your backend. Do you want to continue?",
- buttons_only=True,
- buttons={"yes": "Yes", "no": "Go back"},
- )
- if addit_question.button != "yes":
- continue
-
- api_key = await self.ask_question(
- "Enter your API key here. It will be saved in the .env file on the frontend.",
- allow_empty=False,
- verbose=True,
- )
- options["auth_type"] = "api_key"
- options["api_key"] = api_key.text.strip()
- break
- elif auth_type_question.button == "none":
- options["auth_type"] = "none"
- break
- else:
- auth_type_question_trace = await self.ask_question(
- "We are still working on getting this auth method implemented correctly. Can we contact you to get more info on how you would like it to work?",
- allow_empty=False,
- buttons={"yes": "Yes", "no": "No"},
- default="yes",
- buttons_only=True,
- )
- if auth_type_question_trace.button == "yes":
- await telemetry.trace_code_event(
- "swagger-auth-method",
- {"type": auth_type_question.button},
- )
- await self.send_message("Thank you for submitting your request. We will be in touch.")
- else:
- options["auth_type"] = "login"
-
- # Create a new knowledge base instance for the project state
- knowledge_base = KnowledgeBase(pages=[], apis=[], user_options=options, utility_functions=[])
- session = inspect(self.next_state).async_session
- session.add(knowledge_base)
- self.next_state.knowledge_base = knowledge_base
-
- self.next_state.epics = [
- {
- "id": uuid4().hex,
- "name": "Build frontend",
- "source": "frontend",
- "description": "",
- "messages": [],
- "summary": None,
- "completed": False,
- }
- ]
-
- return True
-
- async def upload_docs(self, docs: str) -> (bool, str, list):
- error = None
- url = urljoin(PYTHAGORA_API, "rag/upload")
- for attempt in range(3):
- log.debug(f"Uploading docs to RAG service... attempt {attempt}")
- try:
- async with httpx.AsyncClient(
- transport=httpx.AsyncHTTPTransport(), timeout=httpx.Timeout(30.0, connect=5.0)
- ) as client:
- resp = await client.post(
- url,
- json={
- "text": docs.strip(),
- "project_id": str(self.state_manager.project.id),
- },
- headers={"Authorization": f"Bearer {self.state_manager.get_access_token()}"},
- )
-
- if resp.status_code == 200:
- log.debug("Uploading docs to RAG service successful")
- resp_body = json.loads(resp.text)
- return True, resp_body["external_api_url"], resp_body["types"]
- elif resp.status_code == 403:
- log.debug("Uploading docs to RAG service failed, trying to refresh token")
- access_token = await self.ui.send_token_expired()
- self.state_manager.update_access_token(access_token)
- else:
- try:
- error = resp.json()["error"]
- except Exception as e:
- capture_exception(e)
- error = e
- log.debug(f"Uploading docs to RAG service failed: {error}")
-
- except Exception as e:
- log.warning(f"Attempt {attempt + 1} failed: {e}", exc_info=True)
- capture_exception(e)
-
- await self.ui.send_message(
- f"An error occurred while uploading the docs. Error: {error if error else 'unknown'}",
- )
- return False
diff --git a/core/cli/helpers.py b/core/cli/helpers.py
index 81b02a796..079fa7db8 100644
--- a/core/cli/helpers.py
+++ b/core/cli/helpers.py
@@ -3,59 +3,21 @@
import os.path
import sys
from argparse import ArgumentParser, ArgumentTypeError, Namespace
-from difflib import unified_diff
from typing import Optional
from urllib.parse import urlparse
from uuid import UUID
from core.config import Config, LLMProvider, LocalIPCConfig, ProviderConfig, UIAdapter, get_config, loader
-from core.config.actions import (
- BH_ADDITIONAL_FEEDBACK,
- BH_HUMAN_TEST_AGAIN,
- BH_IS_BUG_FIXED,
- BH_START_BUG_HUNT,
- BH_START_USER_TEST,
- BH_STARTING_PAIR_PROGRAMMING,
- BH_WAIT_BUG_REP_INSTRUCTIONS,
- CM_UPDATE_FILES,
- DEV_EXECUTE_TASK,
- DEV_TASK_BREAKDOWN,
- DEV_TASK_START,
- DEV_TROUBLESHOOT,
- FE_CHANGE_REQ,
- FE_DONE_WITH_UI,
- HUMAN_INTERVENTION_QUESTION,
- MIX_BREAKDOWN_CHAT_PROMPT,
- RUN_COMMAND,
- TC_TASK_DONE,
- TL_EDIT_DEV_PLAN,
- TS_APP_WORKING,
- TS_DESCRIBE_ISSUE,
-)
from core.config.env_importer import import_from_dotenv
from core.config.version import get_version
-from core.db.models import ProjectState
-from core.db.models.project_state import TaskStatus
from core.db.session import SessionManager
from core.db.setup import run_migrations
-from core.llm.parser import DescriptiveCodeBlockParser
-from core.log import get_logger, setup
+from core.log import setup
from core.state.state_manager import StateManager
-from core.ui.base import AgentSource, UIBase, UISource
+from core.ui.base import UIBase
from core.ui.console import PlainConsoleUI
from core.ui.ipc_client import IPCClientUI
from core.ui.virtual import VirtualUI
-from core.utils.text import trim_logs
-
-log = get_logger(__name__)
-
-try:
- import sentry_sdk
- from sentry_sdk.integrations.asyncio import AsyncioIntegration
-
- SENTRY_AVAILABLE = True
-except ImportError:
- SENTRY_AVAILABLE = False
def parse_llm_endpoint(value: str) -> Optional[tuple[LLMProvider, str]]:
@@ -85,74 +47,6 @@ def parse_llm_endpoint(value: str) -> Optional[tuple[LLMProvider, str]]:
return provider, url.geturl()
-def get_line_changes(old_content: str, new_content: str) -> tuple[int, int]:
- """
- Get the number of added and deleted lines between two files.
-
- This uses Python difflib to produce a unified diff, then counts
- the number of added and deleted lines.
-
- :param old_content: old file content
- :param new_content: new file content
- :return: a tuple (added_lines, deleted_lines)
- """
-
- from_lines = old_content.splitlines(keepends=True)
- to_lines = new_content.splitlines(keepends=True)
-
- diff_gen = unified_diff(from_lines, to_lines)
-
- added_lines = 0
- deleted_lines = 0
-
- for line in diff_gen:
- if line.startswith("+") and not line.startswith("+++"): # Exclude the file headers
- added_lines += 1
- elif line.startswith("-") and not line.startswith("---"): # Exclude the file headers
- deleted_lines += 1
-
- return added_lines, deleted_lines
-
-
-def calculate_pr_changes(convo_entries):
- """
- Calculate file changes between initial and final versions, similar to a Git pull request.
-
- This function tracks the initial (first seen) and final (last seen) versions of each file,
- and calculates the diff between those two versions, ignoring intermediate changes.
-
- :param convo_entries: List of conversation entries containing file changes
- :return: List of file changes with initial and final versions
- """
- file_changes = {}
-
- for entry in convo_entries:
- if not entry.get("files"):
- continue
-
- for file_data in entry.get("files", []):
- path = file_data.get("path")
- if not path:
- continue
-
- if path not in file_changes:
- file_changes[path] = {
- "path": path,
- "old_content": file_data.get("old_content", ""),
- "new_content": file_data.get("new_content", ""),
- }
- else:
- file_changes[path]["new_content"] = file_data.get("new_content", "")
-
- for path, file_info in file_changes.items():
- file_info["n_new_lines"], file_info["n_del_lines"] = get_line_changes(
- old_content=file_info["old_content"], new_content=file_info["new_content"]
- )
-
- # Convert dict to list
- return list(file_changes.values())
-
-
def parse_llm_key(value: str) -> Optional[tuple[LLMProvider, str]]:
"""
Parse --llm-key command-line option.
@@ -201,9 +95,7 @@ def parse_arguments() -> Namespace:
--import-v0: Import data from a v0 (gpt-pilot) database with the given path
--email: User's email address, if provided
--extension-version: Version of the VSCode extension, if used
- --use-git: Use Git for version control
- --access-token: Access token
- --initial-prompt: Initial prompt to automatically start a new project with 'node' stack
+ --no-check: Disable initial LLM API check
:return: Parsed arguments object.
"""
version = get_version()
@@ -221,9 +113,6 @@ def parse_arguments() -> Namespace:
parser.add_argument("--project", help="Load a specific project", type=UUID, required=False)
parser.add_argument("--branch", help="Load a specific branch", type=UUID, required=False)
parser.add_argument("--step", help="Load a specific step in a project/branch", type=int, required=False)
- parser.add_argument(
- "--project-state-id", help="Load a specific project state in a project/branch", type=UUID, required=False
- )
parser.add_argument("--delete", help="Delete a specific project", type=UUID, required=False)
parser.add_argument(
"--llm-endpoint",
@@ -246,38 +135,7 @@ def parse_arguments() -> Namespace:
)
parser.add_argument("--email", help="User's email address", required=False)
parser.add_argument("--extension-version", help="Version of the VSCode extension", required=False)
- parser.add_argument("--use-git", help="Use Git for version control", action="store_true", required=False)
- parser.add_argument(
- "--no-auto-confirm-breakdown",
- help="Disable auto confirm when LLM requests user input",
- action="store_false",
- dest="auto_confirm_breakdown",
- required=False,
- )
- parser.add_argument("--access-token", help="Access token", required=False)
- parser.add_argument(
- "--enable-api-server",
- action="store_true",
- default=True,
- help="Enable IPC server for external clients",
- )
- parser.add_argument(
- "--local-api-server-host",
- type=str,
- default="localhost",
- help="Host for the IPC server (default: localhost)",
- )
- parser.add_argument(
- "--local-api-server-port",
- type=int,
- default=8222,
- help="Port for the IPC server (default: 8222)",
- )
- parser.add_argument(
- "--initial-prompt",
- help="Initial prompt to automatically start a new project with 'node' stack",
- required=False,
- )
+ parser.add_argument("--no-check", help="Disable initial LLM API check", action="store_true")
return parser.parse_args()
@@ -336,522 +194,44 @@ async def list_projects_json(db: SessionManager):
"""
sm = StateManager(db)
projects = await sm.list_projects()
- projects_list = []
- for row in projects:
- project_id, project_name, created_at, folder_name = row
- projects_list.append(
- {
- "id": project_id.hex,
- "name": project_name,
- "folder_name": folder_name,
- "updated_at": created_at.isoformat(),
- }
- )
-
- print(json.dumps(projects_list, indent=2, default=str))
-
-
-def insert_new_task(tasks, new_task):
- # Find the index of the first task with status "todo"
- todo_index = -1
- for i, task in enumerate(tasks):
- if task.get("status") == TaskStatus.TODO:
- todo_index = i
- break
-
- if todo_index != -1:
- tasks.insert(todo_index, new_task)
- else:
- tasks.append(new_task)
-
- return tasks
+ data = []
+ for project in projects:
+ last_updated = None
+ p = {
+ "name": project.name,
+ "id": project.id.hex,
+ "branches": [],
+ }
+ for branch in project.branches:
+ b = {
+ "name": branch.name,
+ "id": branch.id.hex,
+ "steps": [],
+ }
+ for state in branch.states:
+ if not last_updated or state.created_at > last_updated:
+ last_updated = state.created_at
+ s = {
+ "name": state.action or f"Step #{state.step_index}",
+ "step": state.step_index,
+ }
+ b["steps"].append(s)
+ if b["steps"]:
+ b["steps"][-1]["name"] = "Latest step"
+ p["branches"].append(b)
+ p["updated_at"] = last_updated.isoformat() if last_updated else None
+ data.append(p)
-def find_task_by_id(tasks, task_id):
- """
- Find a task by its ID from a list of tasks.
-
- :param tasks: List of task objects
- :param task_id: Task ID to search for
- :return: Task object if found, None otherwise
- """
- for task in tasks:
- if task.get("id") == task_id:
- return task
-
- return None
-
-
-def change_order_of_task(tasks, task_to_move, new_position):
- # Remove the task from its current position
- tasks.remove(task_to_move)
-
- # Insert the task at the new position
- tasks.insert(new_position, task_to_move)
-
- return tasks
-
-
-def find_first_todo_task(tasks):
- """
- Find the first task with status 'todo' from a list of tasks.
-
- :param tasks: List of task objects
- :return: First task with status 'todo', or None if not found
- """
- if not tasks:
- return None
-
- for task in tasks:
- if task.get("status") == "todo":
- return task
-
- return None
-
-
-def find_first_todo_task_index(tasks):
- for i, task in enumerate(tasks):
- if task["status"] == TaskStatus.TODO:
- return i
- return -1
-
-
-def get_epic_task_number(state, current_task) -> (int, int):
- epic_num = -1
- task_num = -1
-
- for task in state.tasks:
- epic_n = task.get("sub_epic_id", 1) + 2
- if epic_n != epic_num:
- epic_num = epic_n
- task_num = 1
-
- if current_task["id"] == task["id"]:
- return epic_num, task_num
-
- task_num += 1
-
- return epic_num, task_num
-
-
-def get_source_for_history(msg_type: Optional[str] = "", question: Optional[str] = ""):
- if question in [TL_EDIT_DEV_PLAN]:
- return AgentSource("Tech Lead", "tech-lead")
-
- if question in [FE_CHANGE_REQ, FE_DONE_WITH_UI]:
- return AgentSource("Frontend", "frontend")
-
- elif question in [
- TS_DESCRIBE_ISSUE,
- BH_HUMAN_TEST_AGAIN,
- BH_IS_BUG_FIXED,
- TS_APP_WORKING,
- BH_ADDITIONAL_FEEDBACK,
- ] or msg_type in ["instructions", "bh_breakdown"]:
- return AgentSource("Bug Hunter", "bug-hunter")
-
- elif msg_type in ["bug_reproduction_instructions", "bug_description"]:
- return AgentSource("Troubleshooter", "troubleshooter")
-
- elif msg_type in ["frontend"]:
- return AgentSource("Frontend", "frontend")
-
- elif HUMAN_INTERVENTION_QUESTION in question:
- return AgentSource("Human Input", "human-input")
-
- elif RUN_COMMAND in question:
- return AgentSource("Executor", "executor")
+ print(json.dumps(data, indent=2))
- elif msg_type in ["task_description", "task_breakdown"]:
- return AgentSource("Developer", "developer")
- else:
- return UISource("Pythagora", "pythagora")
-
-
-"""
-Prints the conversation history to the UI.
-
-:param ui: UI instance to send messages to
-:param convo: List of conversation messages to print
-:param fake: If True, messages will NOT be sent to the extension.
-"""
-
-
-async def print_convo(ui: UIBase, convo: list, fake: Optional[bool] = True):
- msgs = []
- for msg in convo:
- if "frontend" in msg:
- frontend_data = msg["frontend"]
- if isinstance(frontend_data, list):
- for frontend_msg in frontend_data:
- msgs.append(
- await ui.send_message(
- frontend_msg,
- source=get_source_for_history(msg_type="frontend"),
- project_state_id=msg["id"],
- fake=fake,
- )
- )
- else:
- msgs.append(
- await ui.send_message(
- frontend_data,
- source=get_source_for_history(msg_type="frontend"),
- project_state_id=msg["id"],
- fake=fake,
- )
- )
-
- if "bh_breakdown" in msg:
- msgs.append(
- await ui.send_message(
- msg["bh_breakdown"],
- source=get_source_for_history(msg_type="bh_breakdown"),
- project_state_id=msg["id"],
- fake=fake,
- )
- )
-
- if "task_description" in msg:
- msgs.append(
- await ui.send_message(
- msg["task_description"],
- source=get_source_for_history(msg_type="task_description"),
- project_state_id=msg["id"],
- fake=fake,
- )
- )
-
- if "task_breakdown" in msg:
- msgs.append(
- await ui.send_message(
- msg["task_breakdown"],
- source=get_source_for_history(msg_type="task_breakdown"),
- project_state_id=msg["id"],
- fake=fake,
- )
- )
-
- if "test_instructions" in msg:
- msgs.append(
- await ui.send_test_instructions(msg["test_instructions"], project_state_id=msg["id"], fake=fake)
- )
-
- if "bh_testing_instructions" in msg:
- msgs.append(
- await ui.send_test_instructions(msg["bh_testing_instructions"], project_state_id=msg["id"], fake=fake)
- )
-
- if "files" in msg:
- for f in msg["files"]:
- msgs.append(await ui.send_file_status(f["path"], "done", fake=fake))
- msgs.append(
- await ui.generate_diff(
- file_path=f["path"],
- old_content=f.get("old_content", ""),
- new_content=f.get("new_content", ""),
- n_new_lines=f["diff"][0],
- n_del_lines=f["diff"][1],
- fake=fake,
- )
- )
-
- if "user_inputs" in msg and msg["user_inputs"]:
- for input_item in msg["user_inputs"]:
- if "question" in input_item:
- msgs.append(
- await ui.send_message(
- input_item["question"],
- source=get_source_for_history(question=input_item["question"]),
- project_state_id=msg["id"],
- fake=fake,
- )
- )
-
- if "answer" in input_item:
- if input_item["question"] != TL_EDIT_DEV_PLAN:
- msgs.append(
- await ui.send_user_input_history(
- input_item["answer"], project_state_id=msg["id"], fake=fake
- )
- )
-
- return msgs
-
-
-async def load_convo(
- sm: StateManager,
- project_id: Optional[UUID] = None,
- branch_id: Optional[UUID] = None,
- project_states: Optional[list] = None,
-) -> list:
- """
- Loads the conversation from an existing project.
- returns: list of dictionaries with the conversation history
+async def list_projects(db: SessionManager):
"""
- convo = []
-
- if not branch_id:
- branch_id = sm.current_state.branch_id
-
- if project_states is not None and len(project_states) == 0:
- return convo
-
- if not project_states:
- project_states = await sm.get_project_states(project_id, branch_id)
-
- task_counter = 1
- fe_printed_msgs = []
- fe_commands = []
-
- for i, state in enumerate(project_states):
- convo_el = {}
- convo_el["id"] = str(state.id) if state.step_index >= 3 else None
- user_inputs = await sm.find_user_input(state, branch_id)
-
- if state.tasks and state.current_task:
- task_counter = state.tasks.index(state.current_task) + 1
-
- if user_inputs:
- convo_el["user_inputs"] = []
- for ui in user_inputs:
- if ui.question:
- if ui.question == MIX_BREAKDOWN_CHAT_PROMPT:
- if len(state.iterations) > 0:
- # as it's not available in the current state, take the next state's description - that is the bug description!
- next_state = project_states[i + 1] if i + 1 < len(project_states) else None
- if next_state is not None and next_state.iterations is not None:
- si = next_state.iterations[-1]
- if si is not None:
- if si.get("description", None) is not None:
- convo_el["bh_breakdown"] = si["description"]
- else:
- # if there are no iterations, it means developer made task breakdown, take the next state's first task with status = todo
- next_state = project_states[i + 1] if i + 1 < len(project_states) else None
- if next_state is not None:
- task = find_first_todo_task(next_state.tasks)
- if task and task.get("test_instructions", None) is not None:
- convo_el["test_instructions"] = task["test_instructions"]
- if task and task.get("instructions", None) is not None:
- convo_el["task_breakdown"] = task["instructions"]
- # skip parsing that questions and its answers due to the fact that we do not keep states inside breakdown convo
- break
-
- if ui.question == BH_HUMAN_TEST_AGAIN:
- if len(state.iterations) > 0:
- si = state.iterations[-1]
- if si is not None:
- if si.get("bug_reproduction_description", None) is not None:
- convo_el["bh_testing_instructions"] = si["bug_reproduction_description"]
-
- if ui.question == TS_APP_WORKING:
- task = find_first_todo_task(state.tasks)
- if task:
- if task.get("test_instructions", None) is not None:
- convo_el["test_instructions"] = task["test_instructions"]
-
- if ui.question == DEV_EXECUTE_TASK:
- task = find_first_todo_task(state.tasks)
- if task:
- if task.get("description", None) is not None:
- convo_el["task_description"] = f"Task #{task_counter} - " + task["description"]
-
- answer = trim_logs(ui.answer_text) if ui.answer_text is not None else ui.answer_button
- if answer == "bug":
- answer = "There is an issue"
- elif answer == "change":
- answer = "I want to make a change"
- convo_el["user_inputs"].append({"question": ui.question, "answer": answer})
-
- if len(state.epics[-1].get("messages", [])) > 0:
- if convo_el.get("user_inputs", None) is None:
- convo_el["user_inputs"] = []
- parser = DescriptiveCodeBlockParser()
-
- for msg in state.epics[-1].get("messages", []):
- if msg.get("role") == "assistant" and msg["content"] not in fe_printed_msgs:
- if "frontend" not in convo_el:
- convo_el["frontend"] = []
- convo_el["frontend"].append(msg["content"])
- fe_printed_msgs.append(msg["content"])
-
- blocks = parser(msg.get("content", ""))
- files_dict = {}
- for block in blocks.blocks:
- description = block.description.strip()
- content = block.content.strip()
-
- # Split description into lines and check the last line for file path
- description_lines = description.split("\n")
- last_line = description_lines[-1].strip()
-
- if "file:" in last_line:
- # Extract file path from the last line - get everything after "file:"
- file_path = last_line[last_line.index("file:") + 5 :].strip()
- file_path = file_path.strip("\"'`")
- # Skip empty file paths
- if file_path.strip() == "":
- continue
- new_content = content
- old_content = sm.current_state.get_file_content_by_path(file_path)
-
- diff = get_line_changes(
- old_content=old_content,
- new_content=new_content,
- )
-
- if diff != (0, 0):
- files_dict[file_path] = {
- "path": file_path,
- "old_content": old_content,
- "new_content": new_content,
- "diff": diff,
- }
-
- elif "command:" in last_line:
- commands = content.strip().split("\n")
- for command in commands:
- command = command.strip()
- if command and command not in fe_commands:
- if "user_inputs" not in convo_el:
- convo_el["user_inputs"] = []
- convo_el["user_inputs"].append(
- {
- "question": f"{RUN_COMMAND} {command}",
- "answer": "yes",
- }
- )
- fe_commands.append(command)
-
- convo_el["files"] = list(files_dict.values())
-
- if state.action is not None:
- if state.action == DEV_TROUBLESHOOT.format(task_counter):
- if state.iterations is not None and len(state.iterations) > 0:
- si = state.iterations[-1]
- if si is not None:
- if si.get("user_feedback", None) is not None:
- convo_el["user_feedback"] = si["user_feedback"]
- if si.get("description", None) is not None:
- convo_el["description"] = si["description"]
-
- elif state.action == DEV_TASK_BREAKDOWN.format(task_counter):
- if state.tasks and len(state.tasks) >= task_counter:
- task = state.current_task
- if task.get("description", None) is not None:
- convo_el["task_description"] = f"Task #{task_counter} - " + task["description"]
-
- if task.get("instructions", None) is not None:
- convo_el["task_breakdown"] = task["instructions"]
-
- elif state.action == TC_TASK_DONE.format(task_counter):
- if state.tasks:
- next_task = find_first_todo_task(state.tasks)
- if next_task is not None and next_task.get("description", None) is not None:
- convo_el["task_description"] = f"Task #{task_counter} - " + next_task["description"]
-
- elif state.action == DEV_TASK_START.format(task_counter):
- if state.tasks and len(state.tasks) >= task_counter:
- task = state.current_task
- if task.get("instructions", None) is not None:
- convo_el["task_breakdown"] = task["instructions"]
-
- elif state.action == CM_UPDATE_FILES:
- files_dict = {}
- for steps in state.steps:
- if "save_file" in steps and "path" in steps["save_file"]:
- path = steps["save_file"]["path"]
-
- current_file = await sm.get_file_for_project(state.id, path)
- prev_file = (
- await sm.get_file_for_project(state.prev_state_id, path)
- if state.prev_state_id is not None
- else None
- )
-
- old_content = prev_file.content.content if prev_file and prev_file.content else ""
- new_content = current_file.content.content if current_file and current_file.content else ""
-
- diff = get_line_changes(
- old_content=old_content,
- new_content=new_content,
- )
-
- # Only add file if it has changes
- if diff != (0, 0):
- files_dict[path] = {
- "path": path,
- "old_content": old_content,
- "new_content": new_content,
- "diff": diff,
- "bug_hunter": len(state.iterations) > 0
- and len(state.iterations[-1].get("bug_hunting_cycles", [])) > 0,
- }
-
- convo_el["files"] = list(files_dict.values())
-
- if state.iterations is not None and len(state.iterations) > 0:
- si = state.iterations[-1]
-
- if state.action == BH_START_BUG_HUNT.format(task_counter):
- if si.get("user_feedback", None) is not None:
- convo_el["user_feedback"] = si["user_feedback"]
-
- if si.get("description", None) is not None:
- convo_el["description"] = si["description"]
-
- elif state.action == BH_WAIT_BUG_REP_INSTRUCTIONS.format(task_counter):
- for si in state.iterations:
- if si.get("bug_reproduction_description", None) is not None:
- convo_el["bug_reproduction_description"] = si["bug_reproduction_description"]
-
- elif state.action == BH_START_USER_TEST.format(task_counter):
- if si.get("bug_hunting_cycles", None) is not None:
- cycle = si["bug_hunting_cycles"][-1]
- if cycle is not None:
- if "user_feedback" in cycle and cycle["user_feedback"] is not None:
- convo_el["user_feedback"] = cycle["user_feedback"]
- if (
- "human_readable_instructions" in cycle
- and cycle["human_readable_instructions"] is not None
- ):
- convo_el["human_readable_instructions"] = cycle["human_readable_instructions"]
-
- elif state.action == BH_STARTING_PAIR_PROGRAMMING.format(task_counter):
- if "user_feedback" in si and si["user_feedback"] is not None:
- convo_el["user_feedback"] = si["user_feedback"]
- if "initial_explanation" in si and si["initial_explanation"] is not None:
- convo_el["initial_explanation"] = si["initial_explanation"]
-
- convo_el["action"] = state.action
- convo.append(convo_el)
-
- return convo
-
-
-def init_sentry():
- if SENTRY_AVAILABLE:
- sentry_sdk.init(
- dsn="https://4101633bc5560bae67d6eab013ba9686@o4508731634221056.ingest.us.sentry.io/4508732401909760",
- send_default_pii=True,
- traces_sample_rate=1.0,
- integrations=[AsyncioIntegration()],
- )
-
-
-def capture_exception(exc: Exception):
- if SENTRY_AVAILABLE:
- init_sentry()
- sentry_sdk.capture_exception(exc)
-
-
-async def list_projects_branches_states(db: SessionManager):
- """
- List all projects in the database, including their branches and project states
+ List all projects in the database.
"""
sm = StateManager(db)
- projects = await sm.list_projects_with_branches_states()
+ projects = await sm.list_projects()
print(f"Available projects ({len(projects)}):")
for project in projects:
@@ -866,8 +246,7 @@ async def load_project(
project_id: Optional[UUID] = None,
branch_id: Optional[UUID] = None,
step_index: Optional[int] = None,
- project_state_id: Optional[UUID] = None,
-) -> Optional[ProjectState]:
+) -> bool:
"""
Load a project from the database.
@@ -880,26 +259,22 @@ async def load_project(
step_txt = f" step {step_index}" if step_index else ""
if branch_id:
- project_state = await sm.load_project(
- branch_id=branch_id, step_index=step_index, project_state_id=project_state_id
- )
+ project_state = await sm.load_project(branch_id=branch_id, step_index=step_index)
if project_state:
- return project_state
+ return True
else:
print(f"Branch {branch_id}{step_txt} not found; use --list to list all projects", file=sys.stderr)
- return None
+ return False
elif project_id:
- project_state = await sm.load_project(
- project_id=project_id, step_index=step_index, project_state_id=project_state_id
- )
+ project_state = await sm.load_project(project_id=project_id, step_index=step_index)
if project_state:
- return project_state
+ return True
else:
print(f"Project {project_id}{step_txt} not found; use --list to list all projects", file=sys.stderr)
- return None
+ return False
- return None
+ return False
async def delete_project(db: SessionManager, project_id: UUID) -> bool:
@@ -947,16 +322,9 @@ def init() -> tuple[UIBase, SessionManager, Namespace]:
ui = PlainConsoleUI()
run_migrations(config.db)
- db = SessionManager(config.db, args)
+ db = SessionManager(config.db)
return (ui, db, args)
-__all__ = [
- "parse_arguments",
- "load_config",
- "list_projects_json",
- "list_projects_branches_states",
- "load_project",
- "init",
-]
+__all__ = ["parse_arguments", "load_config", "list_projects_json", "list_projects", "load_project", "init"]
diff --git a/core/cli/main.py b/core/cli/main.py
index 85adea3c9..39c74f1e8 100644
--- a/core/cli/main.py
+++ b/core/cli/main.py
@@ -1,66 +1,23 @@
import asyncio
-import atexit
-import gc
-import signal
import sys
-import traceback
from argparse import Namespace
from asyncio import run
-from core.config.actions import FE_ITERATION_DONE
-
-try:
- import sentry_sdk
-
- SENTRY_AVAILABLE = True
-except ImportError:
- SENTRY_AVAILABLE = False
-
from core.agents.orchestrator import Orchestrator
-from core.cli.helpers import (
- capture_exception,
- delete_project,
- init,
- init_sentry,
- list_projects_branches_states,
- list_projects_json,
- load_convo,
- load_project,
- print_convo,
- show_config,
-)
+from core.cli.helpers import delete_project, init, list_projects, list_projects_json, load_project, show_config
+from core.config import LLMProvider, get_config
from core.db.session import SessionManager
from core.db.v0importer import LegacyDatabaseImporter
-from core.llm.anthropic_client import CustomAssertionError
-from core.llm.base import APIError
+from core.llm.base import APIError, BaseLLMClient
from core.log import get_logger
from core.state.state_manager import StateManager
from core.telemetry import telemetry
-from core.ui.api_server import IPCServer
-from core.ui.base import (
- UIBase,
- UIClosedError,
- pythagora_source,
-)
+from core.ui.base import UIBase, UIClosedError, UserInput, pythagora_source
log = get_logger(__name__)
-telemetry_sent = False
-
-
-async def cleanup(ui: UIBase):
- global telemetry_sent
- if not telemetry_sent:
- await telemetry.send()
- telemetry_sent = True
- await ui.stop()
-
-def sync_cleanup(ui: UIBase):
- asyncio.run(cleanup(ui))
-
-
-async def run_project(sm: StateManager, ui: UIBase, args) -> bool:
+async def run_project(sm: StateManager, ui: UIBase) -> bool:
"""
Work on the project.
@@ -69,14 +26,13 @@ async def run_project(sm: StateManager, ui: UIBase, args) -> bool:
:param sm: State manager.
:param ui: User interface.
- :param args: Command-line arguments.
:return: True if the orchestrator exited successfully, False otherwise.
"""
telemetry.set("app_id", str(sm.project.id))
telemetry.set("initial_prompt", sm.current_state.specification.description)
- orca = Orchestrator(sm, ui, args=args)
+ orca = Orchestrator(sm, ui)
success = False
try:
success = await orca.run()
@@ -86,108 +42,111 @@ async def run_project(sm: StateManager, ui: UIBase, args) -> bool:
telemetry.set("end_result", "interrupt")
await sm.rollback()
except APIError as err:
- log.warning(f"an LLM API error occurred: {err.message}")
- await send_error(ui, "error while calling the LLM API", err)
+ log.warning(f"LLM API error occurred: {err.message}")
+ await ui.send_message(
+ f"Stopping Pythagora due to an error while calling the LLM API: {err.message}",
+ source=pythagora_source,
+ )
telemetry.set("end_result", "failure:api-error")
await sm.rollback()
- except CustomAssertionError as err:
- log.warning(f"an Anthropic assertion error occurred: {str(err)}")
- await send_error(ui, "error inside Anthropic SDK", err)
- telemetry.set("end_result", "failure:assertion-error")
- await sm.rollback()
except Exception as err:
log.error(f"Uncaught exception: {err}", exc_info=True)
- await send_error(ui, "an error", err)
-
- telemetry.record_crash(err)
+ stack_trace = telemetry.record_crash(err)
await sm.rollback()
+ await ui.send_message(
+ f"Stopping Pythagora due to error:\n\n{stack_trace}",
+ source=pythagora_source,
+ )
return success
-async def send_error(ui: UIBase, error_source: str, err: Exception):
- stack_trace = traceback.format_exc()
- await ui.send_fatal_error(
- f"Stopping Pythagora due to {error_source}:\n\n{err}",
- source=pythagora_source,
- extra_info={
- "fatal_error": True,
- "stack_trace": stack_trace,
- },
- )
- capture_exception(err)
+async def llm_api_check(ui: UIBase) -> bool:
+ """
+ Check whether the configured LLMs are reachable in parallel.
+
+ :param ui: UI we'll use to report any issues
+ :return: True if all the LLMs are reachable.
+ """
+
+ config = get_config()
+
+ async def handler(*args, **kwargs):
+ pass
+ checked_llms: set[LLMProvider] = set()
+ tasks = []
-async def start_new_project(sm: StateManager, ui: UIBase, args: Namespace = None) -> bool:
+ async def check_llm(llm_config):
+ if llm_config.provider + llm_config.model in checked_llms:
+ return True
+
+ checked_llms.add(llm_config.provider + llm_config.model)
+ client_class = BaseLLMClient.for_provider(llm_config.provider)
+ llm_client = client_class(llm_config, stream_handler=handler, error_handler=handler)
+ try:
+ resp = await llm_client.api_check()
+ if not resp:
+ await ui.send_message(
+ f"API check for {llm_config.provider.value} {llm_config.model} failed.",
+ source=pythagora_source,
+ )
+ log.warning(f"API check for {llm_config.provider.value} {llm_config.model} failed.")
+ return False
+ else:
+ log.info(f"API check for {llm_config.provider.value} {llm_config.model} succeeded.")
+ return True
+ except APIError as err:
+ await ui.send_message(
+ f"API check for {llm_config.provider.value} {llm_config.model} failed with: {err}",
+ source=pythagora_source,
+ )
+ log.warning(f"API check for {llm_config.provider.value} failed with: {err}")
+ return False
+
+ for llm_config in config.all_llms():
+ tasks.append(check_llm(llm_config))
+
+ results = await asyncio.gather(*tasks)
+
+ success = all(results)
+
+ if not success:
+ telemetry.set("end_result", "failure:api-error")
+
+ return success
+
+
+async def start_new_project(sm: StateManager, ui: UIBase) -> bool:
"""
Start a new project.
:param sm: State manager.
:param ui: User interface.
- :param args: Command-line arguments.
:return: True if the project was created successfully, False otherwise.
"""
-
- # Check if initial_prompt is provided, if so, automatically select "node"
- if args and args.initial_prompt:
- stack_button = "node"
-
- await ui.send_back_logs(
- [
- {
- "title": "",
- "project_state_id": "spec",
- "labels": [""],
- "convo": [{"role": "assistant", "content": "Please describe the app you want to build."}],
- }
- ]
- )
- else:
- stack = await ui.ask_question(
- "What do you want to build?",
- allow_empty=False,
- buttons={
- "node": "Full stack app\n(easiest to get started)",
- "swagger": "Frontend only\n(if you have backend with OpenAPI\\Swagger)",
- },
- buttons_only=True,
- source=pythagora_source,
- full_screen=True,
- )
-
- await ui.send_back_logs(
- [
- {
- "title": "",
- "project_state_id": "setup",
- "labels": [""],
- "convo": [{"role": "assistant", "content": "What do you want to build?"}],
- }
- ]
- )
-
- if stack.button == "other":
- language = await ui.ask_question(
- "What language you want to use?",
+ while True:
+ try:
+ user_input = await ui.ask_question(
+ "What is the project name?",
allow_empty=False,
source=pythagora_source,
- full_screen=True,
- )
- await telemetry.trace_code_event(
- "stack-choice-other",
- {"language": language.text},
)
- await ui.send_message("Thank you for submitting your request to support other languages.")
- return False
+ except (KeyboardInterrupt, UIClosedError):
+ user_input = UserInput(cancelled=True)
- stack_button = stack.button
+ if user_input.cancelled:
+ return False
- await telemetry.trace_code_event(
- "stack-choice",
- {"language": stack_button},
- )
+ project_name = user_input.text.strip()
+ if not project_name:
+ await ui.send_message("Please choose a project name", source=pythagora_source)
+ elif len(project_name) > 100:
+ await ui.send_message("Please choose a shorter project name", source=pythagora_source)
+ else:
+ break
- project_state = await sm.create_project(project_type=stack_button)
+ project_state = await sm.create_project(project_name)
return project_state is not None
@@ -201,124 +160,25 @@ async def run_pythagora_session(sm: StateManager, ui: UIBase, args: Namespace):
:return: True if the application ran successfully, False otherwise.
"""
- if args.project or args.branch or args.step or args.project_state_id:
- telemetry.set("is_continuation", True)
- sm.fe_auto_debug = False
- project_state = await load_project(sm, args.project, args.branch, args.step, args.project_state_id)
- if not project_state:
- return False
-
- # SPECIFICATION
- fe_states = await sm.get_fe_states(limit=10)
- be_back_logs, last_task_in_db = await sm.get_be_back_logs()
-
- if sm.current_state.specification:
- if not sm.current_state.specification.original_description:
- spec = sm.current_state.specification
- spec.description = project_state.epics[0]["description"]
- spec.original_description = project_state.epics[0]["description"]
- await sm.update_specification(spec)
-
- await ui.send_front_logs_headers(
- "",
- ["E1 / T1", "Writing Specification", "working" if fe_states == [] else "done"],
- "Writing Specification",
- "",
- )
- await ui.send_back_logs(
- [
- {
- "project_state_id": "spec",
- "disallow_reload": True,
- "labels": ["E1 / T1", "Specs", "working" if fe_states == [] else "done"],
- "title": "Writing Specification",
- "convo": [
- {
- "role": "assistant",
- "content": "What do you want to build?",
- },
- {
- "role": "user",
- "content": sm.current_state.specification.original_description,
- },
- ],
- "start_id": "",
- "end_id": "",
- }
- ]
- )
- if not fe_states and be_back_logs and not last_task_in_db:
- await ui.send_message(
- sm.current_state.specification.description,
- extra_info={"route": "forwardToCenter", "screen": "spec"},
- )
-
- # FRONTEND
-
- if fe_states:
- status = "working" if fe_states[-1].action != FE_ITERATION_DONE else "done"
- await ui.send_front_logs_headers(fe_states[0].id, ["E2 / T1", "Frontend", status], "Building Frontend", "")
- await ui.send_back_logs(
- [
- {
- "labels": ["E2 / T1", "Frontend", status],
- "title": "Building Frontend",
- "convo": [],
- "project_state_id": fe_states[0].id,
- "start_id": fe_states[0].id,
- "end_id": fe_states[-1].id,
- }
- ]
- )
-
- # BACKEND
- if be_back_logs:
- await ui.send_back_logs(be_back_logs)
-
- if not be_back_logs and not last_task_in_db:
- # if no backend logs AND no task is currently active -> we are on frontend -> print frontend convo history
- convo = await load_convo(sm, project_states=fe_states)
- await print_convo(ui=ui, convo=convo, fake=False)
- # Clear fe_states from memory after conversation is loaded
- del fe_states
- gc.collect() # Force garbage collection to free memory immediately
- elif last_task_in_db:
- # Clear fe_states from memory as they're not needed for backend processing
- del fe_states
- gc.collect() # Force garbage collection to free memory immediately
-
- # if there is a task in the db (we are at backend stage), print backend convo history and add task back logs and front logs headers
- await ui.send_front_logs_headers(
- last_task_in_db["start_id"],
- last_task_in_db["labels"],
- last_task_in_db["title"],
- last_task_in_db.get("task_id", ""),
- )
- await ui.send_back_logs(
- [
- {
- "project_state_id": last_task_in_db["start_id"],
- "labels": last_task_in_db["labels"],
- "title": last_task_in_db["title"],
- "convo": [],
- "start_id": last_task_in_db["start_id"],
- "end_id": last_task_in_db["end_id"],
- }
- ]
+ if not args.no_check:
+ if not await llm_api_check(ui):
+ await ui.send_message(
+ "Pythagora cannot start because the LLM API is not reachable.",
+ source=pythagora_source,
)
- be_states = await sm.get_project_states_in_between(last_task_in_db["start_id"], last_task_in_db["end_id"])
- convo = await load_convo(sm, project_states=be_states)
- await print_convo(ui=ui, convo=convo, fake=False)
- # Clear be_states from memory after conversation is loaded
- del be_states
- gc.collect() # Force garbage collection to free memory immediately
+ return False
+ if args.project or args.branch or args.step:
+ telemetry.set("is_continuation", True)
+ success = await load_project(sm, args.project, args.branch, args.step)
+ if not success:
+ return False
else:
- sm.fe_auto_debug = True
- success = await start_new_project(sm, ui, args)
+ success = await start_new_project(sm, ui)
if not success:
return False
- return await run_project(sm, ui, args)
+
+ return await run_project(sm, ui)
async def async_main(
@@ -334,10 +194,9 @@ async def async_main(
:param args: Command-line arguments.
:return: True if the application ran successfully, False otherwise.
"""
- global telemetry_sent
if args.list:
- await list_projects_branches_states(db)
+ await list_projects(db)
return True
elif args.list_json:
await list_projects_json(db)
@@ -354,74 +213,19 @@ async def async_main(
return success
telemetry.set("user_contact", args.email)
-
- if SENTRY_AVAILABLE and args.email:
- init_sentry()
- sentry_sdk.set_user({"email": args.email})
-
if args.extension_version:
- log.debug(f"Extension version: {args.extension_version}")
telemetry.set("is_extension", True)
telemetry.set("extension_version", args.extension_version)
sm = StateManager(db, ui)
- if args.access_token:
- sm.update_access_token(args.access_token)
-
- # Start API server if enabled in config
- api_server = None
- if hasattr(args, "enable_api_server") and args.enable_api_server:
- api_host = getattr(args, "local_api_server_host", "localhost")
- api_port = getattr(args, "local_api_server_port", 8222) # Different from client port
- api_server = IPCServer(api_host, api_port, sm)
- server_started = await api_server.start()
- if not server_started:
- log.warning(f"Failed to start API server on {api_host}:{api_port}")
-
- if not args.auto_confirm_breakdown:
- sm.auto_confirm_breakdown = False
ui_started = await ui.start()
if not ui_started:
- if api_server:
- await api_server.stop()
return False
telemetry.start()
-
- def signal_handler(sig, frame):
- try:
- loop = asyncio.get_running_loop()
-
- def close_all():
- loop.stop()
- sys.exit(0)
-
- if not telemetry_sent:
- cleanup_task = loop.create_task(cleanup(ui))
- cleanup_task.add_done_callback(close_all)
- else:
- close_all()
- except RuntimeError:
- if not telemetry_sent:
- sync_cleanup(ui)
- sys.exit(0)
-
- for sig in (signal.SIGINT, signal.SIGTERM):
- signal.signal(sig, signal_handler)
-
- # Register the cleanup function
- atexit.register(sync_cleanup, ui)
-
- try:
- success = await run_pythagora_session(sm, ui, args)
- except Exception as err:
- log.error(f"Uncaught exception in main session: {err}", exc_info=True)
- await send_error(ui, "an error", err)
- raise
- finally:
- await cleanup(ui)
- if api_server:
- await api_server.stop()
+ success = await run_pythagora_session(sm, ui, args)
+ await telemetry.send()
+ await ui.stop()
return success
diff --git a/core/config/__init__.py b/core/config/__init__.py
index 6ba0503aa..90ac3c6a0 100644
--- a/core/config/__init__.py
+++ b/core/config/__init__.py
@@ -5,8 +5,6 @@
from pydantic import BaseModel, ConfigDict, Field, field_validator
from typing_extensions import Annotated
-from core.config.constants import LOGS_LINE_LIMIT
-
ROOT_DIR = abspath(join(dirname(__file__), "..", ".."))
DEFAULT_IGNORE_PATHS = [
".git",
@@ -38,23 +36,18 @@
DEFAULT_AGENT_NAME = "default"
CODE_MONKEY_AGENT_NAME = "CodeMonkey"
CODE_REVIEW_AGENT_NAME = "CodeMonkey.code_review"
-IMPLEMENT_CHANGES_AGENT_NAME = "CodeMonkey.implement_changes"
DESCRIBE_FILES_AGENT_NAME = "CodeMonkey.describe_files"
CHECK_LOGS_AGENT_NAME = "BugHunter.check_logs"
PARSE_TASK_AGENT_NAME = "Developer.parse_task"
TASK_BREAKDOWN_AGENT_NAME = "Developer.breakdown_current_task"
TROUBLESHOOTER_BUG_REPORT = "Troubleshooter.generate_bug_report"
TROUBLESHOOTER_GET_RUN_COMMAND = "Troubleshooter.get_run_command"
-TROUBLESHOOTER_DEFINE_USER_REVIEW_GOAL = "Troubleshooter.define_user_review_goal"
TECH_LEAD_PLANNING = "TechLead.plan_epic"
-TECH_LEAD_EPIC_BREAKDOWN = "TechLead.epic_breakdown"
SPEC_WRITER_AGENT_NAME = "SpecWriter"
GET_RELEVANT_FILES_AGENT_NAME = "get_relevant_files"
-FRONTEND_AGENT_NAME = "Frontend"
# Endpoint for the external documentation
EXTERNAL_DOCUMENTATION_API = "http://docs-pythagora-io-439719575.us-east-1.elb.amazonaws.com"
-PYTHAGORA_API = "https://api.pythagora.ai"
class _StrictModel(BaseModel):
@@ -73,7 +66,6 @@ class LLMProvider(str, Enum):
"""
OPENAI = "openai"
- RELACE = "relace"
ANTHROPIC = "anthropic"
GROQ = "groq"
LM_STUDIO = "lm-studio"
@@ -109,7 +101,7 @@ class ProviderConfig(_StrictModel):
ge=0.0,
)
read_timeout: float = Field(
- default=60.0,
+ default=20.0,
description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
ge=0.0,
)
@@ -164,7 +156,7 @@ class LLMConfig(_StrictModel):
ge=0.0,
)
read_timeout: float = Field(
- default=60.0,
+ default=20.0,
description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
ge=0.0,
)
@@ -221,13 +213,9 @@ class LogConfig(_StrictModel):
description="Logging format",
)
output: Optional[str] = Field(
- "data/pythagora.log",
+ "pythagora.log",
description="Output file for logs (if not specified, logs are printed to stderr)",
)
- max_lines: int = Field(
- LOGS_LINE_LIMIT,
- description="Maximum number of lines to keep in the log file",
- )
class DBConfig(_StrictModel):
@@ -240,11 +228,10 @@ class DBConfig(_StrictModel):
"""
url: str = Field(
- "sqlite+aiosqlite:///data/database/pythagora.db",
+ "sqlite+aiosqlite:///pythagora.db",
description="Database connection URL",
)
debug_sql: bool = Field(False, description="Log all SQL queries to the console")
- save_llm_requests: bool = Field(False, description="Save LLM requests to db")
@field_validator("url")
@classmethod
@@ -331,85 +318,59 @@ class Config(_StrictModel):
default={
LLMProvider.OPENAI: ProviderConfig(),
LLMProvider.ANTHROPIC: ProviderConfig(),
- LLMProvider.RELACE: ProviderConfig(),
}
)
agent: dict[str, AgentLLMConfig] = Field(
default={
DEFAULT_AGENT_NAME: AgentLLMConfig(),
CHECK_LOGS_AGENT_NAME: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
+ provider=LLMProvider.ANTHROPIC,
+ model="claude-3-5-sonnet-20240620",
temperature=0.5,
),
CODE_MONKEY_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
+ model="gpt-4-0125-preview",
temperature=0.0,
),
CODE_REVIEW_AGENT_NAME: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
+ provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20240620",
temperature=0.0,
),
- IMPLEMENT_CHANGES_AGENT_NAME: AgentLLMConfig(
- provider=LLMProvider.RELACE,
- model="relace-code-merge",
- temperature=0.0, # temperature is unused for relace
- ),
DESCRIBE_FILES_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.OPENAI,
model="gpt-4o-mini-2024-07-18",
temperature=0.0,
),
- FRONTEND_AGENT_NAME: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
- temperature=0.0,
- ),
- GET_RELEVANT_FILES_AGENT_NAME: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="gpt-4o-2024-05-13",
- temperature=0.5,
- ),
PARSE_TASK_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.OPENAI,
- model="claude-3-5-sonnet-20241022",
+ model="gpt-4-0125-preview",
temperature=0.0,
),
SPEC_WRITER_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
+ model="gpt-4-0125-preview",
temperature=0.0,
),
TASK_BREAKDOWN_AGENT_NAME: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
+ provider=LLMProvider.ANTHROPIC,
+ model="claude-3-5-sonnet-20240620",
temperature=0.5,
),
TECH_LEAD_PLANNING: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
+ provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20240620",
temperature=0.5,
),
- TECH_LEAD_EPIC_BREAKDOWN: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="claude-3-5-sonnet-20241022",
- temperature=0.5,
- ),
TROUBLESHOOTER_BUG_REPORT: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
+ provider=LLMProvider.ANTHROPIC,
+ model="claude-3-5-sonnet-20240620",
temperature=0.5,
),
TROUBLESHOOTER_GET_RUN_COMMAND: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
- temperature=0.0,
- ),
- TROUBLESHOOTER_DEFINE_USER_REVIEW_GOAL: AgentLLMConfig(
- provider=LLMProvider.OPENAI,
- model="claude-sonnet-4-20250514",
+ provider=LLMProvider.ANTHROPIC,
+ model="claude-3-5-sonnet-20240620",
temperature=0.0,
),
}
@@ -518,7 +479,6 @@ def adapt_for_bedrock(config: Config) -> Config:
return config
replacement_map = {
- "claude-3-5-sonnet-20241022": "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
"claude-3-5-sonnet-20240620": "us.anthropic.claude-3-5-sonnet-20240620-v1:0",
"claude-3-sonnet-20240229": "us.anthropic.claude-3-sonnet-20240229-v1:0",
"claude-3-haiku-20240307": "us.anthropic.claude-3-haiku-20240307-v1:0",
diff --git a/core/config/actions.py b/core/config/actions.py
deleted file mode 100644
index 308bb85dd..000000000
--- a/core/config/actions.py
+++ /dev/null
@@ -1,58 +0,0 @@
-BH_START_BUG_HUNT = "Start bug hunt for task #{}"
-BH_WAIT_BUG_REP_INSTRUCTIONS = "Awaiting bug reproduction instructions for task #{}"
-BH_START_USER_TEST = "Start user testing for task #{}"
-BH_STARTING_PAIR_PROGRAMMING = "Start pair programming for task #{}"
-
-CM_UPDATE_FILES = "Updating files"
-
-
-DEV_WAIT_TEST = "Awaiting user test"
-DEV_TASK_START = "Task #{} start"
-DEV_TASK_BREAKDOWN = "Task #{} breakdown"
-DEV_TROUBLESHOOT = "Troubleshooting #{}"
-DEV_TASK_REVIEW_FEEDBACK = "Task review feedback"
-
-TC_TASK_DONE = "Task #{} complete"
-
-
-FE_INIT = "Frontend init"
-FE_START = "Frontend start"
-FE_CONTINUE = "Frontend continue"
-FE_ITERATION = "Frontend iteration"
-FE_ITERATION_DONE = "Frontend iteration done"
-
-TL_CREATE_INITIAL_EPIC = "Create initial project epic"
-TL_CREATE_PLAN = "Create a development plan for epic: {}"
-TL_START_FEATURE = "Start of feature #{}"
-TL_INITIAL_PROJECT_NAME = "Initial Project"
-
-TW_WRITE = "Write documentation"
-
-EX_SKIP_COMMAND = 'Skip "{}"'
-EX_RUN_COMMAND = 'Run "{}"'
-
-SPEC_CREATE_STEP_NAME = "Create specification"
-SPEC_CHANGE_STEP_NAME = "Change specification"
-SPEC_CHANGE_FEATURE_STEP_NAME = "Change specification due to new feature"
-
-TS_TASK_REVIEWED = "Task #{} reviewed"
-TS_ALT_SOLUTION = "Alternative solution (attempt #{})"
-TS_APP_WORKING = "Please check if the app is working"
-
-PS_EPIC_COMPLETE = "Epic {} completed"
-
-# other constants
-TL_EDIT_DEV_PLAN = "Open and edit your development plan in the Progress tab"
-MIX_BREAKDOWN_CHAT_PROMPT = "Are you happy with the breakdown? Now is a good time to ask questions or suggest changes."
-FE_CHANGE_REQ = (
- "Do you want to change anything or report a bug? Keep in mind that currently ONLY frontend is implemented."
-)
-FE_DONE_WITH_UI = "Are you sure you're done building the UI and want to start building the backend functionality now?"
-TS_DESCRIBE_ISSUE = "Please describe the issue you found (one at a time) and share any relevant server logs"
-BH_HUMAN_TEST_AGAIN = "Please test the app again."
-BH_IS_BUG_FIXED = "Is the bug you reported fixed now?"
-BH_ADDITIONAL_FEEDBACK = "Please add any additional feedback that could help Pythagora solve this bug"
-HUMAN_INTERVENTION_QUESTION = "I need human intervention"
-CONTINUE_WHEN_DONE = 'When you\'re done, just click "Continue"'
-RUN_COMMAND = "Can I run command:"
-DEV_EXECUTE_TASK = "Do you want to execute the above task?"
diff --git a/core/config/constants.py b/core/config/constants.py
deleted file mode 100644
index aaeeb0399..000000000
--- a/core/config/constants.py
+++ /dev/null
@@ -1,2 +0,0 @@
-CONVO_ITERATIONS_LIMIT = 8
-LOGS_LINE_LIMIT = 20000
diff --git a/core/config/magic_words.py b/core/config/magic_words.py
index 5ebd9245d..089dff37d 100644
--- a/core/config/magic_words.py
+++ b/core/config/magic_words.py
@@ -1,37 +1,27 @@
PROBLEM_IDENTIFIED = "PROBLEM_IDENTIFIED"
ADD_LOGS = "ADD_LOGS"
-ALWAYS_RELEVANT_FILES = [
- "client/src/App.tsx",
+THINKING_LOGS = [
+ "Pythagora is crunching the numbers...",
+ "Pythagora is deep in thought...",
+ "Pythagora is analyzing your request...",
+ "Pythagora is brewing up a solution...",
+ "Pythagora is putting the pieces together...",
+ "Pythagora is working its magic...",
+ "Pythagora is crafting the perfect response...",
+ "Pythagora is decoding your query...",
+ "Pythagora is on the case...",
+ "Pythagora is computing an answer...",
+ "Pythagora is sorting through the data...",
+ "Pythagora is gathering insights...",
+ "Pythagora is making connections...",
+ "Pythagora is tuning the algorithms...",
+ "Pythagora is piecing together the puzzle...",
+ "Pythagora is scanning the possibilities...",
+ "Pythagora is engineering a response...",
+ "Pythagora is building the answer...",
+ "Pythagora is mapping out a solution...",
+ "Pythagora is figuring this out for you...",
+ "Pythagora is thinking hard right now...",
+ "Pythagora is working for you, so relax!",
+ "Pythagora might take some time to figure this out...",
]
-GITIGNORE_CONTENT = """# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
-
-# SQLite databases, data files
-*.db
-*.csv
-
-# Keep environment variables out of version control
-.env
-"""
diff --git a/core/config/version.py b/core/config/version.py
index 2af9a8b49..ee9feb3a1 100644
--- a/core/config/version.py
+++ b/core/config/version.py
@@ -39,30 +39,6 @@ def get_git_commit() -> Optional[str]:
return f.read().strip()
-def get_git_branch() -> Optional[str]:
- """
- Return the current git branch name (if running from a repo).
-
- :return: branch name or None if not on a branch or not a git repo
- """
- if not isdir(GIT_DIR_PATH):
- return None
-
- git_head = join(GIT_DIR_PATH, "HEAD")
- if not isfile(git_head):
- return None
-
- with open(git_head, "r", encoding="utf-8") as f:
- ref = f.read().strip()
-
- if ref.startswith("ref: "):
- # Example: ref: refs/heads/main
- ref_path = ref[5:]
- if ref_path.startswith("refs/heads/"):
- return ref_path[len("refs/heads/") :]
- return None
-
-
def get_package_version() -> str:
"""
Get package version as defined pyproject.toml.
@@ -107,4 +83,4 @@ def get_version() -> str:
return version
-__all__ = ["get_version", "get_git_branch"]
+__all__ = ["get_version"]
diff --git a/core/db/alembic.ini b/core/db/alembic.ini
index 667ce857d..bb4d8250f 100644
--- a/core/db/alembic.ini
+++ b/core/db/alembic.ini
@@ -61,7 +61,7 @@ version_path_separator = os
# are written from script.py.mako
# output_encoding = utf-8
-sqlalchemy.url = sqlite:///data/database/pythagora.db
+sqlalchemy.url = sqlite:///pythagora.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
diff --git a/core/db/fix_migrations.py b/core/db/fix_migrations.py
deleted file mode 100644
index 1acac014d..000000000
--- a/core/db/fix_migrations.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python
-import os
-import sqlite3
-from pathlib import Path
-from typing import Optional
-
-from alembic import command
-from alembic.config import Config
-from alembic.script import ScriptDirectory
-
-
-def get_latest_revision(alembic_cfg: Config) -> Optional[str]:
- """Get the most recent revision from available migration files."""
- script = ScriptDirectory.from_config(alembic_cfg)
- if script.get_heads():
- return script.get_heads()[0]
- return None
-
-
-def fix_alembic_version(db_path: str, version: str) -> None:
- """Manually update alembic_version table to the specified version."""
- conn = sqlite3.connect(db_path)
- try:
- cursor = conn.cursor()
- cursor.execute("UPDATE alembic_version SET version_num = ?", (version,))
- conn.commit()
- print(f"Successfully updated alembic_version to {version}")
- except sqlite3.Error as e:
- print(f"Error updating database: {e}")
- raise
- finally:
- conn.close()
-
-
-def main():
- # Get the project root directory (where core/ is located)
- project_root = Path(__file__).parent.parent.parent
-
- # Configure alembic
- alembic_ini = os.path.join(project_root, "core", "db", "alembic.ini")
- if not os.path.exists(alembic_ini):
- print(f"Error: Could not find alembic.ini at {alembic_ini}")
- return 1
-
- alembic_cfg = Config(alembic_ini)
-
- # Get the database path from alembic.ini
- db_url = alembic_cfg.get_main_option("sqlalchemy.url")
- if not db_url.startswith("sqlite:///"):
- print("Error: This script only works with SQLite databases")
- return 1
-
- db_path = db_url.replace("sqlite:///", "")
- db_path = os.path.join(project_root, db_path)
-
- if not os.path.exists(db_path):
- print(f"Database file not found at {db_path}")
- create_new = input("Would you like to create a new database? (y/n): ")
- if create_new.lower() == "y":
- print("Creating new database and running migrations...")
- command.upgrade(alembic_cfg, "head")
- print("Done!")
- return 0
- return 1
-
- # Get the latest available revision
- latest_revision = get_latest_revision(alembic_cfg)
- if not latest_revision:
- print("Error: No migration versions found")
- return 1
-
- print(f"Latest available revision: {latest_revision}")
-
- try:
- # Update the version in the database
- fix_alembic_version(db_path, latest_revision)
-
- # Run migrations to ensure database schema is up to date
- print("Running migrations to ensure database schema is current...")
- command.upgrade(alembic_cfg, "head")
-
- print("Database successfully fixed and upgraded!")
- return 0
- except Exception as e:
- print(f"Error: {e}")
- return 1
-
-
-if __name__ == "__main__":
- exit(main())
diff --git a/core/db/migrations/versions/0173e14719aa_move_metadata_from_file_to_file_content_.py b/core/db/migrations/versions/0173e14719aa_move_metadata_from_file_to_file_content_.py
deleted file mode 100644
index 0edf72590..000000000
--- a/core/db/migrations/versions/0173e14719aa_move_metadata_from_file_to_file_content_.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""move metadata from file to file content table
-
-Revision ID: 0173e14719aa
-Revises: 3968d770dced
-Create Date: 2025-05-15 15:33:03.084670
-
-"""
-
-from typing import Sequence, Union
-
-import sqlalchemy as sa
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision: str = "0173e14719aa"
-down_revision: Union[str, None] = "3968d770dced"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # Add meta column to file_contents
- with op.batch_alter_table("file_contents", schema=None) as batch_op:
- batch_op.add_column(sa.Column("meta", sa.JSON(), server_default="{}", nullable=False))
-
- # Copy data from files.meta to file_contents.meta
- op.execute("""
- UPDATE file_contents
- SET meta = files.meta
- FROM files
- WHERE file_contents.id = files.content_id
- """)
-
- # Drop meta column from files
- with op.batch_alter_table("files", schema=None) as batch_op:
- batch_op.drop_column("meta")
-
-
-def downgrade() -> None:
- # Add meta column back to files
- with op.batch_alter_table("files", schema=None) as batch_op:
- batch_op.add_column(sa.Column("meta", sa.JSON(), server_default="{}", nullable=False))
-
- # Copy data from file_contents.meta back to files.meta
- op.execute("""
- UPDATE files
- SET meta = file_contents.meta
- FROM file_contents
- WHERE files.content_id = file_contents.id
- """)
-
- # Drop meta column from file_contents
- with op.batch_alter_table("file_contents", schema=None) as batch_op:
- batch_op.drop_column("meta")
diff --git a/core/db/migrations/versions/0173e14719aa_vacuum_database.py b/core/db/migrations/versions/0173e14719aa_vacuum_database.py
deleted file mode 100644
index 560610d46..000000000
--- a/core/db/migrations/versions/0173e14719aa_vacuum_database.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""vacuum database
-
-Revision ID: 0173e14719ab
-Revises: 69e50fdaf067
-Create Date: 2025-05-15 15:33:03.084670
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-from sqlalchemy import text
-
-# revision identifiers, used by Alembic.
-revision: str = "0173e14719ab"
-down_revision: Union[str, None] = "69e50fdaf067"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # Get connection
- connection = op.get_bind()
-
- # Temporarily disable journal mode
- connection.execute(text("PRAGMA journal_mode = OFF"))
-
- # Run VACUUM
- connection.execute(text("VACUUM"))
-
-
-def downgrade() -> None:
- # VACUUM is not reversible
- pass
diff --git a/core/db/migrations/versions/3968d770dced_add_project_type_to_project.py b/core/db/migrations/versions/3968d770dced_add_project_type_to_project.py
deleted file mode 100644
index e01eb6007..000000000
--- a/core/db/migrations/versions/3968d770dced_add_project_type_to_project.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""Add project type to project
-
-Revision ID: 3968d770dced
-Revises: f708791b9270
-Create Date: 2025-02-15 10:30:13.163098
-
-"""
-
-from typing import Sequence, Union
-
-import sqlalchemy as sa
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision: str = "3968d770dced"
-down_revision: Union[str, None] = "f708791b9270"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- with op.batch_alter_table("projects", schema=None) as batch_op:
- batch_op.add_column(sa.Column("project_type", sa.String(), nullable=False, server_default="node"))
-
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- with op.batch_alter_table("projects", schema=None) as batch_op:
- batch_op.drop_column("project_type")
-
- # ### end Alembic commands ###
diff --git a/core/db/migrations/versions/675268601278_add_chat_messages_and_convos.py b/core/db/migrations/versions/675268601278_add_chat_messages_and_convos.py
deleted file mode 100644
index eae3ff39e..000000000
--- a/core/db/migrations/versions/675268601278_add_chat_messages_and_convos.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Add chat messages and convos
-
-Revision ID: 675268601278
-Revises: 0173e14719ab
-Create Date: 2025-05-14 10:38:19.130649
-
-"""
-
-from typing import Sequence, Union
-
-import sqlalchemy as sa
-from alembic import op
-from sqlalchemy import func
-
-# revision identifiers, used by Alembic.
-revision: str = "675268601278"
-down_revision: Union[str, None] = "0173e14719ab"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # Create chat_convos table
- op.create_table(
- "chat_convos",
- sa.Column("id", sa.Integer(), autoincrement=True, primary_key=True),
- sa.Column("convo_id", sa.Uuid(), nullable=False, unique=True),
- sa.Column(
- "project_state_id", sa.Uuid(), sa.ForeignKey("project_states.id", ondelete="CASCADE"), nullable=False
- ),
- sa.Column("created_at", sa.DateTime, server_default=func.now(), nullable=False),
- )
-
- # Create chat_messages table
- op.create_table(
- "chat_messages",
- sa.Column("id", sa.Uuid(), primary_key=True, nullable=False),
- sa.Column("convo_id", sa.Uuid(), sa.ForeignKey("chat_convos.convo_id", ondelete="CASCADE"), nullable=False),
- sa.Column("created_at", sa.DateTime, server_default=func.now(), nullable=False),
- sa.Column("message_type", sa.String(), nullable=False),
- sa.Column("message", sa.String(), nullable=False),
- sa.Column("prev_message_id", sa.Uuid(), sa.ForeignKey("chat_messages.id", ondelete="SET NULL"), nullable=True),
- )
-
-
-def downgrade() -> None:
- op.drop_table("chat_messages")
- op.drop_table("chat_convos")
diff --git a/core/db/migrations/versions/69e50fdaf067_move_knowledge_base_to_separate_table.py b/core/db/migrations/versions/69e50fdaf067_move_knowledge_base_to_separate_table.py
deleted file mode 100644
index e60d2f14c..000000000
--- a/core/db/migrations/versions/69e50fdaf067_move_knowledge_base_to_separate_table.py
+++ /dev/null
@@ -1,143 +0,0 @@
-"""move knowledge base to separate table
-
-Revision ID: 69e50fdaf067
-Revises: 0173e14719aa
-Create Date: 2025-05-15 17:27:50.312917
-
-"""
-
-import json
-from typing import Sequence, Union
-
-import sqlalchemy as sa
-from alembic import op
-from sqlalchemy import JSON, Column, Integer, MetaData, Table, insert, text
-from sqlalchemy.dialects import sqlite
-
-# revision identifiers, used by Alembic.
-revision: str = "69e50fdaf067"
-down_revision: Union[str, None] = "0173e14719aa"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # Create the new knowledge_bases table
- op.create_table(
- "knowledge_bases",
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
- sa.Column("pages", sa.JSON(), server_default="[]", nullable=False),
- sa.Column("apis", sa.JSON(), server_default="[]", nullable=False),
- sa.Column("user_options", sa.JSON(), server_default="{}", nullable=False),
- sa.Column("utility_functions", sa.JSON(), server_default="[]", nullable=False),
- sa.PrimaryKeyConstraint("id", name=op.f("pk_knowledge_bases")),
- )
-
- # Add knowledge_base_id column to project_states
- with op.batch_alter_table("project_states", schema=None) as batch_op:
- batch_op.add_column(sa.Column("knowledge_base_id", sa.Integer(), nullable=True))
-
- # Get connection for data migration
- connection = op.get_bind()
-
- # Create a table object for knowledge_bases
- metadata = MetaData()
- knowledge_bases = Table(
- "knowledge_bases",
- metadata,
- Column("id", Integer, primary_key=True),
- Column("pages", JSON),
- Column("apis", JSON),
- Column("user_options", JSON),
- Column("utility_functions", JSON),
- )
-
- # Keep track of unique knowledge bases to avoid redundancy
- kb_cache = {}
-
- # Migrate data from old knowledge_base column to new table
- project_states = connection.execute(text("SELECT id, knowledge_base FROM project_states")).fetchall()
- for state_id, kb_data_str in project_states:
- if kb_data_str:
- try:
- # Parse the JSON string into a dictionary
- kb_data = json.loads(kb_data_str) if isinstance(kb_data_str, str) else kb_data_str
- except (json.JSONDecodeError, TypeError):
- # If parsing fails, use empty defaults
- kb_data = {}
-
- # Create a cache key from the knowledge base content
- cache_key = json.dumps(
- {
- "pages": kb_data.get("pages", []),
- "apis": kb_data.get("apis", []),
- "user_options": kb_data.get("user_options", {}),
- "utility_functions": kb_data.get("utility_functions", []),
- },
- sort_keys=True,
- )
-
- if cache_key not in kb_cache:
- # Insert new knowledge base record
- stmt = insert(knowledge_bases).values(
- pages=kb_data.get("pages", []),
- apis=kb_data.get("apis", []),
- user_options=kb_data.get("user_options", {}),
- utility_functions=kb_data.get("utility_functions", []),
- )
- result = connection.execute(stmt)
- kb_cache[cache_key] = result.inserted_primary_key[0]
-
- # Update project state to reference the knowledge base
- kb_id = kb_cache[cache_key]
- connection.execute(
- text("UPDATE project_states SET knowledge_base_id = :kb_id WHERE id = :state_id"),
- {"kb_id": kb_id, "state_id": state_id},
- )
-
- # Make knowledge_base_id not nullable and add foreign key constraint
- with op.batch_alter_table("project_states", schema=None) as batch_op:
- batch_op.alter_column("knowledge_base_id", nullable=False)
- batch_op.create_foreign_key(
- batch_op.f("fk_project_states_knowledge_base_id_knowledge_bases"),
- "knowledge_bases",
- ["knowledge_base_id"],
- ["id"],
- )
- batch_op.drop_column("knowledge_base")
-
- # Clean up llm_requests table
- op.execute("DELETE FROM llm_requests")
-
-
-def downgrade() -> None:
- # Add back the knowledge_base column
- with op.batch_alter_table("project_states", schema=None) as batch_op:
- batch_op.add_column(sa.Column("knowledge_base", sqlite.JSON(), server_default=sa.text("'{}'"), nullable=False))
-
- # Get connection for data migration
- connection = op.get_bind()
-
- # Migrate data back from knowledge_bases table to project_states
- results = connection.execute(
- text("""
- SELECT ps.id, kb.pages, kb.apis, kb.user_options, kb.utility_functions
- FROM project_states ps
- JOIN knowledge_bases kb ON ps.knowledge_base_id = kb.id
- """)
- ).fetchall()
-
- for state_id, pages, apis, user_options, utility_functions in results:
- # Create the knowledge base data structure
- kb_data = {"pages": pages, "apis": apis, "user_options": user_options, "utility_functions": utility_functions}
- connection.execute(
- text("UPDATE project_states SET knowledge_base = :kb_data WHERE id = :state_id"),
- {"kb_data": json.dumps(kb_data), "state_id": state_id},
- )
-
- # Remove the knowledge_base_id column and knowledge_bases table
- with op.batch_alter_table("project_states", schema=None) as batch_op:
- batch_op.drop_constraint(batch_op.f("fk_project_states_knowledge_base_id_knowledge_bases"), type_="foreignkey")
- batch_op.drop_column("knowledge_base_id")
-
- op.drop_table("knowledge_bases")
diff --git a/core/db/migrations/versions/f708791b9270_adding_knowledge_base_field_to_.py b/core/db/migrations/versions/f708791b9270_adding_knowledge_base_field_to_.py
deleted file mode 100644
index f19ee00e2..000000000
--- a/core/db/migrations/versions/f708791b9270_adding_knowledge_base_field_to_.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""Adding knowledge_base field to ProjectState
-
-Revision ID: f708791b9270
-Revises: c8905d4ce784
-Create Date: 2024-12-22 12:13:14.979169
-
-"""
-
-from typing import Sequence, Union
-
-import sqlalchemy as sa
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision: str = "f708791b9270"
-down_revision: Union[str, None] = "c8905d4ce784"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- with op.batch_alter_table("project_states", schema=None) as batch_op:
- batch_op.add_column(sa.Column("knowledge_base", sa.JSON(), server_default="{}", nullable=False))
-
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- with op.batch_alter_table("project_states", schema=None) as batch_op:
- batch_op.drop_column("knowledge_base")
-
- # ### end Alembic commands ###
diff --git a/core/db/models/__init__.py b/core/db/models/__init__.py
index 35ff1df3a..1b81fb885 100644
--- a/core/db/models/__init__.py
+++ b/core/db/models/__init__.py
@@ -5,12 +5,9 @@
from .base import Base
from .branch import Branch
-from .chat_convo import ChatConvo
-from .chat_message import ChatMessage
from .exec_log import ExecLog
from .file import File
from .file_content import FileContent
-from .knowledge_base import KnowledgeBase
from .llm_request import LLMRequest
from .project import Project
from .project_state import ProjectState
@@ -24,12 +21,9 @@
"ExecLog",
"File",
"FileContent",
- "KnowledgeBase",
"LLMRequest",
"Project",
"ProjectState",
"Specification",
"UserInput",
- "ChatConvo",
- "ChatMessage",
]
diff --git a/core/db/models/chat_convo.py b/core/db/models/chat_convo.py
deleted file mode 100644
index 1456779b0..000000000
--- a/core/db/models/chat_convo.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from datetime import datetime
-from typing import TYPE_CHECKING, Optional
-from uuid import UUID, uuid4
-
-from sqlalchemy import ForeignKey, select
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from sqlalchemy.sql import func
-
-from core.db.models import Base
-
-if TYPE_CHECKING:
- from core.db.models import ChatMessage, ProjectState
-
-
-class ChatConvo(Base):
- __tablename__ = "chat_convos"
-
- id: Mapped[int] = mapped_column(primary_key=True)
- convo_id: Mapped[UUID] = mapped_column(default=uuid4, unique=True)
- project_state_id: Mapped[UUID] = mapped_column(ForeignKey("project_states.id", ondelete="CASCADE"))
- created_at: Mapped[datetime] = mapped_column(server_default=func.now())
-
- # Relationships
- project_state: Mapped["ProjectState"] = relationship(back_populates="chat_convos", lazy="selectin")
- messages: Mapped[list["ChatMessage"]] = relationship(
- back_populates="convo", cascade="all,delete-orphan", lazy="selectin"
- )
-
- @staticmethod
- async def get_chat_history(session: AsyncSession, convo_id) -> list["ChatMessage"]:
- from core.db.models import ChatMessage
-
- result = await session.execute(select(ChatMessage).where(ChatMessage.convo_id == convo_id))
- return result.scalars().all()
-
- @staticmethod
- async def get_project_state_for_convo_id(session: AsyncSession, convo_id) -> Optional["ProjectState"]:
- from core.db.models import ChatConvo, ProjectState
-
- result = await session.execute(select(ChatConvo).where(ChatConvo.convo_id == convo_id))
- chat_convo = result.scalars().first()
-
- if not chat_convo:
- return None
-
- result = await session.execute(select(ProjectState).where(ProjectState.id == chat_convo.project_state_id))
- return result.scalars().one_or_none()
diff --git a/core/db/models/chat_message.py b/core/db/models/chat_message.py
deleted file mode 100644
index 8b7f43102..000000000
--- a/core/db/models/chat_message.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from datetime import datetime
-from typing import Optional
-from uuid import UUID, uuid4
-
-from sqlalchemy import ForeignKey
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from sqlalchemy.sql import func
-
-from core.db.models import Base, ChatConvo
-
-
-class ChatMessage(Base):
- __tablename__ = "chat_messages"
-
- id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
- convo_id: Mapped[UUID] = mapped_column(ForeignKey("chat_convos.convo_id", ondelete="CASCADE"))
- created_at: Mapped[datetime] = mapped_column(server_default=func.now())
- message_type: Mapped[str] = mapped_column()
- message: Mapped[str] = mapped_column()
- prev_message_id: Mapped[Optional[UUID]] = mapped_column(ForeignKey("chat_messages.id", ondelete="SET NULL"))
-
- # Relationships
- convo: Mapped["ChatConvo"] = relationship(back_populates="messages", lazy="selectin")
diff --git a/core/db/models/file.py b/core/db/models/file.py
index f7fc28ceb..d75ee68f4 100644
--- a/core/db/models/file.py
+++ b/core/db/models/file.py
@@ -1,9 +1,7 @@
-import re
from typing import TYPE_CHECKING, Optional
from uuid import UUID
-from sqlalchemy import ForeignKey, UniqueConstraint, select
-from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.db.models import Base
@@ -23,6 +21,7 @@ class File(Base):
# Attributes
path: Mapped[str] = mapped_column()
+ meta: Mapped[dict] = mapped_column(default=dict, server_default="{}")
# Relationships
project_state: Mapped[Optional["ProjectState"]] = relationship(back_populates="files", lazy="raise")
@@ -40,46 +39,5 @@ def clone(self) -> "File":
project_state=None,
content_id=self.content_id,
path=self.path,
+ meta=self.meta,
)
-
- @staticmethod
- async def get_referencing_files(session: "AsyncSession", project_state, file_path_to_search) -> list["File"]:
- results = await session.execute(select(File).where(File.project_state_id == project_state.id))
- all_files = results.scalars().all()
-
- file_to_search = None
- for file in all_files:
- if file.path == file_path_to_search:
- file_to_search = file
- all_files.remove(file)
- break
-
- if file_to_search is None:
- return []
-
- referencing_files = []
- target_file_name = file_path_to_search.split("/")[-1].split(".")[0]
-
- import_regex = re.compile(
- rf"import.*from\s+['\"](\.?/?(?:{re.escape(target_file_name)}|{re.escape('/api' + '/' + target_file_name)}))(?:['\"])[;]*"
- )
-
- # Extract function names from the target file
- function_names = set()
- for match in re.finditer(r"export\s+const\s+(\w+)\s*=", file_to_search.content.content):
- function_names.add(match.group(1))
- function_names_list = list(function_names)
-
- direct_function_call_regex = None
- if function_names_list:
- direct_function_call_regex = re.compile(rf"({'|'.join(function_names_list)})\(")
-
- for file in all_files:
- if import_regex.search(file.content.content):
- referencing_files.append(file)
- elif any(fn in file.content.content for fn in function_names_list):
- referencing_files.append(file)
- elif direct_function_call_regex and direct_function_call_regex.search(file.content.content):
- referencing_files.append(file)
-
- return referencing_files
diff --git a/core/db/models/file_content.py b/core/db/models/file_content.py
index 042be2879..57cbe8c33 100644
--- a/core/db/models/file_content.py
+++ b/core/db/models/file_content.py
@@ -18,13 +18,12 @@ class FileContent(Base):
# Attributes
content: Mapped[str] = mapped_column()
- meta: Mapped[dict] = mapped_column(default=dict, server_default="{}")
# Relationships
files: Mapped[list["File"]] = relationship(back_populates="content", lazy="raise")
@classmethod
- async def store(cls, session: AsyncSession, hash: str, content: str, meta: dict = None) -> "FileContent":
+ async def store(cls, session: AsyncSession, hash: str, content: str) -> "FileContent":
"""
Store the file content in the database.
@@ -35,17 +34,14 @@ async def store(cls, session: AsyncSession, hash: str, content: str, meta: dict
:param session: The database session.
:param hash: The hash of the file content, used as an unique ID.
:param content: The file content as unicode string.
- :param meta: Optional metadata for the file content.
:return: The file content object.
"""
result = await session.execute(select(FileContent).where(FileContent.id == hash))
fc = result.scalar_one_or_none()
if fc is not None:
- if meta is not None:
- fc.meta = meta
return fc
- fc = cls(id=hash, content=content, meta=meta or {})
+ fc = cls(id=hash, content=content)
session.add(fc)
return fc
diff --git a/core/db/models/knowledge_base.py b/core/db/models/knowledge_base.py
deleted file mode 100644
index cd20b2983..000000000
--- a/core/db/models/knowledge_base.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from copy import deepcopy
-from typing import TYPE_CHECKING
-
-from sqlalchemy import JSON, delete, distinct, select
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-
-from core.db.models.base import Base
-
-if TYPE_CHECKING:
- from core.db.models import ProjectState
-
-
-class KnowledgeBase(Base):
- """Model for storing project knowledge base.
-
- This model stores various pieces of project-related information:
- - pages: List of implemented frontend pages
- - apis: List of API endpoints and their implementation status
- - user_options: User configuration options for the project
- - utility_functions: List of utility functions with their status, input/output values
- """
-
- __tablename__ = "knowledge_bases"
-
- id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
- pages: Mapped[list[str]] = mapped_column(JSON, default=list, server_default="[]")
- apis: Mapped[list[dict]] = mapped_column(JSON, default=list, server_default="[]")
- user_options: Mapped[dict] = mapped_column(JSON, default=dict, server_default="{}")
- utility_functions: Mapped[list[dict]] = mapped_column(JSON, default=list, server_default="[]")
-
- # Relationships
- project_states: Mapped[list["ProjectState"]] = relationship(back_populates="knowledge_base", lazy="raise")
-
- def clone(self) -> "KnowledgeBase":
- """
- Clone the knowledge base.
-
- Creates a new KnowledgeBase instance with the same data but new ID.
- Used when the knowledge base needs to be modified to maintain immutability
- of previous states.
- """
- return KnowledgeBase(
- pages=deepcopy(self.pages),
- apis=deepcopy(self.apis),
- user_options=deepcopy(self.user_options),
- utility_functions=deepcopy(self.utility_functions),
- )
-
- @classmethod
- async def delete_orphans(cls, session: AsyncSession):
- """
- Delete KnowledgeBase objects that are not referenced by any ProjectState object.
-
- :param session: The database session.
- """
- from core.db.models import ProjectState
-
- await session.execute(
- delete(KnowledgeBase).where(~KnowledgeBase.id.in_(select(distinct(ProjectState.knowledge_base_id))))
- )
diff --git a/core/db/models/project.py b/core/db/models/project.py
index 69f4c91a1..4f1a716aa 100644
--- a/core/db/models/project.py
+++ b/core/db/models/project.py
@@ -4,12 +4,12 @@
from unicodedata import normalize
from uuid import UUID, uuid4
-from sqlalchemy import Row, and_, delete, inspect, select
+from sqlalchemy import and_, delete, inspect, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload
from sqlalchemy.sql import func
-from core.db.models import Base, File
+from core.db.models import Base
if TYPE_CHECKING:
from core.db.models import Branch
@@ -27,7 +27,6 @@ class Project(Base):
folder_name: Mapped[str] = mapped_column(
default=lambda context: Project.get_folder_from_project_name(context.get_current_parameters()["name"])
)
- project_type: Mapped[str] = mapped_column(default="node")
# Relationships
branches: Mapped[list["Branch"]] = relationship(back_populates="project", cascade="all", lazy="raise")
@@ -47,31 +46,6 @@ async def get_by_id(session: "AsyncSession", project_id: Union[str, UUID]) -> Op
result = await session.execute(select(Project).where(Project.id == project_id))
return result.scalar_one_or_none()
- @staticmethod
- async def rename(session: "AsyncSession", id: UUID, name: str, dir_name: str) -> Optional["Project"]:
- """
- Rename a project and update its folder name.
-
- :param session: The SQLAlchemy session.
- :param id: The project ID.
- :param name: The new project name.
- :param dir_name: The new folder name for the project.
- :return: The updated Project object if found, None otherwise.
- """
- # Get the project by ID
- query = select(Project).where(Project.id == id)
- result = await session.execute(query)
- project = result.scalar_one_or_none()
-
- if project is None:
- return None
-
- # Update project name and dir name
- project.name = name
- project.folder_name = dir_name
-
- return project
-
async def get_branch(self, name: Optional[str] = None) -> Optional["Branch"]:
"""
Get a project branch by name.
@@ -93,28 +67,7 @@ async def get_branch(self, name: Optional[str] = None) -> Optional["Branch"]:
return result.scalar_one_or_none()
@staticmethod
- async def get_file_for_project(session: AsyncSession, project_state_id: UUID, path: str) -> Optional["File"]:
- file_result = await session.execute(
- select(File).where(File.project_state_id == project_state_id, File.path == path)
- )
- return file_result.scalar_one_or_none()
-
- @staticmethod
- async def get_branches_for_project_id(session: AsyncSession, project_id: UUID) -> list["Branch"]:
- from core.db.models import Branch
-
- branch_result = await session.execute(select(Branch).where(Branch.project_id == project_id))
- return branch_result.scalars().all()
-
- @staticmethod
- async def get_all_projects(session: "AsyncSession") -> list[Row]:
- query = select(Project.id, Project.name, Project.created_at, Project.folder_name).order_by(Project.name)
-
- result = await session.execute(query)
- return result.fetchall()
-
- @staticmethod
- async def get_all_projects_with_branches_states(session: "AsyncSession") -> list["Project"]:
+ async def get_all_projects(session: "AsyncSession") -> list["Project"]:
"""
Get all projects.
diff --git a/core/db/models/project_state.py b/core/db/models/project_state.py
index 90ae3b7ca..3e552641d 100644
--- a/core/db/models/project_state.py
+++ b/core/db/models/project_state.py
@@ -1,30 +1,19 @@
from copy import deepcopy
from datetime import datetime
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING, Optional
from uuid import UUID, uuid4
-from sqlalchemy import ForeignKey, UniqueConstraint, and_, delete, inspect, or_, select
+from sqlalchemy import ForeignKey, UniqueConstraint, delete, inspect
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import Mapped, load_only, mapped_column, relationship
+from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.sql import func
-from core.config.actions import FE_START, PS_EPIC_COMPLETE
-from core.db.models import Base, FileContent
+from core.db.models import Base
from core.log import get_logger
if TYPE_CHECKING:
- from core.db.models import (
- Branch,
- ChatConvo,
- ExecLog,
- File,
- FileContent,
- KnowledgeBase,
- LLMRequest,
- Specification,
- UserInput,
- )
+ from core.db.models import Branch, ExecLog, File, FileContent, LLMRequest, Specification, UserInput
log = get_logger(__name__)
@@ -36,6 +25,7 @@ class TaskStatus:
IN_PROGRESS = "in_progress"
REVIEWED = "reviewed"
DOCUMENTED = "documented"
+ EPIC_UPDATED = "epic_updated"
DONE = "done"
SKIPPED = "skipped"
@@ -69,7 +59,6 @@ class ProjectState(Base):
branch_id: Mapped[UUID] = mapped_column(ForeignKey("branches.id", ondelete="CASCADE"))
prev_state_id: Mapped[Optional[UUID]] = mapped_column(ForeignKey("project_states.id", ondelete="CASCADE"))
specification_id: Mapped[int] = mapped_column(ForeignKey("specifications.id"))
- knowledge_base_id: Mapped[int] = mapped_column(ForeignKey("knowledge_bases.id"))
# Attributes
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
@@ -100,13 +89,9 @@ class ProjectState(Base):
cascade="all,delete-orphan",
)
specification: Mapped["Specification"] = relationship(back_populates="project_states", lazy="selectin")
- knowledge_base: Mapped["KnowledgeBase"] = relationship(back_populates="project_states", lazy="selectin")
llm_requests: Mapped[list["LLMRequest"]] = relationship(back_populates="project_state", cascade="all", lazy="raise")
user_inputs: Mapped[list["UserInput"]] = relationship(back_populates="project_state", cascade="all", lazy="raise")
exec_logs: Mapped[list["ExecLog"]] = relationship(back_populates="project_state", cascade="all", lazy="raise")
- chat_convos: Mapped[list["ChatConvo"]] = relationship(
- back_populates="project_state", cascade="all,delete-orphan", lazy="raise"
- )
@property
def unfinished_steps(self) -> list[dict]:
@@ -136,8 +121,6 @@ def unfinished_iterations(self) -> list[dict]:
:return: List of unfinished iterations.
"""
- if not self.iterations:
- return []
return [
iteration for iteration in self.iterations if iteration.get("status") not in (None, IterationStatus.DONE)
]
@@ -161,8 +144,6 @@ def unfinished_tasks(self) -> list[dict]:
:return: List of unfinished tasks.
"""
- if not self.tasks:
- return []
return [task for task in self.tasks if task.get("status") != TaskStatus.DONE]
@property
@@ -224,48 +205,14 @@ def create_initial_state(branch: "Branch") -> "ProjectState":
:param branch: The branch to create the state for.
:return: The new ProjectState object.
"""
- from core.db.models import KnowledgeBase, Specification
+ from core.db.models import Specification
return ProjectState(
branch=branch,
specification=Specification(),
- knowledge_base=KnowledgeBase(),
step_index=1,
- action="Initial project state",
)
- @staticmethod
- async def get_project_states(
- session: "AsyncSession",
- project_id: Optional[UUID] = None,
- branch_id: Optional[UUID] = None,
- ) -> list["ProjectState"]:
- from core.db.models import Branch, ProjectState
-
- branch = None
- limit = 100
-
- if branch_id:
- branch = await session.execute(select(Branch).where(Branch.id == branch_id))
- branch = branch.scalar_one_or_none()
- elif project_id:
- branch = await session.execute(select(Branch).where(Branch.project_id == project_id))
- branch = branch.scalar_one_or_none()
-
- if branch:
- query = (
- select(ProjectState)
- .where(ProjectState.branch_id == branch.id)
- .order_by(ProjectState.step_index.desc()) # Get the latest 100 states
- .limit(limit)
- )
-
- project_states_result = await session.execute(query)
- project_states = project_states_result.scalars().all()
- return sorted(project_states, key=lambda x: x.step_index)
-
- return []
-
async def create_next_state(self) -> "ProjectState":
"""
Create the next project state for the branch.
@@ -291,7 +238,6 @@ async def create_next_state(self) -> "ProjectState":
tasks=deepcopy(self.tasks),
steps=deepcopy(self.steps),
iterations=deepcopy(self.iterations),
- knowledge_base=self.knowledge_base,
files=[],
relevant_files=deepcopy(self.relevant_files),
modified_files=deepcopy(self.modified_files),
@@ -308,20 +254,17 @@ async def create_next_state(self) -> "ProjectState":
for file in await self.awaitable_attrs.files:
clone = file.clone()
new_state.files.append(clone)
- # Load content for the clone using the same content_id
- result = await session.execute(select(FileContent).where(FileContent.id == file.content_id))
- clone.content = result.scalar_one_or_none()
return new_state
- def complete_step(self, step_type: str):
+ def complete_step(self):
if not self.unfinished_steps:
raise ValueError("There are no unfinished steps to complete")
if "next_state" in self.__dict__:
raise ValueError("Current state is read-only (already has a next state).")
log.debug(f"Completing step {self.unfinished_steps[0]['type']}")
- self.get_steps_of_type(step_type)[0]["completed"] = True
+ self.unfinished_steps[0]["completed"] = True
flag_modified(self, "steps")
def complete_task(self):
@@ -352,8 +295,6 @@ def complete_epic(self):
self.unfinished_epics[0]["completed"] = True
self.tasks = []
flag_modified(self, "epics")
- if len(self.unfinished_epics) > 0:
- self.next_state.action = PS_EPIC_COMPLETE.format(self.unfinished_epics[0]["name"])
def complete_iteration(self):
if not self.unfinished_iterations:
@@ -364,7 +305,6 @@ def complete_iteration(self):
log.debug(f"Completing iteration {self.unfinished_iterations[0]}")
self.unfinished_iterations[0]["status"] = IterationStatus.DONE
self.relevant_files = None
- self.modified_files = {}
self.flag_iterations_as_modified()
def flag_iterations_as_modified(self):
@@ -387,30 +327,6 @@ def flag_tasks_as_modified(self):
"""
flag_modified(self, "tasks")
- def flag_epics_as_modified(self):
- """
- Flag the epic field as having been modified
-
- Used by Agents that perform modifications within the mutable epics field,
- to tell the database that it was modified and should get saved (as SQLalchemy
- can't detect changes in mutable fields by itself).
- """
- flag_modified(self, "epics")
-
- def flag_knowledge_base_as_modified(self):
- """
- Flag the knowledge base fields as having been modified
-
- Used by Agents that perform modifications within the mutable knowledge base fields,
- to tell the database that it was modified and should get saved (as SQLalchemy
- can't detect changes in mutable fields by itself).
-
- This creates a new knowledge base instance to maintain immutability of previous states,
- similar to how specification modifications are handled.
- """
- # Create a new knowledge base instance with the current data
- self.knowledge_base = self.knowledge_base.clone()
-
def set_current_task_status(self, status: str):
"""
Set the status of the current task.
@@ -438,17 +354,6 @@ def get_file_by_path(self, path: str) -> Optional["File"]:
return None
- def get_file_content_by_path(self, path: str) -> Union[FileContent, str]:
- """
- Get a file from the current project state, by the file path.
-
- :param path: The file path.
- :return: The file object, or None if not found.
- """
- file = self.get_file_by_path(path)
-
- return file.content.content if file else ""
-
def save_file(self, path: str, content: "FileContent", external: bool = False) -> "File":
"""
Save a file to the project state.
@@ -491,47 +396,18 @@ def save_file(self, path: str, content: "FileContent", external: bool = False) -
async def delete_after(self):
"""
- Delete all states in the branch after this one, along with related data.
-
- This includes:
- - ProjectState records after this one
- - Related UserInput records (including those for the current state)
- - Related File records
- - Orphaned FileContent records
- - Orphaned KnowledgeBase records
- - Orphaned Specification records
+ Delete all states in the branch after this one.
"""
- from core.db.models import FileContent, KnowledgeBase, Specification, UserInput
session: AsyncSession = inspect(self).async_session
log.debug(f"Deleting all project states in branch {self.branch_id} after {self.id}")
-
- # Get all project states to be deleted
- states_to_delete = await session.execute(
- select(ProjectState).where(
+ await session.execute(
+ delete(ProjectState).where(
ProjectState.branch_id == self.branch_id,
ProjectState.step_index > self.step_index,
)
)
- states_to_delete = states_to_delete.scalars().all()
- state_ids = [state.id for state in states_to_delete]
-
- # Delete user inputs for the current state
- await session.execute(delete(UserInput).where(UserInput.project_state_id == self.id))
-
- if state_ids:
- # Delete related user inputs for states to be deleted
- await session.execute(delete(UserInput).where(UserInput.project_state_id.in_(state_ids)))
-
- # Delete project states
- await session.execute(delete(ProjectState).where(ProjectState.id.in_(state_ids)))
-
- # Clean up orphaned records
- await FileContent.delete_orphans(session)
- await UserInput.delete_orphans(session)
- await KnowledgeBase.delete_orphans(session)
- await Specification.delete_orphans(session)
def get_last_iteration_steps(self) -> list:
"""
@@ -567,512 +443,3 @@ def get_steps_of_type(self, step_type: str) -> [dict]:
"""
li = self.unfinished_steps
return [step for step in li if step.get("type") == step_type] if li else []
-
- def has_frontend(self) -> bool:
- """
- Check if there is a frontend epic in the project state.
-
- :return: True if there is a frontend epic, False otherwise.
- """
- return self.epics and any(epic.get("source") == "frontend" for epic in self.epics)
-
- # function that checks whether old project or new project is currently in frontend stage
- def working_on_frontend(self) -> bool:
- return self.has_frontend() and len(self.epics) == 1
-
- def is_feature(self) -> bool:
- """
- Check if the current epic is a feature.
-
- :return: True if the current epic is a feature, False otherwise.
- """
- return self.epics and self.current_epic and self.current_epic.get("source") == "feature"
-
- @staticmethod
- async def get_state_for_redo_task(session: AsyncSession, project_state: "ProjectState") -> Optional["ProjectState"]:
- states_result = await session.execute(
- select(ProjectState).where(
- and_(
- ProjectState.step_index <= project_state.step_index,
- ProjectState.branch_id == project_state.branch_id,
- )
- )
- )
-
- result = states_result.scalars().all()
-
- result = sorted(result, key=lambda x: x.step_index, reverse=True)
- for state in result:
- if state.tasks:
- for task in state.tasks:
- if task.get("id") == project_state.current_task.get("id") and task.get("instructions") is None:
- if task.get("status") == TaskStatus.TODO:
- return state
-
- return None
-
- @staticmethod
- async def get_by_id(session: "AsyncSession", state_id: UUID) -> Optional["ProjectState"]:
- """
- Retrieve a project state by its ID.
-
- :param session: The SQLAlchemy async session.
- :param state_id: The UUID of the project state to retrieve.
- :return: The ProjectState object if found, None otherwise.
- """
- if not state_id:
- return None
-
- query = select(ProjectState).where(ProjectState.id == state_id)
- result = await session.execute(query)
- return result.scalar_one_or_none()
-
- @staticmethod
- async def get_all_epics_and_tasks(session: "AsyncSession", branch_id: UUID) -> list:
- epics_and_tasks = []
-
- try:
- query = (
- select(ProjectState)
- .options(load_only(ProjectState.id, ProjectState.epics, ProjectState.tasks))
- .where(and_(ProjectState.branch_id == branch_id, ProjectState.action.isnot(None)))
- )
-
- result = await session.execute(query)
- project_states = result.scalars().all()
-
- def has_epic(epic_type: str):
- return any(epic1.get("source", "") == epic_type for epic1 in epics_and_tasks)
-
- def find_epic_by_id(epic_id: str, sub_epic_id: str):
- return next(
- (
- epic
- for epic in epics_and_tasks
- if epic.get("id", "") == epic_id and epic.get("sub_epic_id", "") == sub_epic_id
- ),
- None,
- )
-
- def find_task_in_epic(task_id: str, epic):
- if not epic:
- return None
- return next((task for task in epic.get("tasks", []) if task.get("id", "") == task_id), None)
-
- for state in project_states:
- epics, tasks = state.epics, state.tasks
- epic = epics[-1]
-
- if epics[-1] in ["spec_writer", "frontend"]:
- for epic in state.epics:
- if epic["source"] == "spec_writer" and not has_epic("spec_writer"):
- epics_and_tasks.insert(0, {"source": "spec_writer", "tasks": []})
-
- if epic["source"] == "frontend" and not has_epic("frontend"):
- epics_and_tasks.insert(1, {"source": "frontend", "tasks": []})
-
- else:
- for sub_epic in epic.get("sub_epics", []):
- if not find_epic_by_id(epic["id"], sub_epic["id"]):
- epics_and_tasks.append(
- {
- "id": epic["id"],
- "sub_epic_id": sub_epic["id"],
- "source": epic["source"],
- "description": sub_epic.get("description", ""),
- "tasks": [],
- }
- )
-
- for task in tasks:
- epic_in_list = find_epic_by_id(epic["id"], task.get("sub_epic_id"))
- if not epic_in_list:
- continue
- task_in_epic_list = find_task_in_epic(task["id"], epic_in_list)
- if not task_in_epic_list:
- epic_in_list["tasks"].append(
- {
- "id": task.get("id"),
- "status": task.get("status"),
- "description": task.get("description"),
- }
- )
- else:
- # Update the status of the task if it already exists
- task_in_epic_list["status"] = task.get("status")
-
- except Exception as e:
- log.error(f"Error while getting epics and tasks: {e}")
- return []
-
- return epics_and_tasks
-
- @staticmethod
- async def get_project_states_in_between(
- session: "AsyncSession", branch_id: UUID, start_id: UUID, end_id: UUID, limit: Optional[int] = 100
- ):
- query = select(ProjectState).where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.id == start_id,
- )
- )
- result = await session.execute(query)
- start_state = result.scalars().one_or_none()
-
- query = select(ProjectState).where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.id == end_id,
- )
- )
- result = await session.execute(query)
- end_state = result.scalars().one_or_none()
-
- if not start_state or not end_state:
- log.error(f"Could not find states with IDs {start_id} and {end_id} in branch {branch_id}")
- return []
-
- query = (
- select(ProjectState)
- .where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.step_index >= start_state.step_index,
- ProjectState.step_index <= end_state.step_index,
- )
- )
- .order_by(ProjectState.step_index.desc())
- )
-
- if limit:
- query = query.limit(limit)
-
- result = await session.execute(query)
- states = result.scalars().all()
-
- # Since we always order by step_index desc, we need to reverse to get chronological order
- return list(reversed(states))
-
- @staticmethod
- async def get_task_conversation_project_states(
- session: "AsyncSession",
- branch_id: UUID,
- task_id: UUID,
- first_last_only: bool = False,
- limit: Optional[int] = 25,
- ) -> Optional[list["ProjectState"]]:
- """
- Retrieve the conversation for the task in the project state.
-
- :param session: The SQLAlchemy async session.
- :param branch_id: The UUID of the branch.
- :param task_id: The UUID of the task.
- :param first_last_only: If True, return only first and last states.
- :param limit: Maximum number of states to return (default 25).
- :return: List of conversation messages if found, None otherwise.
- """
- log.debug(
- f"Getting task conversation project states for task {task_id} in branch {branch_id} with first_last_only {first_last_only} and limit {limit}"
- )
- # First, we need to find the start and end step indices
- # Use a more efficient query that only loads necessary fields
- query = (
- select(ProjectState)
- .options(load_only(ProjectState.id, ProjectState.step_index, ProjectState.tasks, ProjectState.action))
- .where(
- and_(
- ProjectState.branch_id == branch_id,
- or_(ProjectState.action.like("%Task #%"), ProjectState.action.like("%Create a development plan%")),
- )
- )
- .order_by(ProjectState.step_index)
- )
-
- result = await session.execute(query)
- states = result.scalars().all()
-
- log.debug(f"Found {len(states)} states with custom action")
-
- start_step_index = None
- end_step_index = None
-
- # for the FIRST task, it is todo in the same state as Create a development plan, while other tasks are "Task #N start" (action)
-
- # this is done solely to be able to reload to the first task, due to the fact that we need the same project_state_id for the send_back_logs
- # for the first task, we need to start from the FIRST state that has that task in TODO status
- # for all other tasks, we need to start from LAST state that has that task in TODO status
- for state in states:
- for task in state.tasks:
- if UUID(task["id"]) == task_id and task.get("status", "") == TaskStatus.TODO:
- if UUID(task["id"]) == UUID(state.tasks[0]["id"]):
- # First task: set start only once (first occurrence)
- if start_step_index is None:
- start_step_index = state.step_index
- else:
- # Other tasks: update start every time (last occurrence)
- start_step_index = state.step_index
-
- if UUID(task["id"]) == task_id and task.get("status", "") in [
- TaskStatus.SKIPPED,
- TaskStatus.DOCUMENTED,
- TaskStatus.REVIEWED,
- TaskStatus.DONE,
- ]:
- end_step_index = state.step_index
-
- if start_step_index is None:
- return []
-
- # Now build the optimized query based on what we need
- if first_last_only:
- # For first_last_only, we only need the first and last states
- # Get first state
- first_query = (
- select(ProjectState)
- .where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.step_index >= start_step_index,
- ProjectState.step_index < end_step_index if end_step_index else True,
- )
- )
- .order_by(ProjectState.step_index.asc())
- .limit(1)
- )
-
- # Get last state (excluding the uncommitted one)
- last_query = (
- select(ProjectState)
- .where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.step_index >= start_step_index,
- ProjectState.step_index < end_step_index if end_step_index else True,
- )
- )
- .order_by(ProjectState.step_index.desc())
- .limit(2)
- ) # Get last 2 to exclude uncommitted
-
- first_result = await session.execute(first_query)
- last_result = await session.execute(last_query)
-
- first_state = first_result.scalars().first()
- last_states = last_result.scalars().all()
-
- # Remove the last state (uncommitted) and get the actual last
- if len(last_states) > 1:
- last_state = last_states[1] # Second to last is the actual last committed
- else:
- last_state = first_state # Only one state
-
- if first_state and last_state and first_state.id != last_state.id:
- return [first_state, last_state]
- elif first_state:
- return [first_state]
- else:
- return []
-
- else:
- # For regular queries, apply limit at the database level
- query = (
- select(ProjectState)
- .where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.step_index >= start_step_index,
- ProjectState.step_index < end_step_index if end_step_index else True,
- )
- )
- .order_by(ProjectState.step_index.asc())
- )
-
- if limit:
- # Apply limit + 1 to account for removing the last uncommitted state
- query = query.limit(limit + 1)
-
- result = await session.execute(query)
- results = result.scalars().all()
-
- log.debug(f"Found {len(results)} states with custom action")
- # Remove the last state from the list because that state is not yet committed in the database!
- if results:
- results = results[:-1]
-
- return results
-
- @staticmethod
- async def get_fe_states(
- session: "AsyncSession", branch_id: UUID, limit: Optional[int] = None
- ) -> Optional["ProjectState"]:
- query = select(ProjectState).where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.action == FE_START,
- )
- )
- result = await session.execute(query)
- fe_start = result.scalars().one_or_none()
-
- if not fe_start:
- return []
-
- query = (
- select(ProjectState)
- .where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.step_index >= fe_start.step_index,
- ProjectState.action.like("%Frontend%"),
- )
- )
- .order_by(ProjectState.step_index.desc())
- .limit(1)
- )
- result = await session.execute(query)
- fe_end = result.scalars().one_or_none()
-
- query = (
- select(ProjectState)
- .where(
- and_(
- ProjectState.branch_id == branch_id,
- ProjectState.step_index >= fe_start.step_index,
- ProjectState.step_index <= fe_end.step_index,
- )
- )
- .order_by(ProjectState.step_index.desc())
- )
-
- if limit:
- query = query.limit(limit)
-
- results = await session.execute(query)
- states = results.scalars().all()
-
- # Since we ordered by step_index desc and limited, we need to reverse to get chronological order
- return list(reversed(states))
-
- @staticmethod
- def get_epic_task_number(state, current_task) -> (int, int):
- epic_num = -1
- task_num = -1
-
- for task in state.tasks:
- epic_n = task.get("sub_epic_id", 1) + 2
- if epic_n != epic_num:
- epic_num = epic_n
- task_num = 1
-
- if current_task["id"] == task["id"]:
- return epic_num, task_num
-
- task_num += 1
-
- return epic_num, task_num
-
- @staticmethod
- async def get_be_back_logs(session: "AsyncSession", branch_id: UUID) -> (list[dict], dict, list["ProjectState"]):
- """
- For each FINISHED task in the branch, find all project states where the task status changes. Additionally, the last task that will be returned is the one that is currently being worked on.
- Returns data formatted for the UI + the project states for history convo.
-
- :param session: The SQLAlchemy async session.
- :param branch_id: The UUID of the branch.
- :return: List of dicts with UI-friendly task conversation format.
- """
- query = select(ProjectState).where(
- and_(
- ProjectState.branch_id == branch_id,
- or_(ProjectState.action.like("%Task #%"), ProjectState.action.like("%Create a development plan%")),
- )
- )
- result = await session.execute(query)
- states = result.scalars().all()
-
- log.debug(f"Found {len(states)} states in branch")
-
- if not states:
- query = select(ProjectState).where(ProjectState.branch_id == branch_id)
- result = await session.execute(query)
- states = result.scalars().all()
-
- task_histories = []
-
- def find_task_history(task_id):
- for th in task_histories:
- if th["task_id"] == task_id:
- return th
- return None
-
- for state in states:
- for task in state.tasks or []:
- task_id = task.get("id")
- if not task_id:
- continue
-
- th = find_task_history(task_id)
- if not th:
- th = {
- "task_id": task_id,
- "title": task.get("description"),
- "labels": [],
- "status": task["status"],
- "start_id": state.id,
- "project_state_id": state.id,
- "end_id": state.id,
- }
- task_histories.append(th)
-
- if task.get("status") == TaskStatus.TODO:
- th["status"] = TaskStatus.TODO
- th["start_id"] = state.id
- th["project_state_id"] = state.id
- th["end_id"] = state.id
-
- elif task.get("status") != th["status"]:
- th["status"] = task.get("status")
- th["end_id"] = state.id
-
- epic_index, task_index = ProjectState.get_epic_task_number(state, task)
- th["labels"] = [
- f"E{str(epic_index)} / T{task_index}",
- "Backend",
- "Working"
- if task.get("status") in [TaskStatus.TODO, TaskStatus.IN_PROGRESS]
- else "Skipped"
- if task.get("status") == TaskStatus.SKIPPED
- else "Done",
- ]
-
- last_task = {}
-
- # todo/in_progress can override done
- # done can override todo/in_progress
- # todo/in_progress can not override todo/in_progress
-
- for th in task_histories:
- if not last_task:
- last_task = th
-
- # if we have multiple tasks being Worked on (todo state) in a row, then we take the first one
- # if we see a Done task, we take that one
- if not (
- last_task["status"] in [TaskStatus.TODO, TaskStatus.IN_PROGRESS]
- and th["status"] in [TaskStatus.TODO, TaskStatus.IN_PROGRESS]
- ):
- last_task = th
-
- if task_histories and last_task:
- task_histories = task_histories[: task_histories.index(last_task) + 1]
-
- if last_task:
- project_states = await ProjectState.get_task_conversation_project_states(
- session, branch_id, UUID(last_task["task_id"])
- )
- if project_states:
- last_task["start_id"] = project_states[0].id
- last_task["project_state_id"] = project_states[0].id
- last_task["end_id"] = project_states[-1].id
- return task_histories, last_task
diff --git a/core/db/models/specification.py b/core/db/models/specification.py
index b0411b343..7631fdeba 100644
--- a/core/db/models/specification.py
+++ b/core/db/models/specification.py
@@ -69,20 +69,3 @@ async def delete_orphans(cls, session: AsyncSession):
await session.execute(
delete(Specification).where(~Specification.id.in_(select(distinct(ProjectState.specification_id))))
)
-
- @staticmethod
- async def update_specification(session: AsyncSession, specification: "Specification") -> Optional["Specification"]:
- """
- Update the specification in the database.
-
- :param session: The database session.
- :param specification: The Specification object to update.
- :return: The updated Specification object or None if not found.
- """
- existing_spec = await session.get(Specification, specification.id)
- if existing_spec:
- for key, value in specification.__dict__.items():
- setattr(existing_spec, key, value)
- await session.commit()
- return existing_spec
- return None
diff --git a/core/db/models/user_input.py b/core/db/models/user_input.py
index d68012b1c..c068143db 100644
--- a/core/db/models/user_input.py
+++ b/core/db/models/user_input.py
@@ -2,8 +2,7 @@
from typing import TYPE_CHECKING, Optional
from uuid import UUID
-from sqlalchemy import ForeignKey, and_, delete, inspect, select
-from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import ForeignKey, inspect
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
@@ -58,24 +57,3 @@ def from_user_input(cls, project_state: "ProjectState", question: str, user_inpu
)
session.add(obj)
return obj
-
- @staticmethod
- async def find_user_inputs(session: AsyncSession, project_state, branch_id) -> Optional[list["UserInput"]]:
- from core.db.models import UserInput
-
- user_input = await session.execute(
- select(UserInput).where(
- and_(UserInput.branch_id == branch_id, UserInput.project_state_id == project_state.id)
- )
- )
- user_input = user_input.scalars().all()
- return user_input if len(user_input) > 0 else []
-
- @classmethod
- async def delete_orphans(cls, session: AsyncSession):
- """
- Delete UserInput objects that have no associated ProjectState.
-
- :param session: The database session.
- """
- await session.execute(delete(UserInput).where(UserInput.project_state_id.is_(None)))
diff --git a/core/db/session.py b/core/db/session.py
index 0b7db8019..3f2b68490 100644
--- a/core/db/session.py
+++ b/core/db/session.py
@@ -1,3 +1,5 @@
+import time
+
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
@@ -18,7 +20,7 @@ class SessionManager:
... # Do something with the session
"""
- def __init__(self, config: DBConfig, args=None):
+ def __init__(self, config: DBConfig):
"""
Initialize the session manager with the given configuration.
@@ -31,9 +33,18 @@ def __init__(self, config: DBConfig, args=None):
self.SessionClass = async_sessionmaker(self.engine, expire_on_commit=False)
self.session = None
self.recursion_depth = 0
- email_is_pythagora = args is not None and args.email is not None and args.email.endswith("@pythagora.ai")
- self.save_llm_requests = config.save_llm_requests or email_is_pythagora
+
event.listen(self.engine.sync_engine, "connect", self._on_connect)
+ event.listen(self.engine.sync_engine, "before_cursor_execute", self.before_cursor_execute)
+ event.listen(self.engine.sync_engine, "after_cursor_execute", self.after_cursor_execute)
+
+ def before_cursor_execute(self, conn, cursor, statement, parameters, context, executemany):
+ conn.info.setdefault("query_start_time", []).append(time.time())
+ log.debug(f"Executing SQL: {statement}")
+
+ def after_cursor_execute(self, conn, cursor, statement, parameters, context, executemany):
+ total = time.time() - conn.info["query_start_time"].pop(-1)
+ log.debug(f"SQL execution time: {total:.3f} seconds")
def _on_connect(self, dbapi_connection, _):
"""Connection event handler"""
@@ -45,6 +56,7 @@ def _on_connect(self, dbapi_connection, _):
# it's a local file. PostgreSQL or other database use a real connection pool
# by default.
dbapi_connection.execute("pragma foreign_keys=on")
+ dbapi_connection.execute("PRAGMA journal_mode=WAL;")
async def start(self) -> AsyncSession:
if self.session is not None:
diff --git a/core/db/v0importer.py b/core/db/v0importer.py
index db02d6704..1ce377276 100644
--- a/core/db/v0importer.py
+++ b/core/db/v0importer.py
@@ -179,7 +179,7 @@ async def save_app(self, app_id: str, app_info: dict):
log.info(f"Importing app {app_info['name']} (id={app_id}) ...")
async with self.session_manager as session:
- project = Project(id=UUID(app_id), name=app_info["name"], project_type="node")
+ project = Project(id=UUID(app_id), name=app_info["name"])
branch = Branch(project=project)
state = ProjectState.create_initial_state(branch)
diff --git a/core/llm/anthropic_client.py b/core/llm/anthropic_client.py
index 796766dd1..501196657 100644
--- a/core/llm/anthropic_client.py
+++ b/core/llm/anthropic_client.py
@@ -1,7 +1,6 @@
-import asyncio
import datetime
import zoneinfo
-from typing import Optional, Tuple
+from typing import Optional
from anthropic import AsyncAnthropic, RateLimitError
from httpx import Timeout
@@ -19,10 +18,6 @@
MAX_TOKENS_SONNET = 8192
-class CustomAssertionError(Exception):
- pass
-
-
class AnthropicClient(BaseLLMClient):
provider = LLMProvider.ANTHROPIC
@@ -43,7 +38,7 @@ def _adapt_messages(self, convo: Convo) -> list[dict[str, str]]:
Adapt the conversation messages to the format expected by the Anthropic Claude model.
Claude only recognizes "user" and "assistant" roles, and requires them to be switched
- for each message (i.e. no consecutive messages from the same role).
+ for each message (ie. no consecutive messages from the same role).
:param convo: Conversation to adapt.
:return: Adapted conversation messages.
@@ -66,60 +61,50 @@ def _adapt_messages(self, convo: Convo) -> list[dict[str, str]]:
return messages
async def _make_request(
- self, convo: Convo, temperature: Optional[float] = None, json_mode: bool = False, retry_count: int = 1
- ) -> Tuple[str, int, int]:
- async def single_attempt() -> Tuple[str, int, int]:
- messages = self._adapt_messages(convo)
- completion_kwargs = {
- "max_tokens": MAX_TOKENS,
- "model": self.config.model,
- "messages": messages,
- "temperature": self.config.temperature if temperature is None else temperature,
- }
-
- if "trybricks" in self.config.base_url:
- completion_kwargs["extra_headers"] = {"x-request-timeout": f"{int(float(self.config.read_timeout))}s"}
-
- if "bedrock/anthropic" in self.config.base_url:
- completion_kwargs["extra_headers"] = {"anthropic-version": "bedrock-2023-05-31"}
-
- if "sonnet" in self.config.model:
- completion_kwargs["max_tokens"] = MAX_TOKENS_SONNET
-
- if json_mode:
- completion_kwargs["response_format"] = {"type": "json_object"}
-
- response = []
- async with self.client.messages.stream(**completion_kwargs) as stream:
- async for content in stream.text_stream:
- response.append(content)
- if self.stream_handler:
- await self.stream_handler(content)
-
- try:
- final_message = await stream.get_final_message()
- final_message.content # Access content to verify it exists
- except AssertionError:
- log.debug("Anthropic package AssertionError")
- raise CustomAssertionError("No final message received.")
-
- response_str = "".join(response)
-
- # Tell the stream handler we're done
- if self.stream_handler:
- await self.stream_handler(None)
-
- return response_str, final_message.usage.input_tokens, final_message.usage.output_tokens
-
- for attempt in range(retry_count + 1):
- try:
- return await single_attempt()
- except CustomAssertionError as e:
- if attempt == retry_count: # If this was our last attempt
- raise CustomAssertionError(f"Request failed after {retry_count + 1} attempts: {str(e)}")
- # Add a small delay before retrying
- await asyncio.sleep(1)
- continue
+ self,
+ convo: Convo,
+ temperature: Optional[float] = None,
+ json_mode: bool = False,
+ ) -> tuple[str, int, int]:
+ messages = self._adapt_messages(convo)
+ completion_kwargs = {
+ "max_tokens": MAX_TOKENS,
+ "model": self.config.model,
+ "messages": messages,
+ "temperature": self.config.temperature if temperature is None else temperature,
+ }
+
+ if "bedrock/anthropic" in self.config.base_url:
+ completion_kwargs["extra_headers"] = {"anthropic-version": "bedrock-2023-05-31"}
+
+ if "sonnet" in self.config.model:
+ if "extra_headers" in completion_kwargs:
+ completion_kwargs["extra_headers"]["anthropic-beta"] = "max-tokens-3-5-sonnet-2024-07-15"
+ else:
+ completion_kwargs["extra_headers"] = {"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"}
+ completion_kwargs["max_tokens"] = MAX_TOKENS_SONNET
+
+ if json_mode:
+ completion_kwargs["response_format"] = {"type": "json_object"}
+
+ response = []
+ async with self.client.messages.stream(**completion_kwargs) as stream:
+ async for content in stream.text_stream:
+ response.append(content)
+ if self.stream_handler:
+ await self.stream_handler(content)
+
+ # TODO: get tokens from the final message
+ final_message = await stream.get_final_message()
+ final_message.content
+
+ response_str = "".join(response)
+
+ # Tell the stream handler we're done
+ if self.stream_handler:
+ await self.stream_handler(None)
+
+ return response_str, final_message.usage.input_tokens, final_message.usage.output_tokens
def rate_limit_sleep(self, err: RateLimitError) -> Optional[datetime.timedelta]:
"""
diff --git a/core/llm/base.py b/core/llm/base.py
index ddf92803f..1c1143ffa 100644
--- a/core/llm/base.py
+++ b/core/llm/base.py
@@ -1,25 +1,18 @@
import asyncio
import datetime
import json
-import sys
from enum import Enum
from time import time
from typing import Any, Callable, Optional, Tuple
import httpx
-import tiktoken
-from httpx import AsyncClient
from core.config import LLMConfig, LLMProvider
from core.llm.convo import Convo
from core.llm.request_log import LLMRequestLog, LLMRequestStatus
from core.log import get_logger
-from core.state.state_manager import StateManager
-from core.ui.base import UIBase, pythagora_source
-from core.utils.text import trim_logs
log = get_logger(__name__)
-tokenizer = tiktoken.get_encoding("cl100k_base")
class LLMError(str, Enum):
@@ -55,11 +48,9 @@ class BaseLLMClient:
def __init__(
self,
config: LLMConfig,
- state_manager: StateManager,
*,
stream_handler: Optional[Callable] = None,
error_handler: Optional[Callable] = None,
- ui: Optional[UIBase] = None,
):
"""
Initialize the client with the given configuration.
@@ -70,8 +61,6 @@ def __init__(
self.config = config
self.stream_handler = stream_handler
self.error_handler = error_handler
- self.ui = ui
- self.state_manager = state_manager
self._init_client()
def _init_client(self):
@@ -164,27 +153,10 @@ async def __call__(
prompts=convo.prompt_log,
)
- prompt_tokens = sum(3 + len(tokenizer.encode(str(msg.get("content", "")))) for msg in convo.messages)
-
- index = -1
- if prompt_tokens > 150_000:
- for i, msg in enumerate(reversed(convo.messages)):
- if "Here are the backend logs" in msg["content"] or "Here are the frontend logs" in msg["content"]:
- index = len(convo.messages) - 1 - i
- break
-
- if index != -1:
- for i, msg in enumerate(convo.messages):
- if i < index:
- convo.messages[i]["content"] = trim_logs(convo.messages[i]["content"])
- else:
- break
-
prompt_length_kb = len(json.dumps(convo.messages).encode("utf-8")) / 1024
log.debug(
- f"Calling {self.provider.value} model {self.config.model} (temp={temperature}), prompt length: {prompt_length_kb:.1f} KB, prompt tokens (approx.): {prompt_tokens:.1f}"
+ f"Calling {self.provider.value} model {self.config.model} (temp={temperature}), prompt length: {prompt_length_kb:.1f} KB"
)
-
t0 = time()
remaining_retries = max_retries
@@ -214,40 +186,6 @@ async def __call__(
response = None
try:
- access_token = self.state_manager.get_access_token()
-
- if access_token:
- # Store the original client
- original_client = self.client
-
- # Copy client based on its type
- if isinstance(original_client, openai.AsyncOpenAI):
- self.client = openai.AsyncOpenAI(
- api_key=original_client.api_key,
- base_url=original_client.base_url,
- timeout=original_client.timeout,
- default_headers={
- "Authorization": f"Bearer {access_token}",
- "Timeout": str(max(self.config.connect_timeout, self.config.read_timeout)),
- },
- )
- elif isinstance(original_client, anthropic.AsyncAnthropic):
- # Create new Anthropic client with custom headers
- self.client = anthropic.AsyncAnthropic(
- api_key=original_client.api_key,
- base_url=original_client.base_url,
- timeout=original_client.timeout,
- default_headers={
- "Authorization": f"Bearer {access_token}",
- "Timeout": str(max(self.config.connect_timeout, self.config.read_timeout)),
- },
- )
- elif isinstance(original_client, AsyncClient):
- self.client = AsyncClient()
- else:
- # Handle other client types or raise exception
- raise ValueError(f"Unsupported client type: {type(original_client)}")
-
response, prompt_tokens, completion_tokens = await self._make_request(
convo,
temperature=temperature,
@@ -306,44 +244,6 @@ async def __call__(
# so we can't be certain that's the problem in Anthropic case.
# Here we try to detect that and tell the user what happened.
log.info(f"API status error: {err}")
- if getattr(err, "status_code", None) in (401, 403):
- if self.ui:
- try:
- await self.ui.send_message("Token expired")
- sys.exit(0)
- # TODO implement this to not crash in parallel
- # access_token = await self.ui.send_token_expired()
- # self.state_manager.update_access_token(access_token)
- # continue
- except Exception:
- raise APIError("Token expired")
-
- if getattr(err, "status_code", None) == 400 and getattr(err, "message", None) == "not_enough_tokens":
- if self.ui:
- try:
- await self.ui.ask_question(
- "",
- buttons={},
- buttons_only=True,
- extra_info={"not_enough_tokens": True},
- source=pythagora_source,
- )
- sys.exit(0)
- # TODO implement this to not crash in parallel
- # user_response = await self.ui.ask_question(
- # 'Not enough tokens left, please top up your account and press "Continue".',
- # buttons={"continue": "Continue", "exit": "Exit"},
- # buttons_only=True,
- # extra_info={"not_enough_tokens": True},
- # source=pythagora_source,
- # )
- # if user_response.button == "continue":
- # continue
- # else:
- # raise APIError("Not enough tokens left")
- except Exception:
- raise APIError("Not enough tokens left")
-
try:
if hasattr(err, "response"):
if err.response.headers.get("Content-Type", "").startswith("application/json"):
@@ -363,7 +263,7 @@ async def __call__(
[
"We sent too large request to the LLM, resulting in an error. ",
"This is usually caused by including framework files in an LLM request. ",
- "Here's how you can get Pythagora to ignore those extra files: ",
+ "Here's how you can get GPT Pilot to ignore those extra files: ",
"https://bit.ly/faq-token-limit-error",
]
)
@@ -394,10 +294,7 @@ async def __call__(
request_log.error = f"Error parsing response: {err}"
request_log.status = LLMRequestStatus.ERROR
log.debug(f"Error parsing LLM response: {err}, asking LLM to retry", exc_info=True)
- if response:
- convo.assistant(response)
- else:
- convo.assistant(".")
+ convo.assistant(response)
convo.user(f"Error parsing response: {err}. Please output your response EXACTLY as requested.")
continue
else:
@@ -412,6 +309,19 @@ async def __call__(
return response, request_log
+ async def api_check(self) -> bool:
+ """
+ Perform an LLM API check.
+
+ :return: True if the check was successful, False otherwise.
+ """
+
+ convo = Convo()
+ msg = "This is a connection test. If you can see this, please respond only with 'START' and nothing else."
+ convo.user(msg)
+ resp, _log = await self(convo)
+ return bool(resp)
+
@staticmethod
def for_provider(provider: LLMProvider) -> type["BaseLLMClient"]:
"""
@@ -424,12 +334,9 @@ def for_provider(provider: LLMProvider) -> type["BaseLLMClient"]:
from .azure_client import AzureClient
from .groq_client import GroqClient
from .openai_client import OpenAIClient
- from .relace_client import RelaceClient
if provider == LLMProvider.OPENAI:
return OpenAIClient
- elif provider == LLMProvider.RELACE:
- return RelaceClient
elif provider == LLMProvider.ANTHROPIC:
return AnthropicClient
elif provider == LLMProvider.GROQ:
diff --git a/core/llm/parser.py b/core/llm/parser.py
index e23306901..a41f88e0d 100644
--- a/core/llm/parser.py
+++ b/core/llm/parser.py
@@ -1,62 +1,11 @@
import json
import re
from enum import Enum
-from typing import List, Optional, Union
+from typing import Optional, Union
from pydantic import BaseModel, ValidationError, create_model
-class CodeBlock(BaseModel):
- description: str
- content: str
-
-
-class ParsedBlocks(BaseModel):
- original_response: str
- blocks: List[CodeBlock]
-
-
-class DescriptiveCodeBlockParser:
- """
- Parse Markdown code blocks with their descriptions from a string.
- Returns both the original response and structured data about each block.
-
- Each block entry contains:
- - description: The text line immediately preceding the code block
- - content: The actual content of the code block
-
- Example usage:
- >>> parser = DescriptiveCodeBlockParser()
- >>> text = '''file: next.config.js
- ... ```js
- ... module.exports = {
- ... reactStrictMode: true,
- ... };
- ... ```'''
- >>> result = parser(text)
- >>> assert result.blocks[0].description == "file: next.config.js"
- """
-
- def __init__(self):
- self.pattern = re.compile(r"^(.*?)\n```([a-z0-9]+\n)?(.*?)^```\s*", re.DOTALL | re.MULTILINE)
-
- def __call__(self, text: str) -> ParsedBlocks:
- # Store original response
- original_response = text.strip()
-
- # Find all blocks with their preceding text
- blocks = []
- for match in self.pattern.finditer(text):
- description = match.group(1).strip()
- content = match.group(3).strip()
-
- # Only add block if we have both description and content
- if description and content:
- blocks.append(CodeBlock(description=description, content=content))
-
- return ParsedBlocks(original_response=original_response, blocks=blocks)
-
-
class MultiCodeBlockParser:
"""
Parse multiple Markdown code blocks from a string.
@@ -120,17 +69,16 @@ def __call__(self, text: str) -> str:
return blocks[0]
-class OptionalCodeBlockParser:
+class OptionalCodeBlockParser(MultiCodeBlockParser):
def __call__(self, text: str) -> str:
- text = text.strip()
- if text.startswith("```") and text.endswith("\n```"):
- # Remove the first and last line. Note the first line may include syntax
- # highlighting, so we can't just remove the first 3 characters.
- text = "\n".join(text.splitlines()[1:-1]).strip()
- elif "\n" not in text and text.startswith("`") and text.endswith("`"):
- # Single-line code blocks are wrapped in single backticks
- text = text[1:-1]
- return text
+ blocks = super().__call__(text)
+ # FIXME: if there are more than 1 code block, this means the output actually contains ```,
+ # so re-parse this with that in mind
+ if len(blocks) > 1:
+ raise ValueError(f"Expected a single code block, got {len(blocks)}")
+ if len(blocks) == 0:
+ return text.strip()
+ return blocks[0]
class JSONParser:
diff --git a/core/llm/relace_client.py b/core/llm/relace_client.py
deleted file mode 100644
index 02908a40e..000000000
--- a/core/llm/relace_client.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from typing import Optional
-
-import httpx
-from httpx import AsyncClient
-
-from core.config import LLMProvider
-from core.llm.base import BaseLLMClient
-from core.llm.convo import Convo
-from core.log import get_logger
-
-log = get_logger(__name__)
-
-
-class RelaceClient(BaseLLMClient):
- provider = LLMProvider.RELACE
-
- def _init_client(self):
- self.headers = {
- "Content-Type": "application/json",
- "Authorization": f"Bearer {self.state_manager.get_access_token() if self.state_manager.get_access_token() is not None else self.config.api_key if self.config.api_key is not None else ''}",
- }
- self.client = AsyncClient()
-
- async def _make_request(
- self,
- convo: Convo,
- temperature: Optional[float] = None,
- json_mode: bool = False,
- ) -> tuple[str, int, int]:
- """
- Make a POST request to the Relace API to merge code snippets.
-
- :param convo: Conversation object containing initial code and edit snippet.
- :param temperature: Not used in this implementation.
- :param json_mode: Not used in this implementation.
- :return: Merged code, input tokens (0), and output tokens (0).
- """
- data = {
- "initialCode": convo.messages[0]["content"]["initialCode"],
- "editSnippet": convo.messages[0]["content"]["editSnippet"],
- "model": self.config.model,
- }
-
- async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport()) as client:
- try:
- response = await client.post(
- "https://api.pythagora.io/v1/relace/merge", headers=self.headers, json=data
- )
- response.raise_for_status()
- response_json = response.json()
- return (
- response_json.get("content", ""),
- response_json.get("inputTokens", 0),
- response_json.get("outputTokens", 0),
- )
- except Exception as e:
- # Fall back to other ai provider
- log.debug(f"Relace API request failed: {e}")
- return ("", 0, 0)
-
-
-__all__ = ["RelaceClient"]
diff --git a/core/log/__init__.py b/core/log/__init__.py
index 4737cdf36..67eb2ffcc 100644
--- a/core/log/__init__.py
+++ b/core/log/__init__.py
@@ -1,65 +1,6 @@
-import os
-from collections import deque
from logging import FileHandler, Formatter, Logger, StreamHandler, getLogger
from core.config import LogConfig
-from core.config.constants import LOGS_LINE_LIMIT
-
-
-class LineCountLimitedFileHandler(FileHandler):
- """
- A file handler that limits the number of lines in the log file.
- It keeps a fixed number of the most recent log lines.
- """
-
- def __init__(self, filename, max_lines=LOGS_LINE_LIMIT, mode="a", encoding=None, delay=False):
- """
- Initialize the handler with the file and max lines.
-
- :param filename: Log file path
- :param max_lines: Maximum number of lines to keep in the file
- :param mode: File open mode
- :param encoding: File encoding
- :param delay: Delay file opening until first emit
- """
- super().__init__(filename, mode, encoding, delay)
- self.max_lines = max_lines
- self.line_buffer = deque(maxlen=max_lines)
- self._load_existing_lines()
-
- def _load_existing_lines(self):
- """Load existing lines from the file into the buffer if the file exists."""
- if os.path.exists(self.baseFilename):
- try:
- with open(self.baseFilename, "r", encoding=self.encoding) as f:
- for line in f:
- if len(self.line_buffer) < self.max_lines:
- self.line_buffer.append(line)
- else:
- self.line_buffer.popleft()
- self.line_buffer.append(line)
- except Exception:
- # If there's an error reading the file, we'll just start with an empty buffer
- self.line_buffer.clear()
-
- def emit(self, record):
- """
- Emit a record and maintain the line count limit.
-
- :param record: Log record to emit
- """
- try:
- msg = self.format(record)
- line = msg + self.terminator
- self.line_buffer.append(line)
-
- # Rewrite the entire file with the current buffer
- with open(self.baseFilename, "w", encoding=self.encoding) as f:
- f.writelines(self.line_buffer)
-
- self.flush()
- except Exception:
- self.handleError(record)
def setup(config: LogConfig, force: bool = False):
@@ -86,9 +27,7 @@ def setup(config: LogConfig, force: bool = False):
formatter = Formatter(config.format)
if config.output:
- # Use our custom handler that limits line count
- max_lines = getattr(config, "max_lines", LOGS_LINE_LIMIT)
- handler = LineCountLimitedFileHandler(config.output, max_lines=max_lines, encoding="utf-8")
+ handler = FileHandler(config.output, encoding="utf-8")
else:
handler = StreamHandler()
diff --git a/core/proc/process_manager.py b/core/proc/process_manager.py
index 45ad34cf0..bd3026f9f 100644
--- a/core/proc/process_manager.py
+++ b/core/proc/process_manager.py
@@ -30,7 +30,6 @@ class LocalProcess:
stdout: str
stderr: str
_process: asyncio.subprocess.Process
- show_output: bool
def __hash__(self) -> int:
return hash(self.id)
@@ -42,7 +41,6 @@ async def start(
cwd: str = ".",
env: dict[str, str],
bg: bool = False,
- show_output: Optional[bool] = True,
) -> "LocalProcess":
log.debug(f"Starting process: {cmd} (cwd={cwd})")
_process = await asyncio.create_subprocess_shell(
@@ -58,7 +56,13 @@ async def start(
_process.stdin.close()
return LocalProcess(
- id=uuid4(), cmd=cmd, cwd=cwd, env=env, stdout="", stderr="", _process=_process, show_output=show_output
+ id=uuid4(),
+ cmd=cmd,
+ cwd=cwd,
+ env=env,
+ stdout="",
+ stderr="",
+ _process=_process,
)
async def wait(self, timeout: Optional[float] = None) -> int:
@@ -186,7 +190,7 @@ async def watcher(self):
for process in procs:
out, err = await process.read_output()
- if process.show_output and self.output_handler and (out or err):
+ if self.output_handler and (out or err):
await self.output_handler(out, err)
if not process.is_running:
@@ -206,11 +210,10 @@ async def start_process(
cwd: str = ".",
env: Optional[dict[str, str]] = None,
bg: bool = True,
- show_output: Optional[bool] = True,
) -> LocalProcess:
env = {**self.default_env, **(env or {})}
abs_cwd = abspath(join(self.root_dir, cwd))
- process = await LocalProcess.start(cmd, cwd=abs_cwd, env=env, bg=bg, show_output=show_output)
+ process = await LocalProcess.start(cmd, cwd=abs_cwd, env=env, bg=bg)
if bg:
self.processes[process.id] = process
return process
@@ -222,7 +225,6 @@ async def run_command(
cwd: str = ".",
env: Optional[dict[str, str]] = None,
timeout: float = MAX_COMMAND_TIMEOUT,
- show_output: Optional[bool] = True,
) -> tuple[Optional[int], str, str]:
"""
Run command and wait for it to finish.
@@ -234,7 +236,6 @@ async def run_command(
:param cwd: Working directory.
:param env: Environment variables.
:param timeout: Timeout in seconds.
- :param show_output: Show output in the ui.
:return: Tuple of (status code, stdout, stderr).
"""
timeout = min(timeout, MAX_COMMAND_TIMEOUT)
@@ -244,7 +245,7 @@ async def run_command(
t0 = time.time()
while process.is_running and (time.time() - t0) < timeout:
out, err = await process.read_output(BUSY_WAIT_INTERVAL)
- if self.output_handler and (out or err) and show_output:
+ if self.output_handler and (out or err):
await self.output_handler(out, err)
if process.is_running:
@@ -255,7 +256,7 @@ async def run_command(
await process.wait()
out, err = await process.read_output()
- if self.output_handler and (out or err) and show_output:
+ if self.output_handler and (out or err):
await self.output_handler(out, err)
if terminated:
diff --git a/core/prompts/bug-hunter/iteration.prompt b/core/prompts/bug-hunter/iteration.prompt
index e93106651..91b19daff 100644
--- a/core/prompts/bug-hunter/iteration.prompt
+++ b/core/prompts/bug-hunter/iteration.prompt
@@ -20,14 +20,10 @@ A part of the app is already finished.
{% include "partials/user_feedback.prompt" %}
-{% if test_instructions %}
+{% if current_task.test_instructions is defined %}
Here are the test instructions the user was following when the issue occurred:
```
-{% for step in test_instructions %}
-Step #{{ loop.index }}
-Action: {{ step.action }}
-Expected result: {{ step.result }}
-{% endfor %}
+{{ current_task.test_instructions }}
```
{% endif %}
@@ -42,7 +38,4 @@ Based on this information, you need to figure out where is the problem that the
If you think we should add more logs around the code to better understand the problem, tell me code snippets in which we should add the logs. If you think you know where the issue is, don't add any new logs but explain what log print tell point you to the problem, what the problem is, what is the solution to this problem and how the solution will fix the problem. What is your answer?
**IMPORTANT**
-If you want code to be written, write **ALL NEW CODE** that needs to be written. If you want to create a new file, write the entire content of that file and if you want to update an existing file, write the new code that needs to be written/updated. You cannot answer with "Ensure that...", "Make sure that...", etc. In these cases, explain how should the reader of your message ensure what you want them to ensure. In most cases, they will need to add some logs to ensure something in which case tell them where to add them.
-
-** IMPORTANT - labels around code **
-Always address code that needs to be changed by files and add labels and around changes for a specific file. (in this case client/src/api/api.ts) - you can mention multiple changes for a single file but never mix changes for multiple files in a single block. Never use any other markers around the code like backticks.
+You cannot answer with "Ensure that...", "Make sure that...", etc. In these cases, explain how should the reader of your message ensure what you want them to ensure. In most cases, they will need to add some logs to ensure something in which case tell them where to add them.
diff --git a/core/prompts/bug-hunter/log_data.prompt b/core/prompts/bug-hunter/log_data.prompt
index a497236b2..511081fd7 100644
--- a/core/prompts/bug-hunter/log_data.prompt
+++ b/core/prompts/bug-hunter/log_data.prompt
@@ -1,14 +1,13 @@
-{% if backend_logs and backend_logs|trim %}
-Here are the logs we added to the backend:
+{% if backend_logs is not none %}Here are the logs we added to the backend:
```
{{ backend_logs }}
```
-{% endif %}{% if frontend_logs and frontend_logs|trim %}
+{% endif %}{% if frontend_logs is not none %}
Here are the logs we added to the frontend:
```
{{ frontend_logs }}
```
-{% endif %}{% if user_feedback and user_feedback|trim %}
+{% endif %}{% if user_feedback is not none %}
Finally, here is a hint from a human who tested the app:
```
{{ user_feedback }}
@@ -17,9 +16,4 @@ When you're thinking about what to do next, take into the account human's feedba
{% endif %}{% if fix_attempted %}
The problem wasn't solved with the last changes. You have 2 options - to tell me exactly where is the problem happening or to add more logs to better determine where is the problem. If you think we should add more logs around the code to better understand the problem, tell me code snippets in which we should add the logs. If you think you know where the issue is, don't add any new logs but explain what log print tell point you to the problem, what the problem is, what is the solution to this problem and how the solution will fix the problem. What is your answer? Make sure not to repeat mistakes from before that didn't work.
{% endif %}
-{% if not (backend_logs and backend_logs|trim) and
- not (frontend_logs and frontend_logs|trim) and
- not (user_feedback and user_feedback|trim) and
- not fix_attempted %}
-Human didn't supply any data
-{% endif %}
+{% if backend_logs is none and frontend_logs is none and user_feedback is none and fix_attempted == false %}Human didn't supply any data{% endif %}
diff --git a/core/prompts/chat-agent/chat.prompt b/core/prompts/chat-agent/chat.prompt
deleted file mode 100644
index fe5f0e4ef..000000000
--- a/core/prompts/chat-agent/chat.prompt
+++ /dev/null
@@ -1,35 +0,0 @@
-This is the description of the app you are working with:
-{{ initial_description }}
-
-{% if task_description is defined and task_description %}
-Currently, you are working on the following task:
-{{ task_description }}
-{% endif %}
-
-{% if bug_hunt_cycle_user_feedback is defined and bug_hunt_cycle_user_feedback %}
-You are currently working on a bug report that the user reported like this:
-{{ bug_hunt_cycle_user_feedback }}
-{% endif %}
-
-{% if testing_instructions is defined and testing_instructions %}
-The user is currently testing how the task was implemented and was given the following instructions to test the app:
-{{ testing_instructions }}
-{% endif %}
-
-{% if command_run is defined and command_run %}
-Currently, you're waiting for the user to approve the following command to be run:
-```text
-{{ command_run }}
-```
-{% endif %}
-
-{% if human_intervention is defined and human_intervention %}
-Currently, you asked the human user to do the following steps in order to continue with the development of the app:
-```text
-{{ human_intervention }}
-```
-{% endif %}
-
-{% if user_input is defined and user_input %}
-Now, the human asked said the following: `{{ user_input }}`. Please respond as a professional developer, be helpful and focus on what the human said.
-{% endif %}
diff --git a/core/prompts/chat-agent/system.prompt b/core/prompts/chat-agent/system.prompt
deleted file mode 100644
index 3cc4d4aff..000000000
--- a/core/prompts/chat-agent/system.prompt
+++ /dev/null
@@ -1,5 +0,0 @@
-You are a world class full stack software developer working in a team.
-
-You write modular, well-organized code split across files that are not too big, so that the codebase is maintainable. You include proper error handling and logging for your clean, readable, production-level quality code.
-
-Your job is to implement tasks assigned by your tech lead, following task implementation instructions.
diff --git a/core/prompts/code-monkey/describe_file.prompt b/core/prompts/code-monkey/describe_file.prompt
index e944e74ca..6a8c1d372 100644
--- a/core/prompts/code-monkey/describe_file.prompt
+++ b/core/prompts/code-monkey/describe_file.prompt
@@ -2,7 +2,7 @@ Your task is to explain the functionality implemented by a particular source cod
Given a file path and file contents, your output should contain:
-* a detailed explanation of what the file is about, max 30 words;
+* a detailed explanation of what the file is about;
* a list of all other files referenced (imported) from this file. note that some libraries, frameworks or libraries assume file extension and don't use it explicitly. For example, "import foo" in Python references "foo.py" without specifying the extension. In your response, use the complete file name including the implied extension (for example "foo.py", not just "foo").
Please analyze file `{{ path }}`, which contains the following content:
diff --git a/core/prompts/code-monkey/implement_changes.prompt b/core/prompts/code-monkey/implement_changes.prompt
index 14f92d0b8..fcd1d3aee 100644
--- a/core/prompts/code-monkey/implement_changes.prompt
+++ b/core/prompts/code-monkey/implement_changes.prompt
@@ -1,20 +1,22 @@
-{% if file_content %}
You are working on a project and your job is to implement new code changes based on given instructions.
Now you have to implement ALL changes that are related to `{{ file_name }}` described in development instructions listed below.
Make sure you don't make any mistakes, especially ones that could affect rest of project. Your changes will be reviewed by very detailed reviewer. Because of that, it is extremely important that you are STRICTLY following ALL the following rules while implementing changes:
-{% else %}
-You are working on a project and your job is to create a new file `{{ file_name }}` based on given instructions. The file should be thoroughly described in the development instructions listed below. You need to follow the coding rules that will be listed below, read the development instructions and respond with the full contents of the file `{{ file_name }}`.
-{% endif %}
+
{% include "partials/coding_rules.prompt" %}
+You are currently working on this task:
+```
+{{ state.current_task.description }}
+```
+
{% include "partials/user_feedback.prompt" %}
-Here are development instructions that were sent to you by a senior developer that you need to carefully follow. Focus only on the code changes for the file `{{ file_name }}`:
-~~~START_OF_DEVELOPMENT_INSTRUCTIONS~~~
+Here are development instructions and now you have to focus only on changes in `{{ file_name }}`:
+---start_of_development_instructions---
{{ instructions }}
-~~~END_OF_DEVELOPMENT_INSTRUCTIONS~~~
+---end_of_development_instructions---
{% if rework_feedback is defined %}
You previously made changes to file `{{ file_name }}` but not all changes were accepted, and the reviewer provided feedback on the changes that you must rework:
@@ -26,15 +28,12 @@ The reviewer accepted some of your changes, and the file now looks like this:
{{ file_content }}
```
{% elif file_content %}
-Now, take a look at how `{{ file_name }}` looks like currently:
+Here is how `{{ file_name }}` looks like currently:
```
{{ file_content }}
```
-
-Ok, now, you have to follow the instructions about `{{ file_name }}` from the development instructions carefully. Reply **ONLY** with the full contents of the file `{{ file_name }}` and nothing else. Do not make any changes to the file that are not mentioned in the development instructions - you must **STRICTLY** follow the instructions.
{% else %}
-You need to create a new file `{{ file_name }}` so respond **ONLY** with the full contents of that file from the development instructions that you read.
+You need to create a new file `{{ file_name }}`.
{% endif %}
-** IMPORTANT **
-Remember, you must **NOT** add anything in your response that is not strictly the code from the file. Do not start or end the response with an explanation or a comment - you must respond with only the code from the file because your response will be directly saved to a file and run.
+{% include "partials/files_list.prompt" %}
diff --git a/core/prompts/developer/breakdown.prompt b/core/prompts/developer/breakdown.prompt
index e905583e6..86ae77956 100644
--- a/core/prompts/developer/breakdown.prompt
+++ b/core/prompts/developer/breakdown.prompt
@@ -1,4 +1,4 @@
-You are working on an app called "{{ state.branch.project.name }}" and you are a primary developer who needs to write and maintain the code for this app.You are currently working on the implementation of one task that I will tell you below. Before that, here is the context of the app you're working on. Each section of the context starts with `~~SECTION_NAME~~` and ends with ~~END_OF_SECTION_NAME~~`.
+You are working on an app called "{{ state.branch.project.name }}" and you need to write code for the entire {% if state.epics|length > 1 %}feature{% else %}app{% endif %} based on the tasks that the tech lead gives you. So that you understand better what you're working on, you're given other specs for "{{ state.branch.project.name }}" as well.
{% include "partials/project_details.prompt" %}
{% include "partials/features_list.prompt" %}
@@ -10,7 +10,6 @@ You are working on an app called "{{ state.branch.project.name }}" and you are a
**IMPORTANT**
Remember, I created an empty folder where I will start writing files that you tell me and that are needed for this app.
{% endif %}
-~~DEVELOPMENT_INSTRUCTIONS~~
{% include "partials/relative_paths.prompt" %}
DO NOT specify commands to create any folders or files, they will be created automatically - just specify the relative path to each file that needs to be written.
@@ -21,50 +20,20 @@ DO NOT specify commands to create any folders or files, they will be created aut
{% include "partials/file_size_limit.prompt" %}
{% include "partials/breakdown_code_instructions.prompt" %}
-{% if state.has_frontend() %}
-The entire backend API needs to be on /api/... routes!
+Never use the port 5000 to run the app, it's reserved.
-** IMPORTANT - Mocking API requests **
-Frontend side is making requests to the backend by calling functions that are defined in the folder client/api/. During the frontend implementation, some API requests were mocked with dummy data that is defined in this folder and the API response data structure is defined in a comment above each API calling function. Whenever you need to implement an API endpoint, you must first find the function on the frontend that should call that API, remove the mocked data and make sure that the API call is properly done and that the response is parsed in a proper way. Whenever you do this, make sure to tell me explicitly which API calling function is being changed and what will be the response from the API.
-Whenever you add an API request from the frontend, make sure to wrap the request in try/catch block and in the catch block, return `throw new Error(error?.response?.data?.error || error.message);` - in the place where the API request function is being called, show a toast message with an error.
-{% endif %}
-
-** IMPORTANT - current implementation **
-Pay close attention to the currently implemented files, and DO NOT tell me to implement something that is already implemented. Similarly, do not change the current implementation if you think it is working correctly. It is not necessary for you to change files - you can leave the files as they are and just tell me that they are correctly implemented.
-
-** IMPORTANT - labels around code **
-Always address code that needs to be changed by files and add labels and around changes for a specific file. (in this case client/src/api/api.ts) - you can mention multiple changes for a single file but never mix changes for multiple files in a single block. Never use any other markers around the code like backticks.
-~~END_OF_DEVELOPMENT_INSTRUCTIONS~~
-
-~~DEVELOPMENT_PLAN~~
+--IMPLEMENTATION INSTRUCTIONS--
We've broken the development of this {% if state.epics|length > 1 %}feature{% else %}app{% endif %} down to these tasks:
```
{% for task in state.tasks %}
-{{ loop.index }}. {{ task.description }} {% if task.get("status") == "done" %} (completed) {% endif %}
-
+{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
{% endfor %}
```
-~~END_OF_DEVELOPMENT_PLAN~~
You are currently working on task #{{ current_task_index + 1 }} with the following description:
```
{{ task.description }}
-
-{% if redo_task_user_feedback is defined and redo_task_user_feedback %}
-You tried implementing this task before but you were unsuccessful. Here is what a human developer told you about what to watch out for while you're implementing this task so you don't make the same mistakes again:
----START_OF_USER_FEEDBACK---
-{{ redo_task_user_feedback }}
----END_OF_USER_FEEDBACK---
-{% endif %}
-
```
-{% if related_api_endpoints|length > 0 %}
-In this task, you need to focus on implementing the following endpoints:{% for api in related_api_endpoints %}{{ "`" ~ api.endpoint ~ "`" }}{% if not loop.last %},{% endif %}{% endfor %}
-
-
-{% endif %}
-You must implement the backend API endpoints, remove the mocked that on the frontend side, and replace it with the real API request, implement the database model (if it's not implemented already), and implement the utility function (eg. 3rd party integration) that is needed for this endpoint.
-
{% if task.get('pre_breakdown_testing_instructions') is not none %}
Here is how this task should be tested:
```
@@ -73,4 +42,4 @@ Here is how this task should be tested:
{% if current_task_index != 0 %}All previous tasks are finished and you don't have to work on them.{% endif %}
-Now, start by writing up what needs to be implemented to get this task working. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to implement ONLY this task and have it fully working and all commands that need to be run to implement this task. Also, add meaningful logs (not too many, just enough) around the created code to help with debugging.
+Now, start by writing up what needs to be implemented to get this task working. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to implement ONLY this task and have it fully working and all commands that need to be run to implement this task.
diff --git a/core/prompts/developer/filter_files.prompt b/core/prompts/developer/filter_files.prompt
index 605a4d39a..264a244a7 100644
--- a/core/prompts/developer/filter_files.prompt
+++ b/core/prompts/developer/filter_files.prompt
@@ -1,27 +1,38 @@
-{% if state.current_task %}
+We're starting work on a new task for a project we're working on.
+
+{% include "partials/project_details.prompt" %}
+{% include "partials/features_list.prompt" %}
+
+We've broken the development of the project down to these tasks:
+```
+{% for task in state.tasks %}
+{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
+
+{% endfor %}
+```
+
The next task we need to work on, and have to focus on, is this task:
```
{{ state.current_task.description }}
```
-{% endif %}
{% if user_feedback %}User who was using the app sent you this feedback:
```
{{ user_feedback }}
```
-{% endif %}
-
-{% if solution_description %}
+{% endif %}{% if solution_description %}
Focus on solving this issue in the following way:
```
{{ solution_description }}
```
{% endif %}
+{% include "partials/files_descriptions.prompt" %}
+
**IMPORTANT**
The files necessary for a developer to understand, modify, implement, and test the current task are considered to be relevant files.
-Your job is select which of existing files below are relevant for the current task. You have to select ALL files that are relevant to the current task. Think step by step of everything that has to be done in this task and which files contain needed information.
+Your job is select which of existing files are relevant for the current task. From the above list of files that app currently contains, you have to select ALL files that are relevant to the current task. Think step by step of everything that has to be done in this task and which files contain needed information.
-{% include "partials/files_descriptions.prompt" %}
+{% include "partials/filter_files_actions.prompt" %}
{% include "partials/relative_paths.prompt" %}
diff --git a/core/prompts/developer/filter_files_loop.prompt b/core/prompts/developer/filter_files_loop.prompt
new file mode 100644
index 000000000..a2da3c5be
--- /dev/null
+++ b/core/prompts/developer/filter_files_loop.prompt
@@ -0,0 +1,13 @@
+{% if read_files %}
+Here are the files that you wanted to read:
+---START_OF_FILES---
+{% for file in read_files %}
+File **`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
+```
+{{ file.content.content }}```
+
+{% endfor %}
+---END_OF_FILES---
+{% endif %}
+
+{% include "partials/filter_files_actions.prompt" %}
diff --git a/core/prompts/developer/parse_task.prompt b/core/prompts/developer/parse_task.prompt
index 5d7ba4610..68511ee02 100644
--- a/core/prompts/developer/parse_task.prompt
+++ b/core/prompts/developer/parse_task.prompt
@@ -1,26 +1,21 @@
-For the implementation instructions defined below, create a list of actionable steps that will be executed by a machine.
+Ok, now, take your response and convert it to a list of actionable steps that will be executed by a machine.
+Analyze the entire message, think step by step and make sure that you don't omit any information
+when converting this message to steps.
-~~~IMPLEMENTATION_INSTRUCTIONS~~~
-{{ implementation_instructions }}
-~~~END_OF_IMPLEMENTATION_INSTRUCTIONS~~~
+Each step can be either:
-Each actionable step can be either:
+* `command` - command to run (must be able to run on a {{ os }} machine, assume current working directory is project root folder)
+* `save_file` - create or update ONE file (only provide file path, not contents)
+* `human_intervention` - if you need the human to do something, use this type of step and explain in details what you want the human to do. NEVER use `human_intervention` for testing, as testing will be done separately by a dedicated QA after all the steps are done. Also you MUST NOT use `human_intervention` to ask the human to write or review code.
-* `command`
- - command to run
- - assume current working directory is project root folder, which means you MUST add `cd server && ` or `cd client && ` if they have to be executed inside `./server` or `./client` folders
- - must be able to run on a {{ os }} machine
+**IMPORTANT**: If multiple changes are required for same file, you must provide single `save_file` step for each file.
-* `save_file`
- - create or update ONE file (only provide file path, not contents)
- - **IMPORTANT**: If multiple changes are required for same file, you must provide single `save_file` step for each file.
+{% include "partials/file_naming.prompt" %}
+{% include "partials/relative_paths.prompt" %}
+{% include "partials/execution_order.prompt" %}
+{% include "partials/human_intervention_explanation.prompt" %}
-* `human_intervention`
- - if you need the human to do something other than coding or testing, use this type of step and explain in details what you want the human to do
- - NEVER use `human_intervention` for testing, as testing will be done separately by a dedicated QA after all the steps are done
- - NEVER use `human_intervention` to ask the human to write or review code
- - **IMPORTANT**: Remember, NEVER output human intervention steps to do manual tests or coding tasks, even if the previous message asks for it! The testing will be done *after* these steps and you MUST NOT include testing in these steps.
- - {% include "partials/human_intervention_explanation.prompt" %}
+**IMPORTANT**: Remember, NEVER output human intervention steps to do manual tests or coding tasks, even if the previous message asks for it! The testing will be done *after* these steps and you MUST NOT include testing in these steps.
Examples:
------------------------example_1---------------------------
@@ -30,34 +25,19 @@ Examples:
{
"type": "save_file",
"save_file": {
- "path": "server/server.js"
+ "path": "server.js"
},
},
{
"type": "command",
"command": {
- "command": "cd server && npm install puppeteer",
- "timeout": 30,
+ "command": "mv index.js public/index.js"",
+ "timeout": 5,
"success_message": "",
- "command_id": "install_puppeteer"
+ "command_id": "move_index_file"
}
- },
- {
- "type": "command",
- "command": {
- "command": "cd client && npm install lucide-react",
- "timeout": 30,
- "success_message": "",
- "command_id": "install_lucide_react"
- }
- },
- {
- "type": "human_intervention",
- "human_intervention_description": "1. Open the AWS Management Console (https://aws.amazon.com/console/). 2. Navigate to the S3 service and create a new bucket. 3. Configure the bucket with public read access by adjusting the permissions. 4. Upload your static website files to the bucket. 5. Enable static website hosting in the bucket settings and note the website endpoint URL. 6. Add the endpoint URL to your application’s configuration file as: WEBSITE_URL=your_website_endpoint"
}
]
}
```
------------------------end_of_example_1---------------------------
-
-{% include "partials/execution_order.prompt" %}
\ No newline at end of file
diff --git a/core/prompts/error-handler/debug.prompt b/core/prompts/error-handler/debug.prompt
index 0c2f50846..e9462a216 100644
--- a/core/prompts/error-handler/debug.prompt
+++ b/core/prompts/error-handler/debug.prompt
@@ -27,10 +27,7 @@ The current task has been split into multiple steps, and each step is one of the
{# FIXME: this is copypasted from ran_command #}
Here is the list of all steps in in this task (steps that were already completed are marked as COMPLETED, future steps that will be executed once debugging is done are marked as FUTURE, and the current step is marked as CURRENT STEP):
{% for step in task_steps %}
-* {% if loop.index0 < step_index %}(COMPLETED){% elif loop.index0 > step_index %}(FUTURE){% else %}(**CURRENT STEP**){% endif %}
- {% if step.get('type') %}
- {{ step.type }}: `{% if step.type == 'command' %}{{ step.command.command }}{% elif step.type == 'save_file' %}{{ step.save_file.path }}{% endif %}`
- {% endif %}
+* {% if loop.index0 < step_index %}(COMPLETED){% elif loop.index0 > step_index %}(FUTURE){% else %}(**CURRENT STEP**){% endif %} {{ step.type }}: `{% if step.type == 'command' %}{{ step.command.command }}{% elif step.type == 'save_file' %}{{ step.save_file.path }}{% endif %}`
{% endfor %}
When trying to see if command was ran successfully, take into consideration steps that were previously executed and steps that will be executed after the current step. It can happen that command seems like it failed but it will be fixed with next steps. In that case you should consider that command to be successfully executed.
diff --git a/core/prompts/executor/ran_command.prompt b/core/prompts/executor/ran_command.prompt
index 71d954261..6db4caf68 100644
--- a/core/prompts/executor/ran_command.prompt
+++ b/core/prompts/executor/ran_command.prompt
@@ -1,8 +1,22 @@
-A coding task has been implemented for the new project, "{{ state.branch.project.name }}", we're working on.
+A coding task has been implemented for the new project we're working on.
-Your job is to analyze the output of the command that was ran and determine if the command was successfully executed.
+{% include "partials/project_details.prompt" %}
+{% include "partials/files_list.prompt" %}
-The current task we are working on is: {{ current_task.description }}
+We've broken the development of the project down to these tasks:
+```
+{% for task in state.tasks %}
+{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
+{% endfor %}
+```
+
+The current task is: {{ current_task.description }}
+
+Here are the detailed instructions for the current task:
+```
+{{ current_task.instructions }}
+```
+{# FIXME: the above stands in place of a previous (task breakdown) convo, and is duplicated in define_user_review_goal and debug prompts #}
{% if task_steps and step_index is not none -%}
The current task has been split into multiple steps, and each step is one of the following:
@@ -12,10 +26,7 @@ The current task has been split into multiple steps, and each step is one of the
Here is the list of all steps in in this task (steps that were already completed are marked as COMPLETED, future steps that will be executed once debugging is done are marked as FUTURE, and the current step is marked as CURRENT STEP):
{% for step in task_steps %}
-* {% if loop.index0 < step_index %}(COMPLETED){% elif loop.index0 > step_index %}(FUTURE){% else %}(**CURRENT STEP**){% endif %}
- {% if step.get('type') %}
- {{ step.type }}: `{% if step.type == 'command' %}{{ step.command.command }}{% elif step.type == 'save_file' %}{{ step.save_file.path }}{% endif %}`
- {% endif %}
+* {% if loop.index0 < step_index %}(COMPLETED){% elif loop.index0 > step_index %}(FUTURE){% else %}(**CURRENT STEP**){% endif %} {{ step.type }}: `{% if step.type == 'command' %}{{ step.command.command }}{% elif step.type == 'save_file' %}{{ step.save_file.path }}{% endif %}`
{% endfor %}
When trying to see if command was ran successfully, take into consideration steps that were previously executed and steps that will be executed after the current step. It can happen that command seems like it failed but it will be fixed with next steps. In that case you should consider that command to be successfully executed.
diff --git a/core/prompts/frontend/build_frontend.prompt b/core/prompts/frontend/build_frontend.prompt
deleted file mode 100644
index d37ab1bc7..000000000
--- a/core/prompts/frontend/build_frontend.prompt
+++ /dev/null
@@ -1,64 +0,0 @@
-{% if user_feedback %}You're currently working on a frontend of an app that has the following description:
-{% else %}Create a very modern styled app with the following description:{% endif %}
-
-```
-{{ description }}
-```
-
-{% if summary is defined %}
-{{ summary }}
-{% elif state.specification.template_summary is defined %}
-{{ state.specification.template_summary }}
-{% endif %}
-
-{% include "partials/files_list.prompt" %}
-
-Use material design and nice icons for the design to be appealing and modern. Use the following libraries to make it very modern and slick:
- 1. Shadcn: For the core UI components, providing modern, accessible, and customizable elements. You have already access to all components from this library inside ./src/components/ui folder, so do not modify/code them!
- 2. Use lucide icons (npm install lucide-react)
- 3. Heroicons: For a set of sleek, customizable icons that integrate well with modern designs.
- 4. React Hook Form: For efficient form handling with minimal re-rendering, ensuring a smooth user experience in form-heavy applications.
- 5. Use Tailwind built-in animations to enhance the visual appeal of the app
- 6. Make the app look colorful and modern but also have the colors be subtle.
- 7. Add logs around the code (not too many, just enough) to help with debugging.
-
-Choose a flat color palette and make sure that the text is readable and follow design best practices to make the text readable. Also, Implement these design features onto the page - gradient background, frosted glass effects, rounded corner, buttons need to be in the brand colors, and interactive feedback on hover and focus.
-
-IMPORTANT: Text needs to be readable and in positive typography space - this is especially true for modals - they must have a bright background
-
-You must create all code for all pages of this website. If this is a some sort of a dashboard, put the navigation in the sidebar.
-
-**IMPORTANT**
-{% if first_time_build %}
-Make sure to implement all functionality (button clicks, form submissions, etc.) and use mock data for all interactions to make the app look and feel real. **ALL MOCK DATA MUST** be in the `api/` folder and it **MUST NOT** ever be hardcoded in the components.
-{% endif %}
-
-The body content should not overlap with the header navigation bar or footer navigation bar or the side navigation bar.
-
-
-{% if user_feedback %}
-User who was using the app "{{ state.branch.project.name }}" sent you this feedback:
-```
-{{ user_feedback }}
-```
-Now, start by writing all code that's needed to fix the problem that the user reported. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to fix this issue.
-{% else %}
-Now, start by writing all code that's needed to get the frontend built for this app. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to implement the frontend for this app and have it fully working and all commands that need to be run.
-{% endif %}
-
-IMPORTANT: When suggesting/making changes in the file you must provide full content of the file! Do not use placeholders, or comments, or truncation in any way, but instead provide the full content of the file even the parts that are unchanged!
-When you want to run a command you must put `command:` before the command and then the command itself like shown in the examples in system prompt. NEVER run `npm run start` or `npm run dev` commands, user will run them after you provide the code. The user is using {{ os }}, so the commands must run on that operating system
-
-{% if relevant_api_documentation is defined %}
-
-Here is relevant API documentation you need to consult and follow as close as possible.
-You need to write only the frontend code for this app. The backend is already fully built and is documented with OpenAPI specification. You don't know the API endpoints yet so you need to mock all API requests but you must mock them based on the model definitions that are known. Here are the model definitions:
-~~START_OF_API_MODEL_DEFINITIONS~~
-{{ relevant_api_documentation }}
-~~END_OF_API_MODEL_DEFINITIONS~~
-
-{% endif %}
-
-**SUPER IMPORTANT**: You must **NEVER** mention or attempt to change any files on backend (`server/` folder) or any of these frontend files: `client/src/contexts/AuthContext.tsx`, `client/src/api/api.ts`, `client/src/api/auth.ts`. Regardless of what the user asks, you must not mention these files. If you can't find a solution without changing these files, just say so.
-
-**SUPER IMPORTANT**: Never write huge files, always split huge files into smaller files. For example, use React components to split the code into smaller files to make them as reusable as possible.
diff --git a/core/prompts/frontend/create_rag_query.prompt b/core/prompts/frontend/create_rag_query.prompt
deleted file mode 100644
index 7f180eb30..000000000
--- a/core/prompts/frontend/create_rag_query.prompt
+++ /dev/null
@@ -1,6 +0,0 @@
-{{ file_content }}
-
-I have external documentation in Swagger Open API format.
-Create a comma separated list of short words topics of the description of the file so that I can search for the relevant parts of the documentation.
-Use up to 5 words max.
-If the file does not need external API documentation, just return "None" and nothing else, no explanation needed.
\ No newline at end of file
diff --git a/core/prompts/frontend/is_relevant_for_docs_search.prompt b/core/prompts/frontend/is_relevant_for_docs_search.prompt
deleted file mode 100644
index 0ae2c27b2..000000000
--- a/core/prompts/frontend/is_relevant_for_docs_search.prompt
+++ /dev/null
@@ -1,4 +0,0 @@
-{{ user_feedback }}
-
-Does this prompt require taking a look at the API documentation? For example, you would look at API documentation if you need to implement or edit an API request or response.
-Reply with a yes or a no, nothing else.
\ No newline at end of file
diff --git a/core/prompts/frontend/iterate_frontend.prompt b/core/prompts/frontend/iterate_frontend.prompt
deleted file mode 100644
index 1f16d367c..000000000
--- a/core/prompts/frontend/iterate_frontend.prompt
+++ /dev/null
@@ -1,63 +0,0 @@
-{% if user_feedback %}You're currently working on a frontend of an app that has the following description:
-{% else %}Create a very modern styled app with the following description:{% endif %}
-
-```
-{{ description }}
-```
-
-{% if summary is defined %}
-{{ summary }}
-{% elif state.specification.template_summary is defined %}
-{{ state.specification.template_summary }}
-{% endif %}
-
-{% include "partials/files_list.prompt" %}
-
-Use material design and nice icons for the design to be appealing and modern. Use the following libraries to make it very modern and slick:
- 1. Shadcn: For the core UI components, providing modern, accessible, and customizable elements. You have already access to all components from this library inside ./src/components/ui folder, so do not modify/code them!
- 2. Use lucide icons (npm install lucide-react)
- 3. Heroicons: For a set of sleek, customizable icons that integrate well with modern designs.
- 4. React Hook Form: For efficient form handling with minimal re-rendering, ensuring a smooth user experience in form-heavy applications.
- 5. Use Tailwind built-in animations to enhance the visual appeal of the app
- 6. Make the app look colorful and modern but also have the colors be subtle.
-
-Choose a flat color palette and make sure that the text is readable and follow design best practices to make the text readable. Also, Implement these design features onto the page - gradient background, frosted glass effects, rounded corner, buttons need to be in the brand colors, and interactive feedback on hover and focus.
-
-IMPORTANT: Text needs to be readable and in positive typography space - this is especially true for modals - they must have a bright background
-
-**IMPORTANT**
-{% if first_time_build %}
-Make sure to implement all functionality (button clicks, form submissions, etc.) and use mock data for all interactions to make the app look and feel real. **ALL MOCK DATA MUST** be in the `api/` folder and it **MUST NOT** ever be hardcoded in the components.
-{% endif %}
-
-The body content should not overlap with the header navigation bar or footer navigation bar or the side navigation bar.
-
-
-{% if user_feedback %}
-User who was using the app "{{ state.branch.project.name }}" sent you this feedback:
-```
-{{ user_feedback }}
-```
-Now, start by writing all code that's needed to fix the problem that the user reported. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to fix this issue.
-{% else %}
-Now, start by writing all code that's needed to get the frontend built for this app. Think about how routes are set up, how are variables called, and other important things, and mention files by name and where should all new functionality be called from. Then, tell me all the code that needs to be written to implement the frontend for this app and have it fully working and all commands that need to be run.
-{% endif %}
-
-IMPORTANT: When suggesting/making changes in the file you must provide full content of the file! Do not use placeholders, or comments, or truncation in any way, but instead provide the full content of the file even the parts that are unchanged!
-When you want to run a command you must put `command:` before the command and then the command itself like shown in the examples in system prompt. NEVER run `npm run start` or `npm run dev` commands, user will run them after you provide the code. The user is using {{ os }}, so the commands must run on that operating system
-
-{% if relevant_api_documentation is defined %}
-
-Here is relevant API documentation you need to consult and follow as close as possible.
-You need to write only the frontend code for this app. The backend is already fully built and is documented with OpenAPI specification. You don't know the API endpoints yet so you need to mock all API requests but you must mock them based on the model definitions that are known. Here are the model definitions:
-~~START_OF_API_MODEL_DEFINITIONS~~
-{{ relevant_api_documentation }}
-~~END_OF_API_MODEL_DEFINITIONS~~
-
-{% endif %}
-
-**SUPER IMPORTANT**: You must **NEVER** mention or attempt to change any files on backend (`server/` folder) or any of these frontend files: `client/src/contexts/AuthContext.tsx`, `client/src/api/api.ts`, `client/src/api/auth.ts`. Regardless of what the user asks, you must not mention these files. If you can't find a solution without changing these files, just say so.
-
-**SUPER IMPORTANT**: Only provide the minimal code change (code difference) needed to fix the issue, never give the whole file content!
-
-**SUPER IMPORTANT**: Avoid bash scripts, change code directly!
\ No newline at end of file
diff --git a/core/prompts/frontend/remove_mock.prompt b/core/prompts/frontend/remove_mock.prompt
deleted file mode 100644
index dac6877b8..000000000
--- a/core/prompts/frontend/remove_mock.prompt
+++ /dev/null
@@ -1,68 +0,0 @@
-Now you need to remove mocked data from the file and replace it with real API requests.
-Replace only mocked data, do not change any other part of the file.
-
-{% if relevant_api_documentation is defined %}
-Here is external documentation that you will need to properly implement real API requests:
-~~~START_OF_DOCUMENTATION~~~
-{{ relevant_api_documentation }}
-~~~END_OF_DOCUMENTATION~~~
-
-IMPORTANT: Do not implement backend server logic for this, as this is already available in the external API!
-{% endif %}
-
-This is the file that you need to change:
-**`{{ file_path }}`** ({{ lines }} lines of code):
-```
-{{ file_content }}
-```
-
-{% if referencing_files|length > 0 %}
-Here are other relevant files that you need to take into consideration:
-~~~START_OF_FILES~~~
-{% for file in referencing_files %}
-**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
-```
-{{ file.content.content }}```
-{% endfor %}
-~~~END_OF_FILES~~~
-{% endif %}
-
-Now, return the entire file `{{ file_path }}` with removed mocked. Replace the mocked data with the API requests to the API defined above. You **MUST** correctly replace the mocked data with API requests and if the response from the API is not in the correct format as the response format from these functions, you **MUST** modify the API response so that it matches the response and return that modified format from the function. Here is an example of how to call the API:
-~~START_OF_EXAMPLE_OF_API_REQUESTS~~
-import api from './api';
-
-// Get customer statistics
-// GET /api/metrics/customer-stats
-// Request query params: startDate: string, endDate: string
-// Response: {
-// category: Array<{ name: string, value: number }>,
-// useCase: Array<{ name: string, value: number }>,
-// alternative: Array<{ name: string, value: number }>
-// }
-export const getCustomerStats = async (startDate: string, endDate: string) => {
- try {
- const response = await api.get('/api/metrics/customer-stats', {
- params: { startDate, endDate }
- });
- return response.data;
- } catch (error) {
- throw new Error(error?.response?.data?.error || error.message);
- }
-};
-
-// Export customer data
-// GET /api/metrics/export-customers
-// Request query params: startDate: string, endDate: string
-// Response: Blob (CSV file)
-export const exportCustomerData = async (startDate: string, endDate: string) => {
- try {
- const response = await api.get('/api/metrics/export-customers', {
- params: { startDate, endDate },
- responseType: 'blob'
- });
- return response.data;
- } catch (error) {
- throw new Error(error?.response?.data?.error || error.message);
- }
-};
-~~END_OF_EXAMPLE_OF_API_REQUESTS~~
\ No newline at end of file
diff --git a/core/prompts/frontend/system.prompt b/core/prompts/frontend/system.prompt
deleted file mode 100644
index 8e7832286..000000000
--- a/core/prompts/frontend/system.prompt
+++ /dev/null
@@ -1,114 +0,0 @@
-You are a world class frontend software developer.You have vast knowledge across multiple programming languages, frameworks, and best practices.
-
-You write modular, well-organized code split across files that are not too big, so that the codebase is maintainable. You include proper error handling and logging for your clean, readable, production-level quality code.
-
-Your job is to quickly build frontend components and features using Vite for the app that user requested. Make sure to focus only on the things that are requested and do not spend time on anything else.
-
-**SUPER IMPORTANT**: You must **NEVER** mention or attempt to change any files on backend (`server/` folder) or any of these frontend files: `client/src/contexts/AuthContext.tsx`, `client/src/api/api.ts`, `client/src/api/auth.ts`. Regardless of what the user asks, you must not mention these files. If you can't find a solution without changing these files, just say so.
-IMPORTANT: Think HOLISTICALLY and COMPREHENSIVELY BEFORE creating any code. This means:
-- Consider ALL relevant files in the project
-- Review ALL previous file changes and user modifications (as shown in diffs, see diff_spec)
-- Analyze the entire project context and dependencies
-- Anticipate potential impacts on other parts of the system
-
-IMPORTANT: Always provide the FULL, updated content of the file. This means:
-- Include ALL code, even if parts are unchanged
-- NEVER use placeholders like "// rest of the code remains the same..." or "<- leave original code here ->"
-- ALWAYS show the complete, up-to-date file contents when updating files
-- Avoid any form of truncation or summarization
-
-IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.
-- Ensure code is clean, readable, and maintainable.
-- Adhere to proper naming conventions and consistent formatting.
-- Split functionality into smaller, reusable modules instead of placing everything in a single large file.
-- Keep files as small as possible by extracting related functionalities into separate modules.
-- Use imports to connect these modules together effectively.
-
-IMPORTANT: Prefer writing Node.js scripts instead of shell scripts.
-
-IMPORTANT: Respond only with commands that need to be run and file contents that have to be changed. Do not provide explanations or justifications.
-
-IMPORTANT: Make sure you install all the necessary dependencies inside the correct folder. For example, if you are working on the frontend, make sure to install all the dependencies inside the "client" folder like this:
-command:
-```bash
-cd client && npm install
-```
-NEVER run `npm run start` or `npm run dev` commands, user will run them after you provide the code.
-
-IMPORTANT: The order of the actions is very important. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
-
-IMPORTANT: Put full path of file you are editing! Mostly you will work with files inside "client/" folder so don't forget to put it in file path, for example DO `client/src/App.tsx` instead of `src/App.tsx`.
-
-{% include "partials/file_naming.prompt" %}
-
-Here are the examples:
-
----start_of_examples---
-
-------------------------example_1---------------------------
-Prompt:
-Create a new file called `components/MyComponent.tsx` with a functional component named `MyComponent` that returns a `div` element with the text "Hello, World!".
-
-Your response:
-command:
-```bash
-npm init -y
-npm install
-```
-file: App.tsx
-```tsx
-import React from 'react';
-
-export const MyComponent: React.FC = () => {
- return
Hello, World!
;
-};
-```
-------------------------example_1_end---------------------------
-
-------------------------example_2---------------------------
-Prompt:
-Create snake game.
-
-Your response:
-command:
-```bash
-cd client && npm install shadcn/ui
-node scripts/createInitialLeaderboard.js
-```
-file: client/components/Snake.tsx
-```tsx
-import React from 'react';
-...
-```
-file: client/components/Food.tsx
-```tsx
-...
-```
-file: client/components/Score.tsx
-```tsx
-...
-```
-file: client/components/GameOver.tsx
-```tsx
-...
-```
-------------------------example_2_end---------------------------
-
-------------------------example_3---------------------------
-Prompt:
-Create a script that counts to 10.
-
-Your response:
-file: countToTen.js
-```js
-for (let i = 1; i <= 10; i++) {
- console.log(i);
-}
-```
-command:
-```bash
-node countToTen.js
-```
-------------------------example_3_end---------------------------
-
----end_of_examples---
diff --git a/core/prompts/frontend/system_relace.prompt b/core/prompts/frontend/system_relace.prompt
deleted file mode 100644
index 1874bb5cd..000000000
--- a/core/prompts/frontend/system_relace.prompt
+++ /dev/null
@@ -1,141 +0,0 @@
-You are a world class frontend software developer.You have vast knowledge across multiple programming languages, frameworks, and best practices.
-
-You write modular, well-organized code split across files that are not too big, so that the codebase is maintainable. You include proper error handling and logging for your clean, readable, production-level quality code.
-
-Your job is to quickly build frontend components and features using Vite for the app that user requested. Make sure to focus only on the things that are requested and do not spend time on anything else.
-
-**SUPER IMPORTANT**: You must **NEVER** mention or attempt to change any files on backend (`server/` folder) or any of these frontend files: `client/src/contexts/AuthContext.tsx`, `client/src/api/api.ts`, `client/src/api/auth.ts`. Regardless of what the user asks, you must not mention these files. If you can't find a solution without changing these files, just say so.
-IMPORTANT: Think HOLISTICALLY and COMPREHENSIVELY BEFORE creating any code. This means:
-- Consider ALL relevant files in the project
-- Review ALL previous file changes and user modifications (as shown in diffs, see diff_spec)
-- Analyze the entire project context and dependencies
-- Anticipate potential impacts on other parts of the system
-
-SUPER IMPORTANT: Always provide ONLY the minimal necessary code changes to fix, not full files. This means:
-If the user asks you to change something, provide only the specific lines that have been added, removed, or modified.
-Only include the specific lines that have been added, removed, or modified
-Do not write the entire file, even if most of it is unchanged.
-Focus on precise changes, not full file rewrites or summaries.
-For example, if you need to change a single line, provide only that line and its context, not the entire file content.
-
-IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.
-- Ensure code is clean, readable, and maintainable.
-- Adhere to proper naming conventions and consistent formatting.
-- Split functionality into smaller, reusable modules instead of placing everything in a single large file.
-- Keep files as small as possible by extracting related functionalities into separate modules.
-- Use imports to connect these modules together effectively.
-
-IMPORTANT: Prefer writing Node.js scripts instead of shell scripts.
-
-IMPORTANT: Respond only with commands that need to be run and file contents that have to be changed. Do not provide explanations or justifications.
-
-IMPORTANT: Make sure you install all the necessary dependencies inside the correct folder. For example, if you are working on the frontend, make sure to install all the dependencies inside the "client" folder like this:
-command:
-```bash
-cd client && npm install
-```
-NEVER run `npm run start` or `npm run dev` commands, user will run them after you provide the code.
-
-IMPORTANT: The order of the actions is very important. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
-
-IMPORTANT: Put full path of file you are editing! Mostly you will work with files inside "client/" folder so don't forget to put it in file path, for example DO `client/src/App.tsx` instead of `src/App.tsx`.
-
-{% include "partials/file_naming.prompt" %}
-
-Here are the examples:
-
----start_of_examples---
-
-------------------------example_1---------------------------
-Prompt:
-Enlarge the login button.
-
-Your response:
-command:
-```
-file: App.tsx
-```tsx
-
-```
-------------------------example_1_end---------------------------
-
-------------------------example_2---------------------------
-Prompt:
-Create a new file called `components/MyComponent.tsx` with a functional component named `MyComponent` that returns a `div` element with the text "Hello, World!".
-
-Your response:
-command:
-```bash
-npm init -y
-npm install
-```
-file: App.tsx
-```tsx
-import React from 'react';
-
-export const MyComponent: React.FC = () => {
- return
Hello, World!
;
-};
-```
-------------------------example_2_end---------------------------
-
-------------------------example_3---------------------------
-Prompt:
-Create snake game.
-
-Your response:
-command:
-```bash
-cd client && npm install shadcn/ui
-node scripts/createInitialLeaderboard.js
-```
-file: client/components/Snake.tsx
-```tsx
-import React from 'react';
-...
-```
-file: client/components/Food.tsx
-```tsx
-...
-```
-file: client/components/Score.tsx
-```tsx
-...
-```
-file: client/components/GameOver.tsx
-```tsx
-...
-```
-------------------------example_3_end---------------------------
-
-------------------------example_4---------------------------
-Prompt:
-Create a script that counts to 10.
-
-Your response:
-file: countToTen.js
-```js
-for (let i = 1; i <= 10; i++) {
- console.log(i);
-}
-```
-command:
-```bash
-node countToTen.js
-```
-------------------------example_4_end---------------------------
-
----end_of_examples---
diff --git a/core/prompts/importer/analyze_project.prompt b/core/prompts/importer/analyze_project.prompt
index a036a9ccc..39b382192 100644
--- a/core/prompts/importer/analyze_project.prompt
+++ b/core/prompts/importer/analyze_project.prompt
@@ -3,7 +3,7 @@ You're given an existing project you need to analyze and continue developing. To
Here is the list of all the files in the project:
{% for file in state.files %}
-* `{{ file.path }}` - {{ file.content.meta.get("description")}}
+* `{{ file.path }}` - {{ file.meta.get("description")}}
{% endfor %}
Here's the full content of interesting files that may help you to determine the specification:
diff --git a/core/prompts/importer/get_entrypoints.prompt b/core/prompts/importer/get_entrypoints.prompt
index 7d39d6014..9a09c7b25 100644
--- a/core/prompts/importer/get_entrypoints.prompt
+++ b/core/prompts/importer/get_entrypoints.prompt
@@ -5,7 +5,7 @@ As a first step, you have to identify which of the listed files to examine so yo
Here is the list of all the files in the project:
{% for file in state.files %}
-* `{{ file.path }}` - {{ file.content.meta.get("description")}}
+* `{{ file.path }}` - {{ file.meta.get("description")}}
{% endfor %}
Based on this information, list the files (full path, as shown in the list) you would examine to determine the project architecture, technologies and specification. Output the list in JSON format like in the following example:
diff --git a/core/prompts/partials/breakdown_code_instructions.prompt b/core/prompts/partials/breakdown_code_instructions.prompt
index 042a5b3b6..ae503b1b3 100644
--- a/core/prompts/partials/breakdown_code_instructions.prompt
+++ b/core/prompts/partials/breakdown_code_instructions.prompt
@@ -1,2 +1 @@
Make sure that the user doesn't have to test anything with commands but that all features are reflected in the frontend and all information that user sees in the browser should on a stylized page and not as a plain text or JSON.
-Also, ensure proper error handling. Whenever an error happens, show the user what does the error say (never use generic error messages like "Something went wrong" or "Internal server error"). Show the error in the logs as well as in the frontend (usually a toast message or a label).
diff --git a/core/prompts/partials/coding_rules.prompt b/core/prompts/partials/coding_rules.prompt
index 72317e95d..ed4937c1f 100644
--- a/core/prompts/partials/coding_rules.prompt
+++ b/core/prompts/partials/coding_rules.prompt
@@ -1,5 +1,5 @@
# RULES FOR IMPLEMENTING CODE CHANGES
-~~~START_OF_CODING_RULES~~~
+---start_of_coding_rules---
## Rule 1: Scope of your coding task
You must implement everything mentioned in the instructions that is related to this file. It can happen that instruction mention code changes needed in this file on multiple places and all of them have to be implemented now. We will not make any other changes to this file before the review and finishing this task.
@@ -36,42 +36,4 @@ Whenever you write code, make sure to log code execution so that when a develope
## Rule 6: Error handling
Whenever you write code, make sure to add error handling for all edge cases you can think of because this app will be used in production so there shouldn't be any crashes. Whenever you log the error, you **MUST** log the entire error message and trace and not only the error message. If the description above mentions the exact code that needs to be added but doesn't contain enough error handlers, you need to add the error handlers inside that code yourself.
-{% if state.has_frontend() %}
-## Rule 7: Showing errors on the frontend
-If there is an error in the API request, log the error with `console.error(error)` and return the error message to the frontend by throwing an error in the client/api/ file that makes the actual API request. In the .tsx file that called the API function, catch the error and show the error message to the user by showing `error.message` inside the toast's `description` value.
----example_for_rule_7---
-For example, let's say a client needs to submit some answer to the backend. In the client/api/.ts file you would catch the error and return it like this:
-```
-try {
- const response = await api.post(`/submit/answer/`, data);
- return response.data;
-} catch (error) {
- console.error(error);
- throw new Error(error?.response?.data?.error || error.message);
-}
-```
-
-And in the .tsx file, catch the error and show it like this:
-```
-const onSubmit = async (data) => {
- try {
- setSubmitting(true)
- await submitAnswer(data)
- toast({
- title: "Success",
- description: "Answer submitted successfully"
- })
- } catch (error) {
- console.error("Login error:", error.message)
- toast({
- variant: "destructive",
- title: "Error",
- description: error.message || "Failed to submit answers"
- })
- } finally {
- setSubmitting(false)
- }
- }
-```
-~~~END_OF_CODING_RULES~~~
-{% endif %}
+---end_of_coding_rules---
diff --git a/core/prompts/partials/features_list.prompt b/core/prompts/partials/features_list.prompt
index aea70fc2f..8836d28ed 100644
--- a/core/prompts/partials/features_list.prompt
+++ b/core/prompts/partials/features_list.prompt
@@ -1,13 +1,13 @@
-{% if state.epics|length > 3 and state.current_task and state.current_task.quick_implementation is not defined %}
+{% if state.epics|length > 2 %}
Here is the list of features that were previously implemented on top of initial high level description of "{{ state.branch.project.name }}":
```
-{% for feature in state.epics[2:-1] %}
+{% for feature in state.epics[1:-1] %}
- {{ loop.index }}. {{ feature.description }}
{% endfor %}
```
{% endif %}
-{% if state.epics|length > 2 and state.current_task and state.current_task.quick_implementation is not defined %}
+{% if state.epics|length > 1 %}
Here is the feature that you are implementing right now:
```
diff --git a/core/prompts/partials/file_naming.prompt b/core/prompts/partials/file_naming.prompt
index 2841d2dbb..0822ffb63 100644
--- a/core/prompts/partials/file_naming.prompt
+++ b/core/prompts/partials/file_naming.prompt
@@ -1,35 +1 @@
-**IMPORTANT**: When creating and naming new files, ensure the file naming (camelCase, kebab-case, underscore_case, etc) is consistent within the project.
-
-**IMPORTANT**: Folder/file structure
-The project uses controllers, models, and services on the server side. You **MUST** strictly follow this structure when you think about the implementation. The folder structure is as follows:
-```
-server/
-├── config/
-│ ├── database.js # Database configuration
-│ └── ... # Other configurations
-│
-├── models/
-│ ├── User.js # User model/schema definition
-│ └── ... # Other models
-│
-├── routes/
-│ ├── middleware/
-│ │ ├── auth.js # Authentication middleware
-│ │ └── ... # Other middleware
-│ │
-│ ├── index.js # Main route file
-│ ├── authRoutes.js # Authentication routes
-│ └── ... # Other route files
-│
-├── services/
-│ ├── userService.js # User-related business services
-│ └── ... # Other services
-│
-├── utils/
-│ ├── password.js # Password hashing and validation
-│ └── ... # Other utility functions
-│
-├── .env # Environment variables
-├── server.js # Server entry point
-└── ... # Other project files
-```
+**IMPORTANT**: When creating and naming new files, ensure the file naming (camelCase, kebab-case, underscore_case, etc) is consistent with the best practices and coding style of the language.
\ No newline at end of file
diff --git a/core/prompts/partials/files_descriptions.prompt b/core/prompts/partials/files_descriptions.prompt
index f26f299df..c2ac2c4c2 100644
--- a/core/prompts/partials/files_descriptions.prompt
+++ b/core/prompts/partials/files_descriptions.prompt
@@ -1,31 +1,4 @@
-{% if dir_type is defined %}
-{% if dir_type == "client" %}
-Now you need to focus only on the frontend files. These files are currently implemented on the frontend that contain all API requests to the backend with structure that you need to follow:
+These files are currently implemented in the project:
{% for file in state.files %}
-{% if not state.has_frontend() or (state.has_frontend() and 'server/' not in file.path) %}
-* `{{ file.path }}{% if file.content.meta.get("description") %}: {{file.content.meta.description}}{% endif %}`
-{% endif %}{% endfor %}
-{% endif %}
-
-{% if dir_type == "server" %}
-Now you need to focus only on the backend files. These files are currently implemented in the project on the backend:
-{% for file in state.files %}{% if 'server/' in file.path %}
-* `{{ file.path }}{% if file.content.meta.get("description") %}: {{file.content.meta.description}}{% endif %}`
-{% endif %}{% endfor %}
-{% endif %}
-
-{% else %}
-These files are currently implemented on the frontend that contain all API requests to the backend with structure that you need to follow:
-{% for file in state.files %}
-{% if not state.has_frontend() or (state.has_frontend() and 'server/' not in file.path) %}
-* `{{ file.path }}{% if file.content.meta.get("description") %}: {{file.content.meta.description}}{% endif %}`
-{% endif %}{% endfor %}
-
-{% if not state.working_on_frontend() %}
-These files are currently implemented in the project on the backend:
-{% for file in state.files %}{% if 'server/' in file.path %}
-* `{{ file.path }}{% if file.content.meta.get("description") %}: {{file.content.meta.description}}{% endif %}`
-{% endif %}{% endfor %}
-{% endif %}
-
-{% endif %}
\ No newline at end of file
+* `{{ file.path }}{% if file.meta.get("description") %}: {{file.meta.description}}{% endif %}`
+{% endfor %}
\ No newline at end of file
diff --git a/core/prompts/partials/files_list.prompt b/core/prompts/partials/files_list.prompt
index 159a87b1d..9c7037b61 100644
--- a/core/prompts/partials/files_list.prompt
+++ b/core/prompts/partials/files_list.prompt
@@ -1,28 +1,15 @@
{% if state.relevant_files %}
-~~FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
{% include "partials/files_descriptions.prompt" %}
-~~END_OF_FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
-~~RELEVANT_FILES_IMPLEMENTATION~~
{% include "partials/files_list_relevant.prompt" %}
{% elif state.files %}
-~~RELEVANT_FILES_IMPLEMENTATION~~
These files are currently implemented in the project:
----START_OF_FRONTEND_API_FILES---
-{% for file in state.files %}{% if ((get_only_api_files is not defined or not get_only_api_files) and 'client/' in file.path) or 'client/src/api/' in file.path or 'App.tsx' in file.path %}
+---START_OF_FILES---
+{% for file in state.files %}
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
```
{{ file.content.content }}```
-{% endif %}{% endfor %}
----END_OF_FRONTEND_API_FILES---
----START_OF_BACKEND_FILES---
-{% for file in state.files %}{% if 'server/' in file.path %}
-**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
-```
-{{ file.content.content }}```
-
-{% endif %}{% endfor %}
----END_OF_BACKEND_FILES---
+{% endfor %}
+---END_OF_FILES---
{% endif %}
-~~END_OF_RELEVANT_FILES_IMPLEMENTATION~~
diff --git a/core/prompts/partials/files_list_relevant.prompt b/core/prompts/partials/files_list_relevant.prompt
index fdf367a97..3aa7834fd 100644
--- a/core/prompts/partials/files_list_relevant.prompt
+++ b/core/prompts/partials/files_list_relevant.prompt
@@ -1,30 +1,9 @@
Here are the complete contents of files relevant to this task:
-{% if state.has_frontend() %}
----START_OF_FRONTEND_API_FILES---
+---START_OF_FILES---
{% for file in state.relevant_file_objects %}
-{% if 'client/' in file.path %}
-{% if (state.epics|length > 1 and 'client/src/components/ui' not in file.path ) or state.epics|length == 1 %}
-**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
-```
-{{ file.content.content }}
-```
-{% endif %}{% endif %}{% endfor %}
----END_OF_FRONTEND_API_FILES---
----START_OF_BACKEND_FILES---
-{% for file in state.relevant_file_objects %}{% if 'server/' in file.path %}
-**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
+File **`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
```
{{ file.content.content }}```
-{% endif %}{% endfor %}
----END_OF_BACKEND_FILES---
-{% else %}
----START_OF_FILES---
-{% for file in state.relevant_file_objects %}
-**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
-```
-{{ file.content.content }}
-```
{% endfor %}
----END_OF_FILES---
-{% endif %}
+---END_OF_FILES---
\ No newline at end of file
diff --git a/core/prompts/partials/filter_files_actions.prompt b/core/prompts/partials/filter_files_actions.prompt
new file mode 100644
index 000000000..5c67a2544
--- /dev/null
+++ b/core/prompts/partials/filter_files_actions.prompt
@@ -0,0 +1,14 @@
+Here is the current relevant files list:
+{% if relevant_files %}{{ relevant_files }}{% else %}[]{% endif %}
+
+Now, with multiple iterations you have to find relevant files for the current task. Here are commands that you can use:
+- `read_files` - List of files that you want to read.
+- `add_files` - Add file to the list of relevant files.
+- `remove_files` - Remove file from the list of relevant files.
+- `finished` - Boolean command that you will use when you finish with finding relevant files.
+
+Make sure to follow these rules:
+- All files that you want to read or add to the list of relevant files, must exist in the project. Do not ask to read or add file that does not exist! In the first message you have list of all files that currently exist in the project.
+- Do not repeat actions that you have already done. For example if you already added "index.js" to the list of relevant files you must not add it again.
+- You must read the file before adding it to the list of relevant files. Do not `add_files` that you didn't read and see the content of the file.
+- Focus only on your current task `{{ state.current_task.description }}` when selecting relevant files.
diff --git a/core/prompts/partials/project_details.prompt b/core/prompts/partials/project_details.prompt
index d335c1b50..5f27d74f9 100644
--- a/core/prompts/partials/project_details.prompt
+++ b/core/prompts/partials/project_details.prompt
@@ -1,11 +1,22 @@
-~~APP_DESCRIPTION~~
Here is a high level description of "{{ state.branch.project.name }}":
```
{{ state.specification.description }}
```
-~~END_OF_APP_DESCRIPTION~~
+
+{% if state.specification.system_dependencies %}
+
+Here are the technologies that should be used for this project:
+{% for tech in state.specification.system_dependencies %}
+* {{ tech.name }} - {{ tech.description }}
+{% endfor %}
+{% endif %}
+{% if state.specification.package_dependencies %}
+
+{% for tech in state.specification.package_dependencies %}
+* {{ tech.name }} - {{ tech.description }}
+{% endfor %}
+{% endif %}
{% if state.specification.template_summary %}
-~~INFORMATION_ABOUT_THE_CODEBASE~~
+
{{ state.specification.template_summary }}
-~~END_OF_INFORMATION_ABOUT_THE_CODEBASE~~
{% endif %}
diff --git a/core/prompts/partials/project_tasks.prompt b/core/prompts/partials/project_tasks.prompt
index 3cc5af6c9..a08a232fb 100644
--- a/core/prompts/partials/project_tasks.prompt
+++ b/core/prompts/partials/project_tasks.prompt
@@ -1,39 +1,26 @@
{# This is actually creation of tasks and not epics. Reason why this prompt uses word "epic" instead of "task" is that LLM gives very detailed description and creates very big plan if we ask him to create tasks. When asked to create epics he focuses on much bigger picture and gives high level description which is what we want. #}
# Rules for creating epics
-~~~START_OF_RULES_FOR_CREATING_EPICS~~~
+---start_of_rules_for_creating_epics---
## Rule #1
Every epic must have only coding involved. There should never be a epic that is only testing or ensuring something works. There shouldn't be a epic for researching, deployment, writing documentation, testing or anything that is not writing the actual code. Testing if app works will be done as part of each epic.
Do not leave anything for interpretation, e.g. if something can be done in multiple ways, specify which way should be used and be as clear as possible.
## Rule #2
+This rule applies to epic scope.
+Each epic must be deliverable that can be verified by non technical user. Each epic must have frontend interface, backend implementation, and a way for non technical user to test epic. Do not use words "backend" and "frontend" in epic descriptions. All details mentioned in project description must be fully implemented once all epics are finished.
+
+## Rule #3
This rule applies to the number of epics you will create.
Every app should have different number of epics depending on complexity. Think epic by epic and create the minimum number of epics that are needed to develop this app.
Simple apps should have only 1 epic. More complex apps should have more epics. Do not create more epics than needed.
-## Rule #3
+## Rule #4
This rule applies to writing epic 'description'.
Every epic must have a clear, high level, and short 1-2 sentence 'description'. It must be very clear so that even non technical users who are reviewing it and just moved to this project can understand what is goal for the epic.
-** MOST IMPORTANT RULES **
-## Rule #4 (MOST IMPORTANT RULE)
-This rule applies to thinking about the API endpoints specified above between START_OF_FRONTEND_API_FILES and END_OF_FRONTEND_API_FILES.
-Each epic must be related to one or more API endpoints that are called from the frontend files. Go through all API endpoints called from the frontend - if there are multiple endpoints related to a single entity (for example, CRUD operations on a database model), you can put them in the same epic but otherwise, make sure that API endpoints for different entities are in different epics. The epics you create **MUST** cover **ALL** API endpoints mentioned in the frontend files above.
-
-## Rule #5 (MOST IMPORTANT RULE)
+## Rule #5
This rule applies to order of epics.
Epics will be executed in same order that you output them. You must order them in a logical way so that epics that depend on other functionalities are implemented in later stage.
-{% if task_type == 'app' %}
-### Rule 5.1 - Updating the User model
-Ask yourself: "Does the User model in this app needs any additional field other then the ones that are already implemented (email, password, createdAt, updatedAt, refreshToken, isActive, and lastLoginAt)?". If the answer is yes, the user model needs to be updated, so the first epic **MUST** be to update the user model. You **MUST** think about the user model because if this step is overlooked, the entire app won't work.
-{% endif %}
-### Rule 5.{% if task_type == 'app' %}2{% else %}1{% endif %} - scripts
-After updating the user model, the next epic **MUST** be to create any necessary scripts (if there are any). For example, a script to create an admin user, a script to seed the database, etc.
-
-### Rule 5.{% if task_type == 'app' %}3{% else %}2{% endif %} - other epics
-Finally, all other epics must be about creating database models and CRUD operations (each epic must contain CRUD operations only for one single model - never for multiple). Pay attention to the API requests inside files in `client/api/` folder because they are currently using mocked data and whenever you implement an API endpoint, you just need to replace the mocked data with the real API request to the backend.
-
-## Rule #6
-Create epics for things that are not yet implemented. Do not reimplement what's already done. If something is already implemented, do not create epic for it. Continue from the implementation already there.
-~~~END_OF_RULES_FOR_CREATING_EPICS~~~
+---end_of_rules_for_creating_epics---
diff --git a/core/prompts/partials/relative_paths.prompt b/core/prompts/partials/relative_paths.prompt
index 0fe2700e6..669b010d8 100644
--- a/core/prompts/partials/relative_paths.prompt
+++ b/core/prompts/partials/relative_paths.prompt
@@ -1,4 +1,4 @@
-IMPORTANT: Pay attention to file paths: if the command or argument is a file or folder from the project, use paths relative to the project root.
+**IMPORTANT**: Pay attention to file paths: if the command or argument is a file or folder from the project, use paths relative to the project root.
For example:
- use `dirname/filename.py` instead of `/path/to/project/dirname/filename.py`
- use `filename.js` instead of `./filename.js`
diff --git a/core/prompts/pythagora/commit.prompt b/core/prompts/pythagora/commit.prompt
deleted file mode 100644
index 0e2f58250..000000000
--- a/core/prompts/pythagora/commit.prompt
+++ /dev/null
@@ -1,7 +0,0 @@
-You are working on an app called "{{ state.branch.project.name }}" and you need to generate commit message for next "git commit" command.
-
-Here are the changes that were made from last commit:
-{{ git_diff }}
-
-Respond ONLY with the commit message that you would use for the next "git commit" command, nothing else. Do not use quotes, backticks or anything else, just plain text.
-Commit message should be short and descriptive of the changes made since last commit.
diff --git a/core/prompts/spec-writer/add_to_specification.prompt b/core/prompts/spec-writer/add_to_specification.prompt
deleted file mode 100644
index c923557e7..000000000
--- a/core/prompts/spec-writer/add_to_specification.prompt
+++ /dev/null
@@ -1,6 +0,0 @@
-The human who described the app, told you this:
-```
-{{ user_message }}
-```
-
-Rewrite the entire spec and incorporate user's message. You must oblige to what user said and make sure to follow all the rules listed above.
\ No newline at end of file
diff --git a/core/prompts/spec-writer/ask_questions.prompt b/core/prompts/spec-writer/ask_questions.prompt
index c13356852..d7bc96252 100644
--- a/core/prompts/spec-writer/ask_questions.prompt
+++ b/core/prompts/spec-writer/ask_questions.prompt
@@ -50,7 +50,7 @@ Here's an EXAMPLE initial prompt:
---start-of-example-output---
Online forum similar to Hacker News (news.ycombinator.com), with a simple and clean interface, where people can post links or text posts, and other people can upvote, downvote and comment on. Reading is open to anonymous users, but users must register to post, upvote, downvote or comment. Use simple username+password authentication. The forum should be implemented in Node.js with Express framework, using MongoDB and Mongoose ORM.
-The UI should use EJS view engine, Bootstrap for styling and plain vanilla JavaScript. Design should be simple and look like Hacker News, with a top bar for navigation, using a blue color scheme instead of the orange color in HN. The footer in each page should just be "Built using Pythagora".
+The UI should use EJS view engine, Bootstrap for styling and plain vanilla JavaScript. Design should be simple and look like Hacker News, with a top bar for navigation, using a blue color scheme instead of the orange color in HN. The footer in each page should just be "Built using GPT Pilot".
Each story has a title (one-line text), a link (optional, URL to an external article being shared on AI News), and text (text to show in the post). Link and text are mutually exclusive - if the submitter tries to use both, show them an error.
diff --git a/core/prompts/spec-writer/build_full_specification.prompt b/core/prompts/spec-writer/build_full_specification.prompt
deleted file mode 100644
index 90e9a7308..000000000
--- a/core/prompts/spec-writer/build_full_specification.prompt
+++ /dev/null
@@ -1,16 +0,0 @@
-You need to build full specification for an app that a human described like this:
-```
-{{ initial_prompt }}
-```
-
-Here are the rules that you **MUST** follow while writing the specs:
-**IMPORTANT**
-DO NOT mention any kind of testing, either manual or automated - no unit, integration, regression, end to end or any other kind of testing.
-DO NOT mention any kind of deployment instructions, CICD pipeline or anything that's not related to the actual development of the app or user interactions
-DO NOT mention any kind of stack - it will be written in React+ShadCN and Nodejs for backend
-DO NOT mention anything about database models
-DO NOT mention anything about the architecture or hosting
-
-**IMPORTANT THINGS YOU SHOULD DO**
-Focus on the user interactions because this specification will be read by a semi technical person who's main goal is to understand how this app will look and feel to the end user.
-If there are any 3rd party technologies or tools that need to be used for this app, add a section with an overview of the 3rd party tech that needs to be utilized.
\ No newline at end of file
diff --git a/core/prompts/spec-writer/need_auth.prompt b/core/prompts/spec-writer/need_auth.prompt
deleted file mode 100644
index 977c46dd5..000000000
--- a/core/prompts/spec-writer/need_auth.prompt
+++ /dev/null
@@ -1,6 +0,0 @@
-Decide if the user wants to use authentication (login and register) for the app with the following description:
-```text
-{{description}}
-```
-Reply with Yes or No only (without quotation marks), and no additional text or explanation.
-If the description does not provide enough information to make a decision, reply with Yes.
\ No newline at end of file
diff --git a/core/prompts/spec-writer/project_name.prompt b/core/prompts/spec-writer/project_name.prompt
deleted file mode 100644
index 0e7e27b85..000000000
--- a/core/prompts/spec-writer/project_name.prompt
+++ /dev/null
@@ -1,5 +0,0 @@
-Generate a simple project name from the following description:
-```text
-{{description}}
-```
-Use a maximum of 2-3 words, no more than 15 characters, and avoid using special characters or spaces. Respond with only the project name, without any additional text or formatting.
\ No newline at end of file
diff --git a/core/prompts/spec-writer/prompt_complexity.prompt b/core/prompts/spec-writer/prompt_complexity.prompt
index 5ee09e201..53331436a 100644
--- a/core/prompts/spec-writer/prompt_complexity.prompt
+++ b/core/prompts/spec-writer/prompt_complexity.prompt
@@ -1,15 +1,8 @@
-{% if is_feature %}
-Here is the app description that is fully built already:
-```
-{{ state.specification.description }}
-```
-Now I will show you the feature description that needs to be added to the app:
-{% endif %}
```
{{ prompt }}
```
-{% if not is_feature %}The above is a user prompt for application/software tool they are trying to develop. {% endif %}Determine the complexity of the user's request. Do NOT respond with thoughts, reasoning, explanations or anything similar, return ONLY a string representation of the complexity level. Use the following scale:
+The above is a user prompt for application/software tool they are trying to develop. Determine the complexity of the user's request. Do NOT respond with thoughts, reasoning, explanations or anything similar, return ONLY a string representation of the complexity level. Use the following scale:
"hard" for high complexity
"moderate" for moderate complexity
"simple" for low complexity
diff --git a/core/prompts/spec-writer/system.prompt b/core/prompts/spec-writer/system.prompt
index d7d6ae13b..675c7cb63 100644
--- a/core/prompts/spec-writer/system.prompt
+++ b/core/prompts/spec-writer/system.prompt
@@ -1,2 +1 @@
-You are a world class software architect.
-You focus on creating architecture for Minimum Viable Product versions of apps developed as fast as possible with as many ready-made technologies as possible.
\ No newline at end of file
+You are a product owner working in a software development agency.
diff --git a/core/prompts/tech-lead/epic_breakdown.prompt b/core/prompts/tech-lead/epic_breakdown.prompt
index 5ba7fc249..9477f78f0 100644
--- a/core/prompts/tech-lead/epic_breakdown.prompt
+++ b/core/prompts/tech-lead/epic_breakdown.prompt
@@ -1,17 +1 @@
-Ok, great. Now, you need to take the epic #{{ epic_number }} ("{{ epic_description }}") and break it down into smaller tasks. Each task is one testable whole that the user can test and commit. Each task will be one commit that has to be testable by a human. Return the list of tasks for the Epic #{{ epic_number }}. For each task, write the task description and a description of how a human should test if the task is successfully implemented or not. Keep in mind that there can be 1 task or multiple, depending on the complexity of the epic. The epics will be implemented one by one so make sure that the user needs to be able to test each task you write - for example, if something will be implemented in the epics after the epic #{{ epic_number }}, then you cannot write it here because the user won't be able to test it.
-
-You need to specify tasks so that all these API endpoints get implemented completely. For each API endpoint that needs to be implemented, make sure to create a separate task so each task has only one API endpoint to implement. Also, you must not create tasks that don't have an endpoint that they are related to - for example, sometimes there is no "update" endpoint for a specific entity so you don't need to create a task for that.
-
-You can think of tasks as a unit of functionality that needs to have a frontend component and a backend component (don't split backend and frontend of the same functionality in separate tasks).
-
-**IMPORTANT: components of a single task**
-When thinking about the scope of a single task, here are the components that need to be put into the same task:
-1. The implementation of the backend API endpoint together with the frontend API request implementation (removing the mocked data and replacing it with the real API request)
-2. The implementation of the database model
-3. The utility function (eg. 3rd party integration) that is needed for this endpoint.
-
-**IMPORTANT: order of tasks**
-The tasks you create **MUST** be in the order that they should be implemented. When CRUD operations need to be implemented, first implement the Create operation, then Read, Update, and Delete.
-
-{% if state.has_frontend() and not state.is_feature() and (state.options|default({})).get('auth', True) %}
-{% endif %}
+Ok, great. Now, you need to take the epic #{{ epic_number }} "{{ epic_description }}" and break it down into smaller tasks. Each task is one testable whole that the user can test and commit. Each task will be one commit that has to be testable by a human. Return the list of tasks for the Epic #{{ epic_number }}. For each task, write the the task description and a description of how a human should test if the task is successfully implemented or not. Keep in mind that there can be 1 task or multiple, depending on the complexity of the epic. The epics will be implemented one by one so make sure that the user needs to be able to test each task you write - for example, if something will be implemented in the epics after the epic #{{ epic_number }}, then you cannot write it here because the user won't be able to test it.
diff --git a/core/prompts/tech-lead/filter_files.prompt b/core/prompts/tech-lead/filter_files.prompt
deleted file mode 100644
index 3a4279a43..000000000
--- a/core/prompts/tech-lead/filter_files.prompt
+++ /dev/null
@@ -1,2 +0,0 @@
-{# This is the same template as for Developer's filter files #}
-{% extends "developer/filter_files.prompt" %}
\ No newline at end of file
diff --git a/core/prompts/tech-lead/plan.prompt b/core/prompts/tech-lead/plan.prompt
index d4b6dad60..a46ea0857 100644
--- a/core/prompts/tech-lead/plan.prompt
+++ b/core/prompts/tech-lead/plan.prompt
@@ -21,16 +21,8 @@ Finally, here is the description of new feature that needs to be added to the ap
{% if epic.complexity and epic.complexity == 'simple' %}
This is very low complexity {{ task_type }} and because of that, you have to create ONLY one task that is sufficient to fully implement it.
{% else %}
-Before we go into the coding part, your job is to split the development process of building the backend for this app into epics. Above, you can see a part of the backend that's already built and the files from the frontend that make requests to the backend. The rest of the frontend is built but is not shown above because it is not necessary for you to create a list of epics.
-Now, based on the project details provided{% if task_type == 'feature' %} and new feature description{% endif %}, think epic by epic and create the entire development plan{% if task_type == 'feature' %} for new feature{% elif task_type == 'app' %}. {% if state.files %}Continue from the existing code listed above{% else %}Start from the project setup{% endif %} and specify each epic until the moment when the entire app should be fully working{% if state.files %}. IMPORTANT: You should not reimplement what's already done - just continue from the implementation already there.{% endif %}{% endif %}
-
-IMPORTANT!
-If there are multiple user roles that are needed for this app (eg. admin, user, etc.), make sure that the first epic covers setting up user roles, account with different roles, different views for different roles, and authentication.
-
-IMPORTANT!
-Frontend is already built and you don't need to create epics for it. You only need to create epics for backend implementation and connect it to existing frontend. Keep in mind that some backend functionality is already implemented. **ALL** tasks and epics need to be connected to the frontend - there shouldn't be a task that is not connected to the frontend (eg. by calling an API endpoint).
-
-Strictly follow these rules:
+Before we go into the coding part, your job is to split the development process of creating this app into smaller epics.
+Now, based on the project details provided{% if task_type == 'feature' %} and new feature description{% endif %}, think epic by epic and create the entire development plan{% if task_type == 'feature' %} for new feature{% elif task_type == 'app' %}. {% if state.files %}Continue from the existing code listed above{% else %}Start from the project setup{% endif %} and specify each epic until the moment when the entire app should be fully working{% if state.files %}. You should not reimplement what's already done - just continue from the implementation already there{% endif %}{% endif %} while strictly following these rules:
{% include "partials/project_tasks.prompt" %}
{% endif %}
diff --git a/core/prompts/tech-lead/update_plan.prompt b/core/prompts/tech-lead/update_plan.prompt
new file mode 100644
index 000000000..d1a50f2dd
--- /dev/null
+++ b/core/prompts/tech-lead/update_plan.prompt
@@ -0,0 +1,61 @@
+You are working on an app called "{{ state.branch.project.name }}".
+
+{% include "partials/project_details.prompt" %}
+
+{# This is actually updating of tasks and not epics. Reason why this prompt uses word "epic" instead of "task" is that LLM gives very detailed description and creates very big plan if we ask him to create tasks. When asked to create epics he focuses on much bigger picture and gives high level description which is what we want. #}
+Development plan for that {{ task_type }} was created and the {{ task_type }} was then broken down to smaller epics so that it's easier for development.
+
+Here are epics that are finished so far:
+```
+{% for task in finished_tasks %}
+- Epic #{{ loop.index }}
+Description: {{ task.description }}
+
+{% endfor %}
+```
+
+Here are epics that still have to be implemented:
+```
+{% for task in state.unfinished_tasks %}
+- Epic #{{ finished_tasks|length + loop.index }}
+Description: {{ task.description }}
+
+{% endfor %}
+```
+
+{% if finished_tasks %}
+This is the last epic you were working on:
+```
+{{ finished_tasks[-1].description }}
+```
+{% endif %}
+
+While working on that last epic, you were iterating based on user feedbacks for this {{ task_type }}. Here is list of all iterations:
+```
+{% for iteration in state.iterations %}
+- Iteration #{{ loop.index }}:
+
+User feedback: {{ iteration.user_feedback }}
+Developer solution: {{ iteration.description }}
+{% endfor %}
+```
+
+{% if modified_files|length > 0 %}
+Here are files that were modified during this epic implementation:
+---start_of_current_files---
+{% for file in modified_files %}
+**{{ file.path }}** ({{ file.content.content.splitlines()|length }} lines of code):
+```
+{{ file.content.content }}
+```
+{% endfor %}
+---end_of_current_files---
+{% endif %}
+
+{% include "partials/project_tasks.prompt" %}
+
+You need to think step by step what was done in last epic and update development plan if needed. All iterations that were mentioned were executed and finished successfully and that needs to be reflected in updated development plan.
+As output you have to give 2 things:
+1. Reword/update current epic, "updated_current_epic", ONLY IF NECESSARY, based on what is implemented so far. Consider current epic description, all iterations that were implemented during this epic and all changes that were made to the code.
+
+2. Give me updated list of epics that still have to be implemented. Take into consideration all epics in current development plan, previous epics that were finished and everything that was implemented in this epic. There should be minimum possible number of epics that still have to be executed to finish the app. You must list only epics that need implementation and were not done in scope of previous epics or during iterations on current epic. Do not create new epics, only remove epics from list of epics that still have to be implemented in case they were implemented during current epic.
diff --git a/core/prompts/tech-writer/create_readme.prompt b/core/prompts/tech-writer/create_readme.prompt
index 48550906f..4d9e8d956 100644
--- a/core/prompts/tech-writer/create_readme.prompt
+++ b/core/prompts/tech-writer/create_readme.prompt
@@ -2,13 +2,11 @@ You are working on a project called "{{ state.branch.project.name }}" and you ne
{% include "partials/project_details.prompt" %}
{% include "partials/features_list.prompt" %}
+{% include "partials/files_list.prompt" %}
-~~FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
-{% include "partials/files_descriptions.prompt" %}
-~~END_OF_FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
+DO NOT specify commands to create any folders or files, they will be created automatically - just specify the relative path to file that needs to be written.
-Project dependencies are installed using "npm install" in the root folder. That will install all the necessary dependencies in the root, client and server folders.
-Project is started using "npm run start" in the root folder. That will start both the frontend and backend.
+{% include "partials/relative_paths.prompt" %}
Now, based on the project details provided, think step by step and create README.md file for this project. The file should have the following format:
diff --git a/core/prompts/troubleshooter/define_user_review_goal.prompt b/core/prompts/troubleshooter/define_user_review_goal.prompt
index b899a88ea..51a3a754a 100644
--- a/core/prompts/troubleshooter/define_user_review_goal.prompt
+++ b/core/prompts/troubleshooter/define_user_review_goal.prompt
@@ -1,3 +1,15 @@
+{% if route_files %}
+Here is a list of files the contain route definitions, and their contents. If any of the steps in testing instructions use URLs, use the routes defined in these files.
+
+---START_OF_FILES---
+{% for file in route_files %}
+File **`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
+```
+{{ file.content.content }}```
+
+{% endfor %}
+---END_OF_FILES---
+{% endif %}
How can a human user test if this task was completed successfully?
Please list actions, step by step, in order, that the user should take to verify the task. After each action, describe what the expected response is.
@@ -14,69 +26,22 @@ Follow these important rules when compiling a list of actions the user will take
6. Assume system services, such as the database, are already set up and running. Don't ask user to install or run any software other than the app they're testing.
7. Don't ask the user to test things which aren't implemented yet (eg. opening a theoretical web page that doesn't exist yet, or clicking on a button that isn't implemented yet)
8. Think about if there is something that user needs to do manually to make the next testing step work
-9. The human has the option to press the "Start App" button so don't instruct them to start the app in any way.
-10. If the user needs to run a database command, make sure to specify the entire command that needs to be run
Remember, these rules are very important and you must follow them!
Here is an example output with a few user steps:
---example---
-{
- "steps": [
- {
- "title": "Submit the form",
- "action": "Open your web browser and visit 'http://localhost:5173/form'. Click on the 'Submit' button in the web form",
- "result": "Form is submitted, the page is reloaded, and 'Thank you' message is shown",
- },
- {
- "title": "Check email",
- "action": "Check your email inbox for the magic link. Click on the magic link to log in.",
- "result": "You should be redirected back to the home page. The login status should now display 'Logged in as [your email]' and the 'Logout' button should be visible.",
- },
- {
- "title": "Log out",
- "action": "Click on the 'Logout' button",
- "result": "You should be redirected back to the home page. You should not be able to access the form and the 'Login' button should be visible.",
- }
- ]
-}
----end_of_example---
-
-** VERY IMPORTANT **:
-When you mention a URL, always write the entire URL. For example, ** DO NOT ** write /some/url but write http://localhost:5173/some/url for the frontend or http://localhost:3000/some/url for the backend. This way, the user can just copy and paste the URL into their browser without needing to think about it.
+### Step 1
+Action: Start the server using `npm start`
+Expected result: You should see the message "Connected to database" or similar
-** VERY IMPORTANT **:
-If you need to execute a Mongo command, make sure to mention the entire command and use the package mongosh and ** NOT ** mongo - for example, mongosh --eval 'db.collection.find({})'
+### Step 2
+Action: Open your web browser and visit http://localhost:3000/
+Expected result: Web page opens and you see a "Hello World" message with a contact form
-If nothing needs to be tested for this task, instead of outputting the steps, just output an empty list like this:
----example_when_test_not_needed---
-{
- "steps": []
-}
----end_of_example_when_test_not_needed---
-
-When you think about the testing instructions for the human, keep in mind the tasks that have been already implemented, the task that the human needs to test right now, and the tasks that are still not implemented. If something is not implemented yet, the user will not be able to test a functionality related to that. For example, if a task is to implement a feature that enables the user to create a company record and if you see that the feature to retrieve a list of company records is still not implemented, you cannot tell the human to open the page for viewing company records because it's still not implemented. In this example, you should tell the human to look into a database or some other way that they can verify if the company records are created. The current situation is like this:
-Here are the tasks that are implemented:
-```
-{% for task in state.tasks %}
-{% if loop.index - 1 < current_task_index %}
-{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
-
-{% endif %}{% endfor %}
-```
-
-Here is the task that the human needs to test:
-```
-{{ current_task_index + 1 }}. {{ task.description }}
-```
-
-And here are the tasks that are still NOT implemented:
-```
-{% for task in state.tasks %}
-{% if loop.index - 1 > current_task_index %}
-{{ loop.index }}. {{ task.description }}{% if task.get("status") == "done" %} (completed){% endif %}
-
-{% endif %}{% endfor %}
-```
+### Step 3
+Action: Click on the "Submit" button in the web form
+Expected result: Form is submitted, page is reloaded and "Thank you" message is shown
+---end_of_example---
-Knowing these rules, tell me, please list actions, step by step, in order, that the user should take to verify the task.
+If nothing needs to be tested for this task, instead of outputting the steps, just output a single word: DONE
diff --git a/core/prompts/troubleshooter/filter_files.prompt b/core/prompts/troubleshooter/filter_files.prompt
index 3a4279a43..99c9ebfc7 100644
--- a/core/prompts/troubleshooter/filter_files.prompt
+++ b/core/prompts/troubleshooter/filter_files.prompt
@@ -1,2 +1,2 @@
-{# This is the same template as for Developer's filter files #}
+{# This is the same template as for Developer's filter files because Troubleshooter is reusing it in a conversation #}
{% extends "developer/filter_files.prompt" %}
\ No newline at end of file
diff --git a/core/prompts/troubleshooter/filter_files_loop.prompt b/core/prompts/troubleshooter/filter_files_loop.prompt
new file mode 100644
index 000000000..74602d7de
--- /dev/null
+++ b/core/prompts/troubleshooter/filter_files_loop.prompt
@@ -0,0 +1,2 @@
+{# This is the same template as for Developer's filter files because Troubleshooter is reusing it in a conversation #}
+{% extends "developer/filter_files_loop.prompt" %}
\ No newline at end of file
diff --git a/core/prompts/troubleshooter/iteration.prompt b/core/prompts/troubleshooter/iteration.prompt
index fc5e9de55..f653c9a07 100644
--- a/core/prompts/troubleshooter/iteration.prompt
+++ b/core/prompts/troubleshooter/iteration.prompt
@@ -21,17 +21,14 @@ A part of the app is already finished.
{% include "partials/user_feedback.prompt" %}
-{% if test_instructions %}
+{% if state.current_task.test_instructions is defined %}
User was testing the current implementation of the app when they requested some changes to the app. These are the testing instructions:
```
-{% for step in test_instructions %}
-Step #{{ loop.index }}
-Action: {{ step.action }}
-Expected result: {{ step.result }}
-{% endfor %}
+{{ state.current_task.test_instructions }}
```
{% endif %}
+
{% if next_solution_to_try is not none %}
Focus on solving this issue in the following way:
```
diff --git a/core/prompts/troubleshooter/system.prompt b/core/prompts/troubleshooter/system.prompt
index 2c93e94f9..e69de29bb 100644
--- a/core/prompts/troubleshooter/system.prompt
+++ b/core/prompts/troubleshooter/system.prompt
@@ -1,3 +0,0 @@
-You are the Troubleshooter in a software development team.
-
-Your primary responsibility is to evaluate the application after each task is implemented, identify any bugs or user-requested changes, and propose an appropriate next step. You act as a QA analyst, bug hunter, and bug fixer all in one. You never assume correctness—you verify it through testing and user feedback.
\ No newline at end of file
diff --git a/core/state/state_manager.py b/core/state/state_manager.py
index 6223fa4dc..5f1abf785 100644
--- a/core/state/state_manager.py
+++ b/core/state/state_manager.py
@@ -1,32 +1,15 @@
import asyncio
import os.path
-import re
import traceback
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Optional
-from urllib.parse import urljoin
from uuid import UUID, uuid4
-import httpx
-from sqlalchemy import Row, inspect, select
-from sqlalchemy.exc import PendingRollbackError
from tenacity import retry, stop_after_attempt, wait_fixed
-from core.config import PYTHAGORA_API, FileSystemType, get_config
-from core.config.version import get_git_branch, get_git_commit, get_version
-from core.db.models import (
- Branch,
- ChatConvo,
- ChatMessage,
- ExecLog,
- File,
- FileContent,
- LLMRequest,
- Project,
- ProjectState,
- UserInput,
-)
-from core.db.models.specification import Complexity, Specification
+from core.config import FileSystemType, get_config
+from core.db.models import Branch, ExecLog, File, FileContent, LLMRequest, Project, ProjectState, UserInput
+from core.db.models.specification import Specification
from core.db.session import SessionManager
from core.disk.ignore import IgnoreMatcher
from core.disk.vfs import LocalDiskVFS, MemoryVFS, VirtualFileSystem
@@ -36,7 +19,6 @@
from core.telemetry import telemetry
from core.ui.base import UIBase
from core.ui.base import UserInput as UserInputData
-from core.utils.text import trim_logs
if TYPE_CHECKING:
from core.agents.base import BaseAgent
@@ -67,18 +49,6 @@ def __init__(self, session_manager: SessionManager, ui: Optional[UIBase] = None)
self.next_state = None
self.current_session = None
self.blockDb = False
- self.git_available = False
- self.git_used = False
- self.auto_confirm_breakdown = True
- self.save_llm_requests = False
- self.options = {}
- self.access_token = None
- self.async_tasks = None
- self.template = None
-
- # Determines whether we enable auto-debugging for frontend agent.
- # We want to avoid using auto-debugging only if user reloads on the iterate_frontend step
- self.fe_auto_debug = True
@asynccontextmanager
async def db_blocker(self):
@@ -91,90 +61,16 @@ async def db_blocker(self):
finally:
self.blockDb = False # Unset the block
- async def list_projects(self) -> list[Row]:
+ async def list_projects(self) -> list[Project]:
"""
- :return: List of projects
- """
- async with self.session_manager as session:
- return await Project.get_all_projects(session)
-
- async def get_referencing_files(self, project_state, file_content: str) -> list["File"]:
- if not self.current_session:
- raise ValueError("No database session open.")
- return await File.get_referencing_files(self.current_session, project_state, file_content)
+ List projects with branches
- async def list_projects_with_branches_states(self) -> list[Project]:
- """
- :return: List of projects with branches and states (old) - for debugging
+ :return: List of projects with all their branches.
"""
async with self.session_manager as session:
- return await Project.get_all_projects_with_branches_states(session)
-
- async def get_project_states(self, project_id: Optional[UUID], branch_id: Optional[UUID]) -> list[ProjectState]:
- return await ProjectState.get_project_states(self.current_session, project_id, branch_id)
-
- async def get_branches_for_project_id(self, project_id: UUID) -> list[Branch]:
- return await Project.get_branches_for_project_id(self.current_session, project_id)
-
- async def get_project_state_for_redo_task(self, project_state: ProjectState) -> Optional[ProjectState]:
- return await ProjectState.get_state_for_redo_task(self.current_session, project_state)
-
- async def get_project_state_by_id(self, state_id: UUID) -> Optional[ProjectState]:
- return await ProjectState.get_by_id(self.current_session, state_id)
-
- async def get_all_epics_and_tasks(self, branch_id: UUID) -> list:
- return await ProjectState.get_all_epics_and_tasks(self.current_session, branch_id)
-
- async def find_user_input(self, project_state, branch_id) -> Optional[list["UserInput"]]:
- return await UserInput.find_user_inputs(self.current_session, project_state, branch_id)
-
- async def get_file_for_project(self, state_id: UUID, path: str):
- return await Project.get_file_for_project(self.current_session, state_id, path)
-
- async def get_chat_history(self, convo_id) -> Optional[list["ChatMessage"]]:
- return await ChatConvo.get_chat_history(self.current_session, convo_id)
-
- async def get_project_state_for_convo_id(self, convo_id) -> Optional["ProjectState"]:
- return await ChatConvo.get_project_state_for_convo_id(self.current_session, convo_id)
-
- async def get_task_conversation_project_states(
- self, task_id: UUID, first_last_only: bool = False, limit: Optional[int] = 25
- ) -> Optional[list[ProjectState]]:
- """
- Get all project states for a specific task conversation.
- This retrieves all project states that are associated with a specific task
- """
- return await ProjectState.get_task_conversation_project_states(
- self.current_session, self.current_state.branch_id, task_id, first_last_only, limit
- )
-
- async def get_project_states_in_between(
- self, start_state_id: UUID, end_state_id: UUID, limit: Optional[int] = 100
- ) -> list[ProjectState]:
- """
- Get all project states in between two states.
- This retrieves all project states that are associated with a specific branch
- """
- return await ProjectState.get_project_states_in_between(
- self.current_session, self.current_state.branch_id, start_state_id, end_state_id, limit
- )
-
- async def get_fe_states(self, limit: Optional[int] = None) -> Optional[ProjectState]:
- return await ProjectState.get_fe_states(self.current_session, self.current_state.branch_id, limit)
-
- async def get_be_back_logs(self):
- """
- Get all project states for a specific branch.
- This retrieves all project states that are associated with a specific branch
- """
- return await ProjectState.get_be_back_logs(self.current_session, self.current_state.branch_id)
+ return await Project.get_all_projects(session)
- async def create_project(
- self,
- name: Optional[str] = "temp-project",
- project_type: Optional[str] = "node",
- folder_name: Optional[str] = "temp-project",
- ) -> Project:
+ async def create_project(self, name: str, folder_name: Optional[str] = None) -> Project:
"""
Create a new project and set it as the current one.
@@ -182,8 +78,7 @@ async def create_project(
:return: The Project object.
"""
session = await self.session_manager.start()
- project = Project(name=name, project_type=project_type, folder_name=folder_name)
- project.id = uuid4()
+ project = Project(name=name, folder_name=folder_name)
branch = Branch(project=project)
state = ProjectState.create_initial_state(branch)
session.add(project)
@@ -192,13 +87,12 @@ async def create_project(
# even for a new project, eg. offline changes check and stats updating
await state.awaitable_attrs.files
+ await session.commit()
+
log.info(
- f'Created new project "{name}" (id={project.id})\n'
- f'with default branch "{branch.name}" (id={branch.id})\n'
- f"and initial state id={state.id} (step_index={state.step_index})\n"
- f"Core version {get_version() if not None else 'unknown'}\n"
- f"Git hash {get_git_commit() if not None else 'unknown'}\n"
- f"Git branch {get_git_branch() if not None else 'unknown'}\n"
+ f'Created new project "{name}" (id={project.id}) '
+ f'with default branch "{branch.name}" (id={branch.id}) '
+ f"and initial state id={state.id} (step_index={state.step_index})"
)
await telemetry.trace_code_event("create-project", {"name": name})
@@ -207,42 +101,7 @@ async def create_project(
self.next_state = state
self.project = project
self.branch = branch
-
- # Store new project in Pythagora database
- error = None
- database_object = {
- "project_id": str(project.id),
- "project_name": project.name,
- "folder_name": project.folder_name,
- }
-
- for attempt in range(3):
- try:
- url = urljoin(PYTHAGORA_API, "projects/project")
- async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport()) as client:
- resp = await client.post(
- url,
- json=database_object,
- headers={"Authorization": f"Bearer {self.get_access_token()}"},
- )
-
- if resp.is_success:
- break
- elif resp.status_code in [401, 403]:
- access_token = await self.ui.send_token_expired()
- self.update_access_token(access_token)
- else:
- try:
- error = resp.json()["error"]
- except Exception as e:
- error = e
- log.warning(f"Failed to upload new project: {error}")
- await self.ui.send_message(f"Failed to upload new project. Retrying... \nError: {error}")
-
- except Exception as e:
- error = e
- log.warning(f"Failed to upload new project: {e}", exc_info=True)
-
+ self.file_system = await self.init_file_system(load_existing=False)
return project
async def delete_project(self, project_id: UUID) -> bool:
@@ -258,24 +117,12 @@ async def delete_project(self, project_id: UUID) -> bool:
log.info(f"Deleted project {project_id}.")
return bool(rows)
- async def update_specification(self, specification: Specification) -> Optional[Specification]:
- """
- Update the specification in the database.
-
- :param specification: The Specification object to update.
- :return: The updated Specification object or None if not found.
- """
- if not self.current_session:
- raise ValueError("No database session open.")
- return await Specification.update_specification(self.current_session, specification)
-
async def load_project(
self,
*,
project_id: Optional[UUID] = None,
branch_id: Optional[UUID] = None,
step_index: Optional[int] = None,
- project_state_id: Optional[UUID] = None,
) -> Optional[ProjectState]:
"""
Load project state from the database.
@@ -287,11 +134,9 @@ async def load_project(
If `step_index' is provided, load the state at the given step
of the branch instead of the last one.
- If `project_state_id` is provided, load the specific project state
-
The returned ProjectState will have branch and branch.project
relationships preloaded. All other relationships must be
- explicitly loaded using ProjectState.awaitable_attrs or
+ excplicitly loaded using ProjectState.awaitable_attrs or
AsyncSession.refresh.
:param project_id: Project ID (keyword-only, optional).
@@ -304,44 +149,33 @@ async def load_project(
log.info("Current session exists, rolling back changes.")
await self.rollback()
- branch = None
state = None
session = await self.session_manager.start()
if branch_id is not None:
branch = await Branch.get_by_id(session, branch_id)
+ if branch is not None:
+ if step_index:
+ state = await branch.get_state_at_step(step_index)
+ else:
+ state = await branch.get_last_state()
+
elif project_id is not None:
project = await Project.get_by_id(session, project_id)
if project is not None:
branch = await project.get_branch()
-
- if branch is None:
- await self.session_manager.close()
- log.debug(f"Unable to find branch (project_id={project_id}, branch_id={branch_id})")
- return None
-
- # Load state based on the provided parameters
- if step_index is not None:
- state = await branch.get_state_at_step(step_index)
- elif project_state_id is not None:
- state = await ProjectState.get_by_id(session, project_state_id)
- # Verify that the state belongs to the branch
- if state and state.branch_id != branch.id:
- log.warning(
- f"Project state {project_state_id} does not belong to branch {branch.id}, "
- "loading last state instead."
- )
- state = None
-
- # If no specific state was requested or found, get the last state
- if state is None:
- state = await branch.get_last_state()
+ if branch is not None:
+ if step_index:
+ state = await branch.get_state_at_step(step_index)
+ else:
+ state = await branch.get_last_state()
+ else:
+ raise ValueError("Project or branch ID must be provided.")
if state is None:
await self.session_manager.close()
log.debug(
- f"Unable to load project state (project_id={project_id}, branch_id={branch_id}, "
- f"step_index={step_index}, project_state_id={project_state_id})"
+ f"Unable to load project state (project_id={project_id}, branch_id={branch_id}, step_index={step_index})"
)
return None
@@ -349,27 +183,6 @@ async def load_project(
await state.delete_after()
await session.commit()
- # TODO: this is a temporary fix to unblock users!
- # TODO: REMOVE THIS AFTER 1 WEEK FROM THIS COMMIT
- # Process tasks before setting current state - trim logs from task descriptions before current task
- if state.tasks and state.current_task:
- try:
- # Find the current task index
- current_task_index = state.tasks.index(state.current_task)
-
- # Trim logs from all tasks before the current task
- for i in range(current_task_index):
- task = state.tasks[i]
- if task.get("description"):
- task["description"] = trim_logs(task["description"])
-
- # Flag tasks as modified so SQLAlchemy knows to save the changes
- state.flag_tasks_as_modified()
- except Exception as e:
- # Handle any error during log trimming gracefully
- log.warning(f"Error during log trimming: {e}, skipping log trimming")
- pass
-
self.current_session = session
self.current_state = state
self.branch = state.branch
@@ -377,12 +190,9 @@ async def load_project(
self.next_state = await state.create_next_state()
self.file_system = await self.init_file_system(load_existing=True)
log.debug(
- f"Loaded project {self.project} ({self.project.id})\n"
- f"Branch {self.branch} ({self.branch.id}\n"
- f"Step {state.step_index} (state id={state.id})\n"
- f"Core version {get_version() if not None else 'unknown'}\n"
- f"Git hash {get_git_commit() if not None else 'unknown'}\n"
- f"Git branch {get_git_branch() if not None else 'unknown'}\n"
+ f"Loaded project {self.project} ({self.project.id}) "
+ f"branch {self.branch} ({self.branch.id}"
+ f"step {state.step_index} (state id={state.id})"
)
if self.current_state.current_epic and self.current_state.current_task and self.ui:
@@ -401,34 +211,11 @@ async def load_project(
self.current_state.tasks,
)
- telemetry.set(
- "architecture",
- {
- "system_dependencies": self.current_state.specification.system_dependencies,
- "package_dependencies": self.current_state.specification.package_dependencies,
- },
- )
- telemetry.set("example_project", self.current_state.specification.example_project)
- telemetry.set("is_complex_app", self.current_state.specification.complexity != Complexity.SIMPLE)
- telemetry.set("templates", self.current_state.specification.templates)
-
return self.current_state
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
async def commit_with_retry(self):
- try:
- await self.current_session.commit()
- except PendingRollbackError as e:
- log.warning(f"Session in pending rollback state, rolling back and retrying: {str(e)}")
- # When PendingRollbackError occurs, we need to rollback the session
- # and re-add the next_state before retrying
- await self.current_session.rollback()
- if self.next_state:
- self.current_session.add(self.next_state)
- raise # Re-raise to trigger retry
- except Exception as e:
- log.error(f"Commit failed: {str(e)}")
- raise
+ await self.current_session.commit()
async def commit(self) -> ProjectState:
"""
@@ -499,24 +286,15 @@ async def log_llm_request(self, request_log: LLMRequestLog, agent: Optional["Bas
:param request_log: The request log to log.
"""
- # Always record telemetry regardless of save_llm_requests setting
- try:
- telemetry.record_llm_request(
- request_log.prompt_tokens + request_log.completion_tokens,
- request_log.duration,
- request_log.status != LLMRequestStatus.SUCCESS,
- )
- except Exception as e:
- if self.ui:
- log.error(f"An error occurred recording telemetry: {e}")
-
- # Only save to database if configured to do so
- if not self.session_manager.save_llm_requests:
- return
-
async with self.db_blocker():
try:
+ telemetry.record_llm_request(
+ request_log.prompt_tokens + request_log.completion_tokens,
+ request_log.duration,
+ request_log.status != LLMRequestStatus.SUCCESS,
+ )
LLMRequest.from_request_log(self.current_state, agent, request_log)
+
except Exception as e:
if self.ui:
await self.ui.send_message(f"An error occurred: {e}")
@@ -570,8 +348,6 @@ async def log_task_completed(self):
telemetry.inc("num_tasks")
if not self.next_state.unfinished_tasks:
if len(self.current_state.epics) == 1:
- telemetry.set("end_result", "success:frontend")
- elif len(self.current_state.epics) == 2:
telemetry.set("end_result", "success:initial-project")
else:
telemetry.set("end_result", "success:feature")
@@ -602,7 +378,6 @@ async def save_file(
:param path: The file path.
:param content: The file content.
:param metadata: Optional metadata (eg. description) to save with the file.
- If not provided, metadata will be reset to force LLM to re-describe the file with CodeMonkey.
:param from_template: Whether the file is part of a template.
"""
try:
@@ -615,11 +390,13 @@ async def save_file(
hash = self.file_system.hash_string(content)
async with self.db_blocker():
- file_content = await FileContent.store(self.current_session, hash, content, metadata)
+ file_content = await FileContent.store(self.current_session, hash, content)
- self.next_state.save_file(path, file_content)
- # if self.ui and not from_template:
- # await self.ui.open_editor(self.file_system.get_full_path(path))
+ file = self.next_state.save_file(path, file_content)
+ if self.ui and not from_template:
+ await self.ui.open_editor(self.file_system.get_full_path(path))
+ if metadata:
+ file.meta = metadata
if not from_template:
delta_lines = len(content.splitlines()) - len(original_content.splitlines())
@@ -658,8 +435,7 @@ async def init_file_system(self, load_existing: bool) -> VirtualFileSystem:
try:
return LocalDiskVFS(root, allow_existing=load_existing, ignore_matcher=ignore_matcher)
- except FileExistsError as e:
- log.debug(e)
+ except FileExistsError:
self.project.folder_name = self.project.folder_name + "-" + uuid4().hex[:7]
log.warning(f"Directory {root} already exists, changing project folder to {self.project.folder_name}")
await self.current_session.commit()
@@ -672,33 +448,9 @@ def get_full_project_root(self) -> str:
"""
config = get_config()
- if self.project is None or self.project.folder_name is None:
- return os.path.join(config.fs.workspace_root, "")
- return os.path.join(config.fs.workspace_root, self.project.folder_name)
-
- def get_full_parent_project_root(self) -> str:
- config = get_config()
-
- if self.project is None:
- raise ValueError("No project loaded")
- return config.fs.workspace_root
-
- def get_project_info(self) -> dict:
- """
- Get project info in the same format as _handle_project_info.
-
- :return: Dictionary containing project info.
- """
if self.project is None:
raise ValueError("No project loaded")
-
- return {
- "name": self.project.name,
- "id": str(self.project.id),
- "branchId": str(self.branch.id) if self.branch else None,
- "folderName": self.project.folder_name,
- "createdAt": self.project.created_at.isoformat() if self.project.created_at else None,
- }
+ return os.path.join(config.fs.workspace_root, self.project.folder_name)
async def import_files(self) -> tuple[list[File], list[File]]:
"""
@@ -721,6 +473,7 @@ async def import_files(self) -> tuple[list[File], list[File]]:
if saved_file and saved_file.content.content == content:
continue
+
# TODO: unify this with self.save_file() / refactor that whole bit
hash = self.file_system.hash_string(content)
log.debug(f"Importing file {path} (hash={hash}, size={len(content)} bytes)")
@@ -791,8 +544,6 @@ async def get_modified_files_with_content(self) -> list[dict]:
:return: List of dictionaries containing paths, old content,
and new content for new or modified files.
"""
- if not self.file_system:
- return []
modified_files = []
files_in_workspace = self.file_system.list()
@@ -810,8 +561,8 @@ async def get_modified_files_with_content(self) -> list[dict]:
modified_files.append(
{
"path": path,
- "old_content": saved_file_content, # Serialized content
- "new_content": content,
+ "file_old": saved_file_content, # Serialized content
+ "file_new": content,
}
)
@@ -822,8 +573,8 @@ async def get_modified_files_with_content(self) -> list[dict]:
modified_files.append(
{
"path": db_file.path,
- "old_content": db_file.content.content, # Serialized content
- "new_content": "", # Empty string as the file is removed
+ "file_old": db_file.content.content, # Serialized content
+ "file_new": "", # Empty string as the file is removed
}
)
@@ -833,214 +584,22 @@ def workspace_is_empty(self) -> bool:
"""
Returns whether the workspace has any files in them or is empty.
"""
- if not self.file_system:
- return False
return not bool(self.file_system.list())
- def get_implemented_pages(self) -> list[str]:
- """
- Get the list of implemented pages.
-
- :return: List of implemented pages.
- """
- # TODO - use self.current_state plus response from the FE iteration
- page_files = [file.path for file in self.next_state.files if "client/src/pages" in file.path]
- return page_files
-
- async def update_implemented_pages_and_apis(self):
- modified = False
- pages = self.get_implemented_pages()
- apis = await self.get_apis()
-
- # Get the current state of pages and apis from knowledge_base
- current_pages = self.next_state.knowledge_base.pages
- current_apis = self.next_state.knowledge_base.apis
-
- # Check if pages or apis have changed
- if pages != current_pages or apis != current_apis:
- modified = True
-
- if modified:
- self.next_state.knowledge_base.pages = pages
- self.next_state.knowledge_base.apis = apis
- self.next_state.flag_knowledge_base_as_modified()
- await self.ui.knowledge_base_update(
- {
- "pages": self.next_state.knowledge_base.pages,
- "apis": self.next_state.knowledge_base.apis,
- "user_options": self.next_state.knowledge_base.user_options,
- "utility_functions": self.next_state.knowledge_base.utility_functions,
- }
- )
-
- async def update_utility_functions(self, utility_function: dict):
- """
- Update the knowledge base with the utility function.
-
- :param utility_function: Utility function to update.
- """
- matched = False
- for kb_util_func in self.next_state.knowledge_base.utility_functions:
- if (
- utility_function["function_name"] == kb_util_func["function_name"]
- and utility_function["file"] == kb_util_func["file"]
- ):
- kb_util_func["return_value"] = utility_function["return_value"]
- kb_util_func["input_value"] = utility_function["input_value"]
- kb_util_func["status"] = utility_function["status"]
- matched = True
- self.next_state.flag_knowledge_base_as_modified()
- break
-
- if not matched:
- self.next_state.knowledge_base.utility_functions.append(utility_function)
- self.next_state.flag_knowledge_base_as_modified()
-
- await self.ui.knowledge_base_update(
- {
- "pages": self.next_state.knowledge_base.pages,
- "apis": self.next_state.knowledge_base.apis,
- "user_options": self.next_state.knowledge_base.user_options,
- "utility_functions": self.next_state.knowledge_base.utility_functions,
- }
- )
-
- async def get_apis(self) -> list[dict]:
- """
- Get the list of APIs.
-
- :return: List of APIs.
- """
- apis = []
- for file in self.next_state.files:
- if "client/src/api" not in file.path:
- continue
- session = inspect(file).async_session
- result = await session.execute(select(FileContent).where(FileContent.id == file.content_id))
- file_content = result.scalar_one_or_none()
- content = file_content.content
- lines = content.splitlines()
- for i, line in enumerate(lines):
- if "// Description:" in line:
- # TODO: Make this better!!!
- description = line.split("Description:")[1]
- endpoint = lines[i + 1].split("Endpoint:")[1] if len(lines[i + 1].split("Endpoint:")) > 1 else ""
- request = lines[i + 2].split("Request:")[1] if len(lines[i + 2].split("Request:")) > 1 else ""
- response = lines[i + 3].split("Response:")[1] if len(lines[i + 3].split("Response:")) > 1 else ""
- backend = await self.find_backend_implementation(endpoint)
-
- apis.append(
- {
- "description": description.strip(),
- "endpoint": endpoint.strip(),
- "request": request.strip(),
- "response": response.strip(),
- "locations": {
- "frontend": {
- "path": file.path,
- "line": i - 1,
- },
- "backend": backend,
- },
- "status": "implemented" if backend is not None else "mocked",
- }
- )
- return apis
-
- async def find_backend_implementation(self, endpoint_line: str) -> dict:
- if not endpoint_line:
- return None
-
- try:
- method = endpoint_line.split("/")[0].strip().lower().strip()
- endpoint = endpoint_line.strip().split("/")[-1].strip()
-
- if not method or not endpoint:
- return None
-
- if ":" in endpoint:
- pattern = re.compile(rf"{method}.*?[\'\"]/?{re.escape(endpoint)}[\'\"]", re.IGNORECASE)
- else:
- pattern = re.compile(rf"\b{method}\b.*?\b{endpoint}\b", re.IGNORECASE)
-
- file = next(
- (
- file
- for file in self.next_state.files
- if "server/" in file.path and pattern.search(file.content.content)
- ),
- None,
- )
-
- if not file:
- return None
-
- match = pattern.search(file.content.content)
- line_number = file.content.content[: match.start()].count("\n") + 1 if match else 0
- return {
- "path": file.path,
- "line": line_number,
- }
- except Exception as e:
- log.error(f"Error finding backend implementation: {e}")
- return None
-
- async def update_apis(self, files_with_implemented_apis: list[dict] = []):
- """
- Update the list of APIs.
- """
- apis = await self.get_apis()
- for file in files_with_implemented_apis:
- for endpoint in file["related_api_endpoints"]:
- api = next((api for api in apis if endpoint["endpoint"] in api["endpoint"]), None)
- if api is not None:
- api["status"] = "implemented"
- api["locations"]["backend"] = {
- "path": file["path"],
- "line": file["line"],
- }
- self.next_state.knowledge_base.apis = apis
- self.next_state.flag_knowledge_base_as_modified()
- await self.ui.knowledge_base_update(
- {
- "pages": self.next_state.knowledge_base.pages,
- "apis": self.next_state.knowledge_base.apis,
- "user_options": self.next_state.knowledge_base.user_options,
- "utility_functions": self.next_state.knowledge_base.utility_functions,
- }
- )
-
@staticmethod
- def get_input_required(content: str, file_path: str) -> list[int]:
+ def get_input_required(content: str) -> list[int]:
"""
Get the list of lines containing INPUT_REQUIRED keyword.
:param content: The file content to search.
- :param file_path: The file path.
:return: Indices of lines with INPUT_REQUIRED keyword, starting from 1.
"""
lines = []
-
- if ".env" not in file_path:
- return lines
-
for i, line in enumerate(content.splitlines(), start=1):
if "INPUT_REQUIRED" in line:
lines.append(i)
return lines
- def update_access_token(self, access_token: str):
- """
- Store the access token in the state manager.
- """
- self.access_token = access_token
-
- def get_access_token(self) -> Optional[str]:
- """
- Get the access token from the state manager.
- """
- return self.access_token or None
-
__all__ = ["StateManager"]
diff --git a/core/telemetry/__init__.py b/core/telemetry/__init__.py
index 4f079ba86..16e62f608 100644
--- a/core/telemetry/__init__.py
+++ b/core/telemetry/__init__.py
@@ -69,9 +69,9 @@ def clear_data(self):
self.data = {
# System platform
"platform": sys.platform,
- # Python version used
+ # Python version used for GPT Pilot
"python_version": sys.version,
- # Core version
+ # GPT Pilot version
"pilot_version": get_version(),
# Pythagora VSCode Extension version
"extension_version": None,
@@ -86,8 +86,8 @@ def clear_data(self):
"updated_prompt": None,
# App complexity
"is_complex_app": None,
- # Optional templates used for the project
- "templates": None,
+ # Optional template used for the project
+ "template": None,
# Optional, example project selected by the user
"example_project": None,
# Optional user contact email
@@ -136,7 +136,7 @@ def clear_counters(self):
"num_tasks": 0,
# Number of seconds elapsed during development
"elapsed_time": 0,
- # Total number of lines created by Pythagora
+ # Total number of lines created by GPT Pilot
"created_lines": 0,
# End result of development:
# - success:initial-project
@@ -150,7 +150,7 @@ def clear_counters(self):
"is_continuation": False,
# Optional user feedback
"user_feedback": None,
- # If Core crashes, record diagnostics
+ # If GPT Pilot crashes, record diagnostics
"crash_diagnostics": None,
# Statistics for large requests
"large_requests": None,
@@ -319,7 +319,7 @@ def calculate_statistics(self):
"median_time": sorted(self.slow_requests)[n_slow // 2] if n_slow > 0 else None,
}
- async def send(self, event: str = "pythagora-core-telemetry"):
+ async def send(self, event: str = "pilot-telemetry"):
"""
Send telemetry data to the phone-home endpoint.
diff --git a/core/templates/base.py b/core/templates/base.py
index 752ae3d49..4194b945f 100644
--- a/core/templates/base.py
+++ b/core/templates/base.py
@@ -1,4 +1,3 @@
-import asyncio
from json import loads
from os.path import dirname, join
from typing import TYPE_CHECKING, Any, Optional, Type
@@ -21,9 +20,6 @@ class NoOptions(BaseModel):
Options class for templates that do not require any options.
"""
- class Config:
- extra = "allow"
-
pass
@@ -90,7 +86,7 @@ async def apply(self) -> Optional[str]:
state = self.state_manager.current_state
project_name = state.branch.project.name
project_folder = state.branch.project.folder_name
- project_description = self.state_manager.current_state.specification.description
+ project_description = state.specification.description
log.info(f"Applying project template {self.name} with options: {self.options_dict}")
@@ -103,7 +99,6 @@ async def apply(self) -> Optional[str]:
"random_secret": uuid4().hex,
"options": self.options_dict,
},
- self.state_manager.file_system.root,
self.filter,
)
@@ -117,10 +112,6 @@ async def apply(self) -> Optional[str]:
from_template=True,
)
- self.state_manager.async_tasks.append(asyncio.create_task(self.install_hook_template()))
- return self.get_summary()
-
- async def install_hook_template(self) -> Any:
try:
await self.install_hook()
except Exception as err:
@@ -129,7 +120,6 @@ async def install_hook_template(self) -> Any:
exc_info=True,
)
- def get_summary(self):
return self.info_renderer.render_template(
join(self.path, "summary.tpl"),
{
diff --git a/core/templates/info/vite_react/summary.tpl b/core/templates/info/vite_react/summary.tpl
deleted file mode 100644
index 99dad0ab7..000000000
--- a/core/templates/info/vite_react/summary.tpl
+++ /dev/null
@@ -1,92 +0,0 @@
-IMPORTANT:
-This app has 2 parts:
-
-** #1 Frontend **
- * ReactJS based frontend in `client/` folder using Vite devserver
- * Integrated shadcn-ui component library with Tailwind CSS framework
- * Client-side routing using `react-router-dom` with page components defined in `client/src/pages/` and other components in `client/src/components`
- * It is running on port 5173 and this port should be used for user testing when possible
- * All requests to the backend need to go to an endpoint that starts with `/api/` (e.g. `/api/companies`)
- * Server proxy configuration is already configured and should not be changed in any way!
- * Implememented pages:
- * Home - home (index) page (`/`){% if options.auth %}
- * Login - login page (`/login/`) - on login, stores the auth tokens to `accessToken` and `refreshToken` variables in local storage
- * Register - register page (`/register/`) - on register, store **ONLY** the `accessToken` variable in local storage{% endif %}
-
-** #2 Backend **
- * Express-based server implementing REST API endpoints in `api/`
- * Has codebase inside "server/" folder
- * Backend is running on port 3000
- * MongoDB database support with Mongoose{% if options.auth %}
- * Token-based authentication (using bearer access and refresh tokens)
- * User authentication (email + password):
- * login/register API endpoints in `/server/routes/auth.js`
- * authorization middleware in `/server/routes/middleware/auth.js`
- * user management logic in `/server/routes/services/user.js`
- * User authentication is implemented and doesn't require any additional work{% endif %}
-
-
-Concurrently is used to run both client and server together with a single command (`npm run start`).
-
-** IMPORTANT - Mocking data on the frontend **
-All API requests from the frontend to the backend must be defined in files inside the api folder (you must never make requests directly from the components) and the data must be mocked during the frontend implementation. Make sure to always add an API request whenever something needs to be sent or fetched from the backend.
-When you add mock data, make sure to mock the data in files in the `client/src/api` folder and above each mocked API request, add a structure that is expected from the API with fields `Description`, `Endpoint`, `Request`, and `Response`. You **MUST NOT** add mock data anywhere else in the frontend codebase.
-Mocking example:
-
-The base client/src/api/api.ts is already created so here are 2 examples for how to write functions to get data from the backend with the mock data:
-—EXAMPLE_1 (file `client/src/api/companies.ts`) —
-import api from './api';
-
-// Description: Get a list of Companies
-// Endpoint: GET /api/companies
-// Request: {}
-// Response: { companies: Array<{ domain: string, name: string, lastContact: string }> }
-export const getCompanies = () => {
- // Mocking the response
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({
- companies: [
- {domain: 'google.com', name: 'Google', lastContact: '2021-08-01'},
- {domain: 'facebook.com', name: 'Facebook', lastContact: '2021-08-02'},
- {domain: 'microsoft.com', name: 'Microsoft', lastContact: '2021-08-03'},
- ],
- });
- }, 500);
- });
- // Uncomment the below lines to make an actual API call
- // try {
- // return await api.get('/api/companies', data);
- // } catch (error) {
- // throw new Error(error?.response?.data?.error || error.message);
- // }
-}
-—END_OF_EXAMPLE_1—
-
-—EXAMPLE_2 (file `client/src/api/work.ts`) —
-import api from './api';
-
-// Description: Add a new Work
-// Endpoint: POST /api/work
-// Request: { work: string, driveLink: string }
-// Response: { success: boolean, message: string }
-export const addWork = (data: { work: string; driveLink: string }) => {
- // Mocking the response
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({success: true, message: 'Work added successfully'});
- }, 500);
- });
- // Uncomment the below lines to make an actual API call
- // try {
- // return await api.post('/api/work/add', data);
- // } catch (error) {
- // throw new Error(error?.response?.data?.error || error.message);
- // }
-}
-—END_OF_EXAMPLE_2—
-
-Whenever you add an API request from the frontend, make sure to wrap the request in try/catch block and in the catch block, return `throw new Error(error?.response?.data?.message || error.message);` - in the place where the API request function is being called, show a toast message with an error.
-
-**IMPORTANT**
-Mongodb is being used as a database so whenever you need to take an `id` of an object on frontend, make sure to take `_id`. For example, if you have a company object, whenever you want to set an id for an element, you should get `company._id` instead of `company.id`.
diff --git a/core/templates/info/vite_react_swagger/summary.tpl b/core/templates/info/vite_react_swagger/summary.tpl
deleted file mode 100644
index 3f8aa603a..000000000
--- a/core/templates/info/vite_react_swagger/summary.tpl
+++ /dev/null
@@ -1,77 +0,0 @@
-IMPORTANT:
-This app has 2 parts:
-
-** #1 Frontend **
- * ReactJS based frontend in root folder using Vite devserver
- * Integrated shadcn-ui component library with Tailwind CSS framework
- * Client-side routing using `react-router-dom` with page components defined in `src/pages/` and other components in `src/components`
- * It is running on port 5173 and this port should be used for user testing when possible
- * All requests to the backend need to go to an endpoint that starts with `/api/` (e.g. `/api/companies`)
- * Server proxy configuration is already configured and should not be changed in any way!
- * Implememented pages:
- * Home - home (index) page (`/`){% if options.auth %}
- * Login - login page (`/login/`) - on login, stores the auth tokens to `accessToken` and `refreshToken` variables in local storage
- * Register - register page (`/register/`) - on register, store **ONLY** the `accessToken` variable in local storage{% endif %}
-
-** IMPORTANT - Mocking data on the frontend **
-All API requests from the frontend to the backend must be defined in files inside the api folder (you must never make requests directly from the components) and the data must be mocked during the frontend implementation. Make sure to always add an API request whenever something needs to be sent or fetched from the backend.
-When you add mock data, make sure to mock the data in files in the `src/api` folder and above each mocked API request, add a structure that is expected from the API with fields `Description`, `Endpoint`, `Request`, and `Response`. You **MUST NOT** add mock data anywhere else in the frontend codebase.
-Mocking example:
-
-The base src/api/api.ts is already created so here are 2 examples for how to write functions to get data from the backend with the mock data:
-—EXAMPLE_1 (file `src/api/companies.ts`) —
-import api from './api';
-
-// Description: Get a list of Companies
-// Endpoint: GET /api/companies
-// Request: {}
-// Response: { companies: Array<{ domain: string, name: string, lastContact: string }> }
-export const getCompanies = () => {
- // Mocking the response
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({
- companies: [
- {domain: 'google.com', name: 'Google', lastContact: '2021-08-01'},
- {domain: 'facebook.com', name: 'Facebook', lastContact: '2021-08-02'},
- {domain: 'microsoft.com', name: 'Microsoft', lastContact: '2021-08-03'},
- ],
- });
- }, 500);
- });
- // Uncomment the below lines to make an actual API call
- // try {
- // return await api.get('/api/companies', data);
- // } catch (error) {
- // throw new Error(error?.response?.data?.error || error.message);
- // }
-}
-—END_OF_EXAMPLE_1—
-
-—EXAMPLE_2 (file `src/api/work.ts`) —
-import api from './api';
-
-// Description: Add a new Work
-// Endpoint: POST /api/work
-// Request: { work: string, driveLink: string }
-// Response: { success: boolean, message: string }
-export const addWork = (data: { work: string; driveLink: string }) => {
- // Mocking the response
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({success: true, message: 'Work added successfully'});
- }, 500);
- });
- // Uncomment the below lines to make an actual API call
- // try {
- // return await api.post('/api/work/add', data);
- // } catch (error) {
- // throw new Error(error?.response?.data?.error || error.message);
- // }
-}
-—END_OF_EXAMPLE_2—
-
-Whenever you add an API request from the frontend, make sure to wrap the request in try/catch block and in the catch block, return `throw new Error(error?.response?.data?.message || error.message);` - in the place where the API request function is being called, show a toast message with an error.
-
-**IMPORTANT**
-Mongodb is being used as a database so whenever you need to take an `id` of an object on frontend, make sure to take `_id`. For example, if you have a company object, whenever you want to set an id for an element, you should get `company._id` instead of `company.id`.
diff --git a/core/templates/javascript_react.py b/core/templates/javascript_react.py
index 06049c193..2778032b7 100644
--- a/core/templates/javascript_react.py
+++ b/core/templates/javascript_react.py
@@ -13,9 +13,9 @@ class JavascriptReactProjectTemplate(BaseProjectTemplate):
".gitignore": "Specifies patterns to exclude files and directories from being tracked by Git version control system. It is used to prevent certain files from being committed to the repository.",
"package.json": "Standard Nodejs package metadata file, specifies dependencies and start scripts. It also specifies that the project is a module.",
"public/.gitkeep": "Empty file",
- "src/app.css": "Contains styling rules for the root element of the application, setting a maximum width, centering it on the page, adding padding, and aligning text to the center.",
+ "src/App.css": "Contains styling rules for the root element of the application, setting a maximum width, centering it on the page, adding padding, and aligning text to the center.",
"src/index.css": "Defines styling rules for the root element, body, and h1 elements of a web page.",
- "src/App.jsx": "Defines a functional component that serves as the root component in the project. The component is exported as the default export. References: src/app.css",
+ "src/App.jsx": "Defines a functional component that serves as the root component in the project. The component is exported as the default export. References: src/App.css",
"src/main.jsx": "Main entry point for a React application. It imports necessary modules, renders the main component 'App' inside a 'React.StrictMode' component, and mounts it to the root element in the HTML document. References: App.jsx, index.css",
"src/assets/.gitkeep": "Empty file",
}
diff --git a/core/templates/registry.py b/core/templates/registry.py
index b8035ab4d..a07a25a39 100644
--- a/core/templates/registry.py
+++ b/core/templates/registry.py
@@ -2,10 +2,8 @@
from core.log import get_logger
-# from .javascript_react import JavascriptReactProjectTemplate
-# from .node_express_mongoose import NodeExpressMongooseProjectTemplate
-from .vite_react import ViteReactProjectTemplate
-from .vite_react_swagger import ViteReactSwaggerProjectTemplate
+from .javascript_react import JavascriptReactProjectTemplate
+from .node_express_mongoose import NodeExpressMongooseProjectTemplate
# from .react_express import ReactExpressProjectTemplate
@@ -15,16 +13,13 @@
class ProjectTemplateEnum(str, Enum):
"""Choices of available project templates."""
- # JAVASCRIPT_REACT = JavascriptReactProjectTemplate.name
- # NODE_EXPRESS_MONGOOSE = NodeExpressMongooseProjectTemplate.name
- VITE_REACT = ViteReactProjectTemplate.name
+ JAVASCRIPT_REACT = JavascriptReactProjectTemplate.name
+ NODE_EXPRESS_MONGOOSE = NodeExpressMongooseProjectTemplate.name
# REACT_EXPRESS = ReactExpressProjectTemplate.name
PROJECT_TEMPLATES = {
- # JavascriptReactProjectTemplate.name: JavascriptReactProjectTemplate,
- # NodeExpressMongooseProjectTemplate.name: NodeExpressMongooseProjectTemplate,
- ViteReactProjectTemplate.name: ViteReactProjectTemplate,
- ViteReactSwaggerProjectTemplate.name: ViteReactSwaggerProjectTemplate,
+ JavascriptReactProjectTemplate.name: JavascriptReactProjectTemplate,
+ NodeExpressMongooseProjectTemplate.name: NodeExpressMongooseProjectTemplate,
# ReactExpressProjectTemplate.name: ReactExpressProjectTemplate,
}
diff --git a/core/templates/render.py b/core/templates/render.py
index dc3e41338..e999f8dba 100644
--- a/core/templates/render.py
+++ b/core/templates/render.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import os
from os import walk
from os.path import join, relpath
from pathlib import Path
@@ -68,13 +67,12 @@ def render_template(self, template: str, context: Any) -> str:
tpl_object = self.jinja_env.get_template(template)
return tpl_object.render(context)
- def render_tree(self, root: str, context: Any, full_root_dir: str, filter: Callable = None) -> dict[str, str]:
+ def render_tree(self, root: str, context: Any, filter: Callable = None) -> dict[str, str]:
"""
Render a tree folder structure of templates using provided context
:param root: Root of the tree (relative to `template_dir`).
:param context: Context to render the templates with.
- :param full_root_dir: Full path to the root of the tree.
:param filter: If defined, will be called for each file to check if it
needs to be processed and determine output file path.
:return: A flat dictionary with path => content structure.
@@ -105,23 +103,8 @@ def render_tree(self, root: str, context: Any, full_root_dir: str, filter: Calla
for path, subdirs, files in walk(full_root):
for file in files:
file_path = join(path, file) # actual full path of the template file
- output_location = Path(file_path).relative_to(full_root).as_posix() # template relative to tree root
-
- # Skip .DS_Store files
- if file == ".DS_Store":
- continue
- elif file.endswith(
- (".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot")
- ):
- with open(file_path, "rb") as f:
- content = f.read()
- final_path = join(full_root_dir, output_location)
- os.makedirs(os.path.dirname(final_path), exist_ok=True)
- with open(final_path, "wb") as out:
- out.write(content)
- continue
-
tpl_location = relpath(file_path, self.template_dir) # template relative to template_dir
+ output_location = Path(file_path).relative_to(full_root).as_posix() # template relative to tree root
if filter:
output_location = filter(output_location)
@@ -129,7 +112,6 @@ def render_tree(self, root: str, context: Any, full_root_dir: str, filter: Calla
continue
contents = self.render_template(tpl_location, context)
- if contents != "":
- retval[output_location] = contents
+ retval[output_location] = contents
return retval
diff --git a/core/templates/tree/add_raw_tags.py b/core/templates/tree/add_raw_tags.py
deleted file mode 100644
index 574a61664..000000000
--- a/core/templates/tree/add_raw_tags.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import os
-import sys
-
-
-def add_raw_tags_to_file(file_path):
- """Add {% raw %} at the beginning and {% endraw %} at the end of the file, if not already present."""
- try:
- # Open the file and read the contents
- with open(file_path, "r", encoding="utf-8") as file:
- content = file.read()
-
- # Check if the tags are already present
- if content.startswith("{% raw %}") and content.endswith("{% endraw %}\n"):
- print(f"Skipping file (tags already added): {file_path}")
- return
-
- # Add {% raw %} at the beginning and {% endraw %} at the end
- modified_content = f"{'{% raw %}'}\n{content}\n{'{% endraw %}'}"
-
- # Write the modified content back to the file
- with open(file_path, "w", encoding="utf-8") as file:
- file.write(modified_content)
-
- print(f"Processed file: {file_path}")
-
- except Exception as e:
- print(f"Error processing {file_path}: {e}")
-
-
-def process_directory(directory):
- """Recursively process all files in the given directory."""
- for root, dirs, files in os.walk(directory):
- for file in files:
- # Construct the full file path
- file_path = os.path.join(root, file)
-
- # Process the file
- add_raw_tags_to_file(file_path)
-
-
-if __name__ == "__main__":
- # Check if the directory path argument is provided
- if len(sys.argv) != 2:
- print("Usage: python add_raw_tags.py ")
- sys.exit(1)
-
- # Get the directory path from the command line argument
- directory_path = sys.argv[1]
-
- # Check if the provided directory exists
- if not os.path.isdir(directory_path):
- print(f"Error: The directory '{directory_path}' does not exist.")
- sys.exit(1)
-
- # Process the directory
- process_directory(directory_path)
diff --git a/core/templates/tree/react_express/ui/pages/Login.jsx b/core/templates/tree/react_express/ui/pages/Login.jsx
index 0bfffda7d..f210dcff1 100644
--- a/core/templates/tree/react_express/ui/pages/Login.jsx
+++ b/core/templates/tree/react_express/ui/pages/Login.jsx
@@ -1,94 +1,98 @@
-import { useState } from "react"
-import { useForm } from "react-hook-form"
-import { useNavigate } from "react-router-dom"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
+import React, { useState } from 'react';
+import axios from 'axios';
+import { useNavigate } from 'react-router-dom';
+import { AlertDestructive } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
-} from "@/components/ui/card"
-import { useToast } from "@/hooks/useToast"
-import { LogIn } from "lucide-react"
-import { useAuth } from "@/contexts/AuthContext"
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
-type LoginForm = {
- email: string
- password: string
-}
+export default function Login() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const navigate = useNavigate();
-export function Login() {
- const [loading, setLoading] = useState(false)
- const { toast } = useToast()
- const navigate = useNavigate()
- const { login } = useAuth()
- const { register, handleSubmit } = useForm()
-
- const onSubmit = async (data: LoginForm) => {
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+ setLoading('');
try {
- setLoading(true)
- await login(data.email, data.password)
- toast({
- title: "Success",
- description: "Logged in successfully",
- })
- navigate("/")
- } catch (error: any) {
- toast({
- variant: "destructive",
- title: "Error",
- description: error.message || "An error occurred",
- })
+ const response = await axios.post('/api/auth/login', { email, password });
+ localStorage.setItem('token', response.data.token); // Save token to local storage
+ navigate('/'); // Redirect to Home
+ } catch (error) {
+ console.error('Login error:', error);
+ setError(error.response?.data?.error || 'An unexpected error occurred');
} finally {
- setLoading(false)
+ setLoading(false);
}
- }
+ };
return (
-
-
+
+
+ {error && }
- Welcome back
- Enter your credentials to continue
+ Login
+
+ Enter your email below to login to your account
+
-