Files
tekton/addons/godot_ai/handlers/_property_errors.gd
T

83 lines
2.8 KiB
GDScript

@tool
class_name McpPropertyErrors
extends RefCounted
## Shared helper for building "Property not found" error messages that include
## "did you mean" suggestions and a tail of available property names. All
## handlers that validate user-supplied property names against a target Object
## (Node, Resource, …) should route through build_message() so agents get
## consistent, actionable errors on typos.
##
## Ranking combines Godot's built-in String.similarity() with a substring
## bonus so both "radus" → "radius" (edit distance) and "top" → "top_radius"
## (substring) surface naturally.
const _SIMILARITY_THRESHOLD: float = 0.4
const _SUBSTRING_BONUS: float = 0.5
const _MAX_SUGGESTIONS: int = 5
const _MAX_TAIL: int = 10
static func build_message(target: Object, bad_name: String) -> String:
if target == null:
return "Property '%s' not found" % bad_name
var class_label := _class_label(target)
var available := _available_property_names(target)
if available.is_empty():
return "Property '%s' not found on %s" % [bad_name, class_label]
var msg := "Property '%s' not found on %s" % [bad_name, class_label]
var suggestions := _rank_suggestions(bad_name, available)
if not suggestions.is_empty():
msg += ". Did you mean: %s?" % ", ".join(suggestions)
var tail_names := available.slice(0, min(_MAX_TAIL, available.size()))
msg += " (available: %s" % ", ".join(tail_names)
if available.size() > tail_names.size():
msg += ", ..."
msg += ")"
return msg
## Prefer a scripted class_name if the target has one, else the engine class.
static func _class_label(target: Object) -> String:
var scr := target.get_script()
if scr != null and scr.has_method("get_global_name"):
var gcn: String = scr.get_global_name()
if not gcn.is_empty():
return gcn
return target.get_class()
## Editor-visible properties, alphabetised, with internal/category entries dropped.
static func _available_property_names(target: Object) -> Array:
var names: Array = []
for p in target.get_property_list():
var usage: int = int(p.get("usage", 0))
if (usage & PROPERTY_USAGE_EDITOR) == 0:
continue
var name: String = p.get("name", "")
if name.is_empty() or name.begins_with("_"):
continue
names.append(name)
names.sort()
return names
static func _rank_suggestions(bad: String, available: Array) -> Array:
if bad.is_empty():
return []
var bad_lower := bad.to_lower()
var scored: Array = []
for n in available:
var score: float = bad.similarity(n)
if n.to_lower().find(bad_lower) != -1 or bad_lower.find(n.to_lower()) != -1:
score += _SUBSTRING_BONUS
if score >= _SIMILARITY_THRESHOLD:
scored.append([score, n])
scored.sort_custom(func(a, b): return a[0] > b[0])
var result: Array = []
for i in range(min(_MAX_SUGGESTIONS, scored.size())):
result.append(scored[i][1])
return result