244 lines
8.8 KiB
Python
244 lines
8.8 KiB
Python
"""
|
|
Design phase handlers: table_design, crud_design, api_design
|
|
"""
|
|
import json
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def handle_table_design(tenant_id, task_id, step_name, input_data, config):
|
|
"""Generate models/*.json from requirement document using LLM."""
|
|
requirement_doc = input_data.get("requirement_review", {}).get("output", {})
|
|
if not requirement_doc:
|
|
requirement_doc = input_data.get("requirement_gathering", {}).get("output", {})
|
|
|
|
prompt = f"""Based on the following requirement document, generate database table definitions
|
|
in the standardized JSON format (database-table-definition-spec).
|
|
|
|
Requirements:
|
|
{json.dumps(requirement_doc, ensure_ascii=False, indent=2)}
|
|
|
|
Rules:
|
|
- Each table must have summary (with primary as array), fields, indexes, codes sections
|
|
- id field: str(32), not null
|
|
- Use abstract types: str, int, text, timestamp, date, datetime, float, double
|
|
- str/float/double need length (and dec for float/double)
|
|
- indexes idxfields must be arrays
|
|
- codes referencing appcodes_kv must use parentid= in cond, never id=
|
|
- Add org_id str(32) default '0' for multi-tenant tables
|
|
|
|
Output a JSON object with key "models" containing an array of table definitions.
|
|
Each table definition must include a "filename" key (e.g., "users.json").
|
|
"""
|
|
|
|
from pipeline_service.llm_bridge import call_llm
|
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
|
|
|
try:
|
|
models_data = json.loads(result)
|
|
except json.JSONDecodeError:
|
|
# Try to extract JSON from response
|
|
import re
|
|
json_match = re.search(r'\{.*\}', result, re.DOTALL)
|
|
if json_match:
|
|
models_data = json.loads(json_match.group())
|
|
else:
|
|
raise ValueError(f"LLM returned non-JSON response: {result[:200]}")
|
|
|
|
if "models" not in models_data:
|
|
models_data = {"models": [models_data]}
|
|
|
|
# Validate each model
|
|
for model in models_data["models"]:
|
|
_validate_table_definition(model)
|
|
|
|
return {
|
|
"models": models_data["models"],
|
|
"table_count": len(models_data["models"]),
|
|
"generated_by": "llm",
|
|
"needs_review": True,
|
|
}
|
|
|
|
|
|
async def handle_crud_design(tenant_id, task_id, step_name, input_data, config):
|
|
"""Generate json/*.json CRUD definitions from table designs."""
|
|
table_design = input_data.get("table_design", {}).get("output", {})
|
|
models = table_design.get("models", [])
|
|
|
|
if not models:
|
|
raise ValueError("No table models found from table_design step")
|
|
|
|
prompt = f"""Based on the following table definitions, generate CRUD definition files
|
|
in the standardized JSON format (crud-definition-spec).
|
|
|
|
Tables:
|
|
{json.dumps(models, ensure_ascii=False, indent=2)}
|
|
|
|
Rules:
|
|
- Root keys must be: tblname, title (optional), params
|
|
- params must have: browserfields, editable (with new/update/delete_data_url)
|
|
- Use {{entire_url('../api/xxx.dspy')}} for editable URLs with ../ prefix
|
|
- Determine tree vs list view based on self-referencing foreign keys
|
|
- Add data_filter for searchable fields
|
|
- Add logined_userorgid for org-scoped tables
|
|
- Use uitype: "code" with dataurl for foreign key dropdowns
|
|
|
|
Output a JSON object with key "cruds" containing an array of CRUD definitions.
|
|
Each definition must include a "filename" key (e.g., "users_list.json").
|
|
"""
|
|
|
|
from pipeline_service.llm_bridge import call_llm
|
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
|
|
|
try:
|
|
cruds_data = json.loads(result)
|
|
except json.JSONDecodeError:
|
|
import re
|
|
json_match = re.search(r'\{.*\}', result, re.DOTALL)
|
|
if json_match:
|
|
cruds_data = json.loads(json_match.group())
|
|
else:
|
|
raise ValueError(f"LLM returned non-JSON response: {result[:200]}")
|
|
|
|
if "cruds" not in cruds_data:
|
|
cruds_data = {"cruds": [cruds_data]}
|
|
|
|
for crud in cruds_data["cruds"]:
|
|
_validate_crud_definition(crud)
|
|
|
|
return {
|
|
"cruds": cruds_data["cruds"],
|
|
"crud_count": len(cruds_data["cruds"]),
|
|
"generated_by": "llm",
|
|
"needs_review": True,
|
|
}
|
|
|
|
|
|
async def handle_api_design(tenant_id, task_id, step_name, input_data, config):
|
|
"""Generate API endpoint list and .dspy specifications."""
|
|
table_design = input_data.get("table_design", {}).get("output", {})
|
|
crud_design = input_data.get("crud_design", {}).get("output", {})
|
|
|
|
models = table_design.get("models", [])
|
|
cruds = crud_design.get("cruds", [])
|
|
|
|
prompt = f"""Based on the following table and CRUD definitions, generate a complete
|
|
API endpoint specification for a Sage module.
|
|
|
|
Tables: {json.dumps(models, ensure_ascii=False)}
|
|
CRUDs: {json.dumps(cruds, ensure_ascii=False)}
|
|
|
|
For each table, generate:
|
|
1. Standard CRUD endpoints: create, update, delete .dspy files in wwwroot/api/
|
|
2. Search endpoints: get_search_{fieldname}.dspy for foreign key dropdowns
|
|
3. Custom business logic endpoints as needed
|
|
|
|
Rules for .dspy files:
|
|
- NO import statements (json, datetime, debug, DBPools, get_user, params_kw etc are pre-loaded)
|
|
- Use return instead of print
|
|
- Use getID() instead of uuid()
|
|
- Use await get_user() not get_user()
|
|
- No ServerEnv() usage
|
|
|
|
Output JSON with key "api_list" containing array of:
|
|
{{"filename": "xxx.dspy", "path": "wwwroot/api/xxx.dspy", "method": "POST", "description": "..."}}
|
|
|
|
Also include "dspy_specs" with the implementation spec for each .dspy file.
|
|
"""
|
|
|
|
from pipeline_service.llm_bridge import call_llm
|
|
result = await call_llm(tenant_id, prompt, config.get("llm_model", "default"))
|
|
|
|
try:
|
|
api_data = json.loads(result)
|
|
except json.JSONDecodeError:
|
|
import re
|
|
json_match = re.search(r'\{.*\}', result, re.DOTALL)
|
|
if json_match:
|
|
api_data = json.loads(json_match.group())
|
|
else:
|
|
raise ValueError(f"LLM returned non-JSON response: {result[:200]}")
|
|
|
|
return {
|
|
"api_list": api_data.get("api_list", []),
|
|
"dspy_specs": api_data.get("dspy_specs", []),
|
|
"endpoint_count": len(api_data.get("api_list", [])),
|
|
"generated_by": "llm",
|
|
"needs_review": True,
|
|
}
|
|
|
|
|
|
def _validate_table_definition(model):
|
|
"""Basic validation of table definition against database-table-definition-spec."""
|
|
errors = []
|
|
|
|
if "summary" not in model:
|
|
errors.append("Missing 'summary' section")
|
|
elif not isinstance(model["summary"], list) or len(model["summary"]) == 0:
|
|
errors.append("'summary' must be a non-empty array")
|
|
else:
|
|
s = model["summary"][0]
|
|
if "primary" not in s:
|
|
errors.append("summary[0] missing 'primary'")
|
|
elif not isinstance(s["primary"], list):
|
|
errors.append(f"summary[0].primary must be array, got {type(s['primary']).__name__}")
|
|
|
|
if "fields" not in model:
|
|
errors.append("Missing 'fields' section")
|
|
elif not isinstance(model["fields"], list):
|
|
errors.append("'fields' must be an array")
|
|
else:
|
|
for f in model["fields"]:
|
|
ftype = f.get("type", "")
|
|
if ftype in ("str", "char") and not f.get("length"):
|
|
errors.append(f"Field '{f.get('name')}' type={ftype} missing length")
|
|
if ftype in ("float", "double", "ddouble"):
|
|
if not f.get("length"):
|
|
errors.append(f"Field '{f.get('name')}' type={ftype} missing length")
|
|
if not f.get("dec"):
|
|
errors.append(f"Field '{f.get('name')}' type={ftype} missing dec")
|
|
|
|
if "indexes" in model:
|
|
for idx in model["indexes"]:
|
|
if "idxfields" not in idx:
|
|
errors.append(f"Index '{idx.get('name')}' missing 'idxfields'")
|
|
elif not isinstance(idx["idxfields"], list):
|
|
errors.append(f"Index '{idx.get('name')}' idxfields must be array")
|
|
|
|
if "codes" in model:
|
|
for code in model["codes"]:
|
|
if code.get("table") == "appcodes_kv":
|
|
cond = code.get("cond", "")
|
|
if "id=" in cond and "parentid=" not in cond:
|
|
errors.append(
|
|
f"Code field '{code.get('field')}' uses id= instead of parentid= for appcodes_kv"
|
|
)
|
|
|
|
if errors:
|
|
logger.warning(f"Table definition validation warnings: {errors}")
|
|
|
|
return errors
|
|
|
|
|
|
def _validate_crud_definition(crud):
|
|
"""Basic validation of CRUD definition against crud-definition-spec."""
|
|
errors = []
|
|
|
|
if "tblname" not in crud:
|
|
errors.append("Missing 'tblname' root key")
|
|
|
|
if "params" not in crud:
|
|
errors.append("Missing 'params' section")
|
|
else:
|
|
params = crud["params"]
|
|
if "editable" not in params:
|
|
errors.append("Missing 'editable' section in params")
|
|
else:
|
|
editable = params["editable"]
|
|
for key in ("new_data_url", "update_data_url", "delete_data_url"):
|
|
if key not in editable:
|
|
errors.append(f"Missing '{key}' in editable")
|
|
|
|
return errors
|