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