All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
The architect API returned "failed to start conversation" because projectRepo.Get() failed — the in-memory K8s repo watches the rdev namespace but projects deploy to the projects namespace. Made project lookup non-fatal with fallback to default pod. Added error logging to all architect handler methods (were silently swallowing errors). Also adds setup-hooks, commit-after-qa, and pre-merge-validate steps to the foundary cookbook tree for git hooks and code quality gates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
5.1 KiB
Go
156 lines
5.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/orchard9/rdev/internal/auth"
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/logging"
|
|
"github.com/orchard9/rdev/internal/service"
|
|
"github.com/orchard9/rdev/internal/validate"
|
|
"github.com/orchard9/rdev/pkg/api"
|
|
)
|
|
|
|
// ArchitectHandler handles architect orchestration endpoints.
|
|
type ArchitectHandler struct {
|
|
architectService *service.ArchitectService
|
|
}
|
|
|
|
// NewArchitectHandler creates a new architect handler.
|
|
func NewArchitectHandler(architectService *service.ArchitectService) *ArchitectHandler {
|
|
return &ArchitectHandler{
|
|
architectService: architectService,
|
|
}
|
|
}
|
|
|
|
// Mount registers architect routes.
|
|
func (h *ArchitectHandler) Mount(r api.Router) {
|
|
r.Route("/projects/{id}/architect", func(r chi.Router) {
|
|
// All architect operations require execute scope
|
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).
|
|
Post("/start", h.StartConversation)
|
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).
|
|
Post("/continue/{conversationId}", h.ContinueConversation)
|
|
r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).
|
|
Post("/generate-blueprint/{conversationId}", h.GenerateBlueprint)
|
|
})
|
|
}
|
|
|
|
// StartConversationRequest is the request body for POST /projects/{id}/architect/start.
|
|
type StartConversationRequest struct {
|
|
Prompt string `json:"prompt"`
|
|
}
|
|
|
|
// StartConversation begins a new architectural conversation.
|
|
// POST /projects/{id}/architect/start
|
|
func (h *ArchitectHandler) StartConversation(w http.ResponseWriter, r *http.Request) {
|
|
projectID := chi.URLParam(r, "id")
|
|
|
|
var req StartConversationRequest
|
|
if err := api.DecodeJSON(r, &req); err != nil {
|
|
api.WriteBadRequest(w, r, "invalid request body")
|
|
return
|
|
}
|
|
|
|
v := validate.New()
|
|
v.Required(req.Prompt, "prompt")
|
|
if err := v.Error(); err != nil {
|
|
api.WriteBadRequest(w, r, err.Error())
|
|
return
|
|
}
|
|
|
|
conv, err := h.architectService.StartConversation(r.Context(), projectID, req.Prompt)
|
|
if err != nil {
|
|
log := logging.FromContext(r.Context()).WithHandler("StartConversation")
|
|
log.Error("failed to start conversation", logging.FieldError, err, logging.FieldProjectID, projectID)
|
|
api.WriteInternalError(w, r, "failed to start conversation")
|
|
return
|
|
}
|
|
|
|
api.WriteCreated(w, r, StartConversationResponse{
|
|
ConversationWithMessagesDTO: *toConversationWithMessagesDTO(conv),
|
|
})
|
|
}
|
|
|
|
// ContinueConversationRequest is the request body for POST /projects/{id}/architect/continue/{conversationId}.
|
|
type ContinueConversationRequest struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// ContinueConversation continues an existing architectural conversation.
|
|
// POST /projects/{id}/architect/continue/{conversationId}
|
|
func (h *ArchitectHandler) ContinueConversation(w http.ResponseWriter, r *http.Request) {
|
|
conversationID := domain.ConversationID(chi.URLParam(r, "conversationId"))
|
|
|
|
var req ContinueConversationRequest
|
|
if err := api.DecodeJSON(r, &req); err != nil {
|
|
api.WriteBadRequest(w, r, "invalid request body")
|
|
return
|
|
}
|
|
|
|
v := validate.New()
|
|
v.Required(req.Message, "message")
|
|
if err := v.Error(); err != nil {
|
|
api.WriteBadRequest(w, r, err.Error())
|
|
return
|
|
}
|
|
|
|
msg, err := h.architectService.ContinueConversation(r.Context(), conversationID, req.Message)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrConversationNotFound) {
|
|
api.WriteNotFound(w, r, "conversation not found")
|
|
return
|
|
}
|
|
log := logging.FromContext(r.Context()).WithHandler("ContinueConversation")
|
|
log.Error("failed to continue conversation", logging.FieldError, err, "conversation_id", conversationID)
|
|
api.WriteInternalError(w, r, "failed to continue conversation")
|
|
return
|
|
}
|
|
|
|
api.WriteCreated(w, r, ContinueConversationResponse{
|
|
Message: toMessageDTO(msg),
|
|
})
|
|
}
|
|
|
|
// GenerateBlueprintRequest is the request body for POST /projects/{id}/architect/generate-blueprint/{conversationId}.
|
|
type GenerateBlueprintRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// GenerateBlueprint generates a structured blueprint from a conversation.
|
|
// POST /projects/{id}/architect/generate-blueprint/{conversationId}
|
|
func (h *ArchitectHandler) GenerateBlueprint(w http.ResponseWriter, r *http.Request) {
|
|
conversationID := domain.ConversationID(chi.URLParam(r, "conversationId"))
|
|
|
|
var req GenerateBlueprintRequest
|
|
if err := api.DecodeJSON(r, &req); err != nil {
|
|
api.WriteBadRequest(w, r, "invalid request body")
|
|
return
|
|
}
|
|
|
|
v := validate.New()
|
|
v.Required(req.Name, "name")
|
|
if err := v.Error(); err != nil {
|
|
api.WriteBadRequest(w, r, err.Error())
|
|
return
|
|
}
|
|
|
|
blueprint, err := h.architectService.GenerateBlueprint(r.Context(), conversationID, req.Name)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrConversationNotFound) {
|
|
api.WriteNotFound(w, r, "conversation not found")
|
|
return
|
|
}
|
|
log := logging.FromContext(r.Context()).WithHandler("GenerateBlueprint")
|
|
log.Error("failed to generate blueprint", logging.FieldError, err, "conversation_id", conversationID)
|
|
api.WriteInternalError(w, r, "failed to generate blueprint")
|
|
return
|
|
}
|
|
|
|
api.WriteCreated(w, r, GenerateBlueprintResponse{
|
|
Blueprint: toBlueprintDTO(blueprint),
|
|
})
|
|
}
|