CODEX_ADK/BACKEND/Codex.Api/Endpoints/ManualEndpointRegistration.cs
jean-philippe 3fae2fcbe1 Initial commit: CODEX_ADK (Svrnty Console) MVP v1.0.0
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>
2025-10-26 18:32:38 -04:00

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