123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- using System.IO;
- using UnityEngine;
- using UnityEditor;
- using System.Linq;
- using System.Text;
- using System.Reflection;
- using System.Collections.Generic;
- using IntelligentProjectAnalyzer.Editor.Graphing;
- namespace LLM.Editor.Analysis
- {
- /// <summary>
- /// Responsible for gathering and serializing context about staged assets
- /// into a detailed, human-readable, and referential format for the LLM.
- /// </summary>
- public static class ContextBuilder
- {
- /// <summary>
- /// Builds a detailed Tier 1 summary of the provided objects, including a pruned hierarchy for GameObjects.
- /// </summary>
- /// <param name="stagedObjects">The list of objects from the UI's staging area.</param>
- /// <returns>A formatted string summarizing the context for the LLM.</returns>
- public static string BuildTier1Summary(List<Object> stagedObjects)
- {
- if (stagedObjects == null || stagedObjects.Count == 0 || stagedObjects.All(o => o == null))
- {
- return "No context provided.";
- }
- var summaryBuilder = new StringBuilder();
-
- for (var i = 0; i < stagedObjects.Count; i++)
- {
- var obj = stagedObjects[i];
- if (obj == null) continue;
- var stableId = GetStableIdForObject(obj);
- summaryBuilder.AppendLine($"\n## Subject {i + 1}: {obj.name} (ID: {stableId})");
- summaryBuilder.AppendLine($"- Type: {obj.GetType().FullName}");
- switch (obj)
- {
- case GameObject go:
- AppendGameObjectContext(go, summaryBuilder);
- break;
- case MonoScript script:
- AppendScriptContext(script, summaryBuilder);
- break;
- case ScriptableObject so:
- AppendScriptableObjectContext(so, summaryBuilder);
- break;
- }
- }
-
- return summaryBuilder.ToString();
- }
- private static void AppendGameObjectContext(GameObject go, StringBuilder builder)
- {
- builder.AppendLine($"- Tag: {go.tag}");
- builder.AppendLine($"- Layer: {LayerMask.LayerToName(go.layer)}");
- var prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(go);
- if (!string.IsNullOrEmpty(prefabPath))
- {
- builder.AppendLine($"- Prefab Source: {prefabPath}");
- }
- builder.AppendLine("- Hierarchy (pruned to show nodes with custom scripts):");
- AppendHierarchyNode(go.transform, builder, " ");
- }
- /// <summary>
- /// Recursively builds a string representation of the hierarchy, intelligently pruning branches
- /// that do not contain any custom user scripts to keep the context concise.
- /// </summary>
- private static void AppendHierarchyNode(Transform transform, StringBuilder builder, string indent)
- {
- // Always include the root of the hierarchy being analyzed.
- builder.AppendLine($"{indent}- {transform.name} (ID: {transform.gameObject.GetInstanceID()})");
- // List components on the current GameObject, prioritizing custom scripts.
- var components = transform.GetComponents<Component>().Where(c => c).ToList();
- var customScripts = components.Where(IsCustomScript).ToList();
- var otherComponents = components.Except(customScripts);
- foreach (var script in customScripts)
- {
- var monoScript = MonoScript.FromMonoBehaviour(script as MonoBehaviour);
- var scriptGuid = monoScript ? AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(monoScript)) : "N/A";
- builder.AppendLine($"{indent} - Component: {script.GetType().FullName} (Custom Script, GUID: {scriptGuid})");
- }
- foreach (var component in otherComponents)
- {
- builder.AppendLine($"{indent} - Component: {component.GetType().FullName}");
- }
- // --- Pruning Logic ---
- var childrenWithScripts = new List<Transform>();
- var childrenWithoutScripts = new List<Transform>();
- foreach (Transform child in transform)
- {
- if (HasCustomScriptInChildren(child))
- {
- childrenWithScripts.Add(child);
- }
- else
- {
- childrenWithoutScripts.Add(child);
- }
- }
- // Recurse into children that have scripts.
- foreach (var child in childrenWithScripts)
- {
- AppendHierarchyNode(child, builder, indent + " ");
- }
- // Summarize the pruned children.
- if (childrenWithoutScripts.Count > 0)
- {
- builder.AppendLine($"{indent} [+ {childrenWithoutScripts.Count} other child object(s) with no custom scripts]");
- }
- }
- private static void AppendScriptContext(MonoScript script, StringBuilder builder)
- {
- var scriptClass = script.GetClass();
- if (scriptClass != null)
- {
- if (scriptClass.BaseType != null && scriptClass.BaseType != typeof(MonoBehaviour))
- {
- builder.AppendLine($"- Inherits from: {scriptClass.BaseType.FullName}");
- }
- var interfaces = scriptClass.GetInterfaces();
- if (interfaces.Length > 0)
- {
- builder.AppendLine($"- Implements: {string.Join(", ", interfaces.Select(i => i.Name))}");
- }
- var methods = scriptClass.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
- .Where(m => !m.IsSpecialName); // Exclude property getters/setters, etc.
- var methodInfos = methods as MethodInfo[] ?? methods.ToArray();
- if (methodInfos.Any())
- {
- builder.AppendLine("- Public API:");
- foreach (var method in methodInfos)
- {
- var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"));
- builder.AppendLine($" - {method.ReturnType.Name} {method.Name}({parameters})");
- }
- }
- }
- var scriptPath = AssetDatabase.GetAssetPath(script);
- if (!File.Exists(scriptPath)) return;
- builder.AppendLine("- Script Content:");
- builder.AppendLine("```csharp");
- builder.AppendLine(File.ReadAllText(scriptPath));
- builder.AppendLine("```");
- // Append dependency graph if available
- var guid = AssetDatabase.AssetPathToGUID(scriptPath);
- _ = GraphJsonExporter.TryAppendHumanReadableGraph(guid, builder);
- }
- private static void AppendScriptableObjectContext(ScriptableObject so, StringBuilder builder)
- {
- builder.AppendLine("- Note: This is a ScriptableObject data asset.");
- var fields = so.GetType()
- .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- .Where(field => field.IsPublic || field.GetCustomAttribute<SerializeField>() != null);
- var fieldInfos = fields as FieldInfo[] ?? fields.ToArray();
- if (!fieldInfos.Any()) return;
-
- builder.AppendLine(" - Public Fields:");
- foreach (var field in fieldInfos)
- {
- builder.AppendLine($" - {field.Name}: {field.GetValue(so)}");
- }
- }
-
- private static string GetStableIdForObject(Object obj)
- {
- if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _)) return obj.GetInstanceID().ToString();
- if (!string.IsNullOrEmpty(guid) && guid != "00000000000000000000000000000000")
- {
- return guid;
- }
- return obj.GetInstanceID().ToString();
- }
- /// <summary>
- /// Checks if a component is a user-defined script (i.e., part of the Assets folder).
- /// </summary>
- private static bool IsCustomScript(Component component)
- {
- if (!component || component is not MonoBehaviour monoBehaviour) return false;
-
- var script = MonoScript.FromMonoBehaviour(monoBehaviour);
- if (!script) return false;
-
- var path = AssetDatabase.GetAssetPath(script);
- return !string.IsNullOrEmpty(path) && path.StartsWith("Assets/");
- }
- /// <summary>
- /// Recursively checks if a transform or any of its descendants contain a custom script.
- /// </summary>
- private static bool HasCustomScriptInChildren(Transform transform)
- {
- if (transform.GetComponents<Component>().Any(IsCustomScript))
- {
- return true;
- }
- foreach (Transform child in transform)
- {
- if (HasCustomScriptInChildren(child))
- {
- return true;
- }
- }
- return false;
- }
- }
- }
|