Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/docker #10

Merged
merged 2 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
ignore = E501,E302,E305,E203
File renamed without changes.
21 changes: 21 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Use an official Python runtime as a parent image
FROM python:3.10

# Set the working directory in the container to /app
WORKDIR /app

# Add the current directory contents into the container at /app
ADD requirements.txt /app

RUN pip install --upgrade pip

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY . /app

# Make port 80 available to the world outside this container
EXPOSE 8001

# Run app.py when the container launches
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
19 changes: 16 additions & 3 deletions backend/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import openai
import json
import os
from typing import Any, List, Optional, Callable
from typing import List, Optional, Callable
from pydantic import BaseModel
from database.my_codebase import MyCodebase

Expand Down Expand Up @@ -180,20 +180,33 @@ def add_line_numbers_to_content(self, content: str) -> str:
lines[i] = f"{i+1} {lines[i]}"
return "\n".join(lines)

def process_json(self, args):
def process_json(self, args: str) -> str:
"""
Process a JSON string, handling any triple-quoted strings within it.

Args:
args (str): The JSON string to process.

Returns:
str: The processed JSON string.
"""
try:
# Attempt to load the JSON string
response = json.loads(args)
return response
except json.decoder.JSONDecodeError:
# If there's a JSONDecodeError, it may be due to triple-quoted strings
# Find all occurrences of triple-quoted strings
triple_quoted_strings = re.findall(r"\"\"\"(.*?)\"\"\"", args, re.DOTALL)

# For each occurrence, replace newlines and triple quotes
for tqs in triple_quoted_strings:
# Replace newlines and double quotes within the triple-quoted string
fixed_string = tqs.replace("\n", "\\n").replace('"', '\\"')
# Replace the original triple-quoted string with the fixed string
response_str = args.replace(tqs, fixed_string)

# Now replace the triple quotes with single quotes
response_str = args.replace('"""', '"')

return response_str
return json.loads(response_str)
62 changes: 38 additions & 24 deletions backend/agent/agent_functions/changes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import os
import openai
import difflib as dl

from dotenv import load_dotenv
from typing import List, Optional, Tuple
from pydantic import Field, field_validator
from openai_function_call import OpenAISchema
import re

load_dotenv()
DIRECTORY = os.getenv("PROJECT_DIRECTORY")
Expand All @@ -16,6 +14,7 @@ class Change(OpenAISchema):
"""
The correct changes to make to a file.
Be mindful of formatting and include the proper newline characters.
Spaces and indentation in the new code should be relative to the old code.

Args:
original (str): The full code block to be replaced; formatted as a string.
Expand Down Expand Up @@ -43,23 +42,23 @@ def to_dict(self) -> dict:

class Changes(OpenAISchema):
"""
A list of changes to make to a file.
Think step by step and ensure the changes cover all requests. Changes will be processed similar to a diff
of the format:
A list of changes similar to the format:

>>>>>> ORIGINAL
old code
=========
new code
<<<<<< UPDATED

All you need to provide is the old code and new code. The system will handle the rest.
The 'original' field cannot be an empty string.
When adding new code, include original code to determine where to put the new code.
The 'updated' field can be blank if you want to delete the old code.
Always use the relative path from the root of the codebase.
Please provide the old code exactly as you read it and the correct new replacement code.
Be mindful of formatting, number of spaces, and newline characters.
Your code is always valid, correct, and addresses the requests of the human.
When adding new code, include original code to determine where to put the new code (The original field should never be an empty string)
The 'updated' field can be blank when you need to delete the old code.
The file_name is always the relative path from the root of the codebase.

Args:
file_name (str): The name of the file to be changed. This needs to be the relative path from the root of the codebase.
file_name (str): The relative path from the root of the codebase.
thought (str): A description of your thought process.
changes (List[Change]): A list of Change Objects that represent all the changes you want to make to this file.
"""
Expand All @@ -76,18 +75,16 @@ def apply_changes(self, changes: List[Change], content: str) -> str:
Applies the given changes to the content.
"""
for change in changes:
# Use regex to find and replace the old string
if content:
print("Content exists!!!!!!!!")
print(f"Type: {type(content)}")
new_content = self.replace_part_with_missing_leading_whitespace(
whole_lines=content.split("\n"),
part_lines=change.original.splitlines(),
replace_lines=change.updated.splitlines(),
)
if not new_content:
print(f"Failed on change: {change.to_dict()}")
raise Exception("Failed to apply changes.")
raise Exception(
f"Failed to apply changes. Original: {change.original}, Updated: {change.updated}"
)

if new_content == content:
print("Warning: Expected content not found. No changes made.")
Expand Down Expand Up @@ -121,12 +118,17 @@ def execute(self) -> str:
try:
with open(file_path, "r") as f:
current_contents = f.read()
current_contents_with_line_numbers = "\n".join(
[
f"{i+1} {line}"
for i, line in enumerate(current_contents.splitlines())
]
)
# current_contents_with_line_numbers = "\n".join(
# [
# f"{i+1} {line}"
# for i, line in enumerate(current_contents.splitlines())
# ]
# )
except PermissionError:
print(
f"Error: Permission denied for file {self.file_name} at file_path: {file_path}\nDirectory: {DIRECTORY}"
)
return "Permission denied. Please check the file permissions."
except FileNotFoundError:
print(
f"Error: File {self.file_name} not found at file_path: {file_path}\nDirectory: {DIRECTORY}"
Expand All @@ -136,7 +138,11 @@ def execute(self) -> str:
print(e)
return "Error: File could not be read. Please try again."

new_text = self.apply_changes(self.changes, current_contents)
try:
new_text = self.apply_changes(self.changes, current_contents)
except Exception as e:
print(e)
return f"Error: Changes could not be applied. Error: {e}."

# if not new_text:
# TODO: This could be a point to retry.
Expand All @@ -149,8 +155,16 @@ def execute(self) -> str:
tofile="b",
n=0,
)
try:
self.save(new_text, file_path)
except PermissionError:
print(
f"Error: Permission denied for file {self.file_name} at file_path: {file_path}\nDirectory: {DIRECTORY}"
)
return (
"Permission denied when saving file. Please check the file permissions."
)

self.save(new_text, file_path)
return "\n\n```diff\n" + "\n".join(str(d) for d in diff) + "\n```\n\n"

def save(self, new_text: str, file_path: str) -> None:
Expand Down
43 changes: 5 additions & 38 deletions backend/agent/agent_functions/shell_commands.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import os
import openai
import pexpect

from dotenv import load_dotenv
from enum import Enum
from typing import Optional, List, Generator
from pydantic import Field, field_validator
from openai_function_call import OpenAISchema
from agent.agent_functions.changes import Changes
from agent.agent_functions.new_file import NewFile

load_dotenv()
DIRECTORY = os.getenv("PROJECT_DIRECTORY")


class CommandType(Enum):
BASH_COMMAND = "bash"
FILE_CHANGE = "file_change"
NEW_FILE = "new_file"


class CommandResult(OpenAISchema):
Expand All @@ -30,41 +25,18 @@ class CommandResults(OpenAISchema):


class Command(OpenAISchema):
"""
A command to be executed by the agent. Available command types are:
- Bash Command: Execute a bash command in the shell.
"""

id: int = Field(..., description="Unique id of the command")
command_type: CommandType = Field(..., description="Type of the command")
dependent_commands: List[int] = Field(
default_factory=list,
description="List of the IDs of commands that need to be completed before this command can be executed.",
)
command_line: Optional[str] = Field(None, description="Command to execute")
file_change: Optional[Changes] = Field(
None, description="File name and changes you would like to request"
)
new_file: Optional[NewFile] = Field(
None,
description="""File name (path from root directory), description, and reference
files (path from root directory) which provide additional context to the AI.""",
)

@field_validator("file_change")
def check_file_change(cls, v, values):
if (
"command_type" in values
and values["command_type"] == CommandType.FILE_CHANGE
and v is None
):
raise ValueError("file_change is required when command_type is FILE_CHANGE")
return v

@field_validator("new_file")
def check_new_file(cls, v, values):
if (
"command_type" in values
and values["command_type"] == CommandType.NEW_FILE
and v is None
):
raise ValueError("new_file is required when command_type is FILE_CHANGE")
return v

@field_validator("command_line")
def check_command(cls, v, values):
Expand All @@ -77,11 +49,6 @@ def check_command(cls, v, values):
return v

def execute(self, with_results: CommandResults) -> CommandResult:
# If a program is set for this command and the command type is PROGRAM, execute the program
if self.command_type == CommandType.FILE_CHANGE:
self.file_change.save()
output = "changes complete"

if self.command_type == CommandType.NEW_FILE:
self.new_file.save()
output = "file created"
Expand Down
17 changes: 4 additions & 13 deletions backend/app_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional, List, Any, Callable
from typing import Any, Callable
from pydantic import BaseModel
from agent.agent_functions.changes import Changes
from agent.agent_functions.shell_commands import CommandPlan
Expand All @@ -19,6 +19,7 @@
CODEAPP_DB_PW = os.getenv("CODEAPP_DB_PW")
CODEAPP_DB_HOST = os.getenv("CODEAPP_DB_HOST")
DIRECTORY = os.getenv("PROJECT_DIRECTORY")
IDENTITY = "You are an AI Pair Programmer and a world class python developer. Your role is to assist the Human in developing, debugging, and optimizing their project. Feel free to ask for more details if something isn't clear."


def create_database_connection() -> connection:
Expand Down Expand Up @@ -78,19 +79,9 @@ def setup_codebase() -> MyCodebase:


def setup_app() -> CodingAgent:
print("Setting up app")
codebase = setup_codebase()
memory = setup_memory_manager(tree=codebase.tree())
agent = CodingAgent(
memory_manager=memory, callables=[CommandPlan, Changes], codebase=codebase
)
return agent, codebase


def setup_app_testing() -> CodingAgent:
codebase = setup_codebase()
memory = setup_memory_manager(tree=codebase.tree(), table_name="test")
memory.cur.execute("TRUNCATE test_memory;")
memory.cur.execute("TRUNCATE test_system_prompt;")
memory = setup_memory_manager(tree=codebase.tree(), identity=IDENTITY)
agent = CodingAgent(
memory_manager=memory, callables=[CommandPlan, Changes], codebase=codebase
)
Expand Down
23 changes: 5 additions & 18 deletions backend/database/my_codebase.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import datetime
import subprocess
from dotenv import load_dotenv
import openai
import numpy as np
Expand All @@ -22,26 +21,14 @@
SUMMARY_MODEL = "gpt-3.5-turbo"
README_MODEL = "gpt-4"
SUMMARY_PROMPT = """
Please summarise the following what the following code is doing.
Please summarise, in bullet points, what the following code is doing.
Please be consise and include all the important informastion.\n\n
CODE:{}
SUMMARY:
"""


def get_git_root(path: str = ".") -> Union[str, None]:
try:
root = (
subprocess.check_output(["git", "rev-parse", "--show-toplevel"], cwd=path)
.decode("utf-8")
.strip()
)
return root
except Exception as e:
print(e)
return None


# new comment
class MyCodebase:
load_dotenv()
IGNORE_DIRS = os.getenv("IGNORE_DIRS")
Expand Down Expand Up @@ -234,7 +221,7 @@ def tree(self) -> str:
]

# Insert each file into the tree structure
for file_path in file_paths:
for file_path in sorted(file_paths):
parts = file_path.split(os.path.sep)
# Find the start_from directory in the path and trim up to it
if start_from in parts:
Expand Down Expand Up @@ -263,7 +250,7 @@ def remove_old_files(self) -> None:
self.cur.execute("SELECT file_path FROM files")
file_paths = [result[0] for result in self.cur.fetchall()]
for file_path in file_paths:
if not os.path.exists(file_path) and file_path in self.file_dict.keys():
if not os.path.exists(file_path):
self.cur.execute(
sql.SQL(
"""
Expand Down Expand Up @@ -292,4 +279,4 @@ def _is_valid_file(file_name):
not file_name.startswith(".")
and not file_name.startswith("_")
and any(file_name.endswith(ext) for ext in MyCodebase.FILE_EXTENSIONS)
)
) or file_name == "Dockerfile"
Loading