CODEX_ADK/BACKEND/Codex.Api/Endpoints/SimpleEndpoints.cs
Svrnty 229a0698a3 Initial commit: CODEX_ADK monorepo
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>
2025-10-26 23:12:32 -04:00

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;
}
}