using Codex.Dal; using Codex.Dal.Enums; using Microsoft.EntityFrameworkCore; namespace Codex.Api.Endpoints; /// /// Simple, pragmatic REST endpoints for MVP. /// No over-engineering. Just JSON lists that work. /// 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(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(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(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(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(200); app.MapGet("/api/executions/status/{status}", async (string status, CodexDbContext db) => { if (!Enum.TryParse(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(200) .Produces(400); return app; } }