Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Infrastructures.Attributes;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -672,7 +673,8 @@ public async Task<ActionResult<List<string>>> GetMockTargets([FromQuery] string?
// Same derivation the authoring prompt uses, so the names the editor offers and the names
// a model is allowed to mock can never diverge. Still names only on the wire: the case
// editor's pickers consume a plain string list.
return MockTargetCatalogue.Names(agent);
var utilityAssistant = await _agents.GetAgent(BuiltInAgentId.UtilityAssistant);
return MockTargetCatalogue.Names(agent, utilityAssistant);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ public async Task<AgentTestAuthorResponse> AuthorAsync(
?? throw new CaseAuthorUnavailableException(
$"agent {agentId} not found, so there is nothing to author a case against");

var targets = MockTargetCatalogue.Describe(agent);
var utilityAssistant = await _agents.GetAgent(BuiltInAgentId.UtilityAssistant);
var targets = MockTargetCatalogue.Describe(agent, utilityAssistant);
var existingCases = await _repo.ListCasesAsync(suite.Id);
var grounding = await LoadGroundingAsync(suite.Id, request.CaseId, ct);
var agentNames = await AgentNamesAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ namespace BotSharp.Plugin.AgentTesting.Services;
/// </summary>
public static class MockTargetCatalogue
{
/// <summary>The prefix a utility item's function name must carry to be loaded as a function.</summary>
/// <remarks>Same constant, same ordinal comparison, as BasicAgentHook.UTIL_PREFIX.</remarks>
private const string UtilityPrefix = "util-";

/// <summary>Function names only, sorted and de-duplicated. The wire shape the UI consumes.</summary>
public static List<string> Names(Agent agent)
=> Describe(agent).Select(t => t.Name).ToList();
public static List<string> Names(Agent agent, Agent? utilityAssistant = null)
=> Describe(agent, utilityAssistant).Select(t => t.Name).ToList();

/// <summary>
/// Every callable function with whatever description and parameter shape the agent definition
Expand All @@ -30,7 +34,12 @@ public static List<string> Names(Agent agent)
/// here -- it is why an MCP mock's argsMatchJson is more likely to need a human fix than a
/// plugin function's.
/// </summary>
public static List<MockTargetInfo> Describe(Agent agent)
/// <param name="agent">The agent whose callable surface is wanted.</param>
/// <param name="utilityAssistant">
/// The UtilityAssistant agent, which is where a utility item's real FunctionDef lives -- an agent
/// only names the utilities it turns on. Pass null and utilities still appear, name-only.
/// </param>
public static List<MockTargetInfo> Describe(Agent agent, Agent? utilityAssistant = null)
{
var targets = new List<MockTargetInfo>();

Expand All @@ -46,6 +55,39 @@ public static List<MockTargetInfo> Describe(Agent agent)
targets.Add(new MockTargetInfo(fn!.Name, null, null));
}

// Utilities are the third way an agent gets a function, and the one that does not show up
// anywhere in Functions/SecondaryFunctions on a stored agent: BasicAgentHook.OnAgentUtilityLoaded
// expands them into SecondaryFunctions at conversation time, and IAgentService.GetAgent -- what
// every caller here holds -- is a plain repository read that never runs that hook. Left out, an
// agent whose whole toolset is utilities (Lessen Work Order Summary, Property Summary) reports
// no callable functions at all, and a case authored against it blocks its own tools on the
// first run.
//
// The filter mirrors that hook: a disabled utility is off, and only a `util-` prefixed name is
// ever loaded as a function. What is deliberately NOT mirrored is VisibilityExpression, which
// needs the conversation's render data and cannot be evaluated against a stored agent -- a
// conditionally-visible utility is offered here and may turn out not to load at run time.
foreach (var utility in agent.Utilities ?? [])
{
if (utility == null || utility.Disabled) continue;

foreach (var item in utility.Items ?? [])
{
var name = item?.FunctionName;
if (string.IsNullOrWhiteSpace(name) || !name.StartsWith(UtilityPrefix, StringComparison.Ordinal)) continue;

// The definition the agent will actually be given, not the agent's own declaration --
// the description and parameter list live on UtilityAssistant.
var definition = utilityAssistant?.Functions?
.FirstOrDefault(f => string.Equals(f?.Name, name, StringComparison.OrdinalIgnoreCase));

targets.Add(new MockTargetInfo(
name!,
definition?.Description ?? item!.Description,
ParameterSummary(definition?.Parameters)));
}
}

// First entry wins on a duplicate name: a function declared both primary and secondary is
// one function, and the primary declaration is the one with the fuller definition.
return targets
Expand Down
Loading