1b58daf3 by Ean Schuessler

Reduce noise in MCP responses - remove unactionable context

ARIA and compact modes now cleaner for smaller models:

- Remove service names from actions (confuses models into trying to call services)
- Rename 'describedby' to 'help' for clarity (points to moqui_get_help tool)
- Remove wiki:screen references (not useful, wiki content already in response)
- Fix grid row refs to use specific IDs (productFeatureId, toProductId) instead
  of repeating generic productId for all rows
- Filter navigation links: remove cross-app links, remove action URLs with
  encoded timestamps (fromDate/thruDate parameters)
- Remove dropdown options counts and random examples - just indicate type,
  use moqui_get_screen_details for actual options

This significantly reduces context volume and eliminates patterns that
caused smaller models to hallucinate service calls.
1 parent b52c9ca0
...@@ -841,26 +841,12 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -841,26 +841,12 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
841 // Add required attribute 841 // Add required attribute
842 if (field.required) node.required = true 842 if (field.required) node.required = true
843 843
844 // For dropdowns, show option count and examples 844 // For dropdowns, just indicate type - use get_screen_details for options
845 if (field.type == "dropdown") { 845 if (field.type == "dropdown") {
846 def optionCount = field.options?.size() ?: 0 846 node.description = "dropdown - use moqui_get_screen_details for options"
847 if (field.totalOptions) optionCount = field.totalOptions 847 // Check for dynamic options (autocomplete)
848 if (optionCount > 0) {
849 node.options = optionCount
850 // Show first few example values
851 if (field.options instanceof List && field.options.size() > 0) {
852 node.examples = field.options.take(3).collect { opt ->
853 opt instanceof Map ? (opt.value ?: opt.label) : opt.toString()
854 }
855 }
856 if (field.optionsTruncated) {
857 node.description = "Use moqui_get_screen_details for all ${optionCount} options"
858 }
859 }
860 // Check for dynamic options
861 if (field.dynamicOptions) { 848 if (field.dynamicOptions) {
862 node.autocomplete = true 849 node.description = "autocomplete - type to search"
863 node.description = "Type to search, options load dynamically"
864 } 850 }
865 } 851 }
866 852
...@@ -952,9 +938,17 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -952,9 +938,17 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
952 gridNode.children = [] 938 gridNode.children = []
953 listData.each { row -> 939 listData.each { row ->
954 def rowNode = [role: "row"] 940 def rowNode = [role: "row"]
955 // Extract key identifying info 941 // Extract key identifying info - prefer specific IDs over generic productId
956 def id = row.pseudoId ?: row.partyId ?: row.productId ?: row.id 942 // For features: productFeatureId, for assocs: toProductId, etc.
957 def name = row.combinedName ?: row.name ?: row.productName 943 def id = row.pseudoId ?: row.toProductId ?: row.productFeatureId ?:
944 row.partyId ?: row.orderId ?: row.id
945 // Avoid using productId as ref when it's the same for all rows (e.g., feature list)
946 if (!id && row.productId) {
947 // Only use productId if there's no better identifier
948 id = row.productId
949 }
950 def name = row.combinedName ?: row.name ?: row.productName ?:
951 row.description ?: row.abbrev
958 if (id) rowNode.ref = id 952 if (id) rowNode.ref = id
959 if (name) rowNode.name = name 953 if (name) rowNode.name = name
960 if (id && name && id != name) rowNode.description = id 954 if (id && name && id != name) rowNode.description = id
...@@ -965,19 +959,42 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -965,19 +959,42 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
965 children << gridNode 959 children << gridNode
966 } 960 }
967 961
968 // Process navigation links (no truncation) 962 // Process navigation links - filter out noise
969 def navLinks = semanticState.data?.links?.findAll { it.type == "navigation" } 963 def navLinks = semanticState.data?.links?.findAll { it.type == "navigation" }
970 if (navLinks && navLinks.size() > 0) { 964 if (navLinks && navLinks.size() > 0) {
965 // Determine current app from target path
966 def currentApp = targetScreenPath?.split('/')?.find { it.startsWith('Popc') || it.startsWith('marble') || it.startsWith('hm') || it.startsWith('my') || it.startsWith('system') || it.startsWith('tools') }
967
968 def filteredLinks = navLinks.findAll { link ->
969 def path = link.path?.toString() ?: ""
970
971 // Skip links with encoded timestamps (delete/update action URLs) - these are action URLs, not navigation
972 if (path.contains("fromDate=") || path.contains("thruDate=")) {
973 return false
974 }
975
976 // Skip cross-app navigation links (apps/marble, apps/hm, etc.) unless they're current app
977 if (path.startsWith("apps/")) {
978 def linkApp = path.split('/')[1]
979 // Keep if same app or if it's a my/User link (global nav)
980 return linkApp == currentApp || path.startsWith("apps/my/")
981 }
982
983 return true
984 }
985
986 if (filteredLinks.size() > 0) {
971 def navNode = [ 987 def navNode = [
972 role: "navigation", 988 role: "navigation",
973 name: "Links", 989 name: "Links",
974 children: navLinks.collect { link -> 990 children: filteredLinks.collect { link ->
975 def linkNode = [role: "link", name: link.text, ref: link.path] 991 def linkNode = [role: "link", name: link.text, ref: link.path]
976 linkNode 992 linkNode
977 } 993 }
978 ] 994 ]
979 children << navNode 995 children << navNode
980 } 996 }
997 }
981 998
982 // Process ALL actions as buttons (unified - no separate transitions/actions, no truncation) 999 // Process ALL actions as buttons (unified - no separate transitions/actions, no truncation)
983 def allActions = semanticState.actions ?: [] 1000 def allActions = semanticState.actions ?: []
...@@ -992,15 +1009,13 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -992,15 +1009,13 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
992 name: action.name, 1009 name: action.name,
993 ref: action.name 1010 ref: action.name
994 ] 1011 ]
995 // Add description based on service or action type 1012 // Add description based on action type (no service names - they confuse models)
996 if (action.service) { 1013 if (action.service) {
997 btnNode.description = actionDescription(action.name, action.service) 1014 btnNode.description = actionDescription(action.name, action.service)
998 btnNode.service = action.service 1015 // Add help reference for moqui_get_help tool
999 // Add describedby for service documentation
1000 // e.g., "mantle.product.ProductServices.create#ProductFeature" -> "wiki:service:ProductServices.create#ProductFeature"
1001 def serviceParts = action.service.split('\\.') 1016 def serviceParts = action.service.split('\\.')
1002 if (serviceParts.length > 0) { 1017 if (serviceParts.length > 0) {
1003 btnNode.describedby = "wiki:service:${serviceParts[-1]}" 1018 btnNode.help = "wiki:service:${serviceParts[-1]}"
1004 } 1019 }
1005 } else if (action.type == "screen-transition") { 1020 } else if (action.type == "screen-transition") {
1006 btnNode.description = "Navigate" 1021 btnNode.description = "Navigate"
...@@ -1021,12 +1036,8 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -1021,12 +1036,8 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
1021 children: children 1036 children: children
1022 ] 1037 ]
1023 1038
1024 // Add describedby reference if wiki instructions exist for this screen 1039 // Note: Removed wiki:screen references - models can't usefully act on them
1025 // This follows ARIA pattern: describedby points to extended documentation 1040 // Wiki instructions are already included in the response when available
1026 // Use full screen path since that's how WikiPage.pagePath is stored
1027 if (targetScreenPath) {
1028 mainNode.describedby = "wiki:screen:${targetScreenPath}"
1029 }
1030 1041
1031 return mainNode 1042 return mainNode
1032 } 1043 }
...@@ -1087,20 +1098,11 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> ...@@ -1087,20 +1098,11 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
1087 def displayName = field.title ?: field.name 1098 def displayName = field.title ?: field.name
1088 1099
1089 if (field.type == "dropdown") { 1100 if (field.type == "dropdown") {
1090 def optionCount = field.totalOptions ?: field.options?.size() ?: 0 1101 // Just indicate it's a dropdown - use get_screen_details for actual options
1091 if (optionCount > 0) { 1102 fieldInfo = [(fieldName): [type: "dropdown"]]
1092 fieldInfo = [(fieldName): [type: "dropdown", options: optionCount]]
1093 // Include first few options as examples
1094 if (field.options && field.options.size() > 0) {
1095 def examples = field.options.take(3).collect { it.value }
1096 fieldInfo[fieldName].examples = examples
1097 }
1098 if (field.dynamicOptions) { 1103 if (field.dynamicOptions) {
1099 fieldInfo[fieldName].autocomplete = true 1104 fieldInfo[fieldName].autocomplete = true
1100 } 1105 }
1101 } else {
1102 fieldInfo = [(fieldName): [type: "dropdown"]]
1103 }
1104 if (displayName != fieldName) fieldInfo[fieldName].label = displayName 1106 if (displayName != fieldName) fieldInfo[fieldName].label = displayName
1105 } else { 1107 } else {
1106 // Simple field - just use name, add label if different 1108 // Simple field - just use name, add label if different
...@@ -1134,9 +1136,7 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> ...@@ -1134,9 +1136,7 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
1134 } 1136 }
1135 if (submitAction) { 1137 if (submitAction) {
1136 formInfo.submit = submitAction.name 1138 formInfo.submit = submitAction.name
1137 if (submitAction.service) { 1139 // Note: Removed service name - it confuses models into trying to call services directly
1138 formInfo.service = submitAction.service
1139 }
1140 } 1140 }
1141 1141
1142 forms[formName] = formInfo 1142 forms[formName] = formInfo
...@@ -1163,9 +1163,14 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> ...@@ -1163,9 +1163,14 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
1163 gridInfo.rows = listData.take(10).collect { row -> 1163 gridInfo.rows = listData.take(10).collect { row ->
1164 def rowInfo = [:] 1164 def rowInfo = [:]
1165 1165
1166 // Get identifying info 1166 // Get identifying info - prefer specific IDs over generic productId
1167 def id = row.pseudoId ?: row.partyId ?: row.productId ?: row.orderId ?: row.communicationEventId ?: row.id 1167 // For features: productFeatureId, for assocs: toProductId, etc.
1168 def name = row.combinedName ?: row.productName ?: row.organizationName ?: row.subject ?: row.name 1168 def id = row.pseudoId ?: row.toProductId ?: row.productFeatureId ?:
1169 row.partyId ?: row.orderId ?: row.communicationEventId ?: row.id
1170 // Avoid using productId as id when it's the same for all rows (e.g., feature list)
1171 if (!id && row.productId) id = row.productId
1172 def name = row.combinedName ?: row.productName ?: row.organizationName ?:
1173 row.subject ?: row.name ?: row.description ?: row.abbrev
1169 1174
1170 if (id) rowInfo.id = id 1175 if (id) rowInfo.id = id
1171 if (name && name != id) rowInfo.name = name 1176 if (name && name != id) rowInfo.name = name
...@@ -1233,10 +1238,16 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> ...@@ -1233,10 +1238,16 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
1233 } 1238 }
1234 if (grids) result.grids = grids 1239 if (grids) result.grids = grids
1235 1240
1236 // Actions - service actions with parameter hints 1241 // Actions - just action names with help references (no service names)
1237 def actionMap = [:] 1242 def actionMap = [:]
1238 actions.findAll { it.type == "service-action" && it.service }.each { action -> 1243 actions.findAll { it.type == "service-action" && it.service }.each { action ->
1239 def actionInfo = [service: action.service] 1244 def actionInfo = [:]
1245
1246 // Add help reference for moqui_get_help tool
1247 def serviceParts = action.service.split('\\.')
1248 if (serviceParts.length > 0) {
1249 actionInfo.help = "wiki:service:${serviceParts[-1]}"
1250 }
1240 1251
1241 // Find form that uses this action to get parameter hints 1252 // Find form that uses this action to get parameter hints
1242 def matchingForm = forms.find { k, v -> v.submit == action.name } 1253 def matchingForm = forms.find { k, v -> v.submit == action.name }
...@@ -1251,7 +1262,7 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> ...@@ -1251,7 +1262,7 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
1251 if (requiredFields) actionInfo.required = requiredFields 1262 if (requiredFields) actionInfo.required = requiredFields
1252 } 1263 }
1253 1264
1254 actionMap[action.name] = actionInfo 1265 if (actionInfo) actionMap[action.name] = actionInfo
1255 } 1266 }
1256 if (actionMap) result.actions = actionMap 1267 if (actionMap) result.actions = actionMap
1257 1268
......