Files
AutoGPT-Code-Ability/codex/requirements/agent.py
T

528 lines
19 KiB
Python

import asyncio
import json
import logging
from asyncio import run
import openai
import prisma
from prisma.enums import AccessLevel
from prisma.models import Specification
from pydantic.json import pydantic_encoder
from codex.api_model import Identifiers
from codex.common.test_const import identifier_1
from codex.prompts.claude.requirements.NestJSDocs import (
NEST_JS_CRUD_GEN,
NEST_JS_FIRST_STEPS,
NEST_JS_MODULES,
NEST_JS_SQL,
)
from codex.requirements import flatten_endpoints
from codex.requirements.blocks.ai_clarify import (
FrontendClarificationBlock,
QuestionAndAnswerClarificationBlock,
UserPersonaClarificationBlock,
UserSkillClarificationBlock,
)
from codex.requirements.blocks.ai_database import DatabaseGenerationBlock
from codex.requirements.blocks.ai_endpoint import EndpointSchemaRefinementBlock
from codex.requirements.blocks.ai_feature import FeatureGenerationBlock
from codex.requirements.blocks.ai_module import (
ModuleGenerationBlock,
ModuleRefinementBlock,
)
from codex.requirements.blocks.ai_requirements import (
BaseRequirementsBlock,
FuncNonFuncRequirementsBlock,
)
from codex.requirements.database import create_spec
from codex.requirements.gather_task_info import gather_task_info_loop
from codex.requirements.hardcoded import (
appointment_optimization_requirements,
availability_checker_requirements,
calendar_booking_system,
distance_calculator_requirements,
inventory_mgmt_system,
invoice_generator_requirements,
invoice_payment_tracking,
profile_management,
)
from codex.requirements.matching import find_best_match
from codex.requirements.model import (
APIRouteRequirement,
ApplicationRequirements,
Clarification,
DBResponse,
Endpoint,
EndpointSchemaRefinementResponse,
Feature,
FeaturesSuperObject,
ModuleRefinement,
ModuleResponse,
QandA,
RequestModel,
RequirementsGenResponse,
RequirementsRefined,
ResponseModel,
StateObj,
)
from codex.requirements.unwrap_schemas import convert_db_schema, convert_endpoint
logger = logging.getLogger(__name__)
async def generate_requirements(
ids: Identifiers,
app_name: str,
description: str,
) -> Specification:
"""
Runs the Requirements Agent to generate the system requirements based
upon the provided task
Args:
ids (Identifiers): Relevant ids for database operations
app_name (str): name of the application
description (str): description of the application
Returns:
ApplicationRequirements: The system requirements for the application
"""
running_state_obj = StateObj(task=description)
logger.info("State Object Created")
# User Interview
project_description, memory = await gather_task_info_loop(
task=running_state_obj.task, ask_callback=None, ids=ids
)
running_state_obj.project_description = project_description
# convert the memory to `tool`: `content`: `response` format
project_description_thoughts = [
f"{item.tool}: {item.content} :{item.response}"
for item in memory
if item.response
]
running_state_obj.project_description_thoughts = "\n".join(
project_description_thoughts
)
logger.info("User Interview Done")
frontend_clarify = FrontendClarificationBlock()
frontend_clarification: Clarification = await frontend_clarify.invoke(
ids=ids,
invoke_params={"project_description": running_state_obj.project_description},
)
running_state_obj.add_clarifying_question(frontend_clarification)
logger.info("Frontend Clarification Done")
user_persona_clarify = UserPersonaClarificationBlock()
user_persona_clarification: Clarification = await user_persona_clarify.invoke(
ids=ids,
invoke_params={
"clarifiying_questions_so_far": running_state_obj.clarifying_questions_as_string(),
"project_description": running_state_obj.project_description,
},
)
running_state_obj.add_clarifying_question(user_persona_clarification)
logger.info("User Persona Clarification Done")
# User Skill
user_skill_clarify = UserSkillClarificationBlock()
user_skill_clarification: Clarification = await user_skill_clarify.invoke(
ids=ids,
invoke_params={
"clarifiying_questions_so_far": running_state_obj.clarifying_questions_as_string(),
"project_description": running_state_obj.project_description,
},
)
running_state_obj.add_clarifying_question(user_skill_clarification)
logger.info("User Skill Clarification Done")
# Clarification Rounds
q_and_a_clarify = QuestionAndAnswerClarificationBlock()
q_and_a_clarification: list[QandA] = await q_and_a_clarify.invoke(
ids=ids,
invoke_params={
"clarifiying_questions_so_far": running_state_obj.clarifying_questions_as_string(),
"project_description": running_state_obj.project_description,
"task": running_state_obj.task,
},
)
running_state_obj.q_and_a = q_and_a_clarification
logger.info("Question and Answer Based Clarification Done")
# Product Name and Description
feature_block = FeatureGenerationBlock()
feature_so: FeaturesSuperObject = await feature_block.invoke(
ids=ids,
invoke_params={
"project_description": running_state_obj.project_description,
"project_description_thoughts": running_state_obj.project_description_thoughts,
"joint_q_and_a": running_state_obj.joint_q_and_a(),
},
)
# Parsing
running_state_obj.product_name = feature_so.project_name
running_state_obj.product_description = feature_so.description
features: list[Feature] = []
for feature in feature_so.features:
features.append(feature.feature)
running_state_obj.features = features
logger.info("Features Done")
# Requirements Start
# Collect the requirements Q&A
base_requirements_block = BaseRequirementsBlock()
base_requirements: RequirementsRefined = await base_requirements_block.invoke(
ids=ids,
invoke_params={
"project_description": running_state_obj.project_description,
"project_description_thoughts": running_state_obj.project_description_thoughts,
"joint_q_and_a": running_state_obj.joint_q_and_a(),
},
)
running_state_obj.requirements_q_and_a = base_requirements.dirty_requirements or []
running_state_obj.refined_requirement_q_a = base_requirements
logger.info("Refined Requirements Done")
# Requirements QA answers
# Build the requirements from the Q&A
requirements_func_nonfunc_block = FuncNonFuncRequirementsBlock()
requirements_func_nonfunc: RequirementsGenResponse = (
await requirements_func_nonfunc_block.invoke(
ids=ids,
invoke_params={
"product_description": running_state_obj.product_description,
"joint_q_and_a": running_state_obj.joint_q_and_a(),
"requirements_q_and_a": running_state_obj.requirements_q_and_a_string(),
"features": json.dumps(
running_state_obj.features, default=pydantic_encoder
),
},
)
)
running_state_obj.requirements = requirements_func_nonfunc.answer
logger.info("Requirements Done")
# Modules seperation
# Build the requirements from the Q&A
module_block = ModuleGenerationBlock()
module_response: ModuleResponse = await module_block.invoke(
ids=ids,
invoke_params={
"NEST_JS_FIRST_STEPS": NEST_JS_FIRST_STEPS,
"NEST_JS_MODULES": NEST_JS_MODULES,
"NEST_JS_SQL": NEST_JS_SQL,
"NEST_JS_CRUD_GEN": NEST_JS_CRUD_GEN,
"product_description": running_state_obj.product_description,
"requirements_q_and_a": running_state_obj.requirements_q_and_a_string(),
"requirements": json.dumps(
running_state_obj.requirements, default=pydantic_encoder
),
"joint_q_and_a": running_state_obj.joint_q_and_a(),
"features": json.dumps(
running_state_obj.features, default=pydantic_encoder
),
},
)
running_state_obj.modules = module_response.modules
logger.info("Modules 1st Step Done")
# Database Design
database_block = DatabaseGenerationBlock()
db_response: DBResponse = await database_block.invoke(
ids=ids,
invoke_params={
"product_spec": running_state_obj.__str__(),
"needed_auth_roles": running_state_obj.refined_requirement_q_a.authorization_roles,
"modules": ", ".join(module.name for module in running_state_obj.modules),
},
)
running_state_obj.database = convert_db_schema(db_response.database_schema)
logger.info("DB Done")
# Module Refinement
# Build the requirements from the Q&A
module_ref_block = ModuleRefinementBlock()
refined_data: ModuleRefinement = await module_ref_block.invoke(
ids=ids,
invoke_params={
"system_requirements": running_state_obj.__str__(),
"modules_list": ", ".join(
[module.name for module in running_state_obj.modules]
),
},
)
logger.info("Refined Modules Generated Done")
# Match modules to completions
for module in refined_data.modules:
# Extract module names from running_state_obj.modules for comparison
existing_module_names = [
existing.name for existing in running_state_obj.modules
]
# Find the best match for module.module_name in existing_module_names
match = find_best_match(module.module_name, existing_module_names, threshold=80)
if match:
best_match, similarity = match[0], match[1]
# If a good match is found, proceed to update the module details
for index, existing in enumerate(running_state_obj.modules):
if existing.name == best_match:
logger.debug(f"Matched a module {existing.name}")
running_state_obj.modules[
index
].description = module.new_description
endpoints = flatten_endpoints.flatten_endpoints(
module.endpoint_groups
)
running_state_obj.modules[index].endpoints = endpoints
requirements = [
requirement for requirement in module.module_requirements
]
running_state_obj.modules[index].requirements = requirements
else:
logger.error(f"No close match found for {module.module_name}")
logger.info("Refined Modules Done")
# DB Schemas
db_table_names: list[str] = [
table.name or "" for table in running_state_obj.database.tables
]
for i, module in enumerate(running_state_obj.modules):
if module.endpoints:
logger.info(f"Endpoints for {module.name} Started")
endpoint_block = EndpointSchemaRefinementBlock()
new_endpoints: list[
EndpointSchemaRefinementResponse
] = await asyncio.gather(
*[
endpoint_block.invoke(
ids=ids,
invoke_params={
"spec": running_state_obj.__str__(),
"db_models": f"[{','.join(db_table_names)}]",
"module_repr": f"{module!r}",
"endpoint_repr": f"{endpoint!r}",
},
)
for endpoint in module.endpoints
]
)
for j, new_endpoint in enumerate(new_endpoints):
converted: Endpoint = convert_endpoint(
input=new_endpoint,
existing=module.endpoints[j],
database=running_state_obj.database,
)
logger.debug(f"{converted!r}")
logger.info(f"Endpoint {converted.type} {converted.name} Done")
running_state_obj.modules[i].endpoints[j] = converted # type: ignore
logger.info(f"Endpoints for {module.name} Done")
logger.info("Endpoints Done")
# Add support and tracking for models by module and add them to the prompt
# Step 5) Define the request and response models
# We maybe able to avoid needing llm calls for that
# Step 6) Generate the complete api route requirements
# Step 7) Compile the application requirements
api_routes: list[APIRouteRequirement] = []
for module in running_state_obj.modules:
if module.endpoints:
for route in module.endpoints:
logger.debug(route)
logger.info(f"Processing route {route.name}")
api_routes.append(
APIRouteRequirement(
function_name=route.name,
method=route.type,
path=route.path,
description=route.description,
request_model=route.request_model
or RequestModel(
name="None Provided",
description="None Proviced",
params=[],
),
response_model=route.response_model
or ResponseModel(
name="None Provided",
description="None Proviced",
params=[],
),
database_schema=route.database_schema,
access_level=AccessLevel.PUBLIC,
data_models=route.data_models,
)
)
full_spec = ApplicationRequirements(
name=running_state_obj.product_name,
context=running_state_obj.__str__(),
api_routes=api_routes,
)
logger.info(f"Full Spec Done")
saved_spec: Specification = await create_spec(ids, full_spec)
logger.info(f"Saved Spec Done")
# Step 8) Return the application requirements
return saved_spec
def hardcoded_requirements(task: str) -> ApplicationRequirements:
"""
This will take the application name and return the manually
defined requirements for the application in the correct format
"""
logger.warning("⚠️ Using hardcoded requirements")
match task:
case "Availability Checker":
return availability_checker_requirements()
case "Invoice Generator":
return invoice_generator_requirements()
case "Appointment Optimization Tool":
return appointment_optimization_requirements()
case "Distance Calculator":
return distance_calculator_requirements()
case "Profile Management System":
return profile_management()
case "Appointment Scheduling System":
return calendar_booking_system()
case "Survey Tool":
raise NotImplementedError("Survey Tool not implemented")
case "Inventory Management System":
return inventory_mgmt_system()
case "Invoiceing and Payment Tracking System":
return invoice_payment_tracking()
case "Client Portal":
raise NotImplementedError("Client Portal not implemented")
case "Tutor Scheduling App":
raise NotImplementedError("Tutor Scheduling App not implemented")
case "Cleaning Services App":
raise NotImplementedError("Cleaning Services App not implemented")
case "Personal Trainer App":
raise NotImplementedError("Personal Trainer App not implemented")
case "Plumber App":
raise NotImplementedError("Plumber App not implemented")
case "Property Management System":
raise NotImplementedError("Property Management System not implemented")
case _:
raise NotImplementedError(f"Task {task} not implemented")
async def populate_database_specs():
"""
This function will populate the database with the hardcoded requirements
id | createdAt | updatedAt | name | deleted | userId
----+-------------------------+-------------------------+-------------------------------+---------+--------
1 | 2024-02-08 11:51:15.216 | 2024-02-08 11:51:15.216 | Availability Checker | f | 1
2 | 2024-02-08 11:51:15.216 | 2024-02-08 11:51:15.216 | Invoice Generator | f | 1
3 | 2024-02-08 11:51:15.216 | 2024-02-08 11:51:15.216 | Appointment Optimization Tool | f | 1
4 | 2024-02-08 11:51:15.216 | 2024-02-08 11:51:15.216 | Distance Calculator | f | 1
5 | 2024-02-08 11:51:15.216 | 2024-02-08 11:51:15.216 | Profile Management System | f | 1
6 | 2024-02-08 11:51:15.216 | 2024-02-08 11:51:15.216 | Survey Tool | f | 2
7 | 2024-02-08 11:51:15.216 | 2024-02-08 11:51:15.216 | Scurvey Tool | t | 2
"""
from codex.api_model import Identifiers
from codex.common.test_const import (
app_id_1,
app_id_2,
app_id_3,
app_id_4,
app_id_5,
app_id_6,
app_id_7,
app_id_8,
)
requirements = [
("Availability Checker", app_id_1),
("Invoice Generator", app_id_2),
("Appointment Optimization Tool", app_id_3),
("Distance Calculator", app_id_4),
("Profile Management System", app_id_5),
# ("Appointment Scheduling System", app_id_6),
# ("Inventory Management System", app_id_7),
# ("Invoiceing and Payment Tracking System", app_id_8),
]
ids = identifier_1
for task, app_id in requirements:
print(f"Creating Spec for {task}, with app_id {app_id}")
spec = hardcoded_requirements(task)
ids.app_id = app_id
print(ids)
await create_spec(ids, spec)
if __name__ == "__main__":
from codex.common.test_const import identifier_1
ids = identifier_1
db_client = prisma.Prisma(auto_register=True)
oai = openai.AsyncOpenAI()
task = """The Tutor App is an app designed for tutors to manage their clients,
schedules, and invoices.
It must support both the client and tutor scheduling, rescheduling and canceling
appointments, and sending invoices after the appointment has passed.
Clients can sign up with OAuth2 or with traditional sign-in authentication. If they sign
up with traditional authentication, it must be safe and secure. There will need to be
password reset and login capabilities.
There will need to be authorization for identifying clients vs the tutor.
Additionally, it will have proper management of financials, including invoice management
and payment tracking. This includes things like paid/failed invoice notifications,
unpaid invoice follow-up, summarizing d/w/m/y income, and generating reports."""
async def run_gen():
await db_client.connect()
logger.info("Starting Requirements Generation")
output = await generate_requirements(
ids=ids, app_name="Tutor", description=task
)
logger.info("Requirements Generation Done")
return output
out = run(run_gen())
logger.info(out)