f000a68c by Ean Schuessler

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
1 parent e781740e
...@@ -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
......