Remove NotFoundException; services return null on lookup miss
A missing record isn't exceptional — services now return null (logged at Info) instead of throwing, and endpoints map null to 404. Agent and import toolsets route not-found through their existing OrNotFound result pattern rather than a caught exception.
This commit is contained in:
@@ -71,9 +71,11 @@ public class NovelAgentService(
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the agent and runs it to completion, executing any tools it
|
||||
/// calls along the way. Returns the assistant's final turn.
|
||||
/// calls along the way. Returns the assistant's final turn. Null when no project has
|
||||
/// this id, or the request names a conversation that does not exist — a lookup miss
|
||||
/// is expected, not exceptional.
|
||||
/// </summary>
|
||||
public async Task<AgentMessage> SendMessageAsync(
|
||||
public async Task<AgentMessage?> SendMessageAsync(
|
||||
Guid projectId, SendAgentMessageRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(projectId, nameof(projectId));
|
||||
@@ -84,19 +86,37 @@ public class NovelAgentService(
|
||||
"Sending agent message for project {ProjectId}, conversation {ConversationId}, message length {MessageLength}",
|
||||
projectId, request.ConversationId, request.Message.Length);
|
||||
|
||||
var conversation = request.ConversationId is { } id
|
||||
? await FindConversationAsync(id, ct)
|
||||
// The id came from the request body, not the route — an unknown id here
|
||||
// is bad input to this call, not a direct "fetch conversation" lookup.
|
||||
?? throw new NotFoundException(nameof(AgentConversation), id)
|
||||
: await StartConversationAsync(projectId, request.Message, ct);
|
||||
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == projectId, ct);
|
||||
if (project is null)
|
||||
{
|
||||
logger.LogInformation("Project {ProjectId} not found", projectId);
|
||||
return null;
|
||||
}
|
||||
|
||||
AgentConversation conversation;
|
||||
if (request.ConversationId is { } id)
|
||||
{
|
||||
// The id came from the request body, not the route — an unknown id here
|
||||
// is bad input to this call, not a direct "fetch conversation" lookup.
|
||||
var found = await FindConversationAsync(id, ct);
|
||||
if (found is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
conversation = found;
|
||||
}
|
||||
else
|
||||
{
|
||||
conversation = StartConversation(projectId, request.Message);
|
||||
}
|
||||
|
||||
// Persist the user's turn before running the loop. The tools save through the
|
||||
// same DbContext, so leaving this pending would entangle it with their writes —
|
||||
// and recording the question even if the model call fails is the behaviour we want.
|
||||
await AppendMessageAsync(conversation, AgentRole.User, request.Message, null, ct);
|
||||
|
||||
var systemPrompt = await BuildSystemPromptAsync(projectId, ct);
|
||||
var systemPrompt = BuildSystemPrompt(project);
|
||||
var transcript = BuildTranscript(conversation);
|
||||
var toolCalls = new List<ToolCallResponse>();
|
||||
var text = new StringBuilder();
|
||||
@@ -197,17 +217,10 @@ public class NovelAgentService(
|
||||
return message;
|
||||
}
|
||||
|
||||
private async Task<AgentConversation> StartConversationAsync(
|
||||
Guid projectId, string firstMessage, CancellationToken ct)
|
||||
private AgentConversation StartConversation(Guid projectId, string firstMessage)
|
||||
{
|
||||
logger.LogDebug("Starting new agent conversation for project {ProjectId}", projectId);
|
||||
|
||||
if (!await db.Projects.AnyAsync(p => p.Id == projectId, ct))
|
||||
{
|
||||
logger.LogWarning("Project {ProjectId} not found", projectId);
|
||||
throw new NotFoundException(nameof(Project), projectId);
|
||||
}
|
||||
|
||||
var conversation = new AgentConversation
|
||||
{
|
||||
ProjectId = projectId,
|
||||
@@ -249,17 +262,8 @@ public class NovelAgentService(
|
||||
[new AgentTextBlock(m.Content)]))
|
||||
];
|
||||
|
||||
private async Task<string> BuildSystemPromptAsync(Guid projectId, CancellationToken ct)
|
||||
private static string BuildSystemPrompt(Project project)
|
||||
{
|
||||
logger.LogDebug("Building system prompt for project {ProjectId}", projectId);
|
||||
|
||||
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == projectId, ct);
|
||||
if (project is null)
|
||||
{
|
||||
logger.LogWarning("Project {ProjectId} not found", projectId);
|
||||
throw new NotFoundException(nameof(Project), projectId);
|
||||
}
|
||||
|
||||
var brief = new StringBuilder();
|
||||
brief.AppendLine($"Title: {project.Title}");
|
||||
if (!string.IsNullOrWhiteSpace(project.Genre)) brief.AppendLine($"Genre: {project.Genre}");
|
||||
|
||||
Reference in New Issue
Block a user