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.
Showing
1 changed file
with
65 additions
and
54 deletions
| ... | @@ -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 | ... | ... |
-
Please register or sign in to post a comment