This is the initial commit for the CODEX_ADK project, a full-stack AI agent management platform featuring: BACKEND (ASP.NET Core 8.0): - CQRS architecture with 6 commands and 7 queries - 16 API endpoints (all working and tested) - PostgreSQL database with 5 entities - AES-256 encryption for API keys - FluentValidation on all commands - Rate limiting and CORS configured - OpenAPI/Swagger documentation - Docker Compose setup (PostgreSQL + Ollama) FRONTEND (Flutter 3.x): - Dark theme with Svrnty branding - Collapsible sidebar navigation - CQRS API client with Result<T> error handling - Type-safe endpoints from OpenAPI schema - Multi-platform support (Web, iOS, Android, macOS, Linux, Windows) DOCUMENTATION: - Comprehensive API reference - Architecture documentation - Development guidelines for Claude Code - API integration guides - context-claude.md project overview Status: Backend ready (Grade A-), Frontend integration pending 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
448 lines
21 KiB
C#
448 lines
21 KiB
C#
using Codex.CQRS.Commands;
|
|
using Codex.CQRS.Queries;
|
|
using Codex.Dal.QueryProviders;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.OpenApi.Models;
|
|
using OpenHarbor.CQRS.Abstractions;
|
|
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
|
|
using PoweredSoft.Data.Core;
|
|
using PoweredSoft.DynamicQuery;
|
|
using PoweredSoft.DynamicQuery.Core;
|
|
|
|
namespace Codex.Api.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Manual endpoint registration to ensure proper OpenAPI documentation for all CQRS endpoints.
|
|
/// Required because OpenHarbor.CQRS doesn't auto-generate Swagger docs for commands with return values and dynamic queries.
|
|
/// </summary>
|
|
public static class ManualEndpointRegistration
|
|
{
|
|
public static WebApplication MapCodexEndpoints(this WebApplication app)
|
|
{
|
|
// ============================================================
|
|
// COMMANDS
|
|
// ============================================================
|
|
|
|
// CreateAgent - No return value (already auto-documented by OpenHarbor)
|
|
// UpdateAgent - No return value (already auto-documented by OpenHarbor)
|
|
// DeleteAgent - No return value (already auto-documented by OpenHarbor)
|
|
|
|
// CreateConversation - Returns Guid
|
|
app.MapPost("/api/command/createConversation",
|
|
async ([FromBody] CreateConversationCommand command,
|
|
ICommandHandler<CreateConversationCommand, Guid> handler) =>
|
|
{
|
|
var result = await handler.HandleAsync(command);
|
|
return Results.Ok(new { id = result });
|
|
})
|
|
.WithName("CreateConversation")
|
|
.WithTags("Commands")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "Creates a new conversation for grouping related messages",
|
|
Description = "Returns the newly created conversation ID",
|
|
RequestBody = new OpenApiRequestBody
|
|
{
|
|
Required = true,
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.Schema,
|
|
Id = nameof(CreateConversationCommand)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse
|
|
{
|
|
Description = "Conversation created successfully",
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Type = "object",
|
|
Properties = new Dictionary<string, OpenApiSchema>
|
|
{
|
|
["id"] = new OpenApiSchema
|
|
{
|
|
Type = "string",
|
|
Format = "uuid",
|
|
Description = "The unique identifier of the created conversation"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
["400"] = new OpenApiResponse { Description = "Validation failed" },
|
|
["500"] = new OpenApiResponse { Description = "Internal server error" }
|
|
}
|
|
})
|
|
.Produces<object>(200)
|
|
.ProducesValidationProblem();
|
|
|
|
// StartAgentExecution - Returns Guid
|
|
app.MapPost("/api/command/startAgentExecution",
|
|
async ([FromBody] StartAgentExecutionCommand command,
|
|
ICommandHandler<StartAgentExecutionCommand, Guid> handler) =>
|
|
{
|
|
var result = await handler.HandleAsync(command);
|
|
return Results.Ok(new { id = result });
|
|
})
|
|
.WithName("StartAgentExecution")
|
|
.WithTags("Commands")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "Starts a new agent execution with a user prompt",
|
|
Description = "Creates an execution record and returns its ID. Use this to track agent runs.",
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse
|
|
{
|
|
Description = "Execution started successfully",
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Type = "object",
|
|
Properties = new Dictionary<string, OpenApiSchema>
|
|
{
|
|
["id"] = new OpenApiSchema
|
|
{
|
|
Type = "string",
|
|
Format = "uuid",
|
|
Description = "The unique identifier of the execution"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
["400"] = new OpenApiResponse { Description = "Validation failed" },
|
|
["404"] = new OpenApiResponse { Description = "Agent not found" },
|
|
["500"] = new OpenApiResponse { Description = "Internal server error" }
|
|
}
|
|
})
|
|
.Produces<object>(200)
|
|
.ProducesValidationProblem();
|
|
|
|
// CompleteAgentExecution - No return value
|
|
app.MapPost("/api/command/completeAgentExecution",
|
|
async ([FromBody] CompleteAgentExecutionCommand command,
|
|
ICommandHandler<CompleteAgentExecutionCommand> handler) =>
|
|
{
|
|
await handler.HandleAsync(command);
|
|
return Results.Ok();
|
|
})
|
|
.WithName("CompleteAgentExecution")
|
|
.WithTags("Commands")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "Marks an agent execution as completed with results",
|
|
Description = "Updates execution status, tokens used, and stores the response",
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse { Description = "Execution completed successfully" },
|
|
["400"] = new OpenApiResponse { Description = "Validation failed" },
|
|
["404"] = new OpenApiResponse { Description = "Execution not found" },
|
|
["500"] = new OpenApiResponse { Description = "Internal server error" }
|
|
}
|
|
})
|
|
.Produces(200)
|
|
.ProducesValidationProblem();
|
|
|
|
// ============================================================
|
|
// QUERIES
|
|
// ============================================================
|
|
|
|
// Health - Already auto-documented
|
|
// GetAgent - Already auto-documented
|
|
|
|
// GetAgentExecution
|
|
app.MapPost("/api/query/getAgentExecution",
|
|
async ([FromBody] GetAgentExecutionQuery query,
|
|
IQueryHandler<GetAgentExecutionQuery, AgentExecutionDetails?> handler) =>
|
|
{
|
|
var result = await handler.HandleAsync(query);
|
|
return result != null ? Results.Ok(result) : Results.NotFound();
|
|
})
|
|
.WithName("GetAgentExecution")
|
|
.WithTags("Queries")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "Get details of a specific agent execution by ID",
|
|
Description = "Returns execution details including tokens, cost, messages, and status",
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse
|
|
{
|
|
Description = "Execution details retrieved successfully",
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.Schema,
|
|
Id = nameof(AgentExecutionDetails)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
["404"] = new OpenApiResponse { Description = "Execution not found" },
|
|
["500"] = new OpenApiResponse { Description = "Internal server error" }
|
|
}
|
|
})
|
|
.Produces<AgentExecutionDetails>(200)
|
|
.Produces(404);
|
|
|
|
// GetConversation
|
|
app.MapPost("/api/query/getConversation",
|
|
async ([FromBody] GetConversationQuery query,
|
|
IQueryHandler<GetConversationQuery, ConversationDetails?> handler) =>
|
|
{
|
|
var result = await handler.HandleAsync(query);
|
|
return result != null ? Results.Ok(result) : Results.NotFound();
|
|
})
|
|
.WithName("GetConversation")
|
|
.WithTags("Queries")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "Get details of a specific conversation by ID",
|
|
Description = "Returns conversation details including messages and execution history",
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse
|
|
{
|
|
Description = "Conversation details retrieved successfully",
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.Schema,
|
|
Id = nameof(ConversationDetails)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
["404"] = new OpenApiResponse { Description = "Conversation not found" },
|
|
["500"] = new OpenApiResponse { Description = "Internal server error" }
|
|
}
|
|
})
|
|
.Produces<ConversationDetails>(200)
|
|
.Produces(404);
|
|
|
|
// ============================================================
|
|
// DYNAMIC QUERIES (Paginated Lists)
|
|
// ============================================================
|
|
// NOTE: Dynamic queries are auto-registered by OpenHarbor but not auto-documented.
|
|
// They work via /api/dynamicquery/{ItemType} but aren't in Swagger without manual registration.
|
|
// The endpoints exist and function - frontend can use them directly from openapi.json examples below.
|
|
|
|
// Manual registration disabled for now - OpenHarbor handles these automatically
|
|
// TODO: Add proper schema documentation for dynamic query request/response
|
|
|
|
/*
|
|
// ListAgents
|
|
app.MapPost("/api/dynamicquery/ListAgentsQueryItem",
|
|
async (HttpContext context,
|
|
IQueryableProvider<ListAgentsQueryItem> provider,
|
|
IAsyncQueryableService queryService) =>
|
|
{
|
|
var query = await context.Request.ReadFromJsonAsync<object>();
|
|
var queryable = await provider.GetQueryableAsync(query!);
|
|
var result = await queryService.ExecuteAsync(queryable, query!);
|
|
return Results.Ok(result);
|
|
})
|
|
.WithName("ListAgents")
|
|
.WithTags("DynamicQuery")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "List agents with filtering, sorting, and pagination",
|
|
Description = @"Dynamic query endpoint supporting:
|
|
- **Filtering**: Filter by any property (Name, Type, Status, etc.)
|
|
- **Sorting**: Sort by one or multiple properties
|
|
- **Pagination**: Page and PageSize parameters
|
|
- **Aggregates**: Count, Sum, Average on numeric fields
|
|
|
|
### Example Request
|
|
```json
|
|
{
|
|
""filters"": [
|
|
{ ""path"": ""Name"", ""operator"": ""Contains"", ""value"": ""search"" },
|
|
{ ""path"": ""Status"", ""operator"": ""Equal"", ""value"": ""Active"" }
|
|
],
|
|
""sorts"": [{ ""path"": ""CreatedAt"", ""descending"": true }],
|
|
""page"": 1,
|
|
""pageSize"": 20
|
|
}
|
|
```",
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse
|
|
{
|
|
Description = "Paginated list of agents",
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Type = "object",
|
|
Properties = new Dictionary<string, OpenApiSchema>
|
|
{
|
|
["data"] = new OpenApiSchema
|
|
{
|
|
Type = "array",
|
|
Items = new OpenApiSchema
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.Schema,
|
|
Id = nameof(ListAgentsQueryItem)
|
|
}
|
|
}
|
|
},
|
|
["page"] = new OpenApiSchema { Type = "integer" },
|
|
["pageSize"] = new OpenApiSchema { Type = "integer" },
|
|
["totalCount"] = new OpenApiSchema { Type = "integer" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.Produces<object>(200);
|
|
|
|
// ListConversations
|
|
app.MapPost("/api/dynamicquery/ListConversationsQueryItem",
|
|
async (HttpContext context,
|
|
IQueryableProvider<ListConversationsQueryItem> provider,
|
|
IAsyncQueryableService queryService) =>
|
|
{
|
|
var query = await context.Request.ReadFromJsonAsync<object>();
|
|
var queryable = await provider.GetQueryableAsync(query!);
|
|
var result = await queryService.ExecuteAsync(queryable, query!);
|
|
return Results.Ok(result);
|
|
})
|
|
.WithName("ListConversations")
|
|
.WithTags("DynamicQuery")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "List conversations with filtering, sorting, and pagination",
|
|
Description = "Returns paginated conversations with message counts and metadata",
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse
|
|
{
|
|
Description = "Paginated list of conversations",
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Type = "object",
|
|
Properties = new Dictionary<string, OpenApiSchema>
|
|
{
|
|
["data"] = new OpenApiSchema
|
|
{
|
|
Type = "array",
|
|
Items = new OpenApiSchema
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.Schema,
|
|
Id = nameof(ListConversationsQueryItem)
|
|
}
|
|
}
|
|
},
|
|
["totalCount"] = new OpenApiSchema { Type = "integer" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.Produces<object>(200);
|
|
|
|
// ListAgentExecutions
|
|
app.MapPost("/api/dynamicquery/ListAgentExecutionsQueryItem",
|
|
async (HttpContext context,
|
|
IQueryableProvider<ListAgentExecutionsQueryItem> provider,
|
|
IAsyncQueryableService queryService) =>
|
|
{
|
|
var query = await context.Request.ReadFromJsonAsync<object>();
|
|
var queryable = await provider.GetQueryableAsync(query!);
|
|
var result = await queryService.ExecuteAsync(queryable, query!);
|
|
return Results.Ok(result);
|
|
})
|
|
.WithName("ListAgentExecutions")
|
|
.WithTags("DynamicQuery")
|
|
.WithOpenApi(operation => new(operation)
|
|
{
|
|
Summary = "List agent executions with filtering, sorting, and pagination",
|
|
Description = "Returns paginated execution history with tokens, costs, and status",
|
|
Responses = new OpenApiResponses
|
|
{
|
|
["200"] = new OpenApiResponse
|
|
{
|
|
Description = "Paginated list of executions",
|
|
Content = new Dictionary<string, OpenApiMediaType>
|
|
{
|
|
["application/json"] = new OpenApiMediaType
|
|
{
|
|
Schema = new OpenApiSchema
|
|
{
|
|
Type = "object",
|
|
Properties = new Dictionary<string, OpenApiSchema>
|
|
{
|
|
["data"] = new OpenApiSchema
|
|
{
|
|
Type = "array",
|
|
Items = new OpenApiSchema
|
|
{
|
|
Reference = new OpenApiReference
|
|
{
|
|
Type = ReferenceType.Schema,
|
|
Id = nameof(ListAgentExecutionsQueryItem)
|
|
}
|
|
}
|
|
},
|
|
["totalCount"] = new OpenApiSchema { Type = "integer" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.Produces<object>(200);
|
|
*/
|
|
|
|
return app;
|
|
}
|
|
}
|