mirror of
https://github.com/Significant-Gravitas/AutoGPT-Code-Ability.git
synced 2026-11-10 08:01:27 -10:00
* Route schema changes * Delete ruff.config.json * save current changes * style fixes by ruff * Add class parsing capability * Fix bugs * Address comments * Fix benchmarks * Fix benchmarks * Fix benchmarks * Add comments * fixing types * Add error handling and status messages for application creation * Refactor fetch_deliverable function and update application creation status * Refactor benchmark output formatting * Remove unnecessary print statement in fetch_deliverable function * Propagate DB schema changes: Function <-> CompiledRouteSpec (#65) Propagate DB schema changes: Function <-> CompiledRouteSpec (#65) --------- Co-authored-by: majdyz <[email protected]> Co-authored-by: Swifty <[email protected]> Co-authored-by: majdyz <[email protected]> * emoved broken hardcoded requirements * Add benchmarking functionality * Update branch restrictions in GitHub workflows * Add pytest-cov to dependencies * Update ruff formatter command to include --check option * Update workflow names * Refactor code formatting and fix indentation issues * Update Python dependency caching and installation process * Add Prisma client generation step to CI workflow * Refactor test_process_object_type to use async/await * Add pytest and ValidationError imports, update arg_types assertion, and add test for ValidationError * Remove unused imports and commented code * Add logger to OpenAIChatClient class * Add async context manager for database connection and disconnection * Add blank line for readability * Refactor function_def.arg_types assignment in ast_test.py * mark integration tests * Update test workflow name * Add OpenAI API key configuration * Better Types for Requirements Request/Response (#76) Better Types for Requirements Request/Response --------- Co-authored-by: majdyz <[email protected]> * [AGPT-474] End-to-end Benchmark (#77) * Remove unnecessary fetch_deliverable calls and update invoice_generator_requirements * Fix typo in hardcoded.py * Add missing app IDs and interview IDs for calendar booking, inventory management, and invoicing systems * Add options to benchmark and example commands * Refactor benchmarking code and add new functions * Add task description method to ExampleTask enum * Added end-to-end running * Add hardcoded flag to filter examples in benchmark.py * Refactor run_benchmark_example function to handle exceptions * Fix error handling in specification routes * Fix Function generation bug on nested object type (#79) Co-authored-by: majdyz <[email protected]> * Speeding up requirements system --------- Co-authored-by: Zamil Majdy <[email protected]> Co-authored-by: majdyz <[email protected]> * Refactor code: Remove unused imports and clean up formatting * [AGPT-478] Add Support for Composite Type with multiple Pydantic/nont-primitive classes (#78) * [AGPT-478] Add support for multiple composite Pydantic objects * Ruff Ruff! * Fix failing test * formatting * fix populate-db command * fix run all benchmarks --------- Co-authored-by: majdyz <[email protected]> Co-authored-by: Swifty <[email protected]> * Refactor package handling in packager.py (#81) * [Fix] User provided app name being replaced (#82) * Only make databases when required (#83) * feat: make the name match * feat: database becomes optional * Update agent.py * Integrate related types into the requirements system (#84) * feat: make the name match * feat: database becomes optional * feat: initial model changes * feat: add prompts * feat: more prompts * fix: nits * fix: wrong type 😢 * feat: improved prompts * Cleanup for field type (#85) Co-authored-by: majdyz <[email protected]> * [AGPT-489] Improve type composite-type matching & extracting algorithm (#86) Co-authored-by: majdyz <[email protected]> * [AGPT-489] Add Autogeneration of Typing import (#87) Co-authored-by: majdyz <[email protected]> * [AGPT-489] Fix broken class compiling logic (#88) Co-authored-by: majdyz <[email protected]> * [AGPT-489] Fix function fetching to always include RelatedTypes (#89) [AGPT-489] Fix function fetching to always include RelatedTypes --------- Co-authored-by: majdyz <[email protected]> * fix lock file * feat: endpoint updates (#95) * feat: endpoint updates * fix: ambiguous param * [AGPT-486] Fix codegen generate code with missing pydantic object (#92) Co-authored-by: majdyz <[email protected]> * Fix retry prompt (#96) Co-authored-by: majdyz <[email protected]> * Allow benchmark on $PORT (#98) * Ntindle/agpt 506 add a bit of debug tooling for requirements gen (#100) * [AGPT-485] Fix missing pydantic object during requirement-step; leading to LLM to guess wrong signature (#97) [AGPT-485] Fix missing pydantic object during requirement-step; leading to LLM to guess wrong signature --------- Co-authored-by: majdyz <[email protected]> * [AGPT-157] Add Database Integration Generation (#90) * Added prisma client to server code * Add DatabaseSchema to INCLUDE_FUNC * Update functions to be async and add database schema documentation * Add DatabaseSchema and DatabaseTables to ApiRouteSpec * Updated dev prompts for db * Update profile management descriptions * Update route function definition to be async * Remove logging of database schema * Add Prisma schema file generation * Refactor benchmark and agent modules * Refactor code and comment out debug statements * formatting * Add available_functions to develop_route * Update prisma import and method calls * loading all related tables * Refactor create_prisma_scheama_file function to handle cases with no database schema * Fix async function call in packager.py * Fix database table query in packager.py * Fix database access and unnecessary imports * review comments * [AGPT-508] Enum support for Codex - Generation Logic Layer (#102) Co-authored-by: majdyz <[email protected]> * Add Enum Generation to Requriements (#101) * Added prisma client to server code * Add DatabaseSchema to INCLUDE_FUNC * Update functions to be async and add database schema documentation * Add DatabaseSchema and DatabaseTables to ApiRouteSpec * Updated dev prompts for db * Update profile management descriptions * Update route function definition to be async * Remove logging of database schema * Add Prisma schema file generation * Refactor benchmark and agent modules * Refactor code and comment out debug statements * formatting * Add available_functions to develop_route * Update prisma import and method calls * loading all related tables * Refactor create_prisma_scheama_file function to handle cases with no database schema * Fix async function call in packager.py * Fix database table query in packager.py * Fix database access and unnecessary imports * feat: add enums to the database gen * fix: formatting * Updated to new schema * update hardcoded requirements * fix: objectType generation without type bool * Add database enum import statement * removed unused var as failing lint check --------- Co-authored-by: SwiftyOS <[email protected]> * More Linting Issues Fixes (#99) * fix: more linting issues * Remove error log for async functions * Fix return_type default value in FunctionDef class * Added another validation check --------- Co-authored-by: Swifty <[email protected]> * [AGPT-463] Add code generation validation using Ruff (#103) --------- Co-authored-by: majdyz <[email protected]> * feat: extract external command on in memory code to a function with tests * feat: prisma model parser * feat: database test updates now requires prisma to run * feat: test for prisma accessible * feat: update to use format and validate Also saves the formatted and validated models in place * fix: lint * fix: remove unused variable * feat: allow multiple generators * .gitignore and .env.example (#107) * feat: .env.example * feat: gitignore * fix: replace all bad chars with NOTHING also fix the format string * Add Database Docker Compose File (#108) * feat: .env.example * feat: gitignore * fix: replace all bad chars with NOTHING also fix the format string * feat: add docker compose with only db --------- Co-authored-by: Swifty <[email protected]> * feat: remove vector requirement (#106) * Fix lifecycle and prisma models and enums missing (#105) * fix: add prisma model imports fro root level * fix: reorder start code and add lifecycle * Validate Database Generation (#104) * feat: extract external command on in memory code to a function with tests * feat: prisma model parser * feat: database test updates now requires prisma to run * feat: test for prisma accessible * feat: update to use format and validate Also saves the formatted and validated models in place * fix: lint * fix: remove unused variable * feat: allow multiple generators * formatting * Change logger.info to logger.debug in exec_external_on_contents function * Update error logging in exec_external_on_contents function * Update response model and route annotation in compile.py * Force * Ruff * Fix Api route spec * formatting * Dont generate envexample passwords with symbols in them (#117) * fix: don't use special chars in db password * fix: linting * [AGPT-540] Add missing initialization of available object types (#113) * [AGPT-540] Add missing initialization of available object types * [AGPT-540] Add missing initialization of available object types * [AGPT-540] Add missing initialization of available object types * Ruff * allow non-annotated type checking * Include all available function for ruff * Fix tests * Make DB work * Make stubbing work * Make papa-lint happy * Ruffs * Add prisma model validation layer * Improve prisma querying by avoiding conflict * Move on during failure formatting --------- Co-authored-by: majdyz <[email protected]> * main into dev (#118) * Update setup guide. * [AGPT-544] Vague 500 errors in interview stage (#115) * Add error handling for user not found in database during interview step. * Add error handling for app not found in database during interview step. * Add error handling for interview not found in database during interview step. * Add error handling for nonexistent questions in interview step. * Add error handling for non-"ask" tool in user answers in interview step. * Clarify comment describing interview next_step. --------- Co-authored-by: Toran Bruce Richards <[email protected]> * [AGPT-511] Resume running from any point (#114) * Refactor populate_db function and add get_resume_points command * Fix app ID reference in benchmark.py * Add ResumePoint model * Added runner funcitons to running codex from the cli * Add application description field to ApplicationBase model and update create_app method in CodexClient * Refactor code to run tasks concurrently * Add include parameter to find_many() method in get_resume_points() * Add disconnect calls to asyncio event loop * Update resume_point name to display only the first 30 characters * Remove commented out code and unused imports * Remove unused code * formatting * Fix validation error handling in develop.py * formatting * fixes * Add validation error for unimplemented main function * Validate and try-fix generated endpoint db and model usage (#116) * feat: log status * feat: trivial implementation of copy works for request -> response and response-> request for unfilled object but not list[unfilled_object] * fix: i cant rememember * feat: more complex * feat: mostly working * fix: fixes from tests * feat: tests * fix: linting problems and test name * feat: add more supported types * fix: remove unused data * fix: stop using print * fix: arrays of arrays * fix: lint * fix: reduce mess * feat: more complex type that I hope we never actually use * fix: lint --------- Co-authored-by: majdyz <[email protected]> Co-authored-by: majdyz <[email protected]> Co-authored-by: Zamil Majdy <[email protected]> Co-authored-by: Nicholas Tindle <[email protected]> Co-authored-by: Nicholas Tindle <[email protected]> Co-authored-by: Toran Bruce Richards <[email protected]>
626 lines
23 KiB
Python
626 lines
23 KiB
Python
import logging
|
|
from datetime import datetime
|
|
from typing import Optional, Tuple
|
|
|
|
import aiohttp
|
|
from prisma import Prisma
|
|
from pydantic import BaseModel, ValidationError
|
|
|
|
from codex.api_model import (
|
|
ApplicationCreate,
|
|
ApplicationResponse,
|
|
DeliverableResponse,
|
|
DeploymentResponse,
|
|
Identifiers,
|
|
InterviewCreate,
|
|
InterviewMessageWithResponse,
|
|
InterviewResponse,
|
|
SpecificationResponse,
|
|
UserCreate,
|
|
UserResponse,
|
|
)
|
|
from codex.common.logging_config import setup_logging
|
|
from codex.interview.model import InterviewMessage
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CodexClient:
|
|
def __init__(
|
|
self,
|
|
client: Prisma,
|
|
base_url: Optional[str] = None,
|
|
auth_key: Optional[str] = None,
|
|
):
|
|
"""
|
|
Initialize the codex client.
|
|
|
|
Args:
|
|
client (Prisma): the prisma client
|
|
base_url (str, optional): the base url for the codex service. Defaults to None to use the production url.
|
|
auth_key (str, optional): the authorization key for the codex service. Defaults to None.
|
|
"""
|
|
# Set the DB Client
|
|
self.client = client
|
|
|
|
# Set the base url for codex
|
|
if not base_url:
|
|
self.base_url = "https://codegen-xca4qjgx4a-uc.a.run.app/api/v1"
|
|
else:
|
|
self.base_url = base_url
|
|
|
|
# Set the headers
|
|
self.headers: dict[str, str] = {"accept": "application/json"}
|
|
if auth_key:
|
|
self.headers["Authorization"] = f"Bearer {auth_key}"
|
|
|
|
self.app_id: Optional[str] = None
|
|
self.interview_id: Optional[str] = None
|
|
self.specification_id: Optional[str] = None
|
|
self.deliverable_id: Optional[str] = None
|
|
self.deployment_id: Optional[str] = None
|
|
|
|
async def init(
|
|
self,
|
|
codex_user_id: Optional[str] = None,
|
|
app_id: Optional[str] = None,
|
|
interview_id: Optional[str] = None,
|
|
specification_id: Optional[str] = None,
|
|
deliverable_id: Optional[str] = None,
|
|
deployment_id: Optional[str] = None,
|
|
):
|
|
"""
|
|
Initialize the async part of the codex client.
|
|
|
|
Sets the user and the codex_user_id.
|
|
|
|
Args:
|
|
cloud_services_user_id (str): the id of the user in the cloud services
|
|
codex_user_id (str): the id of the user in the codex service
|
|
"""
|
|
# require either cloud_services_user_id or discord_user_id
|
|
if not codex_user_id:
|
|
raise ValueError("You must provide a codex_user_id")
|
|
self.codex_user_id = codex_user_id
|
|
if app_id:
|
|
self.app_id = app_id
|
|
if interview_id:
|
|
self.interview_id = interview_id
|
|
if specification_id:
|
|
self.specification_id = specification_id
|
|
if deliverable_id:
|
|
self.deliverable_id = deliverable_id
|
|
if deployment_id:
|
|
self.deployment_id = deployment_id
|
|
|
|
async def create_or_get_codex_user(
|
|
self, discord_id: str, cloud_services_id: str = ""
|
|
) -> UserResponse:
|
|
"""
|
|
Get or create the user from the codex service.
|
|
|
|
This is used in the __init__ so be weary of what self.<attribute> are available.
|
|
|
|
Args:
|
|
discord_id (str): the discord id of the user
|
|
"""
|
|
url = f"{self.base_url}/user"
|
|
obj = UserCreate(cloudServicesId=cloud_services_id, discord_id=discord_id)
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
url, headers=self.headers, params=obj.model_dump()
|
|
) as response:
|
|
response.raise_for_status()
|
|
user_response = UserResponse(**await response.json())
|
|
return user_response
|
|
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error getting user: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing user: {e}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.exception(f"Unknown Error when trying to get user: {e}")
|
|
raise e
|
|
|
|
async def create_app(
|
|
self, app_name: str, app_description: str
|
|
) -> ApplicationResponse:
|
|
"""
|
|
Creates a new app for the given user.
|
|
|
|
Args:
|
|
user_id (int): The ID of the user for whom the app is being created.
|
|
app_name (str): The name of the new app to be created.
|
|
description (str): The description of the new app to be created.
|
|
|
|
Returns:
|
|
ApplicationResponse: The response from the server after attempting to create the app.
|
|
"""
|
|
url = f"{self.base_url}/user/{self.codex_user_id}/apps/"
|
|
# headers = {"accept": "application/json", "Content-Type": "application/json"}
|
|
|
|
data = ApplicationCreate(name=app_name, description=app_description)
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
url, headers=self.headers, json=data.model_dump()
|
|
) as response:
|
|
response.raise_for_status()
|
|
app_response = ApplicationResponse(**await response.json())
|
|
self.app_id = app_response.id
|
|
return app_response
|
|
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error creating app: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing app: {e}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.exception(f"Unknown Error when trying to create an app: {e}")
|
|
raise e
|
|
|
|
async def get_app(self, app_id: Optional[str] = None) -> ApplicationResponse:
|
|
"""
|
|
Get the app from the codex service.
|
|
|
|
Args:
|
|
app_id (str): the id of the app
|
|
|
|
Returns:
|
|
ApplicationResponse: the response from the server after attempting to get the app.
|
|
"""
|
|
if not app_id and not self.app_id:
|
|
raise ValueError("You must provide an app_id to get the app")
|
|
url = f"{self.base_url}/user/{self.codex_user_id}/apps/{app_id or self.app_id}"
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(url, headers=self.headers) as response:
|
|
response.raise_for_status()
|
|
app_response = ApplicationResponse(**await response.json())
|
|
return app_response
|
|
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error getting app: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing app: {e}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.exception(f"Unknown Error when trying to get app: {e}")
|
|
raise e
|
|
|
|
async def start_interview(
|
|
self,
|
|
name: str,
|
|
task: str,
|
|
) -> InterviewResponse:
|
|
"""
|
|
Start an interview for the app.
|
|
"""
|
|
if not self.app_id:
|
|
raise ValueError("You must create an app before starting an interview")
|
|
url = f"{self.base_url}/user/{self.codex_user_id}/apps/{self.app_id}/interview/"
|
|
obj = InterviewCreate(name=name, task=task)
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
url, headers=self.headers, json=obj.model_dump()
|
|
) as response:
|
|
if response.status != 200:
|
|
logger.error(f"Error starting interview: {response.status}")
|
|
logger.error(await response.text())
|
|
response.raise_for_status()
|
|
interview_response = InterviewResponse(**await response.json())
|
|
self.interview_id = interview_response.id
|
|
return interview_response
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error starting interview: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing interview: {e}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.exception(f"Unknown Error when trying to start the interview: {e}")
|
|
raise e
|
|
|
|
async def interview_next(
|
|
self, answers: list[InterviewMessageWithResponse | InterviewMessage]
|
|
) -> InterviewResponse:
|
|
"""
|
|
Answer the next question in the interview.
|
|
|
|
Args:
|
|
user_id (int): The ID of the user for whom the interview is being answered.
|
|
app_id (int): The ID of the app for which the interview is being answered.
|
|
interview_id (int): The ID of the interview for which the question is being answered.
|
|
answers (List[InterviewMessageWithResponse | InterviewMessage]): The list of answers to the questions in the interview.
|
|
|
|
Returns:
|
|
InterviewResponse: The response from the server after attempting to answer the next question in the interview.
|
|
"""
|
|
if not self.app_id or not self.interview_id:
|
|
raise ValueError(
|
|
"You must create an app and participate in an interview before answering the next question"
|
|
)
|
|
url = f"{self.base_url}/user/{self.codex_user_id}/apps/{self.app_id}/interview/{self.interview_id}/next"
|
|
|
|
obj = [model.model_dump() for model in answers]
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
url, headers=self.headers, json=obj
|
|
) as response:
|
|
response.raise_for_status()
|
|
interview_response = InterviewResponse(**await response.json())
|
|
return interview_response
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error answering next question: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing next question: {e}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.exception(
|
|
f"Unknown Error when trying to answer the next question: {e}"
|
|
)
|
|
raise e
|
|
|
|
async def generate_spec(self) -> SpecificationResponse:
|
|
"""
|
|
Generate the requirements for the app based on the given description.
|
|
|
|
Args:
|
|
user_id (int): The ID of the user for whom the spec is being generated.
|
|
app_id (int): The ID of the app for which the spec is being generated.
|
|
description (str): The description of the requirements for the app.
|
|
|
|
Returns:
|
|
SpecificationResponse: The response from the server after attempting to generate the app specifications.
|
|
"""
|
|
if not self.app_id and not self.interview_id:
|
|
raise ValueError(
|
|
"You must create an app and participate in an interview before generating a spec"
|
|
)
|
|
url = f"{self.base_url}/user/{self.codex_user_id}/apps/{self.app_id}/specs/"
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
url,
|
|
headers=self.headers,
|
|
timeout=3000,
|
|
params={"interview_id": self.interview_id},
|
|
) as response:
|
|
response.raise_for_status()
|
|
spec_response = SpecificationResponse(**await response.json())
|
|
self.specification_id = spec_response.id
|
|
return spec_response
|
|
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error generating app spec: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing app spec: {e}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.exception(f"Unknown Error when trying to generate the spec: {e}")
|
|
raise e
|
|
|
|
async def generate_deliverable(self) -> DeliverableResponse:
|
|
"""
|
|
Generate the deliverable for the app based on a specific specification.
|
|
|
|
Returns:
|
|
DeliverableResponse: The response from the server after attempting to generate the app deliverable.
|
|
"""
|
|
if not self.app_id or not self.specification_id:
|
|
raise ValueError(
|
|
"You must create an app and generate a spec before generating a deliverable"
|
|
)
|
|
url = f"{self.base_url}/user/{self.codex_user_id}/apps/{self.app_id}/specs/{self.specification_id}/deliverables/"
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
url, headers=self.headers, timeout=3000
|
|
) as response:
|
|
response.raise_for_status()
|
|
deliverable_response = DeliverableResponse(**await response.json())
|
|
self.deliverable_id = deliverable_response.id
|
|
return deliverable_response
|
|
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error generating app deliverable: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing app deliverable: {e}")
|
|
raise e
|
|
|
|
except Exception as e:
|
|
logger.exception(
|
|
f"Unknown Error when trying to generate the deliverable: {e}"
|
|
)
|
|
raise e
|
|
|
|
async def create_deployment(self) -> DeploymentResponse:
|
|
"""
|
|
Create a deployment for the app based on the deliverable.
|
|
|
|
Args:
|
|
user_id (int): The ID of the user for whom the deployment is being created.
|
|
app_id (int): The ID of the app for which the deployment is being created.
|
|
spec_id (int): The ID of the specification based on which the deployment is created.
|
|
deliverable_id (int): The ID of the deliverable based on which the deployment is created.
|
|
|
|
Returns:
|
|
DeploymentResponse: The response from the server after attempting to create the app deployment.
|
|
"""
|
|
if not self.app_id or not self.specification_id or not self.deliverable_id:
|
|
raise ValueError(
|
|
"You must create an app, generate a spec, and generate a deliverable before creating a deployment"
|
|
)
|
|
url = f"{self.base_url}/user/{self.codex_user_id}/apps/{self.app_id}/specs/{self.specification_id}/deliverables/{self.deliverable_id}/deployments/"
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(url, headers=self.headers) as response:
|
|
if response.status != 200:
|
|
logger.error(f"Error creating deployment: {response.status}")
|
|
logger.error(await response.text())
|
|
response.raise_for_status()
|
|
deployment_response = DeploymentResponse(**await response.json())
|
|
self.deployment_id = deployment_response.id
|
|
return deployment_response
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"Error creating app deployment: {e}")
|
|
raise e
|
|
except ValidationError as e:
|
|
logger.exception(f"Error parsing app deployment: {e}")
|
|
raise e
|
|
except Exception as e:
|
|
logger.exception(f"Unknown Error when trying to create the deployment: {e}")
|
|
raise e
|
|
|
|
async def download_zip(self) -> Tuple[bytes, str]:
|
|
"""
|
|
Step 6: Download the deployment zip file.
|
|
|
|
Note: The request returns a FastAPI streaming response.
|
|
|
|
Returns:
|
|
Tuple[bytes, str]: The bytes of the zip file and the name of the file.
|
|
"""
|
|
if not self.deployment_id:
|
|
raise ValueError("You must create a deployment before downloading a zip")
|
|
url = f"{self.base_url}/deployments/{self.deployment_id}/download"
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(url, headers=self.headers) as response:
|
|
response.raise_for_status()
|
|
content = await response.read()
|
|
filename = response.headers.get("Content-Disposition", "").split(
|
|
"filename="
|
|
)[1]
|
|
return content, filename
|
|
# with requests.get(url, stream=True) as response:
|
|
# response.raise_for_status() # Raises stored HTTPError, if one occurred.
|
|
|
|
# # Extract filename from Content-Disposition header if available.
|
|
# filename = "deployment.zip" # Default filename if not found in headers.
|
|
# cd = response.headers.get("Content-Disposition")
|
|
# if cd:
|
|
# filenames = [
|
|
# fn.strip().split("=")[1] if "filename=" in fn else None
|
|
# for fn in cd.split(";")
|
|
# ]
|
|
# filename = next((fn.strip('"') for fn in filenames if fn), filename)
|
|
|
|
# # Download the content.
|
|
# content = response.content
|
|
|
|
# return content, filename
|
|
except aiohttp.ClientError as e:
|
|
logger.exception(f"HTTP error occurred: {e}")
|
|
raise e
|
|
except Exception as err:
|
|
logger.exception(f"An error occurred: {err}")
|
|
raise
|
|
|
|
@staticmethod
|
|
async def build_codex_client(
|
|
client: "Prisma",
|
|
cloud_services_user_id: str,
|
|
codex_user_id: str,
|
|
base_url: Optional[str] = None,
|
|
auth_key: Optional[str] = None,
|
|
app_id: Optional[str] = None,
|
|
interview_id: Optional[str] = None,
|
|
spec_id: Optional[str] = None,
|
|
deliverable_id: Optional[str] = None,
|
|
deployment_id: Optional[str] = None,
|
|
) -> "CodexClient":
|
|
"""
|
|
Build the codex client from the database.
|
|
|
|
Returns:
|
|
CodexClient: The codex client.
|
|
"""
|
|
codex = CodexClient(
|
|
client=client,
|
|
base_url=base_url,
|
|
auth_key=auth_key,
|
|
)
|
|
await codex.init(
|
|
codex_user_id=codex_user_id,
|
|
)
|
|
|
|
# throw an error if app -> spec -> deliverable -> deployment is not followed
|
|
if interview_id and not app_id:
|
|
raise ValueError(
|
|
"You must provide an app_id if you provide an interview_id"
|
|
)
|
|
if spec_id and not interview_id and not app_id:
|
|
raise ValueError(
|
|
"You must provide an app_id and interview_id if you provide a spec_id"
|
|
)
|
|
if deliverable_id and not spec_id and not app_id:
|
|
raise ValueError(
|
|
"You must provide an app_id and a spec_id if you provide a deliverable_id"
|
|
)
|
|
if deployment_id and not deliverable_id and not spec_id and not app_id:
|
|
raise ValueError(
|
|
"You must provide an app_id, a spec_id, and a deliverable_id if you provide a deployment_id"
|
|
)
|
|
|
|
codex.app_id = app_id
|
|
codex.interview_id = interview_id
|
|
codex.specification_id = spec_id
|
|
codex.deliverable_id = deliverable_id
|
|
codex.deployment_id = deployment_id
|
|
|
|
return codex
|
|
|
|
|
|
class TestModel(BaseModel):
|
|
content: bytes
|
|
filename: str
|
|
deployment: DeploymentResponse
|
|
deliverable: DeliverableResponse
|
|
spec: SpecificationResponse
|
|
interview: InterviewResponse
|
|
app: ApplicationResponse
|
|
|
|
|
|
async def from_existing(db_client: Prisma, identifier: Identifiers) -> TestModel:
|
|
await db_client.connect()
|
|
|
|
if not identifier.cloud_services_id:
|
|
raise ValueError("Cloud Services ID not found")
|
|
if not identifier.user_id:
|
|
raise ValueError("User ID not found")
|
|
|
|
codex_client = await CodexClient.build_codex_client(
|
|
client=db_client,
|
|
cloud_services_user_id=identifier.cloud_services_id,
|
|
codex_user_id=identifier.user_id,
|
|
base_url="http://localhost:8007/api/v1",
|
|
app_id=identifier.app_id,
|
|
interview_id=identifier.spec_id,
|
|
spec_id=identifier.spec_id,
|
|
deliverable_id=identifier.completed_app_id,
|
|
)
|
|
logger.info(f"User: {codex_client.codex_user_id}")
|
|
app = await codex_client.get_app()
|
|
logger.info(f"App: {app.id}")
|
|
# spec = await codex_client.get_spec()
|
|
deliverable = await codex_client.generate_deliverable()
|
|
logger.info(f"Deliverable: {deliverable.id}")
|
|
deployment = await codex_client.create_deployment()
|
|
logger.info(f"Deployment: {deployment.id}")
|
|
content, filename = await codex_client.download_zip()
|
|
await db_client.disconnect()
|
|
return TestModel(
|
|
content=content,
|
|
filename=filename,
|
|
deployment=deployment,
|
|
deliverable=deliverable,
|
|
interview=InterviewResponse(
|
|
id=identifier.interview_id or "", finished=True, uses=[]
|
|
),
|
|
spec=SpecificationResponse(
|
|
id=identifier.spec_id or "",
|
|
name="Test Spec",
|
|
createdAt=datetime.now(),
|
|
context="",
|
|
),
|
|
app=app,
|
|
)
|
|
|
|
|
|
async def partial(db_client: Prisma, identifier: Identifiers) -> TestModel:
|
|
await db_client.connect()
|
|
if not identifier.cloud_services_id:
|
|
raise ValueError("Cloud Services ID not found")
|
|
if not identifier.user_id:
|
|
raise ValueError("User ID not found")
|
|
|
|
codex_client = await CodexClient.build_codex_client(
|
|
client=db_client,
|
|
cloud_services_user_id=identifier.cloud_services_id,
|
|
codex_user_id=identifier.user_id,
|
|
base_url="http://localhost:8007/api/v1",
|
|
app_id=identifier.app_id,
|
|
)
|
|
print(f"User: {codex_client.codex_user_id}")
|
|
app = await codex_client.get_app()
|
|
logger.info(f"App: {app.id}")
|
|
interview = await codex_client.start_interview(
|
|
"Test Interview", "Create a calulator api"
|
|
)
|
|
logger.info(f"Interview: {interview.id}")
|
|
while True:
|
|
interview_next = await codex_client.interview_next(answers=[])
|
|
if interview_next.finished:
|
|
break
|
|
spec = await codex_client.generate_spec()
|
|
logger.info(f"Spec: {spec.id}")
|
|
deliverable = await codex_client.generate_deliverable()
|
|
logger.info(f"Deliverable: {deliverable.id}")
|
|
deployment = await codex_client.create_deployment()
|
|
logger.info(f"Deployment: {deployment.id}")
|
|
content, filename = await codex_client.download_zip()
|
|
|
|
await db_client.disconnect()
|
|
return TestModel(
|
|
content=content,
|
|
filename=filename,
|
|
deployment=deployment,
|
|
deliverable=deliverable,
|
|
spec=spec,
|
|
interview=interview,
|
|
app=app,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
from asyncio import run
|
|
|
|
from common.test_const import identifier_1
|
|
|
|
setup_logging(local=True)
|
|
|
|
db_client = Prisma(auto_register=True)
|
|
|
|
response = run(
|
|
from_existing(
|
|
db_client,
|
|
Identifiers(
|
|
user_id=identifier_1.user_id,
|
|
cloud_services_id=identifier_1.cloud_services_id,
|
|
app_id=identifier_1.app_id,
|
|
spec_id="814c2c78-fa4f-4162-9f25-aa1b947e9874",
|
|
),
|
|
)
|
|
)
|
|
with open(response.filename, "wb") as f:
|
|
f.write(response.content)
|
|
logger.info(f"Downloaded {response.filename}")
|
|
logger.info(f"{response!r}")
|
|
logger.info("Done")
|
|
|
|
response = run(partial(db_client, identifier_1))
|
|
with open(response.filename, "wb") as f:
|
|
f.write(response.content)
|
|
logger.info(f"Downloaded {response.filename}")
|
|
logger.info(f"{response!r}")
|
|
logger.info("Done")
|