This commit is contained in:
aashankhan2981
2025-02-07 18:31:09 +05:00
188 changed files with 9972 additions and 957 deletions

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ config.json
poetry.lock poetry.lock
.DS_Store .DS_Store
*.log *.log
pythagora-vs-code.vsix

168
Dockerfile Normal file
View File

@@ -0,0 +1,168 @@
# Use Ubuntu 22.04 as the base image with multi-arch support
FROM ubuntu:22.04
# Set environment to prevent interactive prompts during builds
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
# Use buildx args for multi-arch support
ARG TARGETPLATFORM
ARG BUILDPLATFORM
# Update package list and install prerequisites
RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common \
build-essential \
curl \
git \
gnupg \
tzdata \
openssh-server \
inotify-tools \
vim \
nano \
&& rm -rf /var/lib/apt/lists/*
# Add deadsnakes PPA for Python 3.12 and install Python
RUN add-apt-repository ppa:deadsnakes/ppa -y && apt-get update && \
apt-get install -y --no-install-recommends \
python3.12 \
python3.12-venv \
python3.12-dev \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Set Python 3.12 as the default python3 and python
RUN 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
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
apt-get install -y nodejs && \
node --version && npm --version
# MongoDB installation with platform-specific approach
RUN case "$TARGETPLATFORM" in \
"linux/amd64") \
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=amd64 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 \
;; \
"linux/arm64"|"linux/arm64/v8") \
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=arm64 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 "Unsupported platform: $TARGETPLATFORM" && exit 1 \
;; \
esac \
&& rm -rf /var/lib/apt/lists/*
# Configure SSH
RUN mkdir -p /run/sshd \
&& sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config \
&& sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config \
&& sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config \
&& sed -i 's/#ChallengeResponseAuthentication yes/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
ENV PYTH_INSTALL_DIR=/pythagora
# Set up work directory
WORKDIR ${PYTH_INSTALL_DIR}/pythagora-core
# Add Python requirements
ADD requirements.txt .
# Create and activate a virtual environment, then install dependencies
RUN python3 -m venv venv && \
. venv/bin/activate && \
pip install -r requirements.txt
# Copy application files
ADD main.py .
ADD core core
ADD config-docker.json config.json
# Set the virtual environment to be automatically activated
ENV VIRTUAL_ENV=${PYTH_INSTALL_DIR}/pythagora-core/venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PYTHAGORA_DATA_DIR=${PYTH_INSTALL_DIR}/pythagora-core/data/
RUN mkdir -p data
# Expose MongoDB and application ports
EXPOSE 27017 8000
# Create a group named "devusergroup" with a specific GID (1000, optional)
RUN groupadd -g 1000 devusergroup
ARG USERNAME=devuser
# Create a user named "devuser" with a specific UID (1000) and assign it to "devusergroup"
RUN useradd -m -u 1000 -g devusergroup -s /bin/bash $USERNAME
# Add the user to sudoers for admin privileges
RUN echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Create an embedded entrypoint script
ADD entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN chown -R $USERNAME:devusergroup /pythagora
# Copy SSH public key from secret
#RUN --mount=type=secret,id=ssh_public_key \
# mkdir -p /home/${USERNAME}/.ssh \
# && cat /run/secrets/ssh_public_key > /home/${USERNAME}/.ssh/authorized_keys \
# && chown -R ${USERNAME}:devusergroup /home/${USERNAME}/.ssh \
# && chmod 700 /home/${USERNAME}/.ssh \
# && chmod 600 /home/${USERNAME}/.ssh/authorized_keys
USER $USERNAME
RUN npx @puppeteer/browsers install chrome@stable
# add this before vscode... better caching of layers
ADD pythagora-vs-code.vsix /var/init_data/pythagora-vs-code.vsix
RUN mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
# ARG commitHash
# # VS Code server installation with platform-specific handling
# RUN case "$TARGETPLATFORM" in \
# "linux/amd64") \
# mkdir -p ~/.vscode-server/cli/servers/Stable-${commitHash} && \
# curl -fsSL https://update.code.visualstudio.com/commit:${commitHash}/server-linux-x64/stable -o server-linux-x64.tar.gz && \
# tar -xz -f server-linux-x64.tar.gz -C ~/.vscode-server/cli/servers/Stable-${commitHash} && \
# mv ~/.vscode-server/cli/servers/Stable-${commitHash}/vscode-server-linux-x64 ~/.vscode-server/cli/servers/Stable-${commitHash}/server \
# ;; \
# "linux/arm64"|"linux/arm64/v8") \
# mkdir -p ~/.vscode-server/cli/servers/Stable-${commitHash} && \
# curl -fsSL https://update.code.visualstudio.com/commit:${commitHash}/server-linux-arm64/stable -o server-linux-arm64.tar.gz && \
# tar -xz -f server-linux-arm64.tar.gz -C ~/.vscode-server/cli/servers/Stable-${commitHash} && \
# mv ~/.vscode-server/cli/servers/Stable-${commitHash}/vscode-server-linux-arm64 ~/.vscode-server/cli/servers/Stable-${commitHash}/server \
# ;; \
# *) \
# echo "Unsupported platform: $TARGETPLATFORM" && exit 1 \
# ;; \
# esac
# Install VS Code extension (platform-agnostic)
# RUN ~/.vscode-server/cli/servers/Stable-${commitHash}/server/bin/code-server --install-extension pythagora-vs-code.vsix
ADD on-event-extension-install.sh /var/init_data/on-event-extension-install.sh
# Create a workspace directory
RUN mkdir -p ${PYTH_INSTALL_DIR}/pythagora-core/workspace
RUN mkdir -p /home/$USERNAME/.vscode-server/cli/servers
USER root
RUN chmod +x /var/init_data/on-event-extension-install.sh
RUN chown -R devuser: /var/init_data/
# Set the entrypoint to the main application
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -107,37 +107,10 @@ 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. 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) # 🔎 [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. [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 ### 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: 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:
@@ -180,14 +153,6 @@ python main.py --delete <app_id>
Delete project with the specified `app_id`. Warning: this cannot be undone! Delete project with the specified `app_id`. Warning: this cannot be undone!
### Import projects from v0.1
```bash
python main.py --import-v0 <path>
```
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 ### 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: 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:

136
config-docker.json Normal file
View File

@@ -0,0 +1,136 @@
{
"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
}
},
"agent": {
"default": {
"provider": "openai",
"model": "gpt-4o-2024-05-13",
"temperature": 0.5
},
"BugHunter.check_logs": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20241022",
"temperature": 0.5
},
"CodeMonkey": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20241022",
"temperature": 0.0
},
"CodeMonkey.code_review": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20240620",
"temperature": 0.0
},
"CodeMonkey.describe_files": {
"provider": "openai",
"model": "gpt-4o-mini-2024-07-18",
"temperature": 0.0
},
"Frontend": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20241022",
"temperature": 0.0
},
"get_relevant_files": {
"provider": "openai",
"model": "gpt-4o-2024-05-13",
"temperature": 0.5
},
"Developer.parse_task": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20241022",
"temperature": 0.0
},
"SpecWriter": {
"provider": "openai",
"model": "gpt-4-0125-preview",
"temperature": 0.0
},
"Developer.breakdown_current_task": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20241022",
"temperature": 0.5
},
"TechLead.plan_epic": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20240620",
"temperature": 0.5
},
"TechLead.epic_breakdown": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20241022",
"temperature": 0.5
},
"Troubleshooter.generate_bug_report": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20240620",
"temperature": 0.5
},
"Troubleshooter.get_run_command": {
"provider": "anthropic",
"model": "claude-3-5-sonnet-20240620",
"temperature": 0.0
}
},
"prompt": {
"paths": [
"/pythagora/pythagora-core/core/prompts"
]
},
"log": {
"level": "DEBUG",
"format": "%(asctime)s %(levelname)s [%(name)s] %(message)s",
"output": "pythagora.log"
},
"db": {
"url": "sqlite+aiosqlite:///data/database/pythagora.db",
"debug_sql": 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
}
}

View File

@@ -1,3 +1,4 @@
import json
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional
@@ -16,7 +17,6 @@ from core.templates.registry import (
PROJECT_TEMPLATES, PROJECT_TEMPLATES,
ProjectTemplateEnum, ProjectTemplateEnum,
) )
from core.ui.base import ProjectStage
ARCHITECTURE_STEP_NAME = "Project architecture" ARCHITECTURE_STEP_NAME = "Project architecture"
WARN_SYSTEM_DEPS = ["docker", "kubernetes", "microservices"] WARN_SYSTEM_DEPS = ["docker", "kubernetes", "microservices"]
@@ -97,8 +97,6 @@ class Architect(BaseAgent):
display_name = "Architect" display_name = "Architect"
async def run(self) -> AgentResponse: async def run(self) -> AgentResponse:
await self.ui.send_project_stage(ProjectStage.ARCHITECTURE)
spec = self.current_state.specification.clone() spec = self.current_state.specification.clone()
if spec.example_project: if spec.example_project:
@@ -137,28 +135,28 @@ class Architect(BaseAgent):
) )
tpl: TemplateSelection = await llm(convo, parser=JSONParser(TemplateSelection)) tpl: TemplateSelection = await llm(convo, parser=JSONParser(TemplateSelection))
templates = {} templates = {}
if tpl.template: # if tpl.template:
answer = await self.ask_question( # answer = await self.ask_question(
f"Do you want to use the '{tpl.template.name}' template?", # f"Do you want to use the '{tpl.template.name}' template?",
buttons={"yes": "Yes", "no": "No"}, # buttons={"yes": "Yes", "no": "No"},
default="yes", # default="yes",
buttons_only=True, # buttons_only=True,
hint="Project templates are here to speed up start of your app development and save tokens and time.\n" # 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" # "Choose 'Yes' to use suggested template for your app.\n"
"If you choose 'No', project will be created from scratch.", # "If you choose 'No', project will be created from scratch.",
) # )
#
if answer.button == "no": # if answer.button == "no":
return tpl.architecture, templates # return tpl.architecture, templates
#
template_class = PROJECT_TEMPLATES.get(tpl.template) # template_class = PROJECT_TEMPLATES.get(tpl.template)
if template_class: # if template_class:
options = await self.configure_template(spec, template_class) # options = await self.configure_template(spec, template_class)
templates[tpl.template] = template_class( # templates[tpl.template] = template_class(
options, # options,
self.state_manager, # self.state_manager,
self.process_manager, # self.process_manager,
) # )
return tpl.architecture, templates return tpl.architecture, templates
@@ -186,6 +184,7 @@ class Architect(BaseAgent):
spec.templates = {t.name: t.options_dict for t in templates.values()} 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.system_dependencies = [d.model_dump() for d in arch.system_dependencies]
spec.package_dependencies = [d.model_dump() for d in arch.package_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: 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] warn_system_deps = [dep.name for dep in arch.system_dependencies if dep.name.lower() in WARN_SYSTEM_DEPS]

View File

@@ -29,6 +29,7 @@ class BaseAgent:
prev_response: Optional["AgentResponse"] = None, prev_response: Optional["AgentResponse"] = None,
process_manager: Optional["ProcessManager"] = None, process_manager: Optional["ProcessManager"] = None,
data: Optional[Any] = None, data: Optional[Any] = None,
args: Optional[Any] = None,
): ):
""" """
Create a new agent. Create a new agent.
@@ -40,6 +41,7 @@ class BaseAgent:
self.prev_response = prev_response self.prev_response = prev_response
self.step = step self.step = step
self.data = data self.data = data
self.args = args
@property @property
def current_state(self) -> ProjectState: def current_state(self) -> ProjectState:
@@ -51,7 +53,7 @@ class BaseAgent:
"""Next state of the project (write-only).""" """Next state of the project (write-only)."""
return self.state_manager.next_state return self.state_manager.next_state
async def send_message(self, message: str): async def send_message(self, message: str, extra_info: Optional[str] = None):
""" """
Send a message to the user. Send a message to the user.
@@ -59,8 +61,11 @@ class BaseAgent:
setting the correct source and project state ID. setting the correct source and project state ID.
:param message: Message to send. :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)) await self.ui.send_message(
message + "\n", source=self.ui_source, project_state_id=str(self.current_state.id), extra_info=extra_info
)
async def ask_question( async def ask_question(
self, self,
@@ -69,9 +74,13 @@ class BaseAgent:
buttons: Optional[dict[str, str]] = None, buttons: Optional[dict[str, str]] = None,
default: Optional[str] = None, default: Optional[str] = None,
buttons_only: bool = False, buttons_only: bool = False,
initial_text: Optional[str] = None,
allow_empty: bool = False, allow_empty: bool = False,
full_screen: Optional[bool] = False,
hint: Optional[str] = None, hint: Optional[str] = None,
verbose: bool = True,
initial_text: Optional[str] = None,
extra_info: Optional[str] = None,
placeholder: Optional[str] = None,
) -> UserInput: ) -> UserInput:
""" """
Ask a question to the user and return the response. Ask a question to the user and return the response.
@@ -85,8 +94,12 @@ class BaseAgent:
:param default: Default button to select. :param default: Default button to select.
:param buttons_only: Only display buttons, no text input. :param buttons_only: Only display buttons, no text input.
:param allow_empty: Allow empty 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 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 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. :return: User response.
""" """
response = await self.ui.ask_question( response = await self.ui.ask_question(
@@ -95,10 +108,14 @@ class BaseAgent:
default=default, default=default,
buttons_only=buttons_only, buttons_only=buttons_only,
allow_empty=allow_empty, allow_empty=allow_empty,
full_screen=full_screen,
hint=hint, hint=hint,
verbose=verbose,
initial_text=initial_text, initial_text=initial_text,
source=self.ui_source, source=self.ui_source,
project_state_id=str(self.current_state.id), project_state_id=str(self.current_state.id),
extra_info=extra_info,
placeholder=placeholder,
) )
await self.state_manager.log_user_input(question, response) await self.state_manager.log_user_input(question, response)
return response return response

View File

@@ -1,15 +1,18 @@
import json
from enum import Enum from enum import Enum
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from core.agents.base import BaseAgent from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo from core.agents.convo import AgentConvo
from core.agents.mixins import ChatWithBreakdownMixin, TestSteps
from core.agents.response import AgentResponse from core.agents.response import AgentResponse
from core.config import CHECK_LOGS_AGENT_NAME, magic_words from core.config import CHECK_LOGS_AGENT_NAME, magic_words
from core.db.models.project_state import IterationStatus from core.db.models.project_state import IterationStatus
from core.llm.parser import JSONParser from core.llm.parser import JSONParser
from core.log import get_logger from core.log import get_logger
from core.telemetry import telemetry from core.telemetry import telemetry
from core.ui.base import ProjectStage, pythagora_source
log = get_logger(__name__) log = get_logger(__name__)
@@ -40,7 +43,7 @@ class ImportantLogsForDebugging(BaseModel):
logs: list[ImportantLog] = Field(description="Important logs that will help the human debug the current bug.") logs: list[ImportantLog] = Field(description="Important logs that will help the human debug the current bug.")
class BugHunter(BaseAgent): class BugHunter(ChatWithBreakdownMixin, BaseAgent):
agent_type = "bug-hunter" agent_type = "bug-hunter"
display_name = "Bug Hunter" display_name = "Bug Hunter"
@@ -63,23 +66,35 @@ class BugHunter(BaseAgent):
return await self.start_pair_programming() return await self.start_pair_programming()
async def get_bug_reproduction_instructions(self): async def get_bug_reproduction_instructions(self):
llm = self.get_llm(stream_output=True) await self.send_message("Finding a way to reproduce the bug ...")
convo = AgentConvo(self).template( llm = self.get_llm()
"get_bug_reproduction_instructions", convo = (
current_task=self.current_state.current_task, AgentConvo(self)
user_feedback=self.current_state.current_iteration["user_feedback"], .template(
user_feedback_qa=self.current_state.current_iteration["user_feedback_qa"], "get_bug_reproduction_instructions",
docs=self.current_state.docs, current_task=self.current_state.current_task,
next_solution_to_try=None, 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]
) )
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): async def check_logs(self, logs_message: str = None):
llm = self.get_llm(CHECK_LOGS_AGENT_NAME, stream_output=True) llm = self.get_llm(CHECK_LOGS_AGENT_NAME, stream_output=True)
convo = self.generate_iteration_convo_so_far() convo = self.generate_iteration_convo_so_far()
await self.ui.start_breakdown_stream()
human_readable_instructions = await llm(convo, temperature=0.5) 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 = ( convo = (
AgentConvo(self) AgentConvo(self)
.template( .template(
@@ -110,12 +125,24 @@ class BugHunter(BaseAgent):
async def ask_user_to_test(self, awaiting_bug_reproduction: bool = False, awaiting_user_test: bool = False): async def ask_user_to_test(self, awaiting_bug_reproduction: bool = False, awaiting_user_test: bool = False):
await self.ui.stop_app() await self.ui.stop_app()
test_instructions = self.current_state.current_iteration["bug_reproduction_description"] test_instructions = self.current_state.current_iteration["bug_reproduction_description"]
await self.send_message("You can reproduce the bug like this:\n\n" + test_instructions) await self.ui.send_message(
await self.ui.send_test_instructions(test_instructions) "Start the app and test it by following these instructions:\n\n", source=pythagora_source
)
await self.send_message("")
await self.ui.send_test_instructions(test_instructions, project_state_id=str(self.current_state.id))
if self.current_state.run_command: if self.current_state.run_command:
await self.ui.send_run_command(self.current_state.run_command) await self.ui.send_run_command(self.current_state.run_command)
await self.ask_question(
"Please test the app again.",
buttons={"done": "I am done testing"},
buttons_only=True,
default="continue",
extra_info="restart_app",
hint="Instructions for testing:\n\n" + self.current_state.current_iteration["bug_reproduction_description"],
)
if awaiting_user_test: if awaiting_user_test:
buttons = {"yes": "Yes, the issue is fixed", "no": "No", "start_pair_programming": "Start Pair Programming"} buttons = {"yes": "Yes, the issue is fixed", "no": "No", "start_pair_programming": "Start Pair Programming"}
user_feedback = await self.ask_question( user_feedback = await self.ask_question(
@@ -137,53 +164,38 @@ class BugHunter(BaseAgent):
awaiting_bug_reproduction = True awaiting_bug_reproduction = True
if awaiting_bug_reproduction: if awaiting_bug_reproduction:
# TODO how can we get FE and BE logs automatically?
buttons = { buttons = {
"copy_backend_logs": "Copy Backend Logs",
"continue": "Continue without logs",
"done": "Bug is fixed", "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", "start_pair_programming": "Start Pair Programming",
} }
backend_logs = await self.ask_question( await self.ui.send_project_stage(
"Please share the relevant Backend logs", {
"stage": ProjectStage.ADDITIONAL_FEEDBACK,
}
)
user_feedback = await self.ask_question(
"Please add any additional feedback that could help Pythagora solve this bug",
buttons=buttons, buttons=buttons,
default="continue", default="continue",
extra_info="collect_logs",
hint="Instructions for testing:\n\n" hint="Instructions for testing:\n\n"
+ self.current_state.current_iteration["bug_reproduction_description"], + self.current_state.current_iteration["bug_reproduction_description"],
) )
if backend_logs.button == "done": if user_feedback.button == "done":
self.next_state.complete_iteration() self.next_state.complete_iteration()
elif backend_logs.button == "start_pair_programming": return AgentResponse.done(self)
elif user_feedback.button == "start_pair_programming":
self.next_state.current_iteration["status"] = IterationStatus.START_PAIR_PROGRAMMING self.next_state.current_iteration["status"] = IterationStatus.START_PAIR_PROGRAMMING
self.next_state.flag_iterations_as_modified() self.next_state.flag_iterations_as_modified()
else: return AgentResponse.done(self)
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"} # TODO select only the logs that are new (with PYTHAGORA_DEBUGGING_LOG)
user_feedback = await self.ask_question( self.next_state.current_iteration["bug_hunting_cycles"][-1]["backend_logs"] = None
"Please add any additional feedback that could help Pythagora solve this bug", self.next_state.current_iteration["bug_hunting_cycles"][-1]["frontend_logs"] = None
buttons=buttons, self.next_state.current_iteration["bug_hunting_cycles"][-1]["user_feedback"] = user_feedback.text
default="continue", self.next_state.current_iteration["status"] = IterationStatus.HUNTING_FOR_BUG
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) return AgentResponse.done(self)
@@ -313,6 +325,7 @@ class BugHunter(BaseAgent):
docs=self.current_state.docs, docs=self.current_state.docs,
magic_words=magic_words, magic_words=magic_words,
next_solution_to_try=None, 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", [])[ hunting_cycles = self.current_state.current_iteration.get("bug_hunting_cycles", [])[

View File

@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from core.agents.base import BaseAgent from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo from core.agents.convo import AgentConvo
from core.agents.mixins import FileDiffMixin
from core.agents.response import AgentResponse, ResponseType from core.agents.response import AgentResponse, ResponseType
from core.config import CODE_MONKEY_AGENT_NAME, CODE_REVIEW_AGENT_NAME, DESCRIBE_FILES_AGENT_NAME 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.llm.parser import JSONParser, OptionalCodeBlockParser
@@ -54,7 +55,7 @@ class FileDescription(BaseModel):
) )
class CodeMonkey(BaseAgent): class CodeMonkey(FileDiffMixin, BaseAgent):
agent_type = "code-monkey" agent_type = "code-monkey"
display_name = "Code Monkey" display_name = "Code Monkey"
@@ -82,13 +83,15 @@ class CodeMonkey(BaseAgent):
attempt = data["attempt"] + 1 attempt = data["attempt"] + 1
feedback = data["feedback"] feedback = data["feedback"]
log.debug(f"Fixing file {file_name} after review feedback: {feedback} ({attempt}. attempt)") log.debug(f"Fixing file {file_name} after review feedback: {feedback} ({attempt}. attempt)")
await self.ui.send_file_status(file_name, "reworking") await self.ui.send_file_status(file_name, "reworking", source=self.ui_source)
else: else:
log.debug(f"Implementing file {file_name}") log.debug(f"Implementing file {file_name}")
if data is None: if data is None:
await self.ui.send_file_status(file_name, "updating" if file_content else "creating") await self.ui.send_file_status(
file_name, "updating" if file_content else "creating", source=self.ui_source
)
else: else:
await self.ui.send_file_status(file_name, "reworking") await self.ui.send_file_status(file_name, "reworking", source=self.ui_source)
self.next_state.action = "Updating files" self.next_state.action = "Updating files"
attempt = 1 attempt = 1
feedback = None feedback = None
@@ -175,7 +178,7 @@ class CodeMonkey(BaseAgent):
# ------------------------------ # ------------------------------
async def run_code_review(self, data: Optional[dict]) -> Union[AgentResponse, dict]: async def run_code_review(self, data: Optional[dict]) -> Union[AgentResponse, dict]:
await self.ui.send_file_status(data["path"], "reviewing") await self.ui.send_file_status(data["path"], "reviewing", source=self.ui_source)
if ( if (
data is not None data is not None
and not data["old_content"] and not data["old_content"]
@@ -202,15 +205,17 @@ class CodeMonkey(BaseAgent):
return await self.accept_changes(data["path"], data["old_content"], approved_content) 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: async def accept_changes(self, file_path: str, old_content: str, new_content: str) -> AgentResponse:
await self.ui.send_file_status(file_path, "done") await self.ui.send_file_status(file_path, "done", source=self.ui_source)
n_new_lines, n_del_lines = self.get_line_changes(old_content, new_content) 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) await self.ui.generate_diff(
file_path, old_content, new_content, n_new_lines, n_del_lines, source=self.ui_source
)
await self.state_manager.save_file(file_path, new_content) await self.state_manager.save_file(file_path, new_content)
self.next_state.complete_step() self.next_state.complete_step("save_file")
input_required = self.state_manager.get_input_required(new_content) input_required = self.state_manager.get_input_required(new_content, file_path)
if input_required: if input_required:
return AgentResponse.input_required( return AgentResponse.input_required(
self, self,
@@ -229,6 +234,7 @@ class CodeMonkey(BaseAgent):
task=task, task=task,
iteration=None, iteration=None,
current_task_index=current_task_index, current_task_index=current_task_index,
related_api_endpoints=task.get("related_api_endpoints", []),
) )
# TODO: We currently show last iteration to the code monkey; we might need to show the task # 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 # breakdown and all the iterations instead? To think about when refactoring prompts
@@ -333,35 +339,6 @@ class CodeMonkey(BaseAgent):
else: else:
return new_content, None 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 @staticmethod
def get_diff_hunks(file_name: str, old_content: str, new_content: str) -> list[str]: def get_diff_hunks(file_name: str, old_content: str, new_content: str) -> list[str]:
""" """

View File

@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
from core.agents.base import BaseAgent from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo from core.agents.convo import AgentConvo
from core.agents.mixins import RelevantFilesMixin from core.agents.mixins import ChatWithBreakdownMixin, RelevantFilesMixin
from core.agents.response import AgentResponse from core.agents.response import AgentResponse
from core.config import PARSE_TASK_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME from core.config import PARSE_TASK_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME
from core.db.models.project_state import IterationStatus, TaskStatus from core.db.models.project_state import IterationStatus, TaskStatus
@@ -15,6 +15,7 @@ from core.db.models.specification import Complexity
from core.llm.parser import JSONParser from core.llm.parser import JSONParser
from core.log import get_logger from core.log import get_logger
from core.telemetry import telemetry from core.telemetry import telemetry
from core.ui.base import ProjectStage
log = get_logger(__name__) log = get_logger(__name__)
@@ -23,6 +24,7 @@ class StepType(str, Enum):
COMMAND = "command" COMMAND = "command"
SAVE_FILE = "save_file" SAVE_FILE = "save_file"
HUMAN_INTERVENTION = "human_intervention" HUMAN_INTERVENTION = "human_intervention"
UTILITY_FUNCTION = "utility_function"
class CommandOptions(BaseModel): class CommandOptions(BaseModel):
@@ -38,6 +40,7 @@ class SaveFileOptions(BaseModel):
class SaveFileStep(BaseModel): class SaveFileStep(BaseModel):
type: Literal[StepType.SAVE_FILE] = StepType.SAVE_FILE type: Literal[StepType.SAVE_FILE] = StepType.SAVE_FILE
save_file: SaveFileOptions save_file: SaveFileOptions
related_api_endpoints: list[str] = Field(description="API endpoints that are implemented in this file", default=[])
class CommandStep(BaseModel): class CommandStep(BaseModel):
@@ -50,8 +53,18 @@ class HumanInterventionStep(BaseModel):
human_intervention_description: str 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[ Step = Annotated[
Union[SaveFileStep, CommandStep, HumanInterventionStep], Union[SaveFileStep, CommandStep, HumanInterventionStep, UtilityFunction],
Field(discriminator="type"), Field(discriminator="type"),
] ]
@@ -60,11 +73,14 @@ class TaskSteps(BaseModel):
steps: list[Step] steps: list[Step]
class Developer(RelevantFilesMixin, BaseAgent): class Developer(ChatWithBreakdownMixin, RelevantFilesMixin, BaseAgent):
agent_type = "developer" agent_type = "developer"
display_name = "Developer" display_name = "Developer"
async def run(self) -> AgentResponse: 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: if not self.current_state.unfinished_tasks:
log.warning("No unfinished tasks found, nothing to do (why am I called? is this a bug?)") log.warning("No unfinished tasks found, nothing to do (why am I called? is this a bug?)")
return AgentResponse.done(self) return AgentResponse.done(self)
@@ -119,9 +135,8 @@ class Developer(RelevantFilesMixin, BaseAgent):
log.debug(f"Breaking down the iteration {description}") log.debug(f"Breaking down the iteration {description}")
if self.current_state.files and self.current_state.relevant_files is None: if self.current_state.files and self.current_state.relevant_files is None:
return await self.get_relevant_files(user_feedback, description) await self.get_relevant_files(user_feedback, description)
await self.send_message("Breaking down the task into steps ...")
await self.ui.send_task_progress( 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, # iterations and reviews can be created only one at a time, so we are always on last one
n_tasks, n_tasks,
@@ -142,6 +157,7 @@ class Developer(RelevantFilesMixin, BaseAgent):
user_feedback_qa=None, user_feedback_qa=None,
next_solution_to_try=None, next_solution_to_try=None,
docs=self.current_state.docs, docs=self.current_state.docs,
test_instructions=json.loads(current_task.get("test_instructions") or "[]"),
) )
.assistant(description) .assistant(description)
.template("parse_task") .template("parse_task")
@@ -195,13 +211,14 @@ class Developer(RelevantFilesMixin, BaseAgent):
log.debug(f"Current state files: {len(self.current_state.files)}, relevant {self.current_state.relevant_files}") 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 # Check which files are relevant to the current task
if self.current_state.files and self.current_state.relevant_files is None: await self.get_relevant_files()
return await self.get_relevant_files()
current_task_index = self.current_state.tasks.index(current_task) 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()
related_api_endpoints = current_task.get("related_api_endpoints", [])
llm = self.get_llm(TASK_BREAKDOWN_AGENT_NAME, stream_output=True) llm = self.get_llm(TASK_BREAKDOWN_AGENT_NAME, stream_output=True)
convo = AgentConvo(self).template( convo = AgentConvo(self).template(
"breakdown", "breakdown",
@@ -209,10 +226,12 @@ class Developer(RelevantFilesMixin, BaseAgent):
iteration=None, iteration=None,
current_task_index=current_task_index, current_task_index=current_task_index,
docs=self.current_state.docs, docs=self.current_state.docs,
related_api_endpoints=related_api_endpoints,
) )
response: str = await llm(convo) response: str = await llm(convo)
convo.assistant(response)
await self.get_relevant_files(None, response) response = await self.chat_with_breakdown(convo, response)
self.next_state.tasks[current_task_index] = { self.next_state.tasks[current_task_index] = {
**current_task, **current_task,
@@ -221,8 +240,7 @@ class Developer(RelevantFilesMixin, BaseAgent):
self.next_state.flag_tasks_as_modified() self.next_state.flag_tasks_as_modified()
llm = self.get_llm(PARSE_TASK_AGENT_NAME) llm = self.get_llm(PARSE_TASK_AGENT_NAME)
await self.send_message("Breaking down the task into steps ...") convo.template("parse_task").require_schema(TaskSteps)
convo.assistant(response).template("parse_task").require_schema(TaskSteps)
response: TaskSteps = await llm(convo, parser=JSONParser(TaskSteps), temperature=0) response: TaskSteps = await llm(convo, parser=JSONParser(TaskSteps), temperature=0)
# There might be state leftovers from previous tasks that we need to clean here # There might be state leftovers from previous tasks that we need to clean here
@@ -289,7 +307,16 @@ class Developer(RelevantFilesMixin, BaseAgent):
buttons["skip"] = "Skip Task" buttons["skip"] = "Skip Task"
description = self.current_state.current_task["description"] description = self.current_state.current_task["description"]
await self.send_message("Starting new task with description:\n\n" + description) task_index = self.current_state.tasks.index(self.current_state.current_task) + 1
await self.ui.send_project_stage(
{
"stage": ProjectStage.STARTING_TASK,
"task_index": task_index,
}
)
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)
user_response = await self.ask_question( user_response = await self.ask_question(
"Do you want to execute the above task?", "Do you want to execute the above task?",
buttons=buttons, buttons=buttons,
@@ -329,3 +356,11 @@ class Developer(RelevantFilesMixin, BaseAgent):
log.info(f"Task description updated to: {user_response.text}") log.info(f"Task description updated to: {user_response.text}")
# Orchestrator will rerun us with the new task description # Orchestrator will rerun us with the new task description
return False 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)

View File

@@ -86,7 +86,9 @@ class Executor(BaseAgent):
q, q,
buttons={"yes": "Yes", "no": "No"}, buttons={"yes": "Yes", "no": "No"},
default="yes", default="yes",
buttons_only=True, buttons_only=False,
initial_text=cmd,
extra_info="remove_button_yes",
) )
if confirm.button == "no": if confirm.button == "no":
log.info(f"Skipping command execution of `{cmd}` (requested by user)") log.info(f"Skipping command execution of `{cmd}` (requested by user)")
@@ -95,6 +97,9 @@ class Executor(BaseAgent):
self.next_state.action = f'Skip "{cmd_name}"' self.next_state.action = f'Skip "{cmd_name}"'
return AgentResponse.done(self) return AgentResponse.done(self)
if confirm.button != "yes":
cmd = confirm.text
started_at = datetime.now(timezone.utc) started_at = datetime.now(timezone.utc)
log.info(f"Running command `{cmd}` with timeout {timeout}s") log.info(f"Running command `{cmd}` with timeout {timeout}s")
@@ -172,4 +177,4 @@ class Executor(BaseAgent):
information we give it. information we give it.
""" """
self.step = None self.step = None
self.next_state.complete_step() self.next_state.complete_step("command")

297
core/agents/frontend.py Normal file
View File

@@ -0,0 +1,297 @@
from uuid import uuid4
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
from core.agents.mixins import FileDiffMixin
from core.agents.response import AgentResponse
from core.config import FRONTEND_AGENT_NAME
from core.llm.parser import DescriptiveCodeBlockParser
from core.log import get_logger
from core.telemetry import telemetry
from core.templates.registry import PROJECT_TEMPLATES
from core.ui.base import ProjectStage
log = get_logger(__name__)
class Frontend(FileDiffMixin, BaseAgent):
agent_type = "frontend"
display_name = "Frontend"
async def run(self) -> AgentResponse:
if not self.current_state.epics:
finished = await self.init_frontend()
elif not self.current_state.epics[0]["messages"]:
finished = await self.start_frontend()
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()
return await self.end_frontend_iteration(finished)
async def init_frontend(self) -> bool:
"""
Builds frontend of the app.
:return: AgentResponse.done(self)
"""
await self.ui.send_project_stage({"stage": ProjectStage.PROJECT_DESCRIPTION})
description = await self.ask_question(
"Please describe the app you want to build.",
allow_empty=False,
full_screen=True,
)
description = description.text.strip()
auth_needed = await self.ask_question(
"Do you need authentication in your app (login, register, etc.)?",
buttons={
"yes": "Yes",
"no": "No",
},
buttons_only=True,
default="no",
)
options = {
"auth": auth_needed.button == "yes",
}
self.next_state.knowledge_base["user_options"] = options
self.state_manager.user_options = options
await self.send_message("Setting up the project...")
self.next_state.epics = [
{
"id": uuid4().hex,
"name": "Build frontend",
"source": "frontend",
"description": description,
"messages": [],
"summary": None,
"completed": False,
}
]
await self.apply_template(options)
return False
async def start_frontend(self):
"""
Starts the frontend of the app.
"""
await self.send_message("Building the frontend... This may take a couple of minutes")
description = self.current_state.epics[0]["description"]
llm = self.get_llm(FRONTEND_AGENT_NAME)
convo = AgentConvo(self).template(
"build_frontend",
description=description,
user_feedback=None,
)
response = await llm(convo, parser=DescriptiveCodeBlockParser())
response_blocks = response.blocks
convo.assistant(response.original_response)
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.
"""
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)
convo = AgentConvo(self)
convo.messages = self.current_state.epics[0]["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 doesn't end by mentioning continuing, respond only with `DONE` and with nothing else."
)
response = await llm(convo, parser=DescriptiveCodeBlockParser())
response_blocks = response.blocks
convo.assistant(response.original_response)
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) > 15
)
self.next_state.flag_epics_as_modified()
return False
async def iterate_frontend(self) -> bool:
"""
Iterates over the frontend.
:return: True if the frontend is fully built, False otherwise.
"""
# 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})
answer = await self.ask_question(
"Do you want to change anything or report a bug? Keep in mind that currently ONLY frontend is implemented.",
buttons={
"yes": "I'm done building the UI",
},
default="yes",
extra_info="restart_app/collect_logs",
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(
"Are you sure you're done building the UI and want to start building the backend functionality now?",
buttons={
"yes": "Yes, let's build the backend",
"no": "No, continue working on the UI",
},
buttons_only=True,
default="yes",
)
if answer.button == "yes":
return True
else:
return False
await self.send_message("Implementing the changes you suggested...")
llm = self.get_llm(FRONTEND_AGENT_NAME, stream_output=True)
convo = AgentConvo(self).template(
"build_frontend",
description=self.current_state.epics[0]["description"],
user_feedback=answer.text,
)
response = await llm(convo, parser=DescriptiveCodeBlockParser())
await self.process_response(response.blocks)
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.complete_epic()
await telemetry.trace_code_event(
"frontend-finished",
{
"description": self.current_state.epics[0]["description"],
"messages": self.current_state.epics[0]["messages"],
},
)
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) -> AgentResponse:
"""
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("\"'`")
new_content = content
old_content = self.current_state.get_file_content_by_path(file_path)
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
)
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}"
await self.send_message(f"Running command: `{command}`...")
await self.process_manager.run_command(command)
else:
log.info(f"Unknown block description: {description}")
return AgentResponse.done(self)
async def apply_template(self, options: dict = {}):
"""
Applies a template to the frontend.
"""
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,
)
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 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)

165
core/agents/git.py Normal file
View File

@@ -0,0 +1,165 @@
import os
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) -> None:
"""
Create a git commit with the specified message.
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}")
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}")

View File

@@ -21,7 +21,7 @@ class HumanInput(BaseAgent):
default="continue", default="continue",
buttons_only=True, buttons_only=True,
) )
self.next_state.complete_step() self.next_state.complete_step("human_intervention")
return AgentResponse.done(self) return AgentResponse.done(self)
async def input_required(self, files: list[dict]) -> AgentResponse: async def input_required(self, files: list[dict]) -> AgentResponse:

View File

@@ -8,7 +8,7 @@ class LegacyHandler(BaseAgent):
async def run(self) -> AgentResponse: async def run(self) -> AgentResponse:
if self.data["type"] == "review_task": if self.data["type"] == "review_task":
self.next_state.complete_step() self.next_state.complete_step("review_task")
return AgentResponse.done(self) return AgentResponse.done(self)
raise ValueError(f"Unknown reason for calling Legacy Handler with data: {self.data}") raise ValueError(f"Unknown reason for calling Legacy Handler with data: {self.data}")

View File

@@ -1,12 +1,15 @@
import json
from difflib import unified_diff
from typing import List, Optional, Union from typing import List, Optional, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from core.agents.convo import AgentConvo from core.agents.convo import AgentConvo
from core.agents.response import AgentResponse from core.agents.response import AgentResponse
from core.config import GET_RELEVANT_FILES_AGENT_NAME, TROUBLESHOOTER_BUG_REPORT from core.config import GET_RELEVANT_FILES_AGENT_NAME, TASK_BREAKDOWN_AGENT_NAME, TROUBLESHOOTER_BUG_REPORT
from core.llm.parser import JSONParser from core.llm.parser import JSONParser
from core.log import get_logger from core.log import get_logger
from core.ui.base import ProjectStage
log = get_logger(__name__) log = get_logger(__name__)
@@ -37,6 +40,58 @@ class RelevantFiles(BaseModel):
action: Union[ReadFilesAction, AddFilesAction, RemoveFilesAction, DoneBooleanAction] action: Union[ReadFilesAction, AddFilesAction, RemoveFilesAction, DoneBooleanAction]
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.
"""
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,
}
)
chat = await self.ask_question(
"Are you happy with the breakdown? Now is a good time to ask questions or suggest changes.",
buttons={"yes": "Yes, looks good!"},
default="yes",
verbose=False,
)
if chat.button == "yes":
break
if len(convo.messages) > 11:
convo.trim(3, 2)
convo.user(chat.text)
breakdown: str = await llm(convo)
convo.assistant(breakdown)
return breakdown
class IterationPromptMixin: class IterationPromptMixin:
""" """
Provides a method to find a solution to a problem based on user feedback. Provides a method to find a solution to a problem based on user feedback.
@@ -68,8 +123,12 @@ class IterationPromptMixin:
user_feedback_qa=user_feedback_qa, user_feedback_qa=user_feedback_qa,
next_solution_to_try=next_solution_to_try, next_solution_to_try=next_solution_to_try,
bug_hunting_cycles=bug_hunting_cycles, bug_hunting_cycles=bug_hunting_cycles,
test_instructions=json.loads(self.current_state.current_task.get("test_instructions") or "[]"),
) )
llm_solution: str = await llm(convo) llm_solution: str = await llm(convo)
llm_solution = await self.chat_with_breakdown(convo, llm_solution)
return llm_solution return llm_solution
@@ -96,9 +155,12 @@ class RelevantFilesMixin:
.require_schema(RelevantFiles) .require_schema(RelevantFiles)
) )
while not done and len(convo.messages) < 13: while not done and len(convo.messages) < 23:
llm_response: RelevantFiles = await llm(convo, parser=JSONParser(RelevantFiles), temperature=0) llm_response: RelevantFiles = await llm(convo, parser=JSONParser(RelevantFiles), temperature=0)
action = llm_response.action action = llm_response.action
if action is None:
convo.remove_last_x_messages(2)
continue
# Check if there are files to add to the list # Check if there are files to add to the list
if getattr(action, "add_files", None): if getattr(action, "add_files", None):
@@ -110,7 +172,9 @@ class RelevantFilesMixin:
# Remove files from relevant_files that are in remove_files # Remove files from relevant_files that are in remove_files
relevant_files.difference_update(action.remove_files) relevant_files.difference_update(action.remove_files)
read_files = [file for file in self.current_state.files if file.path in getattr(action, "read_files", [])] read_files = [
file for file in self.current_state.files if file.path in (getattr(action, "read_files", []) or [])
]
convo.remove_last_x_messages(1) convo.remove_last_x_messages(1)
convo.assistant(llm_response.original_response) convo.assistant(llm_response.original_response)
@@ -121,6 +185,41 @@ class RelevantFilesMixin:
existing_files = {file.path for file in self.current_state.files} existing_files = {file.path for file in self.current_state.files}
relevant_files = [path for path in relevant_files if path in existing_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 self.next_state.relevant_files = relevant_files
return AgentResponse.done(self) return AgentResponse.done(self)
class FileDiffMixin:
"""
Provides a method to generate a diff between two 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.
: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

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import os
from typing import List, Optional, Union from typing import List, Optional, Union
from core.agents.architect import Architect from core.agents.architect import Architect
@@ -9,6 +10,8 @@ from core.agents.developer import Developer
from core.agents.error_handler import ErrorHandler from core.agents.error_handler import ErrorHandler
from core.agents.executor import Executor from core.agents.executor import Executor
from core.agents.external_docs import ExternalDocumentation 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.human_input import HumanInput
from core.agents.importer import Importer from core.agents.importer import Importer
from core.agents.legacy_handler import LegacyHandler from core.agents.legacy_handler import LegacyHandler
@@ -22,12 +25,11 @@ from core.agents.troubleshooter import Troubleshooter
from core.db.models.project_state import IterationStatus, TaskStatus from core.db.models.project_state import IterationStatus, TaskStatus
from core.log import get_logger from core.log import get_logger
from core.telemetry import telemetry from core.telemetry import telemetry
from core.ui.base import ProjectStage
log = get_logger(__name__) log = get_logger(__name__)
class Orchestrator(BaseAgent): class Orchestrator(BaseAgent, GitMixin):
""" """
Main agent that controls the flow of the process. Main agent that controls the flow of the process.
@@ -56,6 +58,11 @@ class Orchestrator(BaseAgent):
await self.init_ui() await self.init_ui()
await self.offline_changes_check() 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()
# TODO: consider refactoring this into two loop; the outer with one iteration per comitted 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 # 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. # handle_done() and let us do other per-step processing (eg. describing files) in between agent runs.
@@ -73,6 +80,27 @@ class Orchestrator(BaseAgent):
) )
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
response = self.handle_parallel_responses(agent[0], responses) 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 len(single_agent.step.get("related_api_endpoints")) > 0
for single_agent in agent
)
if should_update_knowledge_base:
files_with_implemented_apis = [
{
"path": single_agent.step.get("save_file", {}).get("path", None),
"related_api_endpoints": single_agent.step.get("related_api_endpoints"),
"line": 0, # TODO implement getting the line number here
}
for single_agent in agent
if len(single_agent.step.get("related_api_endpoints")) > 0
]
await self.state_manager.update_apis(files_with_implemented_apis)
await self.state_manager.update_implemented_pages_and_apis()
else: else:
log.debug(f"Running agent {agent.__class__.__name__} (step {self.current_state.step_index})") log.debug(f"Running agent {agent.__class__.__name__} (step {self.current_state.step_index})")
response = await agent.run() response = await agent.run()
@@ -88,6 +116,19 @@ class Orchestrator(BaseAgent):
# TODO: rollback changes to "next" so they aren't accidentally committed? # TODO: rollback changes to "next" so they aren't accidentally committed?
return True 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)
def handle_parallel_responses(self, agent: BaseAgent, responses: List[AgentResponse]) -> AgentResponse: def handle_parallel_responses(self, agent: BaseAgent, responses: List[AgentResponse]) -> AgentResponse:
""" """
Handle responses from agents that were run in parallel. Handle responses from agents that were run in parallel.
@@ -215,21 +256,16 @@ class Orchestrator(BaseAgent):
if prev_response.type == ResponseType.UPDATE_SPECIFICATION: if prev_response.type == ResponseType.UPDATE_SPECIFICATION:
return SpecWriter(self.state_manager, self.ui, prev_response=prev_response) return SpecWriter(self.state_manager, self.ui, prev_response=prev_response)
if not state.specification.description: if not state.epics or (state.current_epic and state.current_epic.get("source") == "frontend"):
if state.files: # Build frontend
# The project has been imported, but not analyzed yet return Frontend(self.state_manager, self.ui, process_manager=self.process_manager)
return Importer(self.state_manager, self.ui) elif not state.specification.description:
else: # New project: ask the Spec Writer to refine and save the project specification
# 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)
return SpecWriter(self.state_manager, self.ui, process_manager=self.process_manager)
elif not state.specification.architecture: elif not state.specification.architecture:
# Ask the Architect to design the project architecture and determine dependencies # Ask the Architect to design the project architecture and determine dependencies
return Architect(self.state_manager, self.ui, process_manager=self.process_manager) return Architect(self.state_manager, self.ui, process_manager=self.process_manager)
elif ( elif not self.current_state.unfinished_tasks or (state.specification.templates and not state.files):
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 # 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) return TechLead(self.state_manager, self.ui, process_manager=self.process_manager)
@@ -244,7 +280,7 @@ class Orchestrator(BaseAgent):
return TechnicalWriter(self.state_manager, self.ui) return TechnicalWriter(self.state_manager, self.ui)
elif current_task_status in [TaskStatus.DOCUMENTED, TaskStatus.SKIPPED]: elif current_task_status in [TaskStatus.DOCUMENTED, TaskStatus.SKIPPED]:
# Task is fully done or skipped, call TaskCompleter to mark it as completed # Task is fully done or skipped, call TaskCompleter to mark it as completed
return TaskCompleter(self.state_manager, self.ui) return TaskCompleter(self.state_manager, self.ui, process_manager=self.process_manager)
if not state.steps and not state.iterations: if not state.steps and not state.iterations:
# Ask the Developer to break down current task into actionable steps # Ask the Developer to break down current task into actionable steps
@@ -307,6 +343,8 @@ class Orchestrator(BaseAgent):
return LegacyHandler(self.state_manager, self.ui, data={"type": "review_task"}) return LegacyHandler(self.state_manager, self.ui, data={"type": "review_task"})
elif step_type == "create_readme": elif step_type == "create_readme":
return TechnicalWriter(self.state_manager, self.ui) return TechnicalWriter(self.state_manager, self.ui)
elif step_type == "utility_function":
return Developer(self.state_manager, self.ui)
else: else:
raise ValueError(f"Unknown step type: {step_type}") raise ValueError(f"Unknown step type: {step_type}")
@@ -322,7 +360,7 @@ class Orchestrator(BaseAgent):
input_required_files: list[dict[str, int]] = [] input_required_files: list[dict[str, int]] = []
for file in imported_files: for file in imported_files:
for line in self.state_manager.get_input_required(file.content.content): for line in self.state_manager.get_input_required(file.content.content, file.path):
input_required_files.append({"file": file.path, "line": line}) input_required_files.append({"file": file.path, "line": line})
if input_required_files: if input_required_files:
@@ -340,15 +378,9 @@ class Orchestrator(BaseAgent):
await self.ui.loading_finished() await self.ui.loading_finished()
if self.current_state.epics: if self.current_state.epics:
await self.ui.send_project_stage(ProjectStage.CODING) if len(self.current_state.epics) > 3:
if len(self.current_state.epics) > 2:
# We only want to send previous features, ie. exclude current one and the initial project (first epic) # 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[1:-1]]) await self.ui.send_features_list([e["description"] for e in self.current_state.epics[2:-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: if self.current_state.specification.description:
await self.ui.send_project_description(self.current_state.specification.description) await self.ui.send_project_description(self.current_state.specification.description)

View File

@@ -7,10 +7,6 @@ from core.db.models.project_state import IterationStatus
from core.llm.parser import StringParser from core.llm.parser import StringParser
from core.log import get_logger from core.log import get_logger
from core.telemetry import telemetry from core.telemetry import telemetry
from core.templates.example_project import (
DEFAULT_EXAMPLE_PROJECT,
EXAMPLE_PROJECTS,
)
# If the project description is less than this, perform an analysis using LLM # If the project description is less than this, perform an analysis using LLM
ANALYZE_THRESHOLD = 1500 ANALYZE_THRESHOLD = 1500
@@ -37,25 +33,15 @@ class SpecWriter(BaseAgent):
return await self.initialize_spec() return await self.initialize_spec()
async def initialize_spec(self) -> AgentResponse: async def initialize_spec(self) -> AgentResponse:
response = await self.ask_question( # response = await self.ask_question(
"Describe your app in as much detail as possible", # "Describe your app in as much detail as possible",
allow_empty=False, # allow_empty=False,
buttons={ # )
"example": "Start an example project", # if response.cancelled:
"import": "Import an existing project", # return AgentResponse.error(self, "No project description")
}, #
) # user_description = response.text.strip()
if response.cancelled: user_description = self.current_state.epics[0]["description"]
return AgentResponse.error(self, "No project description")
if response.button == "import":
return AgentResponse.import_project(self)
if response.button == "example":
await self.prepare_example_project(DEFAULT_EXAMPLE_PROJECT)
return AgentResponse.done(self)
user_description = response.text.strip()
complexity = await self.check_prompt_complexity(user_description) complexity = await self.check_prompt_complexity(user_description)
await telemetry.trace_code_event( await telemetry.trace_code_event(
@@ -67,9 +53,9 @@ class SpecWriter(BaseAgent):
) )
reviewed_spec = user_description reviewed_spec = user_description
if len(user_description) < ANALYZE_THRESHOLD and complexity != Complexity.SIMPLE: # if len(user_description) < ANALYZE_THRESHOLD and complexity != Complexity.SIMPLE:
initial_spec = await self.analyze_spec(user_description) # initial_spec = await self.analyze_spec(user_description)
reviewed_spec = await self.review_spec(desc=user_description, spec=initial_spec) # reviewed_spec = await self.review_spec(desc=user_description, spec=initial_spec)
self.next_state.specification = self.current_state.specification.clone() self.next_state.specification = self.current_state.specification.clone()
self.next_state.specification.original_description = user_description self.next_state.specification.original_description = user_description
@@ -95,7 +81,9 @@ class SpecWriter(BaseAgent):
convo = AgentConvo(self).template("add_new_feature", feature_description=feature_description) convo = AgentConvo(self).template("add_new_feature", feature_description=feature_description)
llm_response: str = await llm(convo, temperature=0, parser=StringParser()) llm_response: str = await llm(convo, temperature=0, parser=StringParser())
updated_spec = llm_response.strip() updated_spec = llm_response.strip()
await self.ui.generate_diff("project_specification", self.current_state.specification.description, updated_spec) await self.ui.generate_diff(
"project_specification", self.current_state.specification.description, updated_spec, source=self.ui_source
)
user_response = await self.ask_question( user_response = await self.ask_question(
"Do you accept these changes to the project specification?", "Do you accept these changes to the project specification?",
buttons={"yes": "Yes", "no": "No"}, buttons={"yes": "Yes", "no": "No"},
@@ -113,38 +101,28 @@ class SpecWriter(BaseAgent):
self.next_state.current_iteration["status"] = IterationStatus.FIND_SOLUTION self.next_state.current_iteration["status"] = IterationStatus.FIND_SOLUTION
self.next_state.flag_iterations_as_modified() self.next_state.flag_iterations_as_modified()
else: else:
complexity = await self.check_prompt_complexity(user_response.text) complexity = await self.check_prompt_complexity(feature_description)
self.next_state.current_epic["complexity"] = complexity self.next_state.current_epic["complexity"] = complexity
return AgentResponse.done(self) return AgentResponse.done(self)
async def check_prompt_complexity(self, prompt: str) -> str: 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 ...") await self.send_message("Checking the complexity of the prompt ...")
llm = self.get_llm(SPEC_WRITER_AGENT_NAME) llm = self.get_llm(SPEC_WRITER_AGENT_NAME)
convo = AgentConvo(self).template("prompt_complexity", prompt=prompt) convo = AgentConvo(self).template(
"prompt_complexity",
prompt=prompt,
is_feature=is_feature,
)
llm_response: str = await llm(convo, temperature=0, parser=StringParser()) llm_response: str = await llm(convo, temperature=0, parser=StringParser())
log.info(f"Complexity check response: {llm_response}")
return llm_response.lower() 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: async def analyze_spec(self, spec: str) -> str:
msg = ( msg = (
"Your project description seems a bit short. " "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" "The better you can describe the project, the better Pythagora 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" 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:" "Let's start by refining your project idea:"
) )
@@ -160,12 +138,9 @@ class SpecWriter(BaseAgent):
if len(response) > 500: if len(response) > 500:
# The response is too long for it to be a question, assume it's the updated spec # The response is too long for it to be a question, assume it's the updated spec
confirm = await self.ask_question( confirm = await self.ask_question(
( ("Would you like to change or add anything? Write it out here."),
"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, allow_empty=True,
buttons={"continue": "Continue"}, buttons={"continue": "No thanks, the spec looks good"},
) )
if confirm.cancelled or confirm.button == "continue" or confirm.text == "": if confirm.cancelled or confirm.button == "continue" or confirm.text == "":
updated_spec = response.strip() updated_spec = response.strip()
@@ -186,18 +161,40 @@ class SpecWriter(BaseAgent):
n_questions += 1 n_questions += 1
user_response = await self.ask_question( user_response = await self.ask_question(
response, response,
buttons={"skip": "Skip questions"}, buttons={"skip": "Skip this question", "skip_all": "No more questions"},
verbose=False,
) )
if user_response.cancelled or user_response.button == "skip": if user_response.cancelled or user_response.button == "skip_all":
convo.user( convo.user(
"This is enough clarification, you have all the information. " "This is enough clarification, you have all the information. "
"Please output the spec now, without additional comments or questions." "Please output the spec now, without additional comments or questions."
) )
response: str = await llm(convo) response: str = await llm(convo)
return response.strip() confirm = await self.ask_question(
("Would you like to change or add anything? Write it out here."),
allow_empty=True,
buttons={"continue": "No thanks, the spec looks good"},
)
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)
continue
n_answers += 1 n_answers += 1
convo.user(user_response.text) if user_response.button == "skip":
convo.user("Skip this question.")
continue
else:
convo.user(user_response.text)
async def review_spec(self, desc: str, spec: str) -> str: async def review_spec(self, desc: str, spec: str) -> str:
convo = AgentConvo(self).template("review_spec", desc=desc, spec=spec) convo = AgentConvo(self).template("review_spec", desc=desc, spec=spec)

View File

@@ -1,4 +1,5 @@
from core.agents.base import BaseAgent from core.agents.base import BaseAgent
from core.agents.git import GitMixin
from core.agents.response import AgentResponse from core.agents.response import AgentResponse
from core.log import get_logger from core.log import get_logger
from core.telemetry import telemetry from core.telemetry import telemetry
@@ -6,11 +7,14 @@ from core.telemetry import telemetry
log = get_logger(__name__) log = get_logger(__name__)
class TaskCompleter(BaseAgent): class TaskCompleter(BaseAgent, GitMixin):
agent_type = "pythagora" agent_type = "pythagora"
display_name = "Pythagora" display_name = "Pythagora"
async def run(self) -> AgentResponse: 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 current_task_index1 = self.current_state.tasks.index(self.current_state.current_task) + 1
self.next_state.action = f"Task #{current_task_index1} complete" self.next_state.action = f"Task #{current_task_index1} complete"
self.next_state.complete_task() self.next_state.complete_task()

View File

@@ -1,28 +1,39 @@
import json
from uuid import uuid4 from uuid import uuid4
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from core.agents.base import BaseAgent from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo from core.agents.convo import AgentConvo
from core.agents.mixins import RelevantFilesMixin
from core.agents.response import AgentResponse from core.agents.response import AgentResponse
from core.config import TECH_LEAD_PLANNING from core.config import TECH_LEAD_EPIC_BREAKDOWN, TECH_LEAD_PLANNING
from core.db.models import Complexity
from core.db.models.project_state import TaskStatus from core.db.models.project_state import TaskStatus
from core.llm.parser import JSONParser from core.llm.parser import JSONParser
from core.log import get_logger from core.log import get_logger
from core.telemetry import telemetry from core.telemetry import telemetry
from core.templates.example_project import EXAMPLE_PROJECTS
from core.templates.registry import PROJECT_TEMPLATES from core.templates.registry import PROJECT_TEMPLATES
from core.ui.base import ProjectStage, success_source from core.ui.base import ProjectStage, pythagora_source, success_source
log = get_logger(__name__) 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): class Epic(BaseModel):
description: str = Field(description=("Description of an epic.")) description: str = Field(description="Description of an epic.")
class Task(BaseModel): class Task(BaseModel):
description: str = Field(description="Description of a task.") 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.") testing_instructions: str = Field(description="Instructions for testing the task.")
@@ -34,38 +45,34 @@ class EpicPlan(BaseModel):
plan: list[Task] = Field(description="List of tasks that need to be done to implement the entire epic.") plan: list[Task] = Field(description="List of tasks that need to be done to implement the entire epic.")
class TechLead(BaseAgent): class TechLead(RelevantFilesMixin, BaseAgent):
agent_type = "tech-lead" agent_type = "tech-lead"
display_name = "Tech Lead" display_name = "Tech Lead"
async def run(self) -> AgentResponse: async def run(self) -> AgentResponse:
if len(self.current_state.epics) == 0: # Building frontend is the first epic
if self.current_state.specification.example_project: if len(self.current_state.epics) == 1:
self.plan_example_project() self.create_initial_project_epic()
else:
self.create_initial_project_epic()
return AgentResponse.done(self) return AgentResponse.done(self)
await self.ui.send_project_stage(ProjectStage.CODING) # if self.current_state.specification.templates and len(self.current_state.files) < 2:
# await self.apply_project_templates()
if self.current_state.specification.templates and not self.current_state.files: # self.next_state.action = "Apply project templates"
await self.apply_project_templates() # await self.ui.send_epics_and_tasks(
self.next_state.action = "Apply project templates" # self.next_state.current_epic["sub_epics"],
await self.ui.send_epics_and_tasks( # self.next_state.tasks,
self.next_state.current_epic["sub_epics"], # )
self.next_state.tasks, #
) # inputs = []
# for file in self.next_state.files:
inputs = [] # input_required = self.state_manager.get_input_required(file.content.content)
for file in self.next_state.files: # if input_required:
input_required = self.state_manager.get_input_required(file.content.content) # inputs += [{"file": file.path, "line": line} for line in input_required]
if input_required: #
inputs += [{"file": file.path, "line": line} for line in input_required] # if inputs:
# return AgentResponse.input_required(self, inputs)
if inputs: # else:
return AgentResponse.input_required(self, inputs) # return AgentResponse.done(self)
else:
return AgentResponse.done(self)
if self.current_state.current_epic: if self.current_state.current_epic:
self.next_state.action = "Create a development plan" self.next_state.action = "Create a development plan"
@@ -75,7 +82,7 @@ class TechLead(BaseAgent):
def create_initial_project_epic(self): def create_initial_project_epic(self):
log.debug("Creating initial project Epic") log.debug("Creating initial project Epic")
self.next_state.epics = [ self.next_state.epics = self.current_state.epics + [
{ {
"id": uuid4().hex, "id": uuid4().hex,
"name": "Initial Project", "name": "Initial Project",
@@ -88,6 +95,8 @@ class TechLead(BaseAgent):
"sub_epics": [], "sub_epics": [],
} }
] ]
self.next_state.relevant_files = None
self.next_state.modified_files = {}
async def apply_project_templates(self): async def apply_project_templates(self):
state = self.current_state state = self.current_state
@@ -138,19 +147,21 @@ class TechLead(BaseAgent):
"Do you have a new feature to add to the project? Just write it here:", "Do you have a new feature to add to the project? Just write it here:",
buttons={"continue": "continue", "end": "No, I'm done"}, buttons={"continue": "continue", "end": "No, I'm done"},
allow_empty=False, allow_empty=False,
extra_info="restart_app",
) )
if response.button == "end" or response.cancelled or not response.text: if response.button == "end" or response.cancelled or not response.text:
await self.ui.send_message("Thanks for using Pythagora!") await self.ui.send_message("Thank you for using Pythagora!", source=pythagora_source)
return AgentResponse.exit(self) return AgentResponse.exit(self)
feature_description = response.text
self.next_state.epics = self.current_state.epics + [ self.next_state.epics = self.current_state.epics + [
{ {
"id": uuid4().hex, "id": uuid4().hex,
"name": f"Feature #{len(self.current_state.epics)}", "name": f"Feature #{len(self.current_state.epics)}",
"test_instructions": None, "test_instructions": None,
"source": "feature", "source": "feature",
"description": response.text, "description": feature_description,
"summary": None, "summary": None,
"completed": False, "completed": False,
"complexity": None, # Determined and defined in SpecWriter "complexity": None, # Determined and defined in SpecWriter
@@ -159,12 +170,15 @@ class TechLead(BaseAgent):
] ]
# Orchestrator will rerun us to break down the new feature epic # Orchestrator will rerun us to break down the new feature epic
self.next_state.action = f"Start of feature #{len(self.current_state.epics)}" self.next_state.action = f"Start of feature #{len(self.current_state.epics)}"
return AgentResponse.update_specification(self, response.text) return AgentResponse.update_specification(self, feature_description)
async def plan_epic(self, epic) -> AgentResponse: async def plan_epic(self, epic) -> AgentResponse:
log.debug(f"Planning tasks for the epic: {epic['name']}") log.debug(f"Planning tasks for the epic: {epic['name']}")
await self.send_message("Creating the development plan ...") await self.send_message("Creating the development plan ...")
if epic.get("source") == "feature":
await self.get_relevant_files(user_feedback=epic.get("description"))
llm = self.get_llm(TECH_LEAD_PLANNING) llm = self.get_llm(TECH_LEAD_PLANNING)
convo = ( convo = (
AgentConvo(self) AgentConvo(self)
@@ -174,6 +188,7 @@ class TechLead(BaseAgent):
task_type=self.current_state.current_epic.get("source", "app"), task_type=self.current_state.current_epic.get("source", "app"),
# FIXME: we're injecting summaries to initial description # FIXME: we're injecting summaries to initial description
existing_summary=None, existing_summary=None,
get_only_api_files=True,
) )
.require_schema(DevelopmentPlan) .require_schema(DevelopmentPlan)
) )
@@ -181,12 +196,12 @@ class TechLead(BaseAgent):
response: DevelopmentPlan = await llm(convo, parser=JSONParser(DevelopmentPlan)) response: DevelopmentPlan = await llm(convo, parser=JSONParser(DevelopmentPlan))
convo.remove_last_x_messages(1) convo.remove_last_x_messages(1)
formatted_tasks = [f"Epic #{index}: {task.description}" for index, task in enumerate(response.plan, start=1)] formatted_epics = [f"Epic #{index}: {epic.description}" for index, epic in enumerate(response.plan, start=1)]
tasks_string = "\n\n".join(formatted_tasks) epics_string = "\n\n".join(formatted_epics)
convo = convo.assistant(tasks_string) convo = convo.assistant(epics_string)
llm = self.get_llm(TECH_LEAD_PLANNING) llm = self.get_llm(TECH_LEAD_EPIC_BREAKDOWN)
if epic.get("source") == "feature" or epic.get("complexity") == "simple": if epic.get("source") == "feature" or epic.get("complexity") == Complexity.SIMPLE:
await self.send_message(f"Epic 1: {epic['name']}") await self.send_message(f"Epic 1: {epic['name']}")
self.next_state.current_epic["sub_epics"] = [ self.next_state.current_epic["sub_epics"] = [
{ {
@@ -206,12 +221,8 @@ class TechLead(BaseAgent):
} }
for task in response.plan for task in response.plan
] ]
await self.ui.send_epics_and_tasks(
self.next_state.current_epic["sub_epics"],
self.next_state.tasks,
)
else: 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, "id": sub_epic_number,
"description": sub_epic.description, "description": sub_epic.description,
@@ -221,7 +232,10 @@ class TechLead(BaseAgent):
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}") await self.send_message(f"Epic {sub_epic_number}: {sub_epic.description}")
convo = convo.template( convo = convo.template(
"epic_breakdown", epic_number=sub_epic_number, epic_description=sub_epic.description "epic_breakdown",
epic_number=sub_epic_number,
epic_description=sub_epic.description,
get_only_api_files=True,
).require_schema(EpicPlan) ).require_schema(EpicPlan)
await self.send_message("Creating tasks for this epic ...") await self.send_message("Creating tasks for this epic ...")
epic_plan: EpicPlan = await llm(convo, parser=JSONParser(EpicPlan)) epic_plan: EpicPlan = await llm(convo, parser=JSONParser(EpicPlan))
@@ -233,15 +247,32 @@ class TechLead(BaseAgent):
"pre_breakdown_testing_instructions": task.testing_instructions, "pre_breakdown_testing_instructions": task.testing_instructions,
"status": TaskStatus.TODO, "status": TaskStatus.TODO,
"sub_epic_id": sub_epic_number, "sub_epic_id": sub_epic_number,
"related_api_endpoints": [rae.model_dump() for rae in (task.related_api_endpoints or [])],
} }
for task in epic_plan.plan for task in epic_plan.plan
] ]
convo.remove_last_x_messages(2) convo.remove_last_x_messages(2)
await self.ui.send_epics_and_tasks( await self.ui.send_epics_and_tasks(
self.next_state.current_epic["sub_epics"], self.next_state.current_epic["sub_epics"],
self.next_state.tasks, self.next_state.tasks,
) )
await self.ui.send_project_stage({"stage": ProjectStage.OPEN_PLAN})
response = await self.ask_question(
"Open and edit your development plan in the Progress tab",
buttons={"done_editing": "I'm done editing, the plan looks good"},
default="done_editing",
buttons_only=True,
extra_info="edit_plan",
)
self.update_epics_and_tasks(response.text)
await self.ui.send_epics_and_tasks(
self.next_state.current_epic["sub_epics"],
self.next_state.tasks,
)
await telemetry.trace_code_event( await telemetry.trace_code_event(
"development-plan", "development-plan",
@@ -252,23 +283,37 @@ class TechLead(BaseAgent):
) )
return AgentResponse.done(self) return AgentResponse.done(self)
def plan_example_project(self): def update_epics_and_tasks(self, edited_plan_string):
example_name = self.current_state.specification.example_project edited_plan = json.loads(edited_plan_string)
log.debug(f"Planning example project: {example_name}") updated_tasks = []
example = EXAMPLE_PROJECTS[example_name] existing_tasks_map = {task["description"]: task for task in self.next_state.tasks}
self.next_state.epics = [
{ self.next_state.current_epic["sub_epics"] = []
"name": "Initial Project", for sub_epic_number, sub_epic in enumerate(edited_plan, start=1):
"description": example["description"], self.next_state.current_epic["sub_epics"].append(
"completed": False, {
"complexity": example["complexity"], "id": sub_epic_number,
"sub_epics": [ "description": sub_epic["description"],
{ }
"id": 1, )
"description": "Single Epic Example",
} for task in sub_epic["tasks"]:
], original_task = existing_tasks_map.get(task["description"])
} if original_task and task == original_task:
] updated_task = original_task.copy()
self.next_state.tasks = example["plan"] updated_task["sub_epic_id"] = sub_epic_number
updated_tasks.append(updated_task)
else:
updated_tasks.append(
{
"id": uuid4().hex,
"description": task["description"],
"instructions": None,
"pre_breakdown_testing_instructions": None,
"status": TaskStatus.TODO,
"sub_epic_id": sub_epic_number,
}
)
self.next_state.tasks = updated_tasks

View File

@@ -1,3 +1,4 @@
import json
from typing import Optional from typing import Optional
from uuid import uuid4 from uuid import uuid4
@@ -5,7 +6,7 @@ from pydantic import BaseModel, Field
from core.agents.base import BaseAgent from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo from core.agents.convo import AgentConvo
from core.agents.mixins import IterationPromptMixin, RelevantFilesMixin from core.agents.mixins import ChatWithBreakdownMixin, IterationPromptMixin, RelevantFilesMixin, TestSteps
from core.agents.response import AgentResponse from core.agents.response import AgentResponse
from core.config import TROUBLESHOOTER_GET_RUN_COMMAND from core.config import TROUBLESHOOTER_GET_RUN_COMMAND
from core.db.models.file import File from core.db.models.file import File
@@ -13,6 +14,7 @@ from core.db.models.project_state import IterationStatus, TaskStatus
from core.llm.parser import JSONParser, OptionalCodeBlockParser from core.llm.parser import JSONParser, OptionalCodeBlockParser
from core.log import get_logger from core.log import get_logger
from core.telemetry import telemetry from core.telemetry import telemetry
from core.ui.base import ProjectStage, pythagora_source
log = get_logger(__name__) log = get_logger(__name__)
@@ -29,7 +31,7 @@ class RouteFilePaths(BaseModel):
files: list[str] = Field(description="List of paths for files that contain routes") files: list[str] = Field(description="List of paths for files that contain routes")
class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent): class Troubleshooter(ChatWithBreakdownMixin, IterationPromptMixin, RelevantFilesMixin, BaseAgent):
agent_type = "troubleshooter" agent_type = "troubleshooter"
display_name = "Troubleshooter" display_name = "Troubleshooter"
@@ -72,10 +74,12 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
self.next_state.flag_tasks_as_modified() self.next_state.flag_tasks_as_modified()
return AgentResponse.done(self) return AgentResponse.done(self)
else: else:
await self.send_message("Here are instructions on how to test the app:\n\n" + user_instructions) 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("")
await self.ui.stop_app() await self.ui.stop_app()
await self.ui.send_test_instructions(user_instructions) await self.ui.send_test_instructions(user_instructions, project_state_id=str(self.current_state.id))
# Developer sets iteration as "completed" when it generates the step breakdown, so we can't # Developer sets iteration as "completed" when it generates the step breakdown, so we can't
# use "current_iteration" here # use "current_iteration" here
@@ -106,7 +110,6 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
else: else:
# should be - elif change_description is not None: - but to prevent bugs with the extension # 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 # 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 iteration_status = IterationStatus.NEW_FEATURE_REQUESTED
self.next_state.iterations = self.current_state.iterations + [ self.next_state.iterations = self.current_state.iterations + [
@@ -155,6 +158,7 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
task=task, task=task,
iteration=None, iteration=None,
current_task_index=current_task_index, current_task_index=current_task_index,
related_api_endpoints=task.get("related_api_endpoints", []),
) )
.assistant(self.current_state.current_task["instructions"]) .assistant(self.current_state.current_task["instructions"])
) )
@@ -179,18 +183,30 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
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() route_files = await self._get_route_files()
current_task = self.current_state.current_task
llm = self.get_llm() llm = self.get_llm()
convo = self._get_task_convo().template( convo = (
"define_user_review_goal", task=self.current_state.current_task, route_files=route_files 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)
) )
user_instructions: str = await llm(convo) user_instructions: TestSteps = await llm(convo, parser=JSONParser(TestSteps))
user_instructions = user_instructions.strip() if len(user_instructions.steps) == 0:
if user_instructions.lower() == "done": await self.ui.send_message(
"No testing required for this task, moving on to the next one.", source=pythagora_source
)
log.debug(f"Nothing to do for user testing for task {self.current_state.current_task['description']}") log.debug(f"Nothing to do for user testing for task {self.current_state.current_task['description']}")
return None return None
user_instructions = json.dumps([test.dict() for test in user_instructions.steps])
return user_instructions return user_instructions
async def _get_route_files(self) -> list[File]: async def _get_route_files(self) -> list[File]:
@@ -231,38 +247,62 @@ class Troubleshooter(IterationPromptMixin, RelevantFilesMixin, BaseAgent):
is_loop = False is_loop = False
should_iterate = True should_iterate = True
extra_info = "restart_app" if not self.current_state.iterations else None
test_message = "Please check if the app is working" while True:
if user_instructions: await self.ui.send_project_stage({"stage": ProjectStage.GET_USER_FEEDBACK})
hint = " Here is a description of what should be working:\n\n" + user_instructions
if run_command: test_message = "Please check if the app is working"
await self.ui.send_run_command(run_command) if user_instructions:
hint = " Here is a description of what should be working:\n\n" + user_instructions
buttons = { if run_command:
"continue": "Everything works", await self.ui.send_run_command(run_command)
"change": "I want to make a change",
"bug": "There is an issue",
}
user_response = await self.ask_question( buttons = {
test_message, buttons=buttons, default="continue", buttons_only=True, hint=hint "continue": "Everything works",
) "change": "I want to make a change",
if user_response.button == "continue" or user_response.cancelled: "bug": "There is an issue",
should_iterate = False }
elif user_response.button == "change": user_response = await self.ask_question(
user_description = await self.ask_question( test_message,
"Please describe the change you want to make to the project specification (one at a time)" buttons=buttons,
default="continue",
buttons_only=True,
hint=hint,
extra_info=extra_info,
) )
change_description = user_description.text extra_info = None
elif user_response.button == "bug": if user_response.button == "continue" or user_response.cancelled:
user_description = await self.ask_question( should_iterate = False
"Please describe the issue you found (one at a time) and share any relevant server logs", break
buttons={"copy_server_logs": "Copy Server Logs"},
) elif user_response.button == "change":
bug_report = user_description.text 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(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(
"Please describe the issue you found (one at a time) and share any relevant server logs",
extra_info="collect_logs",
buttons={"back": "Back"},
)
if user_description.button == "back":
continue
bug_report = user_description.text
await self.get_relevant_files(user_feedback=bug_report)
break
return should_iterate, is_loop, bug_report, change_description return should_iterate, is_loop, bug_report, change_description

View File

@@ -96,6 +96,7 @@ def parse_arguments() -> Namespace:
--email: User's email address, if provided --email: User's email address, if provided
--extension-version: Version of the VSCode extension, if used --extension-version: Version of the VSCode extension, if used
--no-check: Disable initial LLM API check --no-check: Disable initial LLM API check
--use-git: Use Git for version control
:return: Parsed arguments object. :return: Parsed arguments object.
""" """
version = get_version() version = get_version()
@@ -136,6 +137,7 @@ def parse_arguments() -> Namespace:
parser.add_argument("--email", help="User's email address", required=False) 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("--extension-version", help="Version of the VSCode extension", required=False)
parser.add_argument("--no-check", help="Disable initial LLM API check", action="store_true") parser.add_argument("--no-check", help="Disable initial LLM API check", action="store_true")
parser.add_argument("--use-git", help="Use Git for version control", action="store_true", required=False)
return parser.parse_args() return parser.parse_args()

View File

@@ -1,4 +1,6 @@
import asyncio import asyncio
import atexit
import signal
import sys import sys
from argparse import Namespace from argparse import Namespace
from asyncio import run from asyncio import run
@@ -8,16 +10,32 @@ from core.cli.helpers import delete_project, init, list_projects, list_projects_
from core.config import LLMProvider, get_config from core.config import LLMProvider, get_config
from core.db.session import SessionManager from core.db.session import SessionManager
from core.db.v0importer import LegacyDatabaseImporter from core.db.v0importer import LegacyDatabaseImporter
from core.llm.anthropic_client import CustomAssertionError
from core.llm.base import APIError, BaseLLMClient from core.llm.base import APIError, BaseLLMClient
from core.log import get_logger from core.log import get_logger
from core.state.state_manager import StateManager from core.state.state_manager import StateManager
from core.telemetry import telemetry from core.telemetry import telemetry
from core.ui.base import UIBase, UIClosedError, UserInput, pythagora_source from core.ui.base import ProjectStage, UIBase, UIClosedError, UserInput, pythagora_source
log = get_logger(__name__) log = get_logger(__name__)
async def run_project(sm: StateManager, ui: UIBase) -> bool: 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:
""" """
Work on the project. Work on the project.
@@ -26,13 +44,14 @@ async def run_project(sm: StateManager, ui: UIBase) -> bool:
:param sm: State manager. :param sm: State manager.
:param ui: User interface. :param ui: User interface.
:param args: Command-line arguments.
:return: True if the orchestrator exited successfully, False otherwise. :return: True if the orchestrator exited successfully, False otherwise.
""" """
telemetry.set("app_id", str(sm.project.id)) telemetry.set("app_id", str(sm.project.id))
telemetry.set("initial_prompt", sm.current_state.specification.description) telemetry.set("initial_prompt", sm.current_state.specification.description)
orca = Orchestrator(sm, ui) orca = Orchestrator(sm, ui, args=args)
success = False success = False
try: try:
success = await orca.run() success = await orca.run()
@@ -49,6 +68,14 @@ async def run_project(sm: StateManager, ui: UIBase) -> bool:
) )
telemetry.set("end_result", "failure:api-error") telemetry.set("end_result", "failure:api-error")
await sm.rollback() await sm.rollback()
except CustomAssertionError as err:
log.warning(f"Anthropic assertion error occurred: {str(err)}")
await ui.send_message(
f"Stopping Pythagora due to an error inside Anthropic SDK. {str(err)}",
source=pythagora_source,
)
telemetry.set("end_result", "failure:assertion-error")
await sm.rollback()
except Exception as err: except Exception as err:
log.error(f"Uncaught exception: {err}", exc_info=True) log.error(f"Uncaught exception: {err}", exc_info=True)
stack_trace = telemetry.record_crash(err) stack_trace = telemetry.record_crash(err)
@@ -125,12 +152,48 @@ async def start_new_project(sm: StateManager, ui: UIBase) -> bool:
:param ui: User interface. :param ui: User interface.
:return: True if the project was created successfully, False otherwise. :return: True if the project was created successfully, False otherwise.
""" """
stack = await ui.ask_question(
"What do you want to use to build your app?",
allow_empty=False,
buttons={"node": "Node.js", "other": "Other (coming soon)"},
buttons_only=True,
source=pythagora_source,
full_screen=True,
)
if stack.button == "other":
language = await ui.ask_question(
"What language you want to use?",
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
elif stack.button == "node":
await telemetry.trace_code_event(
"stack-choice",
{"language": "node"},
)
elif stack.button == "python":
await telemetry.trace_code_event(
"stack-choice",
{"language": "python"},
)
while True: while True:
try: try:
await ui.send_project_stage({"stage": ProjectStage.PROJECT_NAME})
user_input = await ui.ask_question( user_input = await ui.ask_question(
"What is the project name?", "What is the project name?",
allow_empty=False, allow_empty=False,
source=pythagora_source, source=pythagora_source,
full_screen=True,
) )
except (KeyboardInterrupt, UIClosedError): except (KeyboardInterrupt, UIClosedError):
user_input = UserInput(cancelled=True) user_input = UserInput(cancelled=True)
@@ -178,7 +241,7 @@ async def run_pythagora_session(sm: StateManager, ui: UIBase, args: Namespace):
if not success: if not success:
return False return False
return await run_project(sm, ui) return await run_project(sm, ui, args)
async def async_main( async def async_main(
@@ -194,6 +257,7 @@ async def async_main(
:param args: Command-line arguments. :param args: Command-line arguments.
:return: True if the application ran successfully, False otherwise. :return: True if the application ran successfully, False otherwise.
""" """
global telemetry_sent
if args.list: if args.list:
await list_projects(db) await list_projects(db)
@@ -223,9 +287,23 @@ async def async_main(
return False return False
telemetry.start() telemetry.start()
success = await run_pythagora_session(sm, ui, args)
await telemetry.send() # Set up signal handlers
await ui.stop() def signal_handler(sig, frame):
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)
finally:
await cleanup(ui)
return success return success

View File

@@ -43,8 +43,10 @@ TASK_BREAKDOWN_AGENT_NAME = "Developer.breakdown_current_task"
TROUBLESHOOTER_BUG_REPORT = "Troubleshooter.generate_bug_report" TROUBLESHOOTER_BUG_REPORT = "Troubleshooter.generate_bug_report"
TROUBLESHOOTER_GET_RUN_COMMAND = "Troubleshooter.get_run_command" TROUBLESHOOTER_GET_RUN_COMMAND = "Troubleshooter.get_run_command"
TECH_LEAD_PLANNING = "TechLead.plan_epic" TECH_LEAD_PLANNING = "TechLead.plan_epic"
TECH_LEAD_EPIC_BREAKDOWN = "TechLead.epic_breakdown"
SPEC_WRITER_AGENT_NAME = "SpecWriter" SPEC_WRITER_AGENT_NAME = "SpecWriter"
GET_RELEVANT_FILES_AGENT_NAME = "get_relevant_files" GET_RELEVANT_FILES_AGENT_NAME = "get_relevant_files"
FRONTEND_AGENT_NAME = "Frontend"
# Endpoint for the external documentation # Endpoint for the external documentation
EXTERNAL_DOCUMENTATION_API = "http://docs-pythagora-io-439719575.us-east-1.elb.amazonaws.com" EXTERNAL_DOCUMENTATION_API = "http://docs-pythagora-io-439719575.us-east-1.elb.amazonaws.com"
@@ -101,7 +103,7 @@ class ProviderConfig(_StrictModel):
ge=0.0, ge=0.0,
) )
read_timeout: float = Field( read_timeout: float = Field(
default=20.0, default=60.0,
description="Timeout (in seconds) for receiving a new chunk of data from the response stream", description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
ge=0.0, ge=0.0,
) )
@@ -156,7 +158,7 @@ class LLMConfig(_StrictModel):
ge=0.0, ge=0.0,
) )
read_timeout: float = Field( read_timeout: float = Field(
default=20.0, default=60.0,
description="Timeout (in seconds) for receiving a new chunk of data from the response stream", description="Timeout (in seconds) for receiving a new chunk of data from the response stream",
ge=0.0, ge=0.0,
) )
@@ -228,7 +230,7 @@ class DBConfig(_StrictModel):
""" """
url: str = Field( url: str = Field(
"sqlite+aiosqlite:///pythagora.db", "sqlite+aiosqlite:///data/database/pythagora.db",
description="Database connection URL", description="Database connection URL",
) )
debug_sql: bool = Field(False, description="Log all SQL queries to the console") debug_sql: bool = Field(False, description="Log all SQL queries to the console")
@@ -325,12 +327,12 @@ class Config(_StrictModel):
DEFAULT_AGENT_NAME: AgentLLMConfig(), DEFAULT_AGENT_NAME: AgentLLMConfig(),
CHECK_LOGS_AGENT_NAME: AgentLLMConfig( CHECK_LOGS_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.ANTHROPIC, provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20240620", model="claude-3-5-sonnet-20241022",
temperature=0.5, temperature=0.5,
), ),
CODE_MONKEY_AGENT_NAME: AgentLLMConfig( CODE_MONKEY_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.OPENAI, provider=LLMProvider.ANTHROPIC,
model="gpt-4-0125-preview", model="claude-3-5-sonnet-20241022",
temperature=0.0, temperature=0.0,
), ),
CODE_REVIEW_AGENT_NAME: AgentLLMConfig( CODE_REVIEW_AGENT_NAME: AgentLLMConfig(
@@ -343,9 +345,19 @@ class Config(_StrictModel):
model="gpt-4o-mini-2024-07-18", model="gpt-4o-mini-2024-07-18",
temperature=0.0, temperature=0.0,
), ),
PARSE_TASK_AGENT_NAME: AgentLLMConfig( FRONTEND_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20241022",
temperature=0.0,
),
GET_RELEVANT_FILES_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.OPENAI, provider=LLMProvider.OPENAI,
model="gpt-4-0125-preview", model="gpt-4o-2024-05-13",
temperature=0.5,
),
PARSE_TASK_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20241022",
temperature=0.0, temperature=0.0,
), ),
SPEC_WRITER_AGENT_NAME: AgentLLMConfig( SPEC_WRITER_AGENT_NAME: AgentLLMConfig(
@@ -355,7 +367,7 @@ class Config(_StrictModel):
), ),
TASK_BREAKDOWN_AGENT_NAME: AgentLLMConfig( TASK_BREAKDOWN_AGENT_NAME: AgentLLMConfig(
provider=LLMProvider.ANTHROPIC, provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20240620", model="claude-3-5-sonnet-20241022",
temperature=0.5, temperature=0.5,
), ),
TECH_LEAD_PLANNING: AgentLLMConfig( TECH_LEAD_PLANNING: AgentLLMConfig(
@@ -363,6 +375,11 @@ class Config(_StrictModel):
model="claude-3-5-sonnet-20240620", model="claude-3-5-sonnet-20240620",
temperature=0.5, temperature=0.5,
), ),
TECH_LEAD_EPIC_BREAKDOWN: AgentLLMConfig(
provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20241022",
temperature=0.5,
),
TROUBLESHOOTER_BUG_REPORT: AgentLLMConfig( TROUBLESHOOTER_BUG_REPORT: AgentLLMConfig(
provider=LLMProvider.ANTHROPIC, provider=LLMProvider.ANTHROPIC,
model="claude-3-5-sonnet-20240620", model="claude-3-5-sonnet-20240620",
@@ -479,6 +496,7 @@ def adapt_for_bedrock(config: Config) -> Config:
return config return config
replacement_map = { 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-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-sonnet-20240229": "us.anthropic.claude-3-sonnet-20240229-v1:0",
"claude-3-haiku-20240307": "us.anthropic.claude-3-haiku-20240307-v1:0", "claude-3-haiku-20240307": "us.anthropic.claude-3-haiku-20240307-v1:0",

View File

@@ -25,3 +25,35 @@ THINKING_LOGS = [
"Pythagora is working for you, so relax!", "Pythagora is working for you, so relax!",
"Pythagora might take some time to figure this out...", "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
"""

View File

@@ -61,7 +61,7 @@ version_path_separator = os
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = sqlite:///pythagora.db sqlalchemy.url = sqlite:///data/database/pythagora.db
[post_write_hooks] [post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run # post_write_hooks defines scripts or Python functions that are run

View File

@@ -0,0 +1,34 @@
"""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 ###

View File

@@ -1,6 +1,6 @@
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional, Union
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from sqlalchemy import ForeignKey, UniqueConstraint, delete, inspect from sqlalchemy import ForeignKey, UniqueConstraint, delete, inspect
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.sql import func from sqlalchemy.sql import func
from core.db.models import Base from core.db.models import Base, FileContent
from core.log import get_logger from core.log import get_logger
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -67,6 +67,7 @@ class ProjectState(Base):
tasks: Mapped[list[dict]] = mapped_column(default=list) tasks: Mapped[list[dict]] = mapped_column(default=list)
steps: Mapped[list[dict]] = mapped_column(default=list) steps: Mapped[list[dict]] = mapped_column(default=list)
iterations: Mapped[list[dict]] = mapped_column(default=list) iterations: Mapped[list[dict]] = mapped_column(default=list)
knowledge_base: Mapped[dict] = mapped_column(default=dict, server_default="{}")
relevant_files: Mapped[Optional[list[str]]] = mapped_column(default=None) relevant_files: Mapped[Optional[list[str]]] = mapped_column(default=None)
modified_files: Mapped[dict] = mapped_column(default=dict) modified_files: Mapped[dict] = mapped_column(default=dict)
docs: Mapped[Optional[list[dict]]] = mapped_column(default=None) docs: Mapped[Optional[list[dict]]] = mapped_column(default=None)
@@ -238,6 +239,7 @@ class ProjectState(Base):
tasks=deepcopy(self.tasks), tasks=deepcopy(self.tasks),
steps=deepcopy(self.steps), steps=deepcopy(self.steps),
iterations=deepcopy(self.iterations), iterations=deepcopy(self.iterations),
knowledge_base=deepcopy(self.knowledge_base),
files=[], files=[],
relevant_files=deepcopy(self.relevant_files), relevant_files=deepcopy(self.relevant_files),
modified_files=deepcopy(self.modified_files), modified_files=deepcopy(self.modified_files),
@@ -257,14 +259,14 @@ class ProjectState(Base):
return new_state return new_state
def complete_step(self): def complete_step(self, step_type: str):
if not self.unfinished_steps: if not self.unfinished_steps:
raise ValueError("There are no unfinished steps to complete") raise ValueError("There are no unfinished steps to complete")
if "next_state" in self.__dict__: if "next_state" in self.__dict__:
raise ValueError("Current state is read-only (already has a next state).") raise ValueError("Current state is read-only (already has a next state).")
log.debug(f"Completing step {self.unfinished_steps[0]['type']}") log.debug(f"Completing step {self.unfinished_steps[0]['type']}")
self.unfinished_steps[0]["completed"] = True self.get_steps_of_type(step_type)[0]["completed"] = True
flag_modified(self, "steps") flag_modified(self, "steps")
def complete_task(self): def complete_task(self):
@@ -305,6 +307,7 @@ class ProjectState(Base):
log.debug(f"Completing iteration {self.unfinished_iterations[0]}") log.debug(f"Completing iteration {self.unfinished_iterations[0]}")
self.unfinished_iterations[0]["status"] = IterationStatus.DONE self.unfinished_iterations[0]["status"] = IterationStatus.DONE
self.relevant_files = None self.relevant_files = None
self.modified_files = {}
self.flag_iterations_as_modified() self.flag_iterations_as_modified()
def flag_iterations_as_modified(self): def flag_iterations_as_modified(self):
@@ -327,6 +330,26 @@ class ProjectState(Base):
""" """
flag_modified(self, "tasks") 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 field as having been modified
Used by Agents that perform modifications within the mutable knowledge base 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, "knowledge_base")
def set_current_task_status(self, status: str): def set_current_task_status(self, status: str):
""" """
Set the status of the current task. Set the status of the current task.
@@ -354,6 +377,17 @@ class ProjectState(Base):
return None 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": def save_file(self, path: str, content: "FileContent", external: bool = False) -> "File":
""" """
Save a file to the project state. Save a file to the project state.
@@ -443,3 +477,19 @@ class ProjectState(Base):
""" """
li = self.unfinished_steps li = self.unfinished_steps
return [step for step in li if step.get("type") == step_type] if li else [] 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)
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"

View File

@@ -1,5 +1,3 @@
import time
from sqlalchemy import event from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
@@ -35,16 +33,6 @@ class SessionManager:
self.recursion_depth = 0 self.recursion_depth = 0
event.listen(self.engine.sync_engine, "connect", self._on_connect) 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, _): def _on_connect(self, dbapi_connection, _):
"""Connection event handler""" """Connection event handler"""
@@ -56,7 +44,6 @@ class SessionManager:
# it's a local file. PostgreSQL or other database use a real connection pool # it's a local file. PostgreSQL or other database use a real connection pool
# by default. # by default.
dbapi_connection.execute("pragma foreign_keys=on") dbapi_connection.execute("pragma foreign_keys=on")
dbapi_connection.execute("PRAGMA journal_mode=WAL;")
async def start(self) -> AsyncSession: async def start(self) -> AsyncSession:
if self.session is not None: if self.session is not None:

View File

@@ -1,6 +1,7 @@
import asyncio
import datetime import datetime
import zoneinfo import zoneinfo
from typing import Optional from typing import Optional, Tuple
from anthropic import AsyncAnthropic, RateLimitError from anthropic import AsyncAnthropic, RateLimitError
from httpx import Timeout from httpx import Timeout
@@ -18,6 +19,10 @@ MAX_TOKENS = 4096
MAX_TOKENS_SONNET = 8192 MAX_TOKENS_SONNET = 8192
class CustomAssertionError(Exception):
pass
class AnthropicClient(BaseLLMClient): class AnthropicClient(BaseLLMClient):
provider = LLMProvider.ANTHROPIC provider = LLMProvider.ANTHROPIC
@@ -38,7 +43,7 @@ class AnthropicClient(BaseLLMClient):
Adapt the conversation messages to the format expected by the Anthropic Claude model. 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 Claude only recognizes "user" and "assistant" roles, and requires them to be switched
for each message (ie. no consecutive messages from the same role). for each message (i.e. no consecutive messages from the same role).
:param convo: Conversation to adapt. :param convo: Conversation to adapt.
:return: Adapted conversation messages. :return: Adapted conversation messages.
@@ -61,50 +66,60 @@ class AnthropicClient(BaseLLMClient):
return messages return messages
async def _make_request( async def _make_request(
self, self, convo: Convo, temperature: Optional[float] = None, json_mode: bool = False, retry_count: int = 1
convo: Convo, ) -> Tuple[str, int, int]:
temperature: Optional[float] = None, async def single_attempt() -> Tuple[str, int, int]:
json_mode: bool = False, messages = self._adapt_messages(convo)
) -> tuple[str, int, int]: completion_kwargs = {
messages = self._adapt_messages(convo) "max_tokens": MAX_TOKENS,
completion_kwargs = { "model": self.config.model,
"max_tokens": MAX_TOKENS, "messages": messages,
"model": self.config.model, "temperature": self.config.temperature if temperature is None else temperature,
"messages": messages, }
"temperature": self.config.temperature if temperature is None else temperature,
}
if "bedrock/anthropic" in self.config.base_url: if "trybricks" in self.config.base_url:
completion_kwargs["extra_headers"] = {"anthropic-version": "bedrock-2023-05-31"} completion_kwargs["extra_headers"] = {"x-request-timeout": f"{int(float(self.config.read_timeout))}s"}
if "sonnet" in self.config.model: if "bedrock/anthropic" in self.config.base_url:
if "extra_headers" in completion_kwargs: completion_kwargs["extra_headers"] = {"anthropic-version": "bedrock-2023-05-31"}
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: if "sonnet" in self.config.model:
completion_kwargs["response_format"] = {"type": "json_object"} completion_kwargs["max_tokens"] = MAX_TOKENS_SONNET
response = [] if json_mode:
async with self.client.messages.stream(**completion_kwargs) as stream: completion_kwargs["response_format"] = {"type": "json_object"}
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 response = []
final_message = await stream.get_final_message() async with self.client.messages.stream(**completion_kwargs) as stream:
final_message.content async for content in stream.text_stream:
response.append(content)
if self.stream_handler:
await self.stream_handler(content)
response_str = "".join(response) 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.")
# Tell the stream handler we're done response_str = "".join(response)
if self.stream_handler:
await self.stream_handler(None)
return response_str, final_message.usage.input_tokens, final_message.usage.output_tokens # 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
def rate_limit_sleep(self, err: RateLimitError) -> Optional[datetime.timedelta]: def rate_limit_sleep(self, err: RateLimitError) -> Optional[datetime.timedelta]:
""" """

View File

@@ -263,7 +263,7 @@ class BaseLLMClient:
[ [
"We sent too large request to the LLM, resulting in an error. ", "We sent too large request to the LLM, resulting in an error. ",
"This is usually caused by including framework files in an LLM request. ", "This is usually caused by including framework files in an LLM request. ",
"Here's how you can get GPT Pilot to ignore those extra files: ", "Here's how you can get Pythagora to ignore those extra files: ",
"https://bit.ly/faq-token-limit-error", "https://bit.ly/faq-token-limit-error",
] ]
) )

View File

@@ -1,11 +1,62 @@
import json import json
import re import re
from enum import Enum from enum import Enum
from typing import Optional, Union from typing import List, Optional, Union
from pydantic import BaseModel, ValidationError, create_model 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: class MultiCodeBlockParser:
""" """
Parse multiple Markdown code blocks from a string. Parse multiple Markdown code blocks from a string.

View File

@@ -225,6 +225,7 @@ class ProcessManager:
cwd: str = ".", cwd: str = ".",
env: Optional[dict[str, str]] = None, env: Optional[dict[str, str]] = None,
timeout: float = MAX_COMMAND_TIMEOUT, timeout: float = MAX_COMMAND_TIMEOUT,
show_output: Optional[bool] = True,
) -> tuple[Optional[int], str, str]: ) -> tuple[Optional[int], str, str]:
""" """
Run command and wait for it to finish. Run command and wait for it to finish.
@@ -236,6 +237,7 @@ class ProcessManager:
:param cwd: Working directory. :param cwd: Working directory.
:param env: Environment variables. :param env: Environment variables.
:param timeout: Timeout in seconds. :param timeout: Timeout in seconds.
:param show_output: Show output in the ui.
:return: Tuple of (status code, stdout, stderr). :return: Tuple of (status code, stdout, stderr).
""" """
timeout = min(timeout, MAX_COMMAND_TIMEOUT) timeout = min(timeout, MAX_COMMAND_TIMEOUT)
@@ -245,7 +247,7 @@ class ProcessManager:
t0 = time.time() t0 = time.time()
while process.is_running and (time.time() - t0) < timeout: while process.is_running and (time.time() - t0) < timeout:
out, err = await process.read_output(BUSY_WAIT_INTERVAL) out, err = await process.read_output(BUSY_WAIT_INTERVAL)
if self.output_handler and (out or err): if self.output_handler and (out or err) and show_output:
await self.output_handler(out, err) await self.output_handler(out, err)
if process.is_running: if process.is_running:
@@ -256,7 +258,7 @@ class ProcessManager:
await process.wait() await process.wait()
out, err = await process.read_output() out, err = await process.read_output()
if self.output_handler and (out or err): if self.output_handler and (out or err) and show_output:
await self.output_handler(out, err) await self.output_handler(out, err)
if terminated: if terminated:

View File

@@ -20,10 +20,14 @@ A part of the app is already finished.
{% include "partials/user_feedback.prompt" %} {% include "partials/user_feedback.prompt" %}
{% if current_task.test_instructions is defined %} {% if test_instructions %}
Here are the test instructions the user was following when the issue occurred: Here are the test instructions the user was following when the issue occurred:
``` ```
{{ current_task.test_instructions }} {% for step in test_instructions %}
Step #{{ loop.index }}
Action: {{ step.action }}
Expected result: {{ step.result }}
{% endfor %}
``` ```
{% endif %} {% endif %}
@@ -38,4 +42,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? 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** **IMPORTANT**
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. 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.

View File

@@ -1,13 +1,14 @@
{% if backend_logs is not none %}Here are the logs we added to the backend: {% if backend_logs and backend_logs|trim %}
Here are the logs we added to the backend:
``` ```
{{ backend_logs }} {{ backend_logs }}
``` ```
{% endif %}{% if frontend_logs is not none %} {% endif %}{% if frontend_logs and frontend_logs|trim %}
Here are the logs we added to the frontend: Here are the logs we added to the frontend:
``` ```
{{ frontend_logs }} {{ frontend_logs }}
``` ```
{% endif %}{% if user_feedback is not none %} {% endif %}{% if user_feedback and user_feedback|trim %}
Finally, here is a hint from a human who tested the app: Finally, here is a hint from a human who tested the app:
``` ```
{{ user_feedback }} {{ user_feedback }}
@@ -16,4 +17,9 @@ When you're thinking about what to do next, take into the account human's feedba
{% endif %}{% if fix_attempted %} {% 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. 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 %} {% 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 %} {% 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 %}

View File

@@ -1,22 +1,20 @@
{% if file_content %}
You are working on a project and your job is to implement new code changes based on given instructions. 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. 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: 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" %} {% include "partials/coding_rules.prompt" %}
You are currently working on this task:
```
{{ state.current_task.description }}
```
{% include "partials/user_feedback.prompt" %} {% include "partials/user_feedback.prompt" %}
Here are development instructions and now you have to focus only on changes in `{{ file_name }}`: 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--- ~~~START_OF_DEVELOPMENT_INSTRUCTIONS~~~
{{ instructions }} {{ instructions }}
---end_of_development_instructions--- ~~~END_OF_DEVELOPMENT_INSTRUCTIONS~~~
{% if rework_feedback is defined %} {% 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: 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:
@@ -28,12 +26,15 @@ The reviewer accepted some of your changes, and the file now looks like this:
{{ file_content }} {{ file_content }}
``` ```
{% elif file_content %} {% elif file_content %}
Here is how `{{ file_name }}` looks like currently: Now, take a look at how `{{ file_name }}` looks like currently:
``` ```
{{ file_content }} {{ 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 %} {% else %}
You need to create a new file `{{ file_name }}`. 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.
{% endif %} {% endif %}
{% include "partials/files_list.prompt" %} ** 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.

View File

@@ -1,4 +1,4 @@
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. 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~~`.
{% include "partials/project_details.prompt" %} {% include "partials/project_details.prompt" %}
{% include "partials/features_list.prompt" %} {% include "partials/features_list.prompt" %}
@@ -10,6 +10,7 @@ You are working on an app called "{{ state.branch.project.name }}" and you need
**IMPORTANT** **IMPORTANT**
Remember, I created an empty folder where I will start writing files that you tell me and that are needed for this app. Remember, I created an empty folder where I will start writing files that you tell me and that are needed for this app.
{% endif %} {% endif %}
~~DEVELOPMENT_INSTRUCTIONS~~
{% include "partials/relative_paths.prompt" %} {% 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. 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.
@@ -20,20 +21,38 @@ DO NOT specify commands to create any folders or files, they will be created aut
{% include "partials/file_size_limit.prompt" %} {% include "partials/file_size_limit.prompt" %}
{% include "partials/breakdown_code_instructions.prompt" %} {% include "partials/breakdown_code_instructions.prompt" %}
Never use the port 5000 to run the app, it's reserved. {% if state.has_frontend() %}
The entire backend API needs to be on /api/... routes!
--IMPLEMENTATION INSTRUCTIONS-- ** 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, API requests are 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 implementat 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.
~~END_OF_DEVELOPMENT_INSTRUCTIONS~~
~~DEVELOPMENT_PLAN~~
We've broken the development of this {% if state.epics|length > 1 %}feature{% else %}app{% endif %} down to these tasks: 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 %} {% 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 %} {% endfor %}
``` ```
~~END_OF_DEVELOPMENT_PLAN~~
You are currently working on task #{{ current_task_index + 1 }} with the following description: You are currently working on task #{{ current_task_index + 1 }} with the following description:
``` ```
{{ task.description }} {{ task.description }}
``` ```
{% 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. Whenever you need to create a utility function that uses a 3rd party integration, you **MUST** mock that function (create the function, return a mocked data, and specify the structure of the input and the output of the function in the comment above the function).
{% if task.get('pre_breakdown_testing_instructions') is not none %} {% if task.get('pre_breakdown_testing_instructions') is not none %}
Here is how this task should be tested: Here is how this task should be tested:
``` ```

View File

@@ -11,10 +11,12 @@ We've broken the development of the project down to these tasks:
{% endfor %} {% endfor %}
``` ```
{% if state.current_task %}
The next task we need to work on, and have to focus on, is this task: The next task we need to work on, and have to focus on, is this task:
``` ```
{{ state.current_task.description }} {{ state.current_task.description }}
``` ```
{% endif %}
{% if user_feedback %}User who was using the app sent you this feedback: {% if user_feedback %}User who was using the app sent you this feedback:
``` ```

View File

@@ -4,9 +4,20 @@ when converting this message to steps.
Each step can be either: Each 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) * `command`
* `save_file` - create or update ONE file (only provide file path, not contents) - command to run
* `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. - assume current working directory is project root folder, which means you MUST add `cd server && <cmd>` or `cd client && <cmd>` if they have to be executed inside `./server` or `./client` folders
- must be able to run on a {{ os }} machine
* `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
- NEVER use `human_intervention` to ask the human to write or review code
* `utility_function`
- if there is a utility function mentioned, use this type of step and specify the file in which it needs to be put in, the name of the function, the description of what this function should do, the status of the function, the input parameters for this function, and the format of the return value
- if the function is going to be mocked, then put the status `mocked` and if it's going to be implemented in this task, put the status `implemented`
- for each utility function that needs to be mocked or implemented, you must provide a separate `utility_function` step
**IMPORTANT**: If multiple changes are required for same file, you must provide single `save_file` step for each file. **IMPORTANT**: If multiple changes are required for same file, you must provide single `save_file` step for each file.
@@ -25,17 +36,30 @@ Examples:
{ {
"type": "save_file", "type": "save_file",
"save_file": { "save_file": {
"path": "server.js" "path": "server/server.js"
}, },
}, },
{ {
"type": "command", "type": "command",
"command": { "command": {
"command": "mv index.js public/index.js"", "command": "cd server && npm install puppeteer",
"timeout": 5, "timeout": 30,
"success_message": "", "success_message": "",
"command_id": "move_index_file" "command_id": "install_puppeteer"
} }
},
{
"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 applications configuration file as: WEBSITE_URL=your_website_endpoint"
} }
] ]
} }

View File

@@ -1,22 +1,8 @@
A coding task has been implemented for the new project we're working on. A coding task has been implemented for the new project, "{{ state.branch.project.name }}", we're working on.
{% include "partials/project_details.prompt" %} Your job is to analyze the output of the command that was ran and determine if the command was successfully executed.
{% include "partials/files_list.prompt" %}
We've broken the development of the project down to these tasks: The current task we are working on is: {{ current_task.description }}
```
{% 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 -%} {% 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: The current task has been split into multiple steps, and each step is one of the following:

View File

@@ -0,0 +1,40 @@
{% 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 }}
```
{{ state.specification.template_summary }}
{% 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
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**
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.
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

View File

@@ -0,0 +1,115 @@
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.
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 <package-name>
```
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: 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. MOCK DATA should be used for all interactions.
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 <package-name>
```
file: App.tsx
```tsx
import React from 'react';
export const MyComponent: React.FC = () => {
return <div>Hello, World!</div>;
};
```
------------------------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---

View File

@@ -1 +1,2 @@
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. 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).

View File

@@ -1,5 +1,5 @@
# RULES FOR IMPLEMENTING CODE CHANGES # RULES FOR IMPLEMENTING CODE CHANGES
---start_of_coding_rules--- ~~~START_OF_CODING_RULES~~~
## Rule 1: Scope of your coding task ## 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. 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,4 +36,8 @@ Whenever you write code, make sure to log code execution so that when a develope
## Rule 6: Error handling ## 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. 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.
---end_of_coding_rules--- {% if state.has_frontend() %}
## Rule 7: Showing errors on the frontend
If there is an error in the API request, return the error message by throwing an error in the client/api/<FILE> 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.
~~~END_OF_CODING_RULES~~~
{% endif %}

View File

@@ -1,13 +1,13 @@
{% if state.epics|length > 2 %} {% if state.epics|length > 3 %}
Here is the list of features that were previously implemented on top of initial high level description of "{{ state.branch.project.name }}": 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[1:-1] %} {% for feature in state.epics[2:-1] %}
- {{ loop.index }}. {{ feature.description }} - {{ loop.index }}. {{ feature.description }}
{% endfor %} {% endfor %}
``` ```
{% endif %} {% endif %}
{% if state.epics|length > 1 %} {% if state.epics|length > 2 %}
Here is the feature that you are implementing right now: Here is the feature that you are implementing right now:
``` ```

View File

@@ -1 +1,35 @@
**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. **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
```

View File

@@ -1,4 +1,9 @@
These files are currently implemented in the project: 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 %} {% for file in state.files %}
{% if not state.has_frontend() or (state.has_frontend() and state.epics|length > 1 and 'client/src/components/ui' not in file.path ) or (state.has_frontend() and state.epics|length == 1 ) %}
* `{{ file.path }}{% if file.meta.get("description") %}: {{file.meta.description}}{% endif %}` * `{{ file.path }}{% if file.meta.get("description") %}: {{file.meta.description}}{% endif %}`
{% endfor %} {% endif %}{% endfor %}
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.meta.get("description") %}: {{file.meta.description}}{% endif %}`
{% endif %}{% endfor %}

View File

@@ -1,15 +1,28 @@
{% if state.relevant_files %} {% if state.relevant_files %}
~~FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
{% include "partials/files_descriptions.prompt" %} {% include "partials/files_descriptions.prompt" %}
~~END_OF_FILE_DESCRIPTIONS_IN_THE_CODEBASE~~
~~RELEVANT_FILES_IMPLEMENTATION~~
{% include "partials/files_list_relevant.prompt" %} {% include "partials/files_list_relevant.prompt" %}
{% elif state.files %} {% elif state.files %}
~~RELEVANT_FILES_IMPLEMENTATION~~
These files are currently implemented in the project: These files are currently implemented in the project:
---START_OF_FILES--- ---START_OF_FRONTEND_API_FILES---
{% for file in state.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 %}
**`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code): **`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code):
``` ```
{{ file.content.content }}``` {{ file.content.content }}```
{% endfor %} {% endif %}{% endfor %}
---END_OF_FILES--- ---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---
{% endif %} {% endif %}
~~END_OF_RELEVANT_FILES_IMPLEMENTATION~~

View File

@@ -1,9 +1,30 @@
Here are the complete contents of files relevant to this task: Here are the complete contents of files relevant to this task:
---START_OF_FILES--- {% if state.has_frontend() %}
---START_OF_FRONTEND_API_FILES---
{% for file in state.relevant_file_objects %} {% for file in state.relevant_file_objects %}
File **`{{ file.path }}`** ({{file.content.content.splitlines()|length}} lines of code): {% 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.content.content }}``` {{ 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 %} {% endfor %}
---END_OF_FILES--- ---END_OF_FILES---
{% endif %}

View File

@@ -1,14 +1,20 @@
Here is the current relevant files list: Here is the current relevant files list:
{% if relevant_files %}{{ relevant_files }}{% else %}[]{% endif %} {% 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: Now, with multiple iterations you have to find relevant files for the current task. Here are the actions that you can use:
- `read_files` - List of files that you want to read. - `read_files` - List of files that you want to read.
- `add_files` - Add file to the list of relevant files. - `add_files` - Add file to the list of relevant files.
- `remove_files` - Remove file from 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. - `finished` - Boolean that you will use when you finish with finding relevant files.
Make sure to follow these rules: IMPORTANT:
# 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. - 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. - Do not repeat same action for same file that you have already done. For example if you already added "server.js" to the list of relevant files you must not add "server.js" 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. - You must read the file before adding it to the list of relevant files. Do not add 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. {% if state.current_task %}- Focus only on your current task `{{ state.current_task.description }}` when selecting relevant files.{% endif %}
{% if not state.has_frontend() %}- IMPORTANT: You must read and add relevant files from both "client/" and "server/" folders so that implementation can be done correctly! The "client" and "server" must work seamlessly together!{% endif %}
- IMPORTANT: Execute only 1 action per request! Once your response is processed you will be able to choose next action.
- IMPORTANT: It is much better to read and add more files than not adding files that are relevant!
- If there is even a slight chance a file is relevant, you should read it. Make sure to read files in client/src/pages folder that could be relevant.
- Read as many files as possible. You can read and add up to 20 files at once and if you need to see more file contents you can do that in next iterations. Make sure you don't miss any files that are relevant!

View File

@@ -1,22 +1,11 @@
~~APP_DESCRIPTION~~
Here is a high level description of "{{ state.branch.project.name }}": Here is a high level description of "{{ state.branch.project.name }}":
``` ```
{{ state.specification.description }} {{ 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 %} {% if state.specification.template_summary %}
~~INFORMATION_ABOUT_THE_CODEBASE~~
{{ state.specification.template_summary }} {{ state.specification.template_summary }}
~~END_OF_INFORMATION_ABOUT_THE_CODEBASE~~
{% endif %} {% endif %}

View File

@@ -1,26 +1,35 @@
{# 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. #} {# 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 # Rules for creating epics
---start_of_rules_for_creating_epics--- ~~~START_OF_RULES_FOR_CREATING_EPICS~~~
## Rule #1 ## 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. 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. 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 ## 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. 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. 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. Simple apps should have only 1 epic. More complex apps should have more epics. Do not create more epics than needed.
## Rule #4 ## Rule #3
This rule applies to writing epic 'description'. 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. 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.
## Rule #5 ** MOST IMPORTANT RULES **
This rule applies to order of epics. ## Rule #4 (MOST IMPORTANT RULE)
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. 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.
---end_of_rules_for_creating_epics--- ## Rule #5 (MOST IMPORTANT RULE)
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. The general order should be as follows:
{% if task_type == 'app' %}
- The first epic **MUST** be to implement authentication. In the first epic, the first task **MUST** be to remove the mocked data for making a register and login API requests from the frontend (in the client/src/api/auth.js file) - do **NOT** split this task into multiple tasks. Next task, if needed, should be to update the User model with more required parameters like different roles, different user data, etc. In the second task, make sure to add all necessary fields to the User model in the backend.
{% endif %}
- The {% if task_type == 'app' %}second{% else %}first{% endif %} 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.
- {% if task_type == 'app' %}Finally{% else %}Then{% endif %}, 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~~~

View File

@@ -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: For example:
- use `dirname/filename.py` instead of `/path/to/project/dirname/filename.py` - use `dirname/filename.py` instead of `/path/to/project/dirname/filename.py`
- use `filename.js` instead of `./filename.js` - use `filename.js` instead of `./filename.js`

View File

@@ -0,0 +1,7 @@
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.

View File

@@ -50,7 +50,7 @@ Here's an EXAMPLE initial prompt:
---start-of-example-output--- ---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. 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 GPT Pilot". 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".
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. 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.

View File

@@ -1,8 +1,15 @@
{% 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 }} {{ prompt }}
``` ```
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: {% 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:
"hard" for high complexity "hard" for high complexity
"moderate" for moderate complexity "moderate" for moderate complexity
"simple" for low complexity "simple" for low complexity

View File

@@ -1 +1,19 @@
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. 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) %}
**IMPORTANT**
If you are working on the Epic #1 that needs to implement the authentication system. The first task **MUST** be to remove the mocked data for authentication (register and login). After that, add any custom authentication requirements like different roles, different user data, etc.
{% endif %}

View File

@@ -0,0 +1,2 @@
{# This is the same template as for Developer's filter files #}
{% extends "developer/filter_files.prompt" %}

View File

@@ -0,0 +1,2 @@
{# This is the same template as for Developer's filter files #}
{% extends "developer/filter_files_loop.prompt" %}

View File

@@ -21,8 +21,13 @@ Finally, here is the description of new feature that needs to be added to the ap
{% if epic.complexity and epic.complexity == 'simple' %} {% 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. 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 %} {% else %}
Before we go into the coding part, your job is to split the development process of creating this app into smaller epics. 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 %}. You should not reimplement what's already done - just continue from the implementation already there{% endif %}{% endif %} while strictly following these rules: 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!
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.{% if task_type == 'app' and (state.options|default({})).get('auth', True) %} The first epic **MUST** be to implement the authentication system.{% endif %}
Strictly follow these rules:
{% include "partials/project_tasks.prompt" %} {% include "partials/project_tasks.prompt" %}
{% endif %} {% endif %}

View File

@@ -1,61 +0,0 @@
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.

View File

@@ -1,15 +1,3 @@
{% 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? 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. 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.
@@ -26,22 +14,63 @@ 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. 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) 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 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! Remember, these rules are very important and you must follow them!
Here is an example output with a few user steps: Here is an example output with a few user steps:
---example--- ---example---
### Step 1 {
Action: Start the server using `npm start` "steps": [
Expected result: You should see the message "Connected to database" or similar {
"title": "Submit the form",
### Step 2 "action": "Open your web browser and visit 'http://localhost:5173/form'. Click on the 'Submit' button in the web form",
Action: Open your web browser and visit http://localhost:3000/ "result": "Form is submitted, the page is reloaded, and 'Thank you' message is shown",
Expected result: Web page opens and you see a "Hello World" message with a contact form },
{
### Step 3 "title": "Check email",
Action: Click on the "Submit" button in the web form "action": "Check your email inbox for the magic link. Click on the magic link to log in.",
Expected result: Form is submitted, page is reloaded and "Thank you" message is shown "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--- ---end_of_example---
If nothing needs to be tested for this task, instead of outputting the steps, just output a single word: DONE 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 %}
```
Knowing these rules, tell me, please list actions, step by step, in order, that the user should take to verify the task.

View File

@@ -1,2 +1,2 @@
{# This is the same template as for Developer's filter files because Troubleshooter is reusing it in a conversation #} {# This is the same template as for Developer's filter files #}
{% extends "developer/filter_files.prompt" %} {% extends "developer/filter_files.prompt" %}

View File

@@ -1,2 +1,2 @@
{# This is the same template as for Developer's filter files because Troubleshooter is reusing it in a conversation #} {# This is the same template as for Developer's filter files #}
{% extends "developer/filter_files_loop.prompt" %} {% extends "developer/filter_files_loop.prompt" %}

View File

@@ -21,14 +21,17 @@ A part of the app is already finished.
{% include "partials/user_feedback.prompt" %} {% include "partials/user_feedback.prompt" %}
{% if state.current_task.test_instructions is defined %} {% if test_instructions %}
User was testing the current implementation of the app when they requested some changes to the app. These are the testing instructions: User was testing the current implementation of the app when they requested some changes to the app. These are the testing instructions:
``` ```
{{ state.current_task.test_instructions }} {% for step in test_instructions %}
Step #{{ loop.index }}
Action: {{ step.action }}
Expected result: {{ step.result }}
{% endfor %}
``` ```
{% endif %} {% endif %}
{% if next_solution_to_try is not none %} {% if next_solution_to_try is not none %}
Focus on solving this issue in the following way: Focus on solving this issue in the following way:
``` ```

View File

@@ -5,11 +5,12 @@ from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from sqlalchemy import inspect, select
from tenacity import retry, stop_after_attempt, wait_fixed from tenacity import retry, stop_after_attempt, wait_fixed
from core.config import FileSystemType, get_config from core.config import FileSystemType, get_config
from core.db.models import Branch, ExecLog, File, FileContent, LLMRequest, Project, ProjectState, UserInput from core.db.models import Branch, ExecLog, File, FileContent, LLMRequest, Project, ProjectState, UserInput
from core.db.models.specification import Specification from core.db.models.specification import Complexity, Specification
from core.db.session import SessionManager from core.db.session import SessionManager
from core.disk.ignore import IgnoreMatcher from core.disk.ignore import IgnoreMatcher
from core.disk.vfs import LocalDiskVFS, MemoryVFS, VirtualFileSystem from core.disk.vfs import LocalDiskVFS, MemoryVFS, VirtualFileSystem
@@ -49,6 +50,9 @@ class StateManager:
self.next_state = None self.next_state = None
self.current_session = None self.current_session = None
self.blockDb = False self.blockDb = False
self.git_available = False
self.git_used = False
self.options = {}
@asynccontextmanager @asynccontextmanager
async def db_blocker(self): async def db_blocker(self):
@@ -136,7 +140,7 @@ class StateManager:
The returned ProjectState will have branch and branch.project The returned ProjectState will have branch and branch.project
relationships preloaded. All other relationships must be relationships preloaded. All other relationships must be
excplicitly loaded using ProjectState.awaitable_attrs or explicitly loaded using ProjectState.awaitable_attrs or
AsyncSession.refresh. AsyncSession.refresh.
:param project_id: Project ID (keyword-only, optional). :param project_id: Project ID (keyword-only, optional).
@@ -211,11 +215,26 @@ class StateManager:
self.current_state.tasks, 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 return self.current_state
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) @retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
async def commit_with_retry(self): async def commit_with_retry(self):
await self.current_session.commit() try:
await self.current_session.commit()
except Exception as e:
log.error(f"Commit failed: {str(e)}")
raise
async def commit(self) -> ProjectState: async def commit(self) -> ProjectState:
""" """
@@ -348,6 +367,8 @@ class StateManager:
telemetry.inc("num_tasks") telemetry.inc("num_tasks")
if not self.next_state.unfinished_tasks: if not self.next_state.unfinished_tasks:
if len(self.current_state.epics) == 1: 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") telemetry.set("end_result", "success:initial-project")
else: else:
telemetry.set("end_result", "success:feature") telemetry.set("end_result", "success:feature")
@@ -393,8 +414,8 @@ class StateManager:
file_content = await FileContent.store(self.current_session, hash, content) file_content = await FileContent.store(self.current_session, hash, content)
file = self.next_state.save_file(path, file_content) file = self.next_state.save_file(path, file_content)
if self.ui and not from_template: # if self.ui and not from_template:
await self.ui.open_editor(self.file_system.get_full_path(path)) # await self.ui.open_editor(self.file_system.get_full_path(path))
if metadata: if metadata:
file.meta = metadata file.meta = metadata
@@ -586,15 +607,147 @@ class StateManager:
""" """
return not bool(self.file_system.list()) 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.get("pages", None)
current_apis = self.next_state.knowledge_base.get("apis", None)
# 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(self.next_state.knowledge_base)
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.get("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:
if "utility_functions" not in self.next_state.knowledge_base:
self.next_state.knowledge_base["utility_functions"] = []
self.next_state.knowledge_base["utility_functions"].append(utility_function)
self.next_state.flag_knowledge_base_as_modified()
await self.ui.knowledge_base_update(self.next_state.knowledge_base)
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 = (
next(
(
api
for api in self.current_state.knowledge_base.get("apis", [])
if api["endpoint"] == endpoint.strip()
),
{},
)
.get("locations", {})
.get("backend", None)
)
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 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 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(self.next_state.knowledge_base)
@staticmethod @staticmethod
def get_input_required(content: str) -> list[int]: def get_input_required(content: str, file_path: str) -> list[int]:
""" """
Get the list of lines containing INPUT_REQUIRED keyword. Get the list of lines containing INPUT_REQUIRED keyword.
:param content: The file content to search. :param content: The file content to search.
:param file_path: The file path.
:return: Indices of lines with INPUT_REQUIRED keyword, starting from 1. :return: Indices of lines with INPUT_REQUIRED keyword, starting from 1.
""" """
lines = [] lines = []
if ".env" not in file_path:
return lines
for i, line in enumerate(content.splitlines(), start=1): for i, line in enumerate(content.splitlines(), start=1):
if "INPUT_REQUIRED" in line: if "INPUT_REQUIRED" in line:
lines.append(i) lines.append(i)

View File

@@ -69,9 +69,9 @@ class Telemetry:
self.data = { self.data = {
# System platform # System platform
"platform": sys.platform, "platform": sys.platform,
# Python version used for GPT Pilot # Python version used
"python_version": sys.version, "python_version": sys.version,
# GPT Pilot version # Core version
"pilot_version": get_version(), "pilot_version": get_version(),
# Pythagora VSCode Extension version # Pythagora VSCode Extension version
"extension_version": None, "extension_version": None,
@@ -86,8 +86,8 @@ class Telemetry:
"updated_prompt": None, "updated_prompt": None,
# App complexity # App complexity
"is_complex_app": None, "is_complex_app": None,
# Optional template used for the project # Optional templates used for the project
"template": None, "templates": None,
# Optional, example project selected by the user # Optional, example project selected by the user
"example_project": None, "example_project": None,
# Optional user contact email # Optional user contact email
@@ -136,7 +136,7 @@ class Telemetry:
"num_tasks": 0, "num_tasks": 0,
# Number of seconds elapsed during development # Number of seconds elapsed during development
"elapsed_time": 0, "elapsed_time": 0,
# Total number of lines created by GPT Pilot # Total number of lines created by Pythagora
"created_lines": 0, "created_lines": 0,
# End result of development: # End result of development:
# - success:initial-project # - success:initial-project
@@ -150,7 +150,7 @@ class Telemetry:
"is_continuation": False, "is_continuation": False,
# Optional user feedback # Optional user feedback
"user_feedback": None, "user_feedback": None,
# If GPT Pilot crashes, record diagnostics # If Core crashes, record diagnostics
"crash_diagnostics": None, "crash_diagnostics": None,
# Statistics for large requests # Statistics for large requests
"large_requests": None, "large_requests": None,
@@ -319,7 +319,7 @@ class Telemetry:
"median_time": sorted(self.slow_requests)[n_slow // 2] if n_slow > 0 else None, "median_time": sorted(self.slow_requests)[n_slow // 2] if n_slow > 0 else None,
} }
async def send(self, event: str = "pilot-telemetry"): async def send(self, event: str = "pythagora-core-telemetry"):
""" """
Send telemetry data to the phone-home endpoint. Send telemetry data to the phone-home endpoint.

View File

@@ -20,6 +20,9 @@ class NoOptions(BaseModel):
Options class for templates that do not require any options. Options class for templates that do not require any options.
""" """
class Config:
extra = "allow"
pass pass
@@ -99,6 +102,7 @@ class BaseProjectTemplate:
"random_secret": uuid4().hex, "random_secret": uuid4().hex,
"options": self.options_dict, "options": self.options_dict,
}, },
self.state_manager.file_system.root,
self.filter, self.filter,
) )

View File

@@ -0,0 +1,91 @@
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`)
* 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`.

View File

@@ -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.", ".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.", "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", "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/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/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", "src/assets/.gitkeep": "Empty file",
} }

View File

@@ -2,8 +2,9 @@ from enum import Enum
from core.log import get_logger from core.log import get_logger
from .javascript_react import JavascriptReactProjectTemplate # from .javascript_react import JavascriptReactProjectTemplate
from .node_express_mongoose import NodeExpressMongooseProjectTemplate # from .node_express_mongoose import NodeExpressMongooseProjectTemplate
from .vite_react import ViteReactProjectTemplate
# from .react_express import ReactExpressProjectTemplate # from .react_express import ReactExpressProjectTemplate
@@ -13,13 +14,15 @@ log = get_logger(__name__)
class ProjectTemplateEnum(str, Enum): class ProjectTemplateEnum(str, Enum):
"""Choices of available project templates.""" """Choices of available project templates."""
JAVASCRIPT_REACT = JavascriptReactProjectTemplate.name # JAVASCRIPT_REACT = JavascriptReactProjectTemplate.name
NODE_EXPRESS_MONGOOSE = NodeExpressMongooseProjectTemplate.name # NODE_EXPRESS_MONGOOSE = NodeExpressMongooseProjectTemplate.name
VITE_REACT = ViteReactProjectTemplate.name
# REACT_EXPRESS = ReactExpressProjectTemplate.name # REACT_EXPRESS = ReactExpressProjectTemplate.name
PROJECT_TEMPLATES = { PROJECT_TEMPLATES = {
JavascriptReactProjectTemplate.name: JavascriptReactProjectTemplate, # JavascriptReactProjectTemplate.name: JavascriptReactProjectTemplate,
NodeExpressMongooseProjectTemplate.name: NodeExpressMongooseProjectTemplate, # NodeExpressMongooseProjectTemplate.name: NodeExpressMongooseProjectTemplate,
ViteReactProjectTemplate.name: ViteReactProjectTemplate,
# ReactExpressProjectTemplate.name: ReactExpressProjectTemplate, # ReactExpressProjectTemplate.name: ReactExpressProjectTemplate,
} }

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import os
from os import walk from os import walk
from os.path import join, relpath from os.path import join, relpath
from pathlib import Path from pathlib import Path
@@ -67,12 +68,13 @@ class Renderer:
tpl_object = self.jinja_env.get_template(template) tpl_object = self.jinja_env.get_template(template)
return tpl_object.render(context) return tpl_object.render(context)
def render_tree(self, root: str, context: Any, filter: Callable = None) -> dict[str, str]: def render_tree(self, root: str, context: Any, full_root_dir: str, filter: Callable = None) -> dict[str, str]:
""" """
Render a tree folder structure of templates using provided context Render a tree folder structure of templates using provided context
:param root: Root of the tree (relative to `template_dir`). :param root: Root of the tree (relative to `template_dir`).
:param context: Context to render the templates with. :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 :param filter: If defined, will be called for each file to check if it
needs to be processed and determine output file path. needs to be processed and determine output file path.
:return: A flat dictionary with path => content structure. :return: A flat dictionary with path => content structure.
@@ -103,15 +105,31 @@ class Renderer:
for path, subdirs, files in walk(full_root): for path, subdirs, files in walk(full_root):
for file in files: for file in files:
file_path = join(path, file) # actual full path of the template file file_path = join(path, file) # actual full path of the template file
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 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
if filter: if filter:
output_location = filter(output_location) output_location = filter(output_location)
if not output_location: if not output_location:
continue continue
contents = self.render_template(tpl_location, context) contents = self.render_template(tpl_location, context)
retval[output_location] = contents if contents != "":
retval[output_location] = contents
return retval return retval

View File

@@ -0,0 +1,56 @@
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 <directory_path>")
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)

View File

@@ -1,98 +1,94 @@
import React, { useState } from 'react'; import { useState } from "react"
import axios from 'axios'; import { useForm } from "react-hook-form"
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom"
import { AlertDestructive } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"
import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card"
import { Input } from "@/components/ui/input"; import { useToast } from "@/hooks/useToast"
import { Label } from "@/components/ui/label"; import { LogIn } from "lucide-react"
import { useAuth } from "@/contexts/AuthContext"
export default function Login() { type LoginForm = {
const [email, setEmail] = useState(''); email: string
const [password, setPassword] = useState(''); password: string
const [loading, setLoading] = useState(false); }
const [error, setError] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => { export function Login() {
e.preventDefault(); const [loading, setLoading] = useState(false)
setLoading(true); const { toast } = useToast()
setLoading(''); const navigate = useNavigate()
const { login } = useAuth()
const { register, handleSubmit } = useForm<LoginForm>()
const onSubmit = async (data: LoginForm) => {
try { try {
const response = await axios.post('/api/auth/login', { email, password }); setLoading(true)
localStorage.setItem('token', response.data.token); // Save token to local storage await login(data.email, data.password)
navigate('/'); // Redirect to Home toast({
} catch (error) { title: "Success",
console.error('Login error:', error); description: "Logged in successfully",
setError(error.response?.data?.error || 'An unexpected error occurred'); })
navigate("/")
} catch (error: any) {
toast({
variant: "destructive",
title: "Error",
description: error.message || "An error occurred",
})
} finally { } finally {
setLoading(false); setLoading(false)
} }
}; }
return ( return (
<div id="loginPage" className="w-full h-screen flex items-center justify-center px-4"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-secondary p-4">
<Card className="mx-auto max-w-sm"> <Card className="w-full max-w-md">
{error && <AlertDestructive title="Login Error" description={error} />}
<CardHeader> <CardHeader>
<CardTitle className="text-2xl">Login</CardTitle> <CardTitle>Welcome back</CardTitle>
<CardDescription> <CardDescription>Enter your credentials to continue</CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="grid gap-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid gap-2"> <div className="space-y-2">
<Label htmlFor="email">Email</Label> <Label htmlFor="email">Email</Label>
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="m@example.com" placeholder="Enter your email"
value={email} autoComplete="email"
onChange={(e) => setEmail(e.target.value)} {...register("email", { required: true })}
required
/> />
</div> </div>
<div className="grid gap-2"> <div className="space-y-2">
<div className="flex items-center"> <Label htmlFor="password">Password</Label>
<Label htmlFor="password">Password</Label>
{false && <a href="#" className="ml-auto inline-block text-sm underline">
Forgot your password?
</a>}
</div>
<Input <Input
id="password" id="password"
type="password" type="password"
value={password} placeholder="Enter your password"
onChange={(e) => setPassword(e.target.value)} autoComplete="current-password"
required {...register("password", { required: true })}
/> />
</div> </div>
<Button type="submit" className="w-full" disabled={loading}> <Button type="submit" className="w-full" disabled={loading}>
{loading ? ( {loading ? (
<> "Loading..."
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Loading...
</>
) : ( ) : (
'Login' <>
<LogIn className="mr-2 h-4 w-4" />
Sign In
</>
)} )}
</Button> </Button>
</form> </form>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<a href="/register/" className="underline">
Sign up
</a>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); )
} }

View File

@@ -0,0 +1,7 @@
server/node_modules/
server/.env
server/package-lock.json
client/node_modules/
client/package-lock.json
node_modules/
package-lock.json

View File

@@ -0,0 +1,23 @@
{% raw %}
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
{% endraw %}

View File

@@ -0,0 +1,31 @@
{% raw %}
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
{% endraw %}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ project_name }}</title>
<script src="https://s3.us-east-1.amazonaws.com/assets.pythagora.ai/scripts/utils.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,81 @@
{
"name": "vite_client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "npm run dev",
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"axios": "^1.7.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.1",
"input-otp": "^1.4.1",
"lucide-react": "^0.460.0",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.0.1",
"recharts": "^2.13.3",
"sonner": "^1.7.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/node": "^22.9.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.8"
}
}

View File

@@ -0,0 +1,9 @@
{% raw %}
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
{% endraw %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

View File

@@ -0,0 +1,45 @@
{% raw %}
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
{% endraw %}

View File

@@ -0,0 +1,26 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
import { ThemeProvider } from "./components/ui/theme-provider"
import { Toaster } from "./components/ui/toaster"
import { AuthProvider } from "./contexts/AuthContext"
import { Login } from "./pages/Login"
import { Register } from "./pages/Register"
import { Layout } from "./components/Layout"
import { ProtectedRoute } from "./components/ProtectedRoute"
function App() {
return (
<AuthProvider>
<ThemeProvider defaultTheme="light" storageKey="ui-theme">
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/" element={<ProtectedRoute> <Layout /> </ProtectedRoute>} />
</Routes>
</Router>
<Toaster />
</ThemeProvider>
</AuthProvider>
)
}
export default App

View File

@@ -0,0 +1,67 @@
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
const backendURL = '';
const api = axios.create({
baseURL: backendURL,
headers: {
'Content-Type': 'application/json',
},
validateStatus: (status) => {
return status >= 200 && status < 300;
},
});
let accessToken: string | null = null;
{% if options.auth %}
// Axios request interceptor: Attach access token to headers
api.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
if (!accessToken) {
accessToken = localStorage.getItem('accessToken');
}
if (accessToken && config.headers) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error: AxiosError): Promise<AxiosError> => Promise.reject(error)
);
// Axios response interceptor: Handle 401 errors
api.interceptors.response.use(
(response) => response, // If the response is successful, return it
async (error: AxiosError): Promise<any> => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
// If the error is due to an expired access token
if ([401, 403].includes(error.response?.status) && !originalRequest._retry) {
originalRequest._retry = true; // Mark the request as retried
try {
// Attempt to refresh the token
const { data } = await axios.post<{ accessToken: string }>(`${backendURL}/api/auth/refresh`, {
refreshToken: localStorage.getItem('refreshToken'),
});
accessToken = data.accessToken;
// Retry the original request with the new token
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
}
return api(originalRequest);
} catch (err) {
// If refresh fails, clear tokens and redirect to login
localStorage.removeItem('refreshToken');
localStorage.removeItem('accessToken');
accessToken = null;
window.location.href = '/login'; // Redirect to login page
return Promise.reject(err);
}
}
return Promise.reject(error); // Pass other errors through
}
);
{% endif %}
export default api;

View File

@@ -0,0 +1,44 @@
{% if options.auth %}
import api from './api';
// Description: Login user functionality
// Endpoint: POST /api/auth/login
// Request: { email: string, password: string }
// Response: { accessToken: string, refreshToken: string }
export const login = async (email: string, password: string) => {
try {
return { accessToken: '123', refreshToken: '123' }; // this is just a placeholder - remove when the backend is being implemented
const response = await api.post('/api/auth/login', { email, password });
return response.data;
} catch (error) {
console.error('Login error:', error);
throw new Error(error?.response?.data?.message || error.message);
}
};
// Description: Register user functionality
// Endpoint: POST /api/auth/register
// Request: { email: string, password: string }
// Response: { email: string }
export const register = async (email: string, password: string) => {
try {
return { email: 'jake@example.com' }; // this is just a placeholder - remove when the backend is being implemented
const response = await api.post('/api/auth/register', {email, password});
return response.data;
} catch (error) {
throw new Error(error?.response?.data?.message || error.message);
}
};
// Description: Logout
// Endpoint: POST /api/auth/logout
// Request: {}
// Response: { success: boolean, message: string }
export const logout = async () => {
try {
return await api.post('/api/auth/logout');
} catch (error) {
throw new Error(error?.response?.data?.message || error.message);
}
};
{% endif %}

View File

@@ -0,0 +1,13 @@
{% raw %}
export function Footer() {
return (
<footer className="fixed bottom-0 w-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t">
<div className="container flex h-14 items-center justify-between">
<p className="mx-6 text-sm text-muted-foreground">
Built by <a href="https://pythagora.ai" target="_blank" rel="noopener noreferrer">Pythagora</a>
</p>
</div>
</footer>
)
}
{% endraw %}

View File

@@ -0,0 +1,35 @@
{% raw %}
import { Bell{% endraw %}{% if options.auth %}, LogOut{% endif %}{% raw %} } from "lucide-react"
import { Button } from "./ui/button"
import { ThemeToggle } from "./ui/theme-toggle"
import { useAuth } from "@/contexts/AuthContext"
import { useNavigate } from "react-router-dom"
export function Header() {
{% endraw %}
{% if options.auth %}
const { logout } = useAuth()
{% endif %}
const navigate = useNavigate()
{% if options.auth %}
const handleLogout = () => {
logout()
navigate("/login")
}
{% endif %}
return (
<header className="fixed top-0 z-50 w-full border-b bg-background/80 backdrop-blur-sm">
<div className="flex h-16 items-center justify-between px-6">
<div className="text-xl font-bold">Home</div>
<div className="flex items-center gap-4">
<ThemeToggle />
{% if options.auth %}
<Button variant="ghost" size="icon" onClick={handleLogout}>
<LogOut className="h-5 w-5" />
</Button>
{% endif %}
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,19 @@
import { Outlet } from "react-router-dom"
import { Header } from "./Header"
import { Footer } from "./Footer"
export function Layout() {
return (
<div className="min-h-screen bg-gradient-to-br from-background to-secondary">
<Header />
<div className="flex h-[calc(100vh-4rem)] pt-16">
<main className="flex-1 overflow-y-auto p-6">
<div className="mx-auto max-w-7xl">
<Outlet />
</div>
</main>
</div>
<Footer />
</div>
)
}

View File

@@ -0,0 +1,15 @@
{% raw %}
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "@/contexts/AuthContext";
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
{% endraw %}

View File

@@ -0,0 +1,59 @@
{% raw %}
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
{% endraw %}

View File

@@ -0,0 +1,144 @@
{% raw %}
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
{% endraw %}

View File

@@ -0,0 +1,62 @@
{% raw %}
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }
{% endraw %}

View File

@@ -0,0 +1,8 @@
{% raw %}
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }
{% endraw %}

View File

@@ -0,0 +1,53 @@
{% raw %}
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
{% endraw %}

View File

@@ -0,0 +1,39 @@
{% raw %}
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
{% endraw %}

View File

@@ -0,0 +1,118 @@
{% raw %}
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
{% endraw %}

View File

@@ -0,0 +1,59 @@
{% raw %}
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
{% endraw %}

View File

@@ -0,0 +1,67 @@
{% raw %}
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }
{% endraw %}

View File

@@ -0,0 +1,82 @@
{% raw %}
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
{% endraw %}

View File

@@ -0,0 +1,263 @@
{% raw %}
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}
{% endraw %}

View File

@@ -0,0 +1,368 @@
{% raw %}
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}
{% endraw %}

Some files were not shown because too many files have changed in this diff Show More