Multi-agent AI laboratory with ASP.NET Core 8.0 backend and Flutter frontend. Implements CQRS architecture, OpenAPI contract-first API design. BACKEND: Agent management, conversations, executions with PostgreSQL + Ollama FRONTEND: Cross-platform UI with strict typing and Result-based error handling Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
244 lines
8.5 KiB
C#
244 lines
8.5 KiB
C#
using Codex.Dal;
|
|
using Codex.Dal.Enums;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Codex.Api.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Simple, pragmatic REST endpoints for MVP.
|
|
/// No over-engineering. Just JSON lists that work.
|
|
/// </summary>
|
|
public static class SimpleEndpoints
|
|
{
|
|
public static WebApplication MapSimpleEndpoints(this WebApplication app)
|
|
{
|
|
// ============================================================
|
|
// AGENTS
|
|
// ============================================================
|
|
|
|
app.MapGet("/api/agents", async (CodexDbContext db) =>
|
|
{
|
|
var agents = await db.Agents
|
|
.Where(a => !a.IsDeleted)
|
|
.OrderByDescending(a => a.CreatedAt)
|
|
.Select(a => new
|
|
{
|
|
a.Id,
|
|
a.Name,
|
|
a.Description,
|
|
a.Type,
|
|
a.ModelProvider,
|
|
a.ModelName,
|
|
a.ProviderType,
|
|
a.ModelEndpoint,
|
|
a.Status,
|
|
a.CreatedAt,
|
|
a.UpdatedAt,
|
|
ToolCount = a.Tools.Count(t => t.IsEnabled),
|
|
ExecutionCount = a.Executions.Count
|
|
})
|
|
.Take(100) // More than enough for MVP
|
|
.ToListAsync();
|
|
|
|
return Results.Ok(agents);
|
|
})
|
|
.WithName("GetAllAgents")
|
|
.WithTags("Agents")
|
|
.WithOpenApi(operation =>
|
|
{
|
|
operation.Summary = "Get all agents";
|
|
operation.Description = "Returns a list of all active agents with metadata. Limit: 100 most recent.";
|
|
return operation;
|
|
})
|
|
.Produces<object>(200);
|
|
|
|
app.MapGet("/api/agents/{id:guid}/conversations", async (Guid id, CodexDbContext db) =>
|
|
{
|
|
var conversations = await db.AgentExecutions
|
|
.Where(e => e.AgentId == id && e.ConversationId != null)
|
|
.Select(e => e.ConversationId)
|
|
.Distinct()
|
|
.Join(db.Conversations,
|
|
convId => convId,
|
|
c => (Guid?)c.Id,
|
|
(convId, c) => new
|
|
{
|
|
c.Id,
|
|
c.Title,
|
|
c.Summary,
|
|
c.StartedAt,
|
|
c.LastMessageAt,
|
|
c.MessageCount,
|
|
c.IsActive
|
|
})
|
|
.OrderByDescending(c => c.LastMessageAt)
|
|
.ToListAsync();
|
|
|
|
return Results.Ok(conversations);
|
|
})
|
|
.WithName("GetAgentConversations")
|
|
.WithTags("Agents")
|
|
.WithOpenApi(operation =>
|
|
{
|
|
operation.Summary = "Get conversations for an agent";
|
|
operation.Description = "Returns all conversations associated with a specific agent.";
|
|
return operation;
|
|
})
|
|
.Produces<object>(200);
|
|
|
|
app.MapGet("/api/agents/{id:guid}/executions", async (Guid id, CodexDbContext db) =>
|
|
{
|
|
var executions = await db.AgentExecutions
|
|
.Where(e => e.AgentId == id)
|
|
.OrderByDescending(e => e.StartedAt)
|
|
.Select(e => new
|
|
{
|
|
e.Id,
|
|
e.AgentId,
|
|
e.ConversationId,
|
|
UserPrompt = e.UserPrompt.Substring(0, Math.Min(e.UserPrompt.Length, 200)), // Truncate for list view
|
|
e.Status,
|
|
e.StartedAt,
|
|
e.CompletedAt,
|
|
e.InputTokens,
|
|
e.OutputTokens,
|
|
e.EstimatedCost,
|
|
MessageCount = e.Messages.Count,
|
|
e.ErrorMessage
|
|
})
|
|
.Take(100)
|
|
.ToListAsync();
|
|
|
|
return Results.Ok(executions);
|
|
})
|
|
.WithName("GetAgentExecutions")
|
|
.WithTags("Agents")
|
|
.WithOpenApi(operation =>
|
|
{
|
|
operation.Summary = "Get execution history for an agent";
|
|
operation.Description = "Returns the 100 most recent executions for a specific agent.";
|
|
return operation;
|
|
})
|
|
.Produces<object>(200);
|
|
|
|
// ============================================================
|
|
// CONVERSATIONS
|
|
// ============================================================
|
|
|
|
app.MapGet("/api/conversations", async (CodexDbContext db) =>
|
|
{
|
|
var conversations = await db.Conversations
|
|
.OrderByDescending(c => c.LastMessageAt)
|
|
.Select(c => new
|
|
{
|
|
c.Id,
|
|
c.Title,
|
|
c.Summary,
|
|
c.StartedAt,
|
|
c.LastMessageAt,
|
|
c.MessageCount,
|
|
c.IsActive,
|
|
ExecutionCount = db.AgentExecutions.Count(e => e.ConversationId == c.Id)
|
|
})
|
|
.Take(100)
|
|
.ToListAsync();
|
|
|
|
return Results.Ok(conversations);
|
|
})
|
|
.WithName("GetAllConversations")
|
|
.WithTags("Conversations")
|
|
.WithOpenApi(operation =>
|
|
{
|
|
operation.Summary = "Get all conversations";
|
|
operation.Description = "Returns the 100 most recent conversations.";
|
|
return operation;
|
|
})
|
|
.Produces<object>(200);
|
|
|
|
// ============================================================
|
|
// EXECUTIONS
|
|
// ============================================================
|
|
|
|
app.MapGet("/api/executions", async (CodexDbContext db) =>
|
|
{
|
|
var executions = await db.AgentExecutions
|
|
.Include(e => e.Agent)
|
|
.OrderByDescending(e => e.StartedAt)
|
|
.Select(e => new
|
|
{
|
|
e.Id,
|
|
e.AgentId,
|
|
AgentName = e.Agent.Name,
|
|
e.ConversationId,
|
|
UserPrompt = e.UserPrompt.Substring(0, Math.Min(e.UserPrompt.Length, 200)),
|
|
e.Status,
|
|
e.StartedAt,
|
|
e.CompletedAt,
|
|
e.InputTokens,
|
|
e.OutputTokens,
|
|
e.EstimatedCost,
|
|
MessageCount = e.Messages.Count,
|
|
e.ErrorMessage
|
|
})
|
|
.Take(100)
|
|
.ToListAsync();
|
|
|
|
return Results.Ok(executions);
|
|
})
|
|
.WithName("GetAllExecutions")
|
|
.WithTags("Executions")
|
|
.WithOpenApi(operation =>
|
|
{
|
|
operation.Summary = "Get all executions";
|
|
operation.Description = "Returns the 100 most recent executions across all agents.";
|
|
return operation;
|
|
})
|
|
.Produces<object>(200);
|
|
|
|
app.MapGet("/api/executions/status/{status}", async (string status, CodexDbContext db) =>
|
|
{
|
|
if (!Enum.TryParse<ExecutionStatus>(status, true, out var executionStatus))
|
|
{
|
|
return Results.BadRequest(new { error = $"Invalid status: {status}. Valid values: Pending, Running, Completed, Failed, Cancelled" });
|
|
}
|
|
|
|
var executions = await db.AgentExecutions
|
|
.Include(e => e.Agent)
|
|
.Where(e => e.Status == executionStatus)
|
|
.OrderByDescending(e => e.StartedAt)
|
|
.Select(e => new
|
|
{
|
|
e.Id,
|
|
e.AgentId,
|
|
AgentName = e.Agent.Name,
|
|
e.ConversationId,
|
|
UserPrompt = e.UserPrompt.Substring(0, Math.Min(e.UserPrompt.Length, 200)),
|
|
e.Status,
|
|
e.StartedAt,
|
|
e.CompletedAt,
|
|
e.InputTokens,
|
|
e.OutputTokens,
|
|
e.EstimatedCost,
|
|
MessageCount = e.Messages.Count,
|
|
e.ErrorMessage
|
|
})
|
|
.Take(100)
|
|
.ToListAsync();
|
|
|
|
return Results.Ok(executions);
|
|
})
|
|
.WithName("GetExecutionsByStatus")
|
|
.WithTags("Executions")
|
|
.WithOpenApi(operation =>
|
|
{
|
|
operation.Summary = "Get executions by status";
|
|
operation.Description = "Returns executions filtered by status (Pending, Running, Completed, Failed, Cancelled).";
|
|
return operation;
|
|
})
|
|
.Produces<object>(200)
|
|
.Produces(400);
|
|
|
|
return app;
|
|
}
|
|
}
|