mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 22:24:14 -04:00
feat(mcp): describe each engine in autonest_plate and default fill tools to Fill
The engine argument's description now names every built-in engine with what it suits, so a model can choose one; omitting it still means Default. The fill tools resolve an omitted strategy to Fill directly instead of through the session's whole-job default. Front-end tests cover the MCP and console defaults and keep the description in step with the registry's built-ins.
This commit is contained in:
1 parent
5af44b251c
commit
89356371db
4 files changed
+136
-21
No files matched your search
@@ -0,0 +1,106 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Mcp;
|
||||
using OpenNest.Mcp.Tools;
|
||||
|
||||
namespace OpenNest.FrontEnd.Tests;
|
||||
|
||||
/// <summary>Front ends that are not told an engine use Default; fill tools use Fill.</summary>
|
||||
[Collection("FrontEndRegistry")]
|
||||
public class DefaultEngineSelectionTests : IDisposable
|
||||
{
|
||||
private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-default-engine-" + Guid.NewGuid());
|
||||
|
||||
public DefaultEngineSelectionTests() => Directory.CreateDirectory(directory);
|
||||
public void Dispose() => Directory.Delete(directory, true);
|
||||
|
||||
private static NestSession Session(bool occupied = false)
|
||||
{
|
||||
var shape = new Shape();
|
||||
shape.Entities.Add(new Line(new Vector(0, 0), new Vector(2, 0)));
|
||||
shape.Entities.Add(new Line(new Vector(2, 0), new Vector(2, 2)));
|
||||
shape.Entities.Add(new Line(new Vector(2, 2), new Vector(0, 2)));
|
||||
shape.Entities.Add(new Line(new Vector(0, 2), new Vector(0, 0)));
|
||||
var session = new NestSession { Nest = new Nest("fixture") };
|
||||
session.Nest.Drawings.Add(new Drawing("square", OpenNest.Converters.ConvertGeometry.ToProgram(shape)));
|
||||
var plate = session.Nest.CreatePlate();
|
||||
plate.Size = new Size(20, 30);
|
||||
plate.PartSpacing = 0.2;
|
||||
plate.EdgeSpacing = new Spacing(0.5, 0.5, 0.5, 0.5);
|
||||
if (occupied) // The nest writer keeps only plates that hold parts; autonest replaces them.
|
||||
plate.Parts.Add(new Part(session.Nest.Drawings.Single()));
|
||||
return session;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpAutonestWithoutAnEngineUsesDefault()
|
||||
{
|
||||
var session = Session();
|
||||
|
||||
var response = new NestingTools(session).AutoNestPlate(0, "square", "2");
|
||||
|
||||
Assert.Contains("(Default engine): success", response);
|
||||
Assert.Equal(2, session.GetPlate(0).Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpFillToolsWithoutAnEngineUseFill()
|
||||
{
|
||||
var session = Session();
|
||||
session.DefaultEngineName = "Rectangles"; // The session default is for autonest only.
|
||||
|
||||
var response = new NestingTools(session).FillPlate(0, "square", 2);
|
||||
|
||||
Assert.Contains("(Fill): success", response);
|
||||
Assert.Equal(2, session.GetPlate(0).Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpAutonestEngineDescriptionNamesEveryBuiltInEngine()
|
||||
{
|
||||
var engine = typeof(NestingTools).GetMethod(nameof(NestingTools.AutoNestPlate))!
|
||||
.GetParameters().Single(p => p.Name == "engine");
|
||||
var description = engine.GetCustomAttribute<DescriptionAttribute>()!.Description;
|
||||
var builtIns = NestingEngineRegistry.AvailableEngines
|
||||
.Where(e => e.Factory().GetType().Assembly == typeof(NestingEngineRegistry).Assembly)
|
||||
.Select(e => e.Name)
|
||||
.ToList();
|
||||
|
||||
Assert.Contains("Default", builtIns);
|
||||
Assert.All(builtIns, name => Assert.Matches(@"\b" + Regex.Escape(name) + @"\b", description));
|
||||
Assert.StartsWith("Whole-job engine. Default (used when omitted)", description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsoleAutonestWithoutAnEngineUsesDefault()
|
||||
{
|
||||
var input = Path.Combine(directory, "input.nest");
|
||||
var output = Path.Combine(directory, "output.nest");
|
||||
Assert.True(new NestWriter(Session(occupied: true).Nest).Write(input));
|
||||
var method = Assembly.Load("OpenNest.Console").GetType("NestConsole")!
|
||||
.GetMethod("Run", BindingFlags.Static | BindingFlags.Public)!;
|
||||
var (originalOut, originalError) = (Console.Out, Console.Error);
|
||||
using var captured = new StringWriter();
|
||||
int exit;
|
||||
try
|
||||
{
|
||||
Console.SetOut(captured);
|
||||
Console.SetError(captured);
|
||||
exit = (int)method.Invoke(null, new object[] { new[] { input, "--autonest", "--quantity", "2", "--output", output } })!;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
Console.SetError(originalError);
|
||||
}
|
||||
|
||||
Assert.True(exit == 0, captured.ToString());
|
||||
Assert.Contains("Engine: Default", captured.ToString());
|
||||
Assert.Equal(2, Assert.Single(new NestReader(output).Read().Plates).Parts.Count);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ namespace OpenNest.Mcp
|
||||
public List<Drawing> Drawings { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Session-default engine/strategy name used by nesting tools when a call does not pass
|
||||
/// an explicit <c>engine</c> argument. Instance state only — the process-global legacy
|
||||
/// registry is never read or written.
|
||||
/// Whole-job engine <c>autonest_plate</c> uses when a call does not pass an explicit
|
||||
/// <c>engine</c> argument; fill tools default to the Fill strategy instead. Instance state
|
||||
/// only — the process-global legacy registry is never read or written.
|
||||
/// </summary>
|
||||
public string DefaultEngineName { get; set; } = "Default";
|
||||
|
||||
|
||||
@@ -18,6 +18,22 @@ namespace OpenNest.Mcp.Tools
|
||||
{
|
||||
private readonly NestSession _session;
|
||||
|
||||
private const string FillStrategies =
|
||||
"Placement strategy: Fill (default; lattice fill of repeated copies), Strip, "
|
||||
+ "Vertical Remnant (keeps a right-side drop), Horizontal Remnant (keeps a top-side drop)";
|
||||
|
||||
/// <summary>Built-in engines with what each suits; a test keeps this in step with the registry.</summary>
|
||||
private const string JobEngines =
|
||||
"Whole-job engine. Default (used when omitted): suits any job; runs Irregular and Rectangles "
|
||||
+ "and keeps the cheapest valid layout. "
|
||||
+ "Irregular: irregular profiles; no-fit-polygon packing into notches and gaps, best-fit pairs. "
|
||||
+ "Rectangles: plain and near-rectangular parts; packs each part as its bounding box, fastest. "
|
||||
+ "Fill: many copies of few drawings; lattice fill with pairs and rectangle best-fit. "
|
||||
+ "Strip: Fill variant laying mixed drawings in strips. "
|
||||
+ "Vertical Remnant, Horizontal Remnant: Fill variants keeping a right-side or top-side drop. "
|
||||
+ "StockLadder: constrained parts first, then area repacking; meant for several stock sizes. "
|
||||
+ "Plug-ins loaded from Engines/ use their class name.";
|
||||
|
||||
public NestingTools(NestSession session)
|
||||
{
|
||||
_session = session;
|
||||
@@ -31,7 +47,7 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("Index of the plate to fill")] int plateIndex,
|
||||
[Description("Name of the drawing to fill with")] string drawingName,
|
||||
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0,
|
||||
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
|
||||
[Description(FillStrategies)]
|
||||
string engine = null
|
||||
)
|
||||
{
|
||||
@@ -84,7 +100,7 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("Width of the area")] double width,
|
||||
[Description("Length of the area")] double length,
|
||||
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0,
|
||||
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
|
||||
[Description(FillStrategies)]
|
||||
string engine = null
|
||||
)
|
||||
{
|
||||
@@ -134,7 +150,7 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("Index of the plate")] int plateIndex,
|
||||
[Description("Name of the drawing to fill with")] string drawingName,
|
||||
[Description("Maximum quantity per remnant (0 = unlimited)")] int quantity = 0,
|
||||
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
|
||||
[Description(FillStrategies)]
|
||||
string engine = null
|
||||
)
|
||||
{
|
||||
@@ -197,7 +213,7 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("Index of the plate")] int plateIndex,
|
||||
[Description("Comma-separated drawing names")] string drawingNames,
|
||||
[Description("Comma-separated quantities for each drawing")] string quantities,
|
||||
[Description("Placement strategy: Fill, Strip, Vertical Remnant, Horizontal Remnant")]
|
||||
[Description(FillStrategies)]
|
||||
string engine = null
|
||||
)
|
||||
{
|
||||
@@ -255,7 +271,7 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("Index of the empty plate")] int plateIndex,
|
||||
[Description("Comma-separated drawing names")] string drawingNames,
|
||||
[Description("Comma-separated positive quantities")] string quantities,
|
||||
[Description("Registered jobs engine name, including loaded plug-ins")] string engine = null,
|
||||
[Description(JobEngines)] string engine = null,
|
||||
[Description("Explicitly keep representable layouts despite validation violations")] bool allow_invalid = false,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
@@ -323,18 +339,14 @@ namespace OpenNest.Mcp.Tools
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the requested fill strategy, falling back to the session default. Returns null
|
||||
/// when the name is not a single-plate placement strategy.
|
||||
/// Resolves the requested fill strategy; an omitted name means Fill. Returns null when the
|
||||
/// name is not a single-plate placement strategy.
|
||||
/// </summary>
|
||||
private string ResolveStrategy(string engine)
|
||||
private static string ResolveStrategy(string engine)
|
||||
{
|
||||
var requested = string.IsNullOrWhiteSpace(engine)
|
||||
? _session.DefaultEngineName
|
||||
: engine.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
return PlateFillService.ResolveStrategy(requested);
|
||||
return PlateFillService.ResolveStrategy(engine?.Trim());
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
@@ -342,10 +354,7 @@ namespace OpenNest.Mcp.Tools
|
||||
}
|
||||
}
|
||||
|
||||
private string EngineError(string engine) =>
|
||||
UnknownEngineMessage(
|
||||
string.IsNullOrWhiteSpace(engine) ? _session.DefaultEngineName : engine.Trim()
|
||||
);
|
||||
private static string EngineError(string engine) => UnknownEngineMessage(engine.Trim());
|
||||
|
||||
private static string UnknownEngineMessage(string engineName)
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ Console `--autonest` uses the jobs engine named by `--engine` (Default when omit
|
||||
|
||||
Invalid output is printed and rejected with exit code 2 without saving or posting. `--allow-invalid` explicitly accepts representable layout violations. Malformed output, multiple returned sheets, and zero placements are never saved by this path. Unknown engines exit 1. `--autonest --keep-parts` rejects an occupied target: use the plain interactive fill path for existing obstacles instead.
|
||||
|
||||
MCP `autonest_plate` requires an empty target. The stdio server serializes all tool calls sharing its mutable session, so another request cannot change drawings or occupy a target during a solve. `allow_invalid` defaults to false. It reports violations and makes no changes on rejection, including with an override when the output is unrepresentable or contains multiple sheets. Existing fill tools remain separate. Console and MCP load jobs plug-ins from `Engines/` beside their executable.
|
||||
MCP `autonest_plate` requires an empty target. The stdio server serializes all tool calls sharing its mutable session, so another request cannot change drawings or occupy a target during a solve. `allow_invalid` defaults to false. It reports violations and makes no changes on rejection, including with an override when the output is unrepresentable or contains multiple sheets. `engine` defaults to Default, and its description lists the built-in engines with what each suits. Fill tools (`fill_plate`, `fill_area`, `fill_remnants`, `pack_plate`) remain separate and default to the Fill strategy. Console and MCP load jobs plug-ins from `Engines/` beside their executable.
|
||||
|
||||
### MCP engine development harness
|
||||
|
||||
|
||||
Reference in new issue
Block a user