Skip to content

Commit

Permalink
Add integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
majdyz authored and majdyz committed May 9, 2024
1 parent 750c80a commit 1c438c1
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 125 deletions.
50 changes: 50 additions & 0 deletions .github/actions/setup-env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Setup Environment and Dependencies
description: Setup the environment and install dependencies for the project

runs:
using: composite
steps:
- uses: actions/checkout@v3

- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.11

- name: Set up Python dependency cache
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('pyproject.toml') }}-${{ steps.get_date.outputs.date }}

- name: Install Python dependencies
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install
- name: Install Dependencies
run: poetry install
if: steps.cache.outputs.cache-hit != 'true'

- name: Generate Prisma Client
run: poetry run prisma generate

- name: Set DB URL
run: echo "DATABASE_URL=postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASS }}@localhost:5432/${{ secrets.DB_NAME }}" >> $GITHUB_ENV

- name: Run Database Migrations
run: |
poetry run prisma migrate dev --name updates
- name: Populate Database
run: |
./run populate-db
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
DB_PORT: 5432
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
RUN_ENV: local
PORT: 8080
35 changes: 2 additions & 33 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,8 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.11

- name: Set up Python dependency cache
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('pyproject.toml') }}-${{ steps.get_date.outputs.date }}

- name: Install Python dependencies
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install
- name: Install Dependencies
run: poetry install
if: steps.cache.outputs.cache-hit != 'true'

- name: Generate Prisma Client
run: poetry run prisma generate

- name: Set DB URL
run: echo "DATABASE_URL=postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASS }}@localhost:5432/${{ secrets.DB_NAME }}" >> $GITHUB_ENV
- name: Set Environment and Install Dependencies
uses: actions/setup-env

- name: Run server
run: |
Expand All @@ -71,14 +48,6 @@ jobs:
RUN_ENV: local
PORT: 8080

- name: Run Database Migrations
run: |
poetry run prisma migrate dev --name updates
- name: Populate Database
run: |
./run populate-db
- name: Run Tests
run: |
./run benchmark
Expand Down
26 changes: 3 additions & 23 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,8 @@ jobs:
with:
fetch-depth: 1

- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.11

- name: Set up Python dependency cache
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('pyproject.toml') }}-${{ steps.get_date.outputs.date }}

- name: Install Python dependencies
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install
- name: Install Dependencies
run: poetry install
if: steps.cache.outputs.cache-hit != 'true'

- name: Generate Prisma Client
run: poetry run prisma generate
- name: Set Environment and Install Dependencies
uses: actions/setup-env

- name: Test with pytest
run: poetry run pytest --cov --without-integration .
run: poetry run pytest --cov .
2 changes: 2 additions & 0 deletions codex/common/exec_external_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ async def setup_if_required(
return path

if copy_from_parent and cwd != PROJECT_PARENT_DIR:
if (cwd / "venv").exists():
await execute_command(["rm", "-rf", str(cwd / "venv")], cwd, None)
await execute_command(
["virtualenv-clone", str(PROJECT_PARENT_DIR / "venv"), str(cwd / "venv")],
cwd,
Expand Down
29 changes: 15 additions & 14 deletions codex/common/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class FunctionDef(BaseModel):

def __generate_function_template(f) -> str:
args_str = ", ".join([f"{name}: {type}" for name, type in f.arg_types])
arg_desc = f"\n{' '*12}".join(
arg_desc = f"\n{' '*4}".join(
[
f'{name} ({type}): {f.arg_descs.get(name, "-")}'
for name, type in f.arg_types
Expand All @@ -97,21 +97,22 @@ def __generate_function_template(f) -> str:

def_str = "async def" if "await " in f.function_code else "def"
ret_type_str = f" -> {f.return_type}" if f.return_type else ""
func_desc = f.function_desc.replace("\n", "\n ")

template = f"""
{def_str} {f.name}({args_str}){ret_type_str}:
\"\"\"
{f.function_desc}
Args:
{arg_desc}
Returns:
{f.return_type}: {f.return_desc}
\"\"\"
pass
"""
return "\n".join([line[8:] for line in template.split("\n")]).strip()
{def_str} {f.name}({args_str}){ret_type_str}:
\"\"\"
{func_desc}
Args:
{arg_desc}
Returns:
{f.return_type}{': ' + f.return_desc if f.return_desc else ''}
\"\"\"
pass
"""
return "\n".join([line for line in template.split("\n")]).strip()

def __init__(self, function_template: Optional[str] = None, **data):
super().__init__(**data)
Expand Down
7 changes: 3 additions & 4 deletions codex/deploy/packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
from codex.common.constants import PRISMA_FILE_HEADER
from codex.common.database import get_database_schema
from codex.common.exec_external_tool import execute_command
from codex.deploy.model import Application
from codex.deploy.actions_workflows import manual_deploy, auto_deploy
from codex.deploy.actions_workflows import auto_deploy, manual_deploy
from codex.deploy.backend_chat_script import script

from codex.deploy.model import Application

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -262,7 +261,7 @@ async def create_prisma_schema_file(spec: Specification) -> str:
return prisma_file


async def create_github_repo(application: Application) -> (str, str):
async def create_github_repo(application: Application) -> tuple[str, str]:
"""
Creates a new GitHub repository under agpt-coder.
Expand Down
1 change: 1 addition & 0 deletions codex/develop/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ async def develop_user_interface(ids: Identifiers) -> CompletedApp:
for route in completed_app.CompiledRoutes or []
if route.RootFunction
]
ids.spec_id = completed_app.specificationId

functions_code = []
for func in available_functions:
Expand Down
47 changes: 37 additions & 10 deletions codex/develop/code_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
)
from codex.common.model import FunctionDef
from codex.develop.ai_extractor import DocumentationExtractor
from codex.develop.database import get_ids_from_function_id_and_compiled_route
from codex.develop.database import (
get_ids_from_function_id_and_compiled_route,
get_object_type_referred_functions,
)
from codex.develop.function import generate_object_code
from codex.develop.function_visitor import FunctionVisitor
from codex.develop.model import GeneratedFunctionResponse, Package
Expand Down Expand Up @@ -177,6 +180,11 @@ async def validate_code(
)
).strip()

# TODO(majdyz):
# Find Nicegui ui.link in `function_code` and verify that the targetted link references a valid function mentioned in the function docstring
# if self.use_nicegui and "ui.link" in function_code: validation.
# Challenge: ui.link can be used in multiple ways, e.g. ui.link('route_name') or ui.link('text', 'route_name'), ui.link(target='route_name', text='text')

# No need to validate main function if it's not provided (compiling a server code)
if self.func_name:
func_id, main_func = self.__validate_main_function(
Expand Down Expand Up @@ -374,7 +382,9 @@ async def __execute_ruff(
if re.match(r"Found \d+ errors?\.*", v) is None
]

added_imports, error_messages = __fix_missing_imports(error_messages, func)
added_imports, error_messages = await __fix_missing_imports(
error_messages, func
)

# Append problematic line to the error message or add it as TODO line
validation_errors: list[ValidationError] = []
Expand Down Expand Up @@ -765,7 +775,7 @@ async def get_error_enhancements(
AUTO_IMPORT_TYPES[t] = f"from typing import {t}"


def __fix_missing_imports(
async def __fix_missing_imports(
errors: list[str], func: GeneratedFunctionResponse
) -> tuple[set[str], list[str]]:
"""
Expand Down Expand Up @@ -793,13 +803,30 @@ def __fix_missing_imports(
filtered_errors.append(error)
continue

missing_type = match.group(1)
if missing_type in schema_imports:
missing_imports.append(schema_imports[missing_type])
elif missing_type in AUTO_IMPORT_TYPES:
missing_imports.append(AUTO_IMPORT_TYPES[missing_type])
elif missing_type in func.available_functions:
missing_imports.append(f"from project.{missing_type}_service import *")
missing = match.group(1)
if missing in schema_imports:
missing_imports.append(schema_imports[missing])
elif missing in AUTO_IMPORT_TYPES:
missing_imports.append(AUTO_IMPORT_TYPES[missing])
elif missing in func.available_functions:
missing_imports.append(f"from project.{missing}_service import {missing}")
elif missing in func.available_objects:
object_type_id = func.available_objects[missing].id
functions = await get_object_type_referred_functions(object_type_id)
service_name = next(
iter([f for f in functions if f in func.available_functions]), None
)
if service_name:
missing_imports.append(
f"from project.{service_name}_service import {missing}"
)
else:
logger.error(
"[AUTO-IMPORT] Unable to find function that uses object type `%s` ID #%s",
missing,
object_type_id,
)
filtered_errors.append(error)
else:
filtered_errors.append(error)

Expand Down
24 changes: 23 additions & 1 deletion codex/develop/database.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from typing import List, Tuple

from prisma.models import CompiledRoute, CompletedApp, Function, Specification
from prisma.models import (
CompiledRoute,
CompletedApp,
Function,
ObjectType,
Specification,
)

from codex.api_model import Identifiers, Pagination
from codex.common.database import INCLUDE_API_ROUTE, INCLUDE_FUNC
Expand Down Expand Up @@ -133,3 +139,19 @@ async def get_ids_from_function_id_and_compiled_route(
function_id=function.id,
completed_app_id=compiled_route.completedAppId,
)


async def get_object_type_referred_functions(object_type_id: str) -> list[str]:
referred_object_fields = await ObjectType.prisma().find_first_or_raise(
where={"id": object_type_id},
include={
"ReferredRequestAPIRoutes": True,
"ReferredResponseAPIRoutes": True,
},
)
functions_on_request = referred_object_fields.ReferredRequestAPIRoutes or []
functions_on_response = referred_object_fields.ReferredResponseAPIRoutes or []
referred_functions = []
for route in functions_on_request + functions_on_response:
referred_functions.append(route.functionName)
return referred_functions
2 changes: 1 addition & 1 deletion codex/develop/develop.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ async def on_failed(self, ids: Identifiers, invoke_params: dict):
function_signature = invoke_params.get("function_signature", "Unknown")
try:
logger.error(
f"AI Failed to write the function {function_name}. Signiture of failed function:\n{function_signature}",
f"AI Failed to write the function {function_name}. Signature of failed function:\n{function_signature}",
extra=ids.model_dump(),
)
await Function.prisma().update(
Expand Down
22 changes: 11 additions & 11 deletions codex/develop/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def generate_object_code(obj: ObjectTypeModel) -> str:
return "" # Avoid generating an empty object

# Auto-generate a template for the object, this will not capture any class functions.
fields = f"\n{' ' * 8}".join(
fields = f"\n{' ' * 4}".join(
[
f"{field.name}: {field.type} "
f"{('= '+field.value) if field.value else ''} "
Expand All @@ -91,22 +91,22 @@ def generate_object_code(obj: ObjectTypeModel) -> str:

doc_string = (
f"""\"\"\"
{obj.description}
\"\"\""""
{obj.description}
\"\"\""""
if obj.description
else ""
)

method_body = ("\n" + " " * 8).join(obj.code.split("\n")) + "\n" if obj.code else ""
method_body = ("\n" + " " * 4).join(obj.code.split("\n")) + "\n" if obj.code else ""

template = f"""
class {obj.name}({parent_class}):
{doc_string if doc_string else ""}
{fields if fields else ""}
{method_body if method_body else ""}
{"pass" if not fields and not method_body else ""}
"""
return "\n".join([line[4:] for line in template.split("\n")]).strip()
class {obj.name}({parent_class}):
{doc_string if doc_string else ""}
{fields if fields else ""}
{method_body if method_body else ""}
{"pass" if not fields and not method_body else ""}
"""
return "\n".join(line for line in template.split("\n")).strip()


def generate_object_template(obj: ObjectType) -> str:
Expand Down
Loading

0 comments on commit 1c438c1

Please sign in to comment.