""" 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