mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 01:49:41 +00:00
Rebuilt how AI Studio knows what a model can do (#960)
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
This commit is contained in:
1 parent
d21e09dd1e
commit
d85b4e71b6
287 files changed
+18341
-3677
No files matched your search
@@ -6,3 +6,4 @@
|
||||
---------|------------------|----------|--------------------------
|
||||
MBI001 | SourceGeneration | Info | MappingRegistryGenerator
|
||||
MBI002 | SourceGeneration | Warning | MappingRegistryGenerator
|
||||
MDR001 | SourceGeneration | Warning | ModelRegistryGenerator
|
||||
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace SourceGeneratedMappings;
|
||||
|
||||
/// <summary>
|
||||
/// Collects every model family and every model host of the compilation into one list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Adding a family has to be one action, not two. A registry which somebody has to remember to add
|
||||
/// to is a registry which will be incomplete, and the failure it produces is the quietest one there
|
||||
/// is: a family which simply never answers, so its models fall into the global default and look
|
||||
/// merely unremarkable.
|
||||
///
|
||||
/// Searching for the types at startup through reflection would do the same job, but this app
|
||||
/// publishes trimmed and uses reflection nowhere else. So the search happens while compiling, and
|
||||
/// what ships is a plain array.
|
||||
/// </remarks>
|
||||
[Generator]
|
||||
#pragma warning disable RS1036
|
||||
public sealed class ModelRegistryGenerator : IIncrementalGenerator
|
||||
#pragma warning restore RS1036
|
||||
{
|
||||
private const string GENERATED_NAMESPACE = "AIStudio.Models.Registry";
|
||||
private const string GENERATED_TYPE_NAME = "ModelRegistrations";
|
||||
private const string FAMILY_BASE_TYPE = "AIStudio.Models.ModelFamily";
|
||||
private const string HOST_INTERFACE = "AIStudio.Models.Hosting.IModelHost";
|
||||
|
||||
private static readonly DiagnosticDescriptor CANNOT_BE_REGISTERED = new(
|
||||
id: "MDR001",
|
||||
title: "A model family or host cannot be registered",
|
||||
messageFormat: "'{0}' is a model family or host, but the generated registry cannot create it: {1}. It will answer for no model at all.",
|
||||
category: "SourceGeneration",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
description: "The generated registry creates every family and host with its parameterless constructor. A type it cannot create is left out, which makes it silently ineffective.");
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var candidates = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(static (node, _) => CouldBeARegistration(node), static (syntax, _) => Inspect(syntax))
|
||||
.Where(static candidate => candidate.FullName is not null)
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(candidates, Generate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a syntax node is worth asking the semantic model about.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Runs on every node of every keystroke, so it only looks at the syntax: a class with a base
|
||||
/// list which is neither abstract nor static. Everything else is decided once a symbol exists.
|
||||
/// </remarks>
|
||||
/// <param name="node">The node to look at.</param>
|
||||
/// <returns>True, when the node could be a family or a host.</returns>
|
||||
private static bool CouldBeARegistration(SyntaxNode node) =>
|
||||
node is ClassDeclarationSyntax declaration &&
|
||||
declaration.BaseList is { Types.Count: > 0 } &&
|
||||
!declaration.Modifiers.Any(SyntaxKind.AbstractKeyword) &&
|
||||
!declaration.Modifiers.Any(SyntaxKind.StaticKeyword);
|
||||
|
||||
private static Candidate Inspect(GeneratorSyntaxContext context)
|
||||
{
|
||||
var declaration = (ClassDeclarationSyntax) context.Node;
|
||||
if (context.SemanticModel.GetDeclaredSymbol(declaration) is not { } symbol)
|
||||
return default;
|
||||
|
||||
var isFamily = DerivesFrom(symbol, FAMILY_BASE_TYPE);
|
||||
var isHost = symbol.AllInterfaces.Any(candidate => candidate.ToDisplayString() == HOST_INTERFACE);
|
||||
if (!isFamily && !isHost)
|
||||
return default;
|
||||
|
||||
return new Candidate(symbol.ToDisplayString(), isFamily, isHost, WhyItCannotBeCreated(symbol), declaration.Identifier.GetLocation());
|
||||
}
|
||||
|
||||
private static bool DerivesFrom(INamedTypeSymbol symbol, string baseTypeName)
|
||||
{
|
||||
for (var current = symbol.BaseType; current is not null; current = current.BaseType)
|
||||
if (current.ToDisplayString() == baseTypeName)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Why the generated registry could not create this type, or null when it can.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The type to look at.</param>
|
||||
/// <returns>A phrase which completes the diagnostic message, or null.</returns>
|
||||
private static string? WhyItCannotBeCreated(INamedTypeSymbol symbol)
|
||||
{
|
||||
if (symbol.IsAbstract)
|
||||
return "it is abstract";
|
||||
|
||||
if (symbol.IsGenericType)
|
||||
return "it is generic";
|
||||
|
||||
if (symbol.ContainingType is not null)
|
||||
return "it is nested inside another type";
|
||||
|
||||
if (symbol.DeclaredAccessibility is Accessibility.Private or Accessibility.Protected or Accessibility.ProtectedAndInternal)
|
||||
return "the registry cannot reach it from outside its own type";
|
||||
|
||||
var hasParameterlessConstructor = symbol.InstanceConstructors.Any(constructor =>
|
||||
constructor.Parameters.Length == 0 &&
|
||||
constructor.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal);
|
||||
|
||||
return hasParameterlessConstructor ? null : "it has no parameterless constructor the registry can reach";
|
||||
}
|
||||
|
||||
private static void Generate(SourceProductionContext context, ImmutableArray<Candidate> candidates)
|
||||
{
|
||||
var families = new List<string>();
|
||||
var hosts = new List<string>();
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (candidate.FullName is null)
|
||||
continue;
|
||||
|
||||
if (candidate.Problem is not null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(CANNOT_BE_REGISTERED, candidate.Location ?? Location.None, candidate.FullName, candidate.Problem));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate.IsFamily)
|
||||
families.Add(candidate.FullName);
|
||||
|
||||
if (candidate.IsHost)
|
||||
hosts.Add(candidate.FullName);
|
||||
}
|
||||
|
||||
//
|
||||
// Sorted by name and without repeats, so that the same sources produce the same file: a
|
||||
// partial class arrives here once per part, and the order syntax nodes are visited in is
|
||||
// not something to build a shipped artefact on.
|
||||
//
|
||||
var source = RenderSource(Ordered(families), Ordered(hosts));
|
||||
context.AddSource("ModelFamilies.g.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> Ordered(IEnumerable<string> typeNames) => typeNames.Distinct(StringComparer.Ordinal).OrderBy(static name => name, StringComparer.Ordinal).ToList();
|
||||
|
||||
private static string RenderSource(IReadOnlyList<string> families, IReadOnlyList<string> hosts)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
builder.AppendLine("#nullable enable");
|
||||
builder.AppendLine();
|
||||
builder.Append("namespace ").Append(GENERATED_NAMESPACE).AppendLine(";");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("/// <summary>");
|
||||
builder.AppendLine("/// Every model family and every model host this assembly declares.");
|
||||
builder.AppendLine("/// </summary>");
|
||||
builder.Append("public static class ").AppendLine(GENERATED_TYPE_NAME);
|
||||
builder.AppendLine("{");
|
||||
|
||||
AppendFactory(builder, "CreateFamilies", FAMILY_BASE_TYPE, families);
|
||||
builder.AppendLine();
|
||||
AppendFactory(builder, "CreateHosts", HOST_INTERFACE, hosts);
|
||||
|
||||
builder.AppendLine("}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendFactory(StringBuilder builder, string methodName, string typeName, IReadOnlyList<string> typeNames)
|
||||
{
|
||||
builder.Append(" public static global::System.Collections.Generic.IReadOnlyList<global::").Append(typeName).Append("> ").Append(methodName).AppendLine("() =>");
|
||||
builder.Append(" new global::").Append(typeName).AppendLine("[]");
|
||||
builder.AppendLine(" {");
|
||||
|
||||
foreach (var name in typeNames)
|
||||
builder.Append(" new global::").Append(name).AppendLine("(),");
|
||||
|
||||
builder.AppendLine(" };");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What the syntax pass found out about one type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A struct with value equality, because this travels through the incremental pipeline: two
|
||||
/// runs finding the same types have to compare as equal, or nothing downstream is ever cached.
|
||||
/// </remarks>
|
||||
private readonly struct Candidate(string? fullName, bool isFamily, bool isHost, string? problem, Location? location) : IEquatable<Candidate>
|
||||
{
|
||||
public string? FullName { get; } = fullName;
|
||||
|
||||
public bool IsFamily { get; } = isFamily;
|
||||
|
||||
public bool IsHost { get; } = isHost;
|
||||
|
||||
public string? Problem { get; } = problem;
|
||||
|
||||
public Location? Location { get; } = location;
|
||||
|
||||
public bool Equals(Candidate other) =>
|
||||
this.FullName == other.FullName &&
|
||||
this.IsFamily == other.IsFamily &&
|
||||
this.IsHost == other.IsHost &&
|
||||
this.Problem == other.Problem &&
|
||||
Equals(this.Location, other.Location);
|
||||
|
||||
public override bool Equals(object? obj) => obj is Candidate other && this.Equals(other);
|
||||
|
||||
public override int GetHashCode() => this.FullName?.GetHashCode() ?? 0;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user