Files
Swiftyandcodiumai-pr-agent-pro[bot] dcbfe2dc01 feat: create single function (#269)
* feat: create single function

* update prompts to disable database stuff when no database schema is passed

* added abilty to test function endpoint

* feat: add write_function_sepec endpoint

* del tmp json file

* Updated endpoints

* changed function routes to a functions group

* Update codex/develop/agent.py

Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>

* feat: add FunctionResponse return type to write_function endpoint

* unified endpoints

* fixed pr agents error

---------

Co-authored-by: codiumai-pr-agent-pro[bot] <151058649+codiumai-pr-agent-pro[bot]@users.noreply.github.com>
2024-05-11 08:49:20 +02:00

455 lines
15 KiB
Python

import asyncio
import logging
import os
import prisma
import prisma.enums
from langsmith import traceable
from prisma.enums import FunctionState
from prisma.models import (
Application,
CompiledRoute,
CompletedApp,
Function,
ObjectType,
Specification,
)
from prisma.types import CompiledRouteCreateInput
import codex
import codex.common
import codex.common.database
from codex.api_model import Identifiers
from codex.common.ai_block import LLMFailure
from codex.common.database import INCLUDE_FUNC
from codex.common.model import APIRouteSpec, FunctionDef, FunctionSpec
from codex.database import create_completed_app
from codex.develop.compile import (
compile_route,
get_object_field_deps,
get_object_type_deps,
)
from codex.develop.database import get_compiled_route, get_deliverable
from codex.develop.develop import DevelopAIBlock, NiceGUIDevelopAIBlock
from codex.develop.function import construct_function, generate_object_template
from codex.requirements.blocks.ai_page_decompose import PageDecompositionBlock
from codex.requirements.database import create_single_function_spec, get_specification
RECURSION_DEPTH_LIMIT = int(os.environ.get("RECURSION_DEPTH_LIMIT", 2))
logger = logging.getLogger(__name__)
@traceable
async def write_function(
ids: Identifiers, app: Application, function_spec: FunctionSpec
) -> CompiledRoute:
api_route_spec = APIRouteSpec(
module_name="Solo Function",
http_verb=prisma.enums.HTTPVerb.POST,
function_name=function_spec.name,
path="/",
description=function_spec.description,
access_level=prisma.enums.AccessLevel.PUBLIC,
allowed_access_roles=[],
request_model=function_spec.func_args,
response_model=function_spec.return_type,
)
spec: Specification = await create_single_function_spec(ids, app, api_route_spec)
completed_app = await create_completed_app(ids, spec)
if not spec.Modules:
raise ValueError("No modules found in the specification.")
if not spec.Modules[0].ApiRouteSpecs:
raise ValueError(
"No API routes found in the first module of the specification."
)
return await process_api_route(
api_route=spec.Modules[0].ApiRouteSpecs[0],
ids=ids,
spec=spec,
completed_app=completed_app,
lang="python",
)
@traceable
async def process_api_route(
api_route: prisma.models.APIRouteSpec,
ids: Identifiers,
spec: prisma.models.Specification,
completed_app: prisma.models.CompletedApp,
extra_functions: list[Function] = [],
lang: str = "python",
) -> CompiledRoute:
if not api_route.RequestObject:
types = []
descs = {}
elif api_route.RequestObject.Fields:
# Unwrap the first level of the fields if it's a nested object.
types = [(f.name, f.typeName) for f in api_route.RequestObject.Fields]
descs = {f.name: f.description or "" for f in api_route.RequestObject.Fields}
else:
types = [("request", api_route.RequestObject.name)]
descs = {"request": api_route.RequestObject.description or ""}
if not api_route.ResponseObject:
ret_type = None
ret_desc = ""
else:
ret_type = api_route.ResponseObject.name
ret_desc = api_route.ResponseObject.description or ""
function_def = FunctionDef(
name=api_route.functionName,
arg_types=types,
arg_descs=descs,
return_type=ret_type,
return_desc=ret_desc,
is_implemented=False,
function_desc=api_route.description,
function_code="",
)
object_type_ids = set()
available_types = {
dep_type.name: dep_type
for obj in [api_route.RequestObject, api_route.ResponseObject]
if obj
for dep_type in await get_object_type_deps(obj.id, object_type_ids)
}
if api_route.RequestObject and api_route.RequestObject.Fields:
# RequestObject is unwrapped, so remove it from the available types
available_types.pop(api_route.RequestObject.name, None)
file_name = (
api_route.functionName + "_service.py"
if lang == "python"
else "ui_" + api_route.functionName + ".py"
)
compiled_route = await CompiledRoute.prisma().create(
data=CompiledRouteCreateInput(
description=api_route.description,
fileName=file_name,
mainFunctionName=api_route.functionName,
compiledCode="", # This will be updated by compile_route
RootFunction={
"create": await construct_function(function_def, available_types)
},
CompletedApp={"connect": {"id": completed_app.id}},
ApiRouteSpec={"connect": {"id": api_route.id}},
),
include={"RootFunction": INCLUDE_FUNC}, # type: ignore
)
if not compiled_route.RootFunction:
raise ValueError("Root function not created")
# Set all the ids for logging
ids.spec_id = spec.id
ids.compiled_route_id = compiled_route.id
ids.function_id = compiled_route.RootFunction.id
route_root_func = await develop_route(
ids=ids,
goal_description=completed_app.description or "",
function=compiled_route.RootFunction,
spec=spec,
lang=lang,
extra_functions=extra_functions,
)
logger.info(f"Route function id: {route_root_func.id}")
available_funcs, available_objs = await populate_available_functions_objects(
extra_functions
)
return await compile_route(
compiled_route.id, route_root_func, spec, available_funcs, available_objs
)
@traceable
async def develop_user_interface(ids: Identifiers) -> CompletedApp:
if not ids.user_id or not ids.app_id or not ids.completed_app_id:
raise ValueError("user_id, app_id, and completed_app_id are required")
completed_app = await get_deliverable(ids.completed_app_id)
available_functions = [
route.RootFunction
for route in completed_app.CompiledRoutes or []
if route.RootFunction
]
ids.spec_id = completed_app.specificationId
functions_code = []
for func in available_functions:
if func.FunctionReturn and func.FunctionReturn.RelatedTypes:
functions_code += [
generate_object_template(t) for t in func.FunctionReturn.RelatedTypes
]
if func.FunctionArgs:
functions_code += [
generate_object_template(t)
for arg in func.FunctionArgs
for t in arg.RelatedTypes or []
]
functions_code.append(func.template)
ai_block = PageDecompositionBlock()
frontend_spec = await ai_block.invoke(
ids=ids,
invoke_params={
"available_functions": {f.functionName: f for f in available_functions},
"functions_code": functions_code,
},
)
# Connect db table from the existing spec to the new spec
if completed_app.specificationId:
backend_spec = await get_specification(
ids.user_id, ids.app_id, completed_app.specificationId
)
frontend_spec.DatabaseSchema = backend_spec.DatabaseSchema
return await develop_application(ids, frontend_spec, lang="nicegui")
@traceable
async def develop_application(
ids: Identifiers,
spec: Specification,
lang: str = "python",
eat_errors: bool = True,
) -> CompletedApp:
"""
Develops an application based on the given identifiers and specification.
Args:
ids (Identifiers): The identifiers used for development.
spec (Specification): The specification of the application.
Returns:
CompletedApp: The completed application.
"""
completed_app = await create_completed_app(ids, spec)
# If the completed app is defined, use the functions from the provided completed app as already implemented functions.
# This flow is used for developing the user interface.
if ids.completed_app_id:
existing_completed_app = await get_deliverable(ids.completed_app_id)
extra_functions = [
route.RootFunction
for route in existing_completed_app.CompiledRoutes or []
if route.RootFunction
]
else:
extra_functions = []
tasks = []
if spec.Modules:
api_routes = []
for module in spec.Modules:
if module.ApiRouteSpecs:
api_routes.extend(module.ApiRouteSpecs)
for api_route in api_routes:
# Schedule each API route for processing
task = process_api_route(
api_route,
ids,
spec,
completed_app,
extra_functions,
lang,
)
tasks.append(task)
# Run the tasks concurrently
results = await asyncio.gather(*tasks, return_exceptions=True)
if any(isinstance(r, Exception) for r in results):
exceptions = [r for r in results if isinstance(r, Exception)]
# if the only exceptions are LLMFailures, we can continue
if eat_errors and all(isinstance(r, LLMFailure) for r in exceptions):
logger.warning(
f"App Id: {ids.app_id} Eating Errors developing API routes: {exceptions}"
)
pass
else:
error_message = "".join(f"\n* {e}" for e in exceptions)
raise LLMFailure(
f"App Id: {ids.app_id} Error developing API routes: \n{error_message}"
)
else:
logger.info("🚀 All API routes developed successfully")
return completed_app
@traceable
async def populate_available_functions_objects(
functions: list[Function],
) -> tuple[dict[str, Function], dict[str, ObjectType]]:
generated_func: dict[str, Function] = {}
generated_objs: dict[str, ObjectType] = {}
object_ids = set()
for func in functions:
generated_func[func.functionName] = func
# Populate generated request objects from the LLM
for arg in func.FunctionArgs or []:
for type in await get_object_field_deps(arg, object_ids):
generated_objs[type.name] = type
# Populate generated response objects from the LLM
if func.FunctionReturn:
for type in await get_object_field_deps(func.FunctionReturn, object_ids):
generated_objs[type.name] = type
return generated_func, generated_objs
@traceable
async def develop_route(
ids: Identifiers,
goal_description: str,
function: Function,
spec: Specification,
lang: str,
extra_functions: list[Function] = [],
depth: int = 0,
) -> Function:
"""
Recursively develops a function and its child functions
based on the provided function definition and api route spec.
Args:
ids (Identifiers): The identifiers for the function.
goal_description (str): The high-level goal of the function to create.
function (Function): The function to develop.
depth (int): The depth of the recursion.
Returns:
Function: The developed route function.
"""
logger.info(
f"Developing for compiled route: "
f"{ids.compiled_route_id} - Func: {function.functionName}, depth: {depth}"
)
if not ids.compiled_route_id:
raise ValueError("ids.compiled_route_id is required for developing a function")
# Set the function id for logging
ids.function_id = function.id
if depth > RECURSION_DEPTH_LIMIT:
raise ValueError("Recursion depth exceeded")
compiled_route = await get_compiled_route(ids.compiled_route_id)
functions = []
if compiled_route.Functions:
functions += compiled_route.Functions
if compiled_route.RootFunction:
functions.append(compiled_route.RootFunction)
if extra_functions:
functions += extra_functions
generated_func, generated_objs = await populate_available_functions_objects(
functions
)
provided_functions = [
func.template
for func in list(generated_func.values()) + extra_functions
if func.functionName != function.functionName
] + [generate_object_template(f) for f in generated_objs.values()]
database_schema = codex.common.database.get_database_schema(spec)
dev_invoke_params = {
"route_path": compiled_route.ApiRouteSpec.path
if compiled_route.ApiRouteSpec
else "/",
"database_schema": database_schema,
"function_name": function.functionName,
"goal": goal_description,
"function_signature": function.template,
# function_args is not used by the prompt, but used for function validation
"function_args": [(f.name, f.typeName) for f in function.FunctionArgs or []],
# function_rets is not used by the prompt, but used for function validation
"function_rets": function.FunctionReturn.typeName
if function.FunctionReturn
else None,
"provided_functions": provided_functions,
# compiled_route_id is not used by the prompt, but is used by the function
"compiled_route_id": ids.compiled_route_id,
# available_objects is not used by the prompt, but is used by the function
"available_objects": generated_objs,
# function_id is used, so we can update the function with the implementation
"function_id": function.id,
"available_functions": generated_func,
"allow_stub": depth < RECURSION_DEPTH_LIMIT,
}
if lang == "nicegui":
ai_block = NiceGUIDevelopAIBlock()
else:
ai_block = DevelopAIBlock()
route_function = await ai_block.invoke(ids=ids, invoke_params=dev_invoke_params)
if route_function.ChildFunctions:
logger.info(
f"\tDeveloping {len(route_function.ChildFunctions)} child functions"
)
tasks = [
develop_route(
ids=ids,
goal_description=goal_description,
function=child,
spec=spec,
lang=lang,
extra_functions=extra_functions,
depth=depth + 1,
)
for child in route_function.ChildFunctions
if child.state == FunctionState.DEFINITION
]
await asyncio.gather(*tasks)
else:
logger.info("📦 No child functions to develop")
logger.debug(route_function.rawCode)
return route_function
if __name__ == "__main__":
import prisma
import codex.common.test_const as test_consts
from codex.common.ai_model import OpenAIChatClient
from codex.common.logging_config import setup_logging
from codex.requirements.database import get_latest_specification
OpenAIChatClient.configure({})
setup_logging()
client = prisma.Prisma(auto_register=True)
async def run_me():
await client.connect()
ids = test_consts.identifier_1
assert ids.user_id
assert ids.app_id
spec = await get_latest_specification(ids.user_id, ids.app_id)
ans = await develop_application(ids=ids, spec=spec)
await client.disconnect()
return ans
ans = asyncio.run(run_me())
logger.info(ans)