using System.Text.Json;
using Autodesk.Revit.DB;
namespace RevitBridge.Tools
{
///
/// Model audit. Document.GetWarnings() returns FailureMessage API objects that
/// do not survive plain JSON serialization, so they are flattened here into
/// clean DTOs grouped by description, plus basic structure counts: worksharing
/// status and user worksets, phases, design options, in-place families, and
/// the total element count.
///
internal sealed class GetModelHealth : ITool
{
private const int MaxGroups = 100;
private const int MaxElementsPerGroup = 20;
public string Name => "get_model_health";
public string Label => "Get Model Health";
public string Description => "Audit the open Revit model: every document warning grouped by description (description, severity, occurrence count, failing element ids+names — the top 100 groups ordered by count, up to 20 elements each), plus structure counts: worksharing status with user worksets, phases, design options, in-place families, and the total element count. Read-only; use it to review model quality, e.g. before and after bulk edits.";
public string Tier => "advanced";
public object ParametersSchema => new { type = "object", properties = new { }, required = Array.Empty() };
public string? PromptSnippet => "Audit the model: warnings grouped by description plus workset/phase/design-option counts.";
public IReadOnlyList? PromptGuidelines => new[]
{
"Run get_model_health to review Revit warnings (grouped, with failing element ids) before and after bulk modifications.",
};
public object? Execute(JsonElement args, ToolContext context)
{
var doc = context.Document ?? throw new NoActiveDocumentException();
var warnings = doc.GetWarnings();
var groups = warnings
.GroupBy(SafeDescription, StringComparer.Ordinal)
.Select(group =>
{
var elementIds = group
.SelectMany(SafeFailingElements)
.Distinct()
.ToList();
return new Dictionary
{
["description"] = group.Key,
["severity"] = SafeSeverity(group.First()),
["count"] = group.Count(),
["elementCount"] = elementIds.Count,
["elements"] = elementIds
.Take(MaxElementsPerGroup)
.Select(id => new Dictionary
{
["id"] = id.Value,
["name"] = SafeName(doc, id),
})
.ToList(),
["elementsTruncated"] = elementIds.Count > MaxElementsPerGroup,
};
})
.OrderByDescending(group => (int)group["count"]!)
.ToList();
bool groupsTruncated = groups.Count > MaxGroups;
var topGroups = groups.Take(MaxGroups).ToList();
bool workshared = doc.IsWorkshared;
List? worksetNames = null;
if (workshared)
{
worksetNames = new FilteredWorksetCollector(doc)
.OfKind(WorksetKind.UserWorkset)
.ToWorksets()
.Select(workset => workset.Name)
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
var phaseNames = new List();
foreach (Phase phase in doc.Phases)
phaseNames.Add(phase.Name);
var designOptionNames = new FilteredElementCollector(doc)
.OfClass(typeof(DesignOption))
.Select(option => option.Name)
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.ToList();
int inPlaceFamilies = new FilteredElementCollector(doc)
.OfClass(typeof(Family))
.Cast()
.Count(family => family.IsInPlace);
int totalElements = new FilteredElementCollector(doc)
.WhereElementIsNotElementType()
.GetElementCount();
var payload = new Dictionary
{
["warnings"] = new Dictionary
{
["total"] = warnings.Count,
["groupCount"] = groups.Count,
["groups"] = topGroups,
["groupsTruncated"] = groupsTruncated,
},
["workshared"] = workshared,
["worksets"] = worksetNames is null ? null : new Dictionary
{
["count"] = worksetNames.Count,
["names"] = worksetNames,
},
["phases"] = new Dictionary
{
["count"] = phaseNames.Count,
["names"] = phaseNames,
},
["designOptions"] = new Dictionary
{
["count"] = designOptionNames.Count,
["names"] = designOptionNames,
},
["inPlaceFamilies"] = inPlaceFamilies,
["totalElements"] = totalElements,
};
string topWarning = topGroups.Count > 0
? $" Top: '{Shorten((string)topGroups[0]["description"]!, 80)}' x{topGroups[0]["count"]}."
: string.Empty;
string compact = $"{warnings.Count} warning(s) in {groups.Count} group(s); "
+ (workshared ? $"workshared ({worksetNames!.Count} workset(s)); " : "not workshared; ")
+ $"{phaseNames.Count} phase(s); {designOptionNames.Count} design option(s); {inPlaceFamilies} in-place families; {totalElements} elements."
+ topWarning;
return new ToolOutput(payload, compact);
}
// ------------------------------------------------- FailureMessage flattening
private static string SafeDescription(FailureMessage warning)
{
try
{
string text = warning.GetDescriptionText();
return string.IsNullOrWhiteSpace(text) ? "(no description)" : text.Trim();
}
catch
{
return "(no description)";
}
}
private static string? SafeSeverity(FailureMessage warning)
{
try
{
return warning.GetSeverity().ToString();
}
catch
{
return null;
}
}
private static IEnumerable SafeFailingElements(FailureMessage warning)
{
try
{
return warning.GetFailingElements();
}
catch
{
return Enumerable.Empty();
}
}
private static string? SafeName(Document doc, ElementId id)
{
try
{
return doc.GetElement(id)?.Name;
}
catch
{
return null;
}
}
private static string Shorten(string text, int max)
=> text.Length <= max ? text : text[..(max - 3)] + "...";
}
}