Add batch character assignment for chapter outline beats
Lets a writer select several beats and add one character to all of them at once, without disturbing each beat's existing characters. Surfaced through the REST API, the embedded agent, and the MCP server per the project's rule that all three share the same service methods.
This commit is contained in:
@@ -11,7 +11,10 @@ namespace Novelly.Api.Agent;
|
||||
|
||||
public record AgentToolResult(string Content, bool IsError);
|
||||
|
||||
internal record ToolNotFound(string Message);
|
||||
internal record ToolNotFound(string Entity, Guid Id)
|
||||
{
|
||||
public string Message => $"{Entity} '{Id}' was not found.";
|
||||
}
|
||||
|
||||
public record AgentTool(
|
||||
string Name,
|
||||
@@ -58,7 +61,7 @@ public class NovelAgentToolset(
|
||||
|
||||
if (result is ToolNotFound notFound)
|
||||
{
|
||||
logger.LogInformation("Tool {Tool} for project {ProjectId} found nothing: {Message}", name, projectId, notFound.Message);
|
||||
logger.LogWarning("Tool {Tool} for project {ProjectId} found no {Entity} {EntityId}", name, projectId, notFound.Entity, notFound.Id);
|
||||
return new AgentToolResult(notFound.Message, true);
|
||||
}
|
||||
|
||||
@@ -78,14 +81,14 @@ public class NovelAgentToolset(
|
||||
}
|
||||
|
||||
private static async Task<object> OrNotFound<T>(Task<T?> lookup, string entity, Guid id) where T : class =>
|
||||
await lookup as object ?? new ToolNotFound($"{entity} '{id}' was not found.");
|
||||
await lookup as object ?? new ToolNotFound(entity, id);
|
||||
|
||||
private static async Task<object> OrNotFound<TEntity, TResponse>(
|
||||
Task<TEntity?> lookup, Func<TEntity, TResponse> map, string entity, Guid id) where TEntity : class =>
|
||||
await lookup is { } value ? map(value)! : new ToolNotFound($"{entity} '{id}' was not found.");
|
||||
await lookup is { } value ? map(value)! : new ToolNotFound(entity, id);
|
||||
|
||||
private static async Task<object> DeletedOrNotFound(Task<bool> delete, string entity, Guid id) =>
|
||||
await delete ? new { deleted = true } : new ToolNotFound($"{entity} '{id}' was not found.");
|
||||
await delete ? new { deleted = true } : new ToolNotFound(entity, id);
|
||||
|
||||
private Dictionary<string, AgentTool> ByName => _byName ??= Build().ToDictionary(t => t.Name);
|
||||
|
||||
@@ -265,6 +268,27 @@ public class NovelAgentToolset(
|
||||
.Where(g => g != Guid.Empty)]), ct), list => list.Select(b => b.ToResponse()), "Chapter", chapterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"assign_character_to_beats",
|
||||
"Add a character to several beats at once. Leaves each beat's existing characters and "
|
||||
+ "other fields alone — this only adds, it never removes.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("chapter_id", "Id of the chapter the beats belong to.", required: true)
|
||||
.Str("character_id", "Id of the character to add.", required: true)
|
||||
.StringArray("beat_ids", "Ids of the beats to add the character to.", required: true)
|
||||
.Build(),
|
||||
async (_, input, ct) =>
|
||||
{
|
||||
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
|
||||
return await OrNotFound(beats.AssignCharacterAsync(
|
||||
chapterId,
|
||||
new AssignCharacterToBeatsRequest(
|
||||
JsonInput.RequiredGuid(input, "character_id"),
|
||||
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
|
||||
.Select(id => Guid.TryParse(id, out var g) ? g : Guid.Empty)
|
||||
.Where(g => g != Guid.Empty)]), ct), list => list.Select(b => b.ToResponse()), "Chapter", chapterId);
|
||||
});
|
||||
|
||||
yield return new AgentTool(
|
||||
"list_tags",
|
||||
"List the project's tags with how many characters, chapters and beats carry each. "
|
||||
|
||||
Reference in New Issue
Block a user