Skip to content

Commit

Permalink
Some AI added tests, subdividing code into context endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
marknagelberg committed Apr 22, 2023
1 parent 5d59973 commit 6d3466f
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 112 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
venv*
.env
__pycache__
.pytest_cache
1 change: 1 addition & 0 deletions .llmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
venv*
.env
__pycache__
.pytest_cache
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@ move files or directories, and get the file structure of your repository.

To get started with LLM Repo Assistant, follow these steps:

1. Clone the repo
2. Follow the `.env-template` example to create an `.env` file in the top
directory of this repo to define `REPO_ROOT` which points to the top level directory
of the repo you want the API to be able to read / edit.
3. Add an `.llmignore` file to the `REPO_ROOT` directory - this tells the API about
files in your repo that you want ignored by the LLM.
4. Run the API with `source start.sh`. The API should then be running on `localhost:8000`
5. View and test the API by visiting `localhost:8000/docs`
1. Install Docker
2. Clone the repo
3. Add an `.llmignore` file to the root directory of your repository - this tells
LLM Repo Assistant about files in your repo that you want ignored by the LLM.
4. Navigate to the `llm_repo_assistant` cloned repository and build the
Docker image to run LLM Repo Assistant with `docker build -t llm_repo_assistant .`
5. Run LLM Repo Assistant locally in a docker container with the following command:
`docker run --rm -v "/path/to/cloned/repo/llm_repo_assistant:/app" -v "/path/to/your/code/repo:/repo" -p 8000:8000 --name llm_repo_assistant llm_repo_assistant`
6. View and test the API endpoints by visiting `localhost:8000/docs`
7. Install the ChatGPT plugin by going to `Plugin store -> Develop your own plugin`.
Type in `localhost:8000` and click `Find manifest file`. The plugin should now be installed
and ready to use.


## Adding ChatGPT plugin

Expand Down
32 changes: 32 additions & 0 deletions prompts/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Suggested Prompts for Testing Using LLM Repo Assistant

### Developing Tests

Develop a comprehensive set of tests for endpoints related to **Insert Functionality you want to Test**
code repo, which is a **insert high-level details about the repo**. The tests should be added to a
file within the **Give directory where tests should be stored** directory and should follow best practices
for writing programming tests.

The goal of this prompt is to ensure that the repo is
functioning properly and free from errors by creating a thorough suite
of tests for its file-related endpoints.

Think step by step about how you will complete this task. Run the steps by me
and prompt me for approval before you execute each subsequent step.


### Running Tests

I have developed a set of tests in my code repo which
**insert high-level details about the repo**.
The tests are located in the file **Insert File Path**.
Please run these tests and fix issues
that arise, which likely means a mistake in the code but also may be a mistake
in the test. Do this step by step and ask for my approval before proceeding
through each step.

Use the functionality of the LLM Code Repo Assistant to the fullest extent.
Reduce the size of API calls when possible.


### Refactoring Tests
8 changes: 4 additions & 4 deletions src/ai-plugin.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"schema_version": "v1",
"name_for_human": "LLM Code Repo Assistant",
"name_for_model": "LLMCodeRepoAssistant",
"description_for_human": "Plugin for performing read, write, and execute filesystem operations on a user's local code repo.",
"description_for_model": "Plugin for performing read and write filesystem operations on a code repo, including functionality specifically for reading / writing code, as well as limited operations to execute code / tests to enable development.",
"name_for_human": "LLM Repo Assistant",
"name_for_model": "LLMRepoAssistant",
"description_for_human": "Plugin for ChatGPT to help build your local code repo.",
"description_for_model": "Plugin providing an API to work on the user's code repo. Before asking user for more details, try to get all context you need via the context endpoints or summary-related endpoints. If possible, satisfy requests using precise endpoints then expand to broader endpoints if deemed necessary (e.g. read a function first then a whole file if it's not enough info). User should be using git version control and should commit before providing tasks to you, so `git diff` represents changes you have made so far - however, this is not guaranteed as the user may have forgotten. Implement your solution step-by-step and ask the user permission for making write operations to the repo (unless they specifically instruct you to proceed with a batch of writes).",
"auth": {
"type": "none"
},
Expand Down
3 changes: 2 additions & 1 deletion src/api/api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from fastapi import APIRouter

from src.api.endpoints import files, directories, utils, programming, command, ai_plugin
from src.api.endpoints import files, directories, utils, programming, command, context, ai_plugin

api_router = APIRouter()
api_router.include_router(files.router, prefix="/files", tags=["files"])
api_router.include_router(directories.router, prefix="/directories", tags=["directories"])
api_router.include_router(programming.router, prefix="/programming", tags=["programming"])
api_router.include_router(command.router, prefix="/command", tags=["command"])
api_router.include_router(context.router, prefix="/context", tags=["context"])
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])

ai_plugin_router = APIRouter()
Expand Down
43 changes: 18 additions & 25 deletions src/api/endpoints/command.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import os
import stat
from enum import Enum
import subprocess
from typing import Optional, List
from fastapi import HTTPException, APIRouter

from src.schemas import TestRunRequest
from src.core.config import settings
from src.utils import get_git_diff

from src.utils import get_full_path


router = APIRouter()
Expand All @@ -16,12 +16,15 @@
class TestFramework(str, Enum):
pytest = "pytest"

def run_tests(test_command: List[str], test_name: Optional[str] = None) -> str:
if test_name:
test_command.extend(["-k", test_name])

test_script = os.path.join(settings.REPO_ROOT, "run_tests.sh")
test_command.insert(0, test_script)
def run_tests(test_command: List[str],
test_file_path: Optional[str] = None,
test_function_name: Optional[str] = None) -> str:
if test_file_path and not test_function_name:
test_command.extend([test_file_path])
if test_function_name and not test_file_path:
test_command.extend([test_function_name])
if test_function_name and test_file_path:
test_command.extend([test_file_path + "::" + test_function_name])

original_cwd = os.getcwd()
os.chdir(settings.REPO_ROOT)
Expand All @@ -38,31 +41,21 @@ def run_tests(test_command: List[str], test_name: Optional[str] = None) -> str:
@router.post("/run_tests/{test_framework}")
async def run_tests_endpoint(test_framework: TestFramework, test_run_request: TestRunRequest):
"""
Runs tests for repo. Endpoint executes file named `run_tests.sh`
in root of repo which activates programming environment and runs tests.
`run_tests.sh` should be executable and contain
`exec python -m pytest "$@"` after activating the environment.
Runs tests for repo. Endpoint executes tests with optional specification of test file and test function.
If no test file or function is specified, all available tests are run. Currently only `pytest` is supported.
"""

test_file_path = ""
if test_run_request.test_file_path:
test_file_path = get_full_path(test_run_request.test_file_path)

if test_framework.lower() == "pytest":
test_command = ["pytest"]
else:
raise HTTPException(status_code=400, detail="Unsupported test framework")

test_output = run_tests(test_command, test_run_request.test_name)
test_output = run_tests(test_command, test_file_path, test_run_request.test_function_name)
return {"status": "success", "test_output": test_output}



@router.get("/git_diff")
async def git_diff():
"""
Returns `git diff` for the repo (changes since last commit).
"""
try:
diff = get_git_diff(settings.REPO_ROOT)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

return {"diff": diff}

86 changes: 86 additions & 0 deletions src/api/endpoints/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from fastapi import HTTPException, Query, APIRouter
from pathlib import Path as FilePath
import fnmatch
import os
import shutil
from typing import Any, Dict

from src.utils import get_full_path
from src.core.config import settings
from src.utils import is_llmignored, get_git_diff

router = APIRouter()

def search_files(query: str, root_dir: FilePath):
matches = []
for root, dirnames, filenames in os.walk(root_dir):
for filename in fnmatch.filter(filenames, f"*{query}*"):
if not is_llmignored(os.path.join(root, filename)):
matches.append(os.path.join(root, filename))
for dirname in fnmatch.filter(dirnames, f"*{query}*"):
if not is_llmignored(os.path.join(root, dirname)):
matches.append(os.path.join(root, dirname))
return matches


@router.get("/search")
async def search_files_and_directories(q: str = Query(..., min_length=1)):
root_dir = FilePath(settings.REPO_ROOT) # Set your root directory here
search_results = search_files(q, root_dir)
return {"matches": search_results}


def get_directory_structure(path: str) -> Dict[str, Any]:
item = {
"name": os.path.basename(path),
"path": path[len(str(settings.REPO_ROOT)):], # Remove the repo root from the path
"type": "file" if os.path.isfile(path) else "directory"
}

# Ignore any path / files that are in .llmignore or are part of a .git directory
if is_llmignored(item["path"]) or item["path"][-4:] == '.git':
return None

if os.path.isfile(path):

item["metadata"] = {
"num_tokens": 0, # Replace with actual token count calculation
"file_size_bytes": os.path.getsize(path)
}
else:
children = [
get_directory_structure(os.path.join(path, child))
for child in os.listdir(path)
]
item["children"] = [child for child in children if child is not None]

return item


@router.post("/file_structure/{dir_path:path}")
async def get_file_structure(dir_path: str):
"""
Get file structure of given directory and subdirectories. Ignores `.git` directory
and any file matching `.llmignore` patterns. `.llmignore` must be in the
root directory of repo and follow same structure as `.gitignore`.
"""
dir_path = get_full_path(dir_path)
if not os.path.exists(dir_path):
raise HTTPException(status_code=404, detail="Directory not found")
structure = get_directory_structure(dir_path)
return structure


@router.get("/git_diff")
async def git_diff():
"""
Returns `git diff` for the repo (changes since last commit).
"""
try:
diff = get_git_diff(settings.REPO_ROOT)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

return {"diff": diff}


6 changes: 3 additions & 3 deletions src/api/endpoints/directories.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
router = APIRouter()


@router.get("/directories/{dir_path:path}")
@router.get("/{dir_path:path}")
async def list_directory_contents(dir_path: str):
target_path = FilePath(dir_path)
if is_llmignored(str(target_path)):
Expand All @@ -20,7 +20,7 @@ async def list_directory_contents(dir_path: str):
raise HTTPException(status_code=404, detail="Directory not found")


@router.post("/directories")
@router.post("/")
async def create_directory(directory_request: DirectoryRequest):
target_path = FilePath(directory_request.path) / directory_request.dir_name
if is_llmignored(str(target_path)):
Expand All @@ -32,7 +32,7 @@ async def create_directory(directory_request: DirectoryRequest):
raise HTTPException(status_code=409, detail="Directory already exists")


@router.delete("/directories/{dir_path:path}")
@router.delete("/{dir_path:path}")
async def delete_directory(dir_path: str):
target_path = FilePath(dir_path)
if is_llmignored(str(target_path)):
Expand Down
4 changes: 2 additions & 2 deletions src/api/endpoints/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
async def read_file(file_path: str):
path = get_full_path(file_path)

if not os.path.exists(path):
raise HTTPException(status_code=404, detail="File not found")
if is_llmignored(path):
raise HTTPException(status_code=404, detail="File is ignored in `.llmignore`")
if not os.path.exists(path):
raise HTTPException(status_code=404, detail="File not found")

if os.path.isfile(path):
try:
Expand Down
68 changes: 2 additions & 66 deletions src/api/endpoints/utils.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,12 @@
from fastapi import FastAPI, HTTPException, Query, APIRouter
from fastapi import HTTPException, APIRouter
from pathlib import Path as FilePath
import fnmatch
import os
import shutil
from typing import Any, Dict, List
from fnmatch import fnmatch

from src.schemas import MoveRequest
from src.utils import get_full_path
from src.core.config import settings
from src.utils import load_llmignore_patterns, is_llmignored
from src.utils import is_llmignored

router = APIRouter()

def search_files(query: str, root_dir: FilePath):
matches = []
for root, dirnames, filenames in os.walk(root_dir):
for filename in fnmatch.filter(filenames, f"*{query}*"):
if not is_llmignored(os.path.join(root, filename)):
matches.append(os.path.join(root, filename))
for dirname in fnmatch.filter(dirnames, f"*{query}*"):
if not is_llmignored(os.path.join(root, dirname)):
matches.append(os.path.join(root, dirname))
return matches


@router.get("/search")
async def search_files_and_directories(q: str = Query(..., min_length=1)):
root_dir = FilePath(".") # Set your root directory here
search_results = search_files(q, root_dir)
return {"matches": search_results}


@router.post("/move")
async def move_file_or_directory(move_request: MoveRequest):
Expand All @@ -44,43 +20,3 @@ async def move_file_or_directory(move_request: MoveRequest):
else:
raise HTTPException(status_code=404, detail="Source not found")


def get_directory_structure(path: str) -> Dict[str, Any]:
item = {
"name": os.path.basename(path),
"path": path[len(str(settings.REPO_ROOT)):], # Remove the repo root from the path
"type": "file" if os.path.isfile(path) else "directory"
}

if is_llmignored(item["path"]):
return None

if os.path.isfile(path):

item["metadata"] = {
"num_tokens": 0, # Replace with actual token count calculation
"file_size_bytes": os.path.getsize(path)
}
else:
children = [
get_directory_structure(os.path.join(path, child))
for child in os.listdir(path)
]
item["children"] = [child for child in children if child is not None]

return item


@router.post("/file_structure/{dir_path:path}")
async def get_file_structure(dir_path: str):
"""
Get file structure of given directory and subdirectories. Ignores `.git` directory
and any file matching `.llmignore` patterns. `.llmignore` must be in the
root directory of repo and follow same structure as `.gitignore`.
"""
dir_path = get_full_path(dir_path)
if not os.path.exists(dir_path):
raise HTTPException(status_code=404, detail="Directory not found")
structure = get_directory_structure(dir_path)
return structure

4 changes: 3 additions & 1 deletion src/run_tests_example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ VENV_PATH="/path/to/your/venv"
# Activate the virtual environment
source "${VENV_PATH}/bin/activate"

TEST_DIRECTORY="./top/dir/where/tests/live"

# Run pytest or any other test command
exec python -m pytest "$@"
exec python -m pytest $(TEST_DIRECTORY) "$@"
Loading

0 comments on commit 6d3466f

Please sign in to comment.