Expand type system for semantic state extraction.
- Add field types: radio, display, link, file-upload, hidden - Add link types: action, delete, external, button - Add action types: service-action, form-action, screen-transition, delete-action - Enhance parameter metadata with type, required, defaultValue - Classify actions automatically by service/name patterns - Update narrative builder to describe action/link types - Defensive parameter extraction with reflection fallbacks
Showing
3 changed files
with
181 additions
and
21 deletions
| ... | @@ -79,7 +79,11 @@ | ... | @@ -79,7 +79,11 @@ |
| 79 | 79 | ||
| 80 | [${linkText}](${slashPath})<#t> | 80 | [${linkText}](${slashPath})<#t> |
| 81 | <#if mcpSemanticData??> | 81 | <#if mcpSemanticData??> |
| 82 | <#assign dummy = ec.resource.expression("mcpSemanticData.links.add([text: '" + (linkText!"")?js_string + "', path: '" + (slashPath!"")?js_string + "', type: 'navigation'])", "")!> | 82 | <#assign linkType = "navigation"> |
| 83 | <#if slashPath?starts_with("#")><#assign linkType = "action"></#if> | ||
| 84 | <#if slashPath?starts_with("http://") || slashPath?starts_with("https://")><#assign linkType = "external"></#if> | ||
| 85 | <#if .node["@icon"]?has_content && .node["@icon"]?contains("trash")><#assign linkType = "delete"></#if> | ||
| 86 | <#assign dummy = ec.resource.expression("mcpSemanticData.links.add([text: '" + (linkText!"")?js_string + "', path: '" + (slashPath!"")?js_string + "', type: '" + linkType + "'])", "")!> | ||
| 83 | </#if> | 87 | </#if> |
| 84 | </#if> | 88 | </#if> |
| 85 | </#macro> | 89 | </#macro> |
| ... | @@ -122,7 +126,12 @@ | ... | @@ -122,7 +126,12 @@ |
| 122 | <#if fieldSubNode["text-area"]?has_content><#assign fieldMeta = fieldMeta + {"type": "textarea"}></#if> | 126 | <#if fieldSubNode["text-area"]?has_content><#assign fieldMeta = fieldMeta + {"type": "textarea"}></#if> |
| 123 | <#if fieldSubNode["drop-down"]?has_content><#assign fieldMeta = fieldMeta + {"type": "dropdown"}></#if> | 127 | <#if fieldSubNode["drop-down"]?has_content><#assign fieldMeta = fieldMeta + {"type": "dropdown"}></#if> |
| 124 | <#if fieldSubNode["check"]?has_content><#assign fieldMeta = fieldMeta + {"type": "checkbox"}></#if> | 128 | <#if fieldSubNode["check"]?has_content><#assign fieldMeta = fieldMeta + {"type": "checkbox"}></#if> |
| 129 | <#if fieldSubNode["radio"]?has_content><#assign fieldMeta = fieldMeta + {"type": "radio"}></#if> | ||
| 125 | <#if fieldSubNode["date-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "date"}></#if> | 130 | <#if fieldSubNode["date-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "date"}></#if> |
| 131 | <#if fieldSubNode["display"]?has_content || fieldSubNode["display-entity"]?has_content><#assign fieldMeta = fieldMeta + {"type": "display"}></#if> | ||
| 132 | <#if fieldSubNode["link"]?has_content><#assign fieldMeta = fieldMeta + {"type": "link"}></#if> | ||
| 133 | <#if fieldSubNode["file"]?has_content><#assign fieldMeta = fieldMeta + {"type": "file-upload"}></#if> | ||
| 134 | <#if fieldSubNode["hidden"]?has_content><#assign fieldMeta = fieldMeta + {"type": "hidden"}></#if> | ||
| 126 | 135 | ||
| 127 | <#assign fieldMetaList = fieldMetaList + [fieldMeta]> | 136 | <#assign fieldMetaList = fieldMetaList + [fieldMeta]> |
| 128 | </#if> | 137 | </#if> | ... | ... |
| ... | @@ -891,22 +891,94 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) | ... | @@ -891,22 +891,94 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) |
| 891 | } | 891 | } |
| 892 | } | 892 | } |
| 893 | 893 | ||
| 894 | // Extract transitions (Actions) with metadata (from screen definition, not macros) | 894 | // Extract transitions (Actions) with type classification and metadata |
| 895 | semanticState.actions = [] | 895 | semanticState.actions = [] |
| 896 | finalScreenDef.getAllTransitions().each { trans -> | 896 | finalScreenDef.getAllTransitions().each { trans -> |
| 897 | def transName = trans.getName() | ||
| 898 | def service = trans.getSingleServiceName() | ||
| 899 | |||
| 900 | // Classify action type | ||
| 901 | def actionType = "screen-transition" | ||
| 902 | def transNameLower = transName?.toString()?.toLowerCase() ?: '' | ||
| 903 | |||
| 904 | if (service) { | ||
| 905 | actionType = "service-action" | ||
| 906 | } else if (transNameLower.contains('delete')) { | ||
| 907 | actionType = "delete-action" | ||
| 908 | } else if (transNameLower.startsWith('form') || transNameLower == 'find' || transNameLower == 'search') { | ||
| 909 | actionType = "form-action" | ||
| 910 | } | ||
| 911 | |||
| 897 | def actionInfo = [ | 912 | def actionInfo = [ |
| 898 | name: trans.getName(), | 913 | name: transName, |
| 899 | service: trans.getSingleServiceName() | 914 | service: service, |
| 915 | type: actionType | ||
| 900 | ] | 916 | ] |
| 917 | |||
| 901 | semanticState.actions << actionInfo | 918 | semanticState.actions << actionInfo |
| 902 | } | 919 | } |
| 903 | 920 | ||
| 904 | // 3. Extract parameters that are currently set | 921 | // 3. Extract parameters with metadata |
| 905 | semanticState.parameters = [:] | 922 | semanticState.parameters = [:] |
| 906 | if (finalScreenDef.parameterByName) { | 923 | if (finalScreenDef.parameterByName) { |
| 907 | finalScreenDef.parameterByName.each { name, param -> | 924 | finalScreenDef.parameterByName.each { name, param -> |
| 908 | def value = postContext.get(name) ?: parameters?.get(name) | 925 | def value = postContext.get(name) ?: parameters?.get(name) |
| 909 | if (value != null) semanticState.parameters[name] = serializeMoquiObject(value, 0, isTerse) | 926 | |
| 927 | // Build parameter metadata | ||
| 928 | def paramInfo = [:] | ||
| 929 | |||
| 930 | // Add value if exists | ||
| 931 | if (value != null) { | ||
| 932 | paramInfo.value = serializeMoquiObject(value, 0, isTerse) | ||
| 933 | } | ||
| 934 | |||
| 935 | // Extract parameter type - try multiple approaches | ||
| 936 | def type = "string" | ||
| 937 | try { | ||
| 938 | // Try to get type via reflection or known properties | ||
| 939 | if (param.hasProperty('type')) { | ||
| 940 | def typeObj = param.type | ||
| 941 | if (typeObj != null) type = typeObj.toString().toLowerCase() | ||
| 942 | } else if (param.hasProperty('parameterType')) { | ||
| 943 | def typeObj = param.parameterType | ||
| 944 | if (typeObj != null) type = typeObj.toString().toLowerCase() | ||
| 945 | } | ||
| 946 | } catch (Exception e) { | ||
| 947 | // Fall back to type inference from value | ||
| 948 | } | ||
| 949 | |||
| 950 | // Infer type from value if type couldn't be extracted | ||
| 951 | if (type == "string" && value != null) { | ||
| 952 | if (value instanceof Number) { | ||
| 953 | type = (value instanceof Integer || value instanceof Long) ? "long" : "decimal" | ||
| 954 | } else if (value instanceof Boolean) { | ||
| 955 | type = "boolean" | ||
| 956 | } else if (value instanceof Collection || value instanceof Map) { | ||
| 957 | type = (value instanceof Collection) ? "list" : "map" | ||
| 958 | } | ||
| 959 | } | ||
| 960 | paramInfo.type = type | ||
| 961 | |||
| 962 | // Extract required flag - defensive check | ||
| 963 | paramInfo.required = false | ||
| 964 | try { | ||
| 965 | if (param.hasProperty('required')) { | ||
| 966 | paramInfo.required = (param.required == true) | ||
| 967 | } | ||
| 968 | } catch (Exception e) { | ||
| 969 | // Skip if property doesn't exist | ||
| 970 | } | ||
| 971 | |||
| 972 | // Extract default value - defensive check | ||
| 973 | try { | ||
| 974 | if (param.hasProperty('defaultValue') && param.defaultValue != null) { | ||
| 975 | paramInfo.defaultValue = param.defaultValue.toString() | ||
| 976 | } | ||
| 977 | } catch (Exception e) { | ||
| 978 | // Skip if property doesn't exist | ||
| 979 | } | ||
| 980 | |||
| 981 | semanticState.parameters[name] = paramInfo | ||
| 910 | } | 982 | } |
| 911 | } | 983 | } |
| 912 | 984 | ... | ... |
| ... | @@ -105,23 +105,28 @@ class UiNarrativeBuilder { | ... | @@ -105,23 +105,28 @@ class UiNarrativeBuilder { |
| 105 | 105 | ||
| 106 | List<String> describeActions(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath, boolean isTerse) { | 106 | List<String> describeActions(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath, boolean isTerse) { |
| 107 | def actions = [] | 107 | def actions = [] |
| 108 | 108 | ||
| 109 | def transitions = semanticState?.actions | 109 | def transitions = semanticState?.actions |
| 110 | if (transitions) { | 110 | if (transitions) { |
| 111 | transitions.each { trans -> | 111 | transitions.each { trans -> |
| 112 | def transName = trans.name?.toString() | 112 | def transName = trans.name?.toString() |
| 113 | def service = trans.service?.toString() | 113 | def service = trans.service?.toString() |
| 114 | 114 | def actionType = classifyActionType(trans, semanticState) | |
| 115 | |||
| 115 | if (transName) { | 116 | if (transName) { |
| 116 | if (service) { | 117 | if (actionType == 'service-action' && service) { |
| 118 | actions << buildServiceActionNarrative(transName, service, currentPath, semanticState) | ||
| 119 | } else if (actionType == 'form-action') { | ||
| 120 | actions << buildFormActionNarrative(transName, currentPath, semanticState) | ||
| 121 | } else if (actionType == 'screen-transition') { | ||
| 122 | actions << buildScreenTransitionNarrative(transName, currentPath, semanticState) | ||
| 123 | } else if (service) { | ||
| 117 | actions << buildServiceActionNarrative(transName, service, currentPath, semanticState) | 124 | actions << buildServiceActionNarrative(transName, service, currentPath, semanticState) |
| 118 | } else if (transName.toLowerCase().startsWith('create') || transName.toLowerCase().startsWith('update')) { | ||
| 119 | actions << buildTransitionActionNarrative(transName, currentPath, semanticState) | ||
| 120 | } | 125 | } |
| 121 | } | 126 | } |
| 122 | } | 127 | } |
| 123 | } | 128 | } |
| 124 | 129 | ||
| 125 | def forms = semanticState?.data | 130 | def forms = semanticState?.data |
| 126 | if (forms) { | 131 | if (forms) { |
| 127 | def formNames = forms.keySet().findAll { k -> k.contains('Form') || k.contains('form') } | 132 | def formNames = forms.keySet().findAll { k -> k.contains('Form') || k.contains('form') } |
| ... | @@ -129,14 +134,33 @@ class UiNarrativeBuilder { | ... | @@ -129,14 +134,33 @@ class UiNarrativeBuilder { |
| 129 | actions << buildFormSubmitNarrative(formName, currentPath, semanticState) | 134 | actions << buildFormSubmitNarrative(formName, currentPath, semanticState) |
| 130 | } | 135 | } |
| 131 | } | 136 | } |
| 132 | 137 | ||
| 133 | if (actions.isEmpty()) { | 138 | if (actions.isEmpty()) { |
| 134 | actions << "No explicit actions available on this screen. Use navigation links to explore." | 139 | actions << "No explicit actions available on this screen. Use navigation links to explore." |
| 135 | } | 140 | } |
| 136 | 141 | ||
| 137 | return actions | 142 | return actions |
| 138 | } | 143 | } |
| 139 | 144 | ||
| 145 | private String classifyActionType(def trans, Map semanticState) { | ||
| 146 | def transName = trans.name?.toString()?.toLowerCase() ?: '' | ||
| 147 | def service = trans.service?.toString() | ||
| 148 | |||
| 149 | // Delete actions are special | ||
| 150 | if (transName.contains('delete')) return 'delete-action' | ||
| 151 | |||
| 152 | // Service actions | ||
| 153 | if (service) return 'service-action' | ||
| 154 | |||
| 155 | // Form actions (built-in) | ||
| 156 | if (transName.startsWith('form') || transName == 'find' || transName == 'search') { | ||
| 157 | return 'form-action' | ||
| 158 | } | ||
| 159 | |||
| 160 | // Screen transitions | ||
| 161 | return 'screen-transition' | ||
| 162 | } | ||
| 163 | |||
| 140 | List<String> describeLinks(Map<String, Object> semanticState, String currentPath, boolean isTerse) { | 164 | List<String> describeLinks(Map<String, Object> semanticState, String currentPath, boolean isTerse) { |
| 141 | def navigation = [] | 165 | def navigation = [] |
| 142 | 166 | ||
| ... | @@ -151,11 +175,17 @@ class UiNarrativeBuilder { | ... | @@ -151,11 +175,17 @@ class UiNarrativeBuilder { |
| 151 | def linkType = link.type?.toString() ?: 'navigation' | 175 | def linkType = link.type?.toString() ?: 'navigation' |
| 152 | 176 | ||
| 153 | if (linkPath) { | 177 | if (linkPath) { |
| 154 | if (linkPath.startsWith('#')) { | 178 | if (linkType == 'action' || linkPath.startsWith('#')) { |
| 155 | def actionName = linkPath.substring(1) | 179 | def actionName = linkPath.startsWith('#') ? linkPath.substring(1) : linkPath |
| 156 | navigation << "To ${linkText.toLowerCase()}, use the '${actionName}' action (see actions section)." | 180 | navigation << "To ${linkText.toLowerCase()}, use action '${actionName}' (type: action)." |
| 181 | } else if (linkType == 'delete') { | ||
| 182 | navigation << "To ${linkText.toLowerCase() ?: 'delete'}, call moqui_render_screen(path='${linkPath}') (type: delete)." | ||
| 183 | } else if (linkType == 'external') { | ||
| 184 | navigation << "To ${linkText.toLowerCase() ?: 'external link'}, visit ${linkPath} (type: external)." | ||
| 185 | } else if (linkType == 'button') { | ||
| 186 | navigation << "To ${linkText.toLowerCase()}, click button action (type: button)." | ||
| 157 | } else { | 187 | } else { |
| 158 | navigation << "To ${linkText.toLowerCase()}, call moqui_render_screen(path='${linkPath}')." | 188 | navigation << "To ${linkText.toLowerCase()}, call moqui_render_screen(path='${linkPath}') (type: navigation)." |
| 159 | } | 189 | } |
| 160 | } | 190 | } |
| 161 | } | 191 | } |
| ... | @@ -164,6 +194,19 @@ class UiNarrativeBuilder { | ... | @@ -164,6 +194,19 @@ class UiNarrativeBuilder { |
| 164 | if (navigation.isEmpty()) { | 194 | if (navigation.isEmpty()) { |
| 165 | def parentPath = getParentPath(currentPath) | 195 | def parentPath = getParentPath(currentPath) |
| 166 | if (parentPath) { | 196 | if (parentPath) { |
| 197 | navigation << "To go back, call moqui_render_screen(path='${parentPath}')." | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | return navigation | ||
| 202 | } | ||
| 203 | } | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | if (navigation.isEmpty()) { | ||
| 208 | def parentPath = getParentPath(currentPath) | ||
| 209 | if (parentPath) { | ||
| 167 | navigation << "To go back, call moqui_browse_screens(path='${parentPath}')." | 210 | navigation << "To go back, call moqui_browse_screens(path='${parentPath}')." |
| 168 | } | 211 | } |
| 169 | } | 212 | } |
| ... | @@ -250,11 +293,47 @@ class UiNarrativeBuilder { | ... | @@ -250,11 +293,47 @@ class UiNarrativeBuilder { |
| 250 | private String buildFormSubmitNarrative(String formName, String currentPath, Map semanticState) { | 293 | private String buildFormSubmitNarrative(String formName, String currentPath, Map semanticState) { |
| 251 | def formFriendly = formFriendlyName(formName) | 294 | def formFriendly = formFriendlyName(formName) |
| 252 | def params = extractFormParameters(formName, semanticState) | 295 | def params = extractFormParameters(formName, semanticState) |
| 253 | 296 | ||
| 254 | def sb = new StringBuilder() | 297 | def sb = new StringBuilder() |
| 255 | sb.append("To submit ${formFriendly.toLowerCase()}, call moqui_render_screen(path='${currentPath}', parameters={${params}}). ") | 298 | sb.append("To submit ${formFriendly.toLowerCase()}, call moqui_render_screen(path='${currentPath}', parameters={${params}}). ") |
| 256 | sb.append("This filters or processes the ${formFriendly.toLowerCase()} form.") | 299 | sb.append("This filters or processes ${formFriendly.toLowerCase()} form.") |
| 257 | 300 | ||
| 301 | return sb.toString() | ||
| 302 | } | ||
| 303 | |||
| 304 | private String buildFormActionNarrative(String actionName, String currentPath, Map semanticState) { | ||
| 305 | def actionLower = actionName.toLowerCase() | ||
| 306 | def verb = actionLower.startsWith('find') ? 'find' : actionLower.startsWith('search') ? 'search' : 'filter' | ||
| 307 | |||
| 308 | def params = extractTransitionParameters(actionName, semanticState) | ||
| 309 | |||
| 310 | def sb = new StringBuilder() | ||
| 311 | sb.append("To ${verb} ") | ||
| 312 | sb.append("results, call moqui_render_screen(path='${currentPath}', action='${actionName}'") | ||
| 313 | |||
| 314 | if (params) { | ||
| 315 | sb.append(", parameters={${params}}") | ||
| 316 | } | ||
| 317 | |||
| 318 | sb.append("). ") | ||
| 319 | sb.append("This is a built-in form action (type: form-action).") | ||
| 320 | |||
| 321 | return sb.toString() | ||
| 322 | } | ||
| 323 | |||
| 324 | private String buildScreenTransitionNarrative(String actionName, String currentPath, Map semanticState) { | ||
| 325 | def params = extractTransitionParameters(actionName, semanticState) | ||
| 326 | |||
| 327 | def sb = new StringBuilder() | ||
| 328 | sb.append("To execute '${actionName}', call moqui_render_screen(path='${currentPath}', action='${actionName}'") | ||
| 329 | |||
| 330 | if (params) { | ||
| 331 | sb.append(", parameters={${params}}") | ||
| 332 | } | ||
| 333 | |||
| 334 | sb.append("). ") | ||
| 335 | sb.append("This triggers a screen transition (type: screen-transition).") | ||
| 336 | |||
| 258 | return sb.toString() | 337 | return sb.toString() |
| 259 | } | 338 | } |
| 260 | 339 | ... | ... |
-
Please register or sign in to post a comment