1122 lines
33 KiB
Go
1122 lines
33 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/fasthttp/router"
|
|
"github.com/google/uuid"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/configstore"
|
|
"github.com/maximhq/bifrost/framework/configstore/tables"
|
|
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// PromptCacheReloader is implemented by the prompts plugin to allow the HTTP handler
|
|
// to trigger an in-memory cache refresh after any repository mutation.
|
|
type PromptCacheReloader interface {
|
|
Reload(ctx context.Context) error
|
|
}
|
|
|
|
// PromptsHandler handles prompt repository endpoints
|
|
type PromptsHandler struct {
|
|
store configstore.ConfigStore
|
|
reloader PromptCacheReloader // optional; nil when the prompts plugin is not loaded
|
|
}
|
|
|
|
// NewPromptsHandler creates a new PromptsHandler.
|
|
// reloader may be nil; when set, the in-memory prompt cache is refreshed after mutations.
|
|
func NewPromptsHandler(store configstore.ConfigStore, reloader PromptCacheReloader) *PromptsHandler {
|
|
if store == nil {
|
|
return nil
|
|
}
|
|
return &PromptsHandler{store: store, reloader: reloader}
|
|
}
|
|
|
|
// reloadCache triggers a cache refresh if a reloader is configured.
|
|
// Errors are logged but do not fail the originating request.
|
|
func (h *PromptsHandler) reloadCache(ctx context.Context) {
|
|
if h.reloader == nil {
|
|
return
|
|
}
|
|
if err := h.reloader.Reload(ctx); err != nil {
|
|
logger.Error("failed to reload prompt cache: %v", err)
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers the routes for the PromptsHandler
|
|
func (h *PromptsHandler) RegisterRoutes(r *router.Router, middlewares ...schemas.BifrostHTTPMiddleware) {
|
|
// Folders
|
|
r.GET("/api/prompt-repo/folders", lib.ChainMiddlewares(h.getFolders, middlewares...))
|
|
r.GET("/api/prompt-repo/folders/{id}", lib.ChainMiddlewares(h.getFolderByID, middlewares...))
|
|
r.POST("/api/prompt-repo/folders", lib.ChainMiddlewares(h.createFolder, middlewares...))
|
|
r.PUT("/api/prompt-repo/folders/{id}", lib.ChainMiddlewares(h.updateFolder, middlewares...))
|
|
r.DELETE("/api/prompt-repo/folders/{id}", lib.ChainMiddlewares(h.deleteFolder, middlewares...))
|
|
|
|
// Prompts
|
|
r.GET("/api/prompt-repo/prompts", lib.ChainMiddlewares(h.getPrompts, middlewares...))
|
|
r.GET("/api/prompt-repo/prompts/{id}", lib.ChainMiddlewares(h.getPromptByID, middlewares...))
|
|
r.POST("/api/prompt-repo/prompts", lib.ChainMiddlewares(h.createPrompt, middlewares...))
|
|
r.PUT("/api/prompt-repo/prompts/{id}", lib.ChainMiddlewares(h.updatePrompt, middlewares...))
|
|
r.DELETE("/api/prompt-repo/prompts/{id}", lib.ChainMiddlewares(h.deletePrompt, middlewares...))
|
|
|
|
// Versions
|
|
r.GET("/api/prompt-repo/prompts/{id}/versions", lib.ChainMiddlewares(h.getPromptVersions, middlewares...))
|
|
r.GET("/api/prompt-repo/versions/{id}", lib.ChainMiddlewares(h.getVersionByID, middlewares...))
|
|
r.POST("/api/prompt-repo/prompts/{id}/versions", lib.ChainMiddlewares(h.createVersion, middlewares...))
|
|
r.DELETE("/api/prompt-repo/versions/{id}", lib.ChainMiddlewares(h.deleteVersion, middlewares...))
|
|
|
|
// Sessions
|
|
r.GET("/api/prompt-repo/prompts/{id}/sessions", lib.ChainMiddlewares(h.getPromptSessions, middlewares...))
|
|
r.GET("/api/prompt-repo/sessions/{id}", lib.ChainMiddlewares(h.getSessionByID, middlewares...))
|
|
r.POST("/api/prompt-repo/prompts/{id}/sessions", lib.ChainMiddlewares(h.createSession, middlewares...))
|
|
r.PUT("/api/prompt-repo/sessions/{id}", lib.ChainMiddlewares(h.updateSession, middlewares...))
|
|
r.DELETE("/api/prompt-repo/sessions/{id}", lib.ChainMiddlewares(h.deleteSession, middlewares...))
|
|
r.PUT("/api/prompt-repo/sessions/{id}/rename", lib.ChainMiddlewares(h.renameSession, middlewares...))
|
|
r.POST("/api/prompt-repo/sessions/{id}/commit", lib.ChainMiddlewares(h.commitSession, middlewares...))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Request/Response Types
|
|
// ============================================================================
|
|
|
|
// CreateFolderRequest represents the request body for creating a folder
|
|
type CreateFolderRequest struct {
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
}
|
|
|
|
// UpdateFolderRequest represents the request body for updating a folder
|
|
type UpdateFolderRequest struct {
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
DescriptionExists bool `json:"-"` // true when description key is present in JSON (even if null)
|
|
}
|
|
|
|
// UnmarshalJSON implements custom unmarshalling to detect presence of description key
|
|
func (r *UpdateFolderRequest) UnmarshalJSON(data []byte) error {
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
if v, ok := raw["name"]; ok {
|
|
if err := json.Unmarshal(v, &r.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v, ok := raw["description"]; ok {
|
|
if err := json.Unmarshal(v, &r.Description); err != nil {
|
|
return err
|
|
}
|
|
r.DescriptionExists = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreatePromptRequest represents the request body for creating a prompt
|
|
type CreatePromptRequest struct {
|
|
Name string `json:"name"`
|
|
FolderID *string `json:"folder_id,omitempty"`
|
|
}
|
|
|
|
// UpdatePromptRequest represents the request body for updating a prompt
|
|
type UpdatePromptRequest struct {
|
|
Name string `json:"name"`
|
|
FolderID *string `json:"folder_id"`
|
|
FolderIDExists bool `json:"-"` // true when folder_id key is present in JSON (even if null)
|
|
}
|
|
|
|
// CreateVersionRequest represents the request body for creating a version
|
|
type CreateVersionRequest struct {
|
|
CommitMessage string `json:"commit_message"`
|
|
Messages []tables.PromptMessage `json:"messages"`
|
|
ModelParams tables.ModelParams `json:"model_params"`
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
Variables tables.PromptVariables `json:"variables,omitempty"`
|
|
}
|
|
|
|
// CreateSessionRequest represents the request body for creating a session
|
|
type CreateSessionRequest struct {
|
|
Name string `json:"name"`
|
|
VersionID *uint `json:"version_id,omitempty"`
|
|
Messages []tables.PromptMessage `json:"messages,omitempty"`
|
|
ModelParams tables.ModelParams `json:"model_params"`
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
Variables tables.PromptVariables `json:"variables,omitempty"`
|
|
}
|
|
|
|
// UpdateSessionRequest represents the request body for updating a session
|
|
type UpdateSessionRequest struct {
|
|
Name string `json:"name"`
|
|
Messages []tables.PromptMessage `json:"messages"`
|
|
ModelParams tables.ModelParams `json:"model_params"`
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
Variables tables.PromptVariables `json:"variables,omitempty"`
|
|
}
|
|
|
|
// RenameSessionRequest represents the request body for renaming a session
|
|
type RenameSessionRequest struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// CommitSessionRequest represents the request body for committing a session as a version
|
|
type CommitSessionRequest struct {
|
|
CommitMessage string `json:"commit_message"`
|
|
MessageIndices *[]int `json:"message_indices,omitempty"` // optional: indices of messages to include (0-based). If nil/absent, all messages are included.
|
|
}
|
|
|
|
// ============================================================================
|
|
// Folder Handlers
|
|
// ============================================================================
|
|
|
|
// getFolders handles GET /api/prompt-repo/folders
|
|
func (h *PromptsHandler) getFolders(ctx *fasthttp.RequestCtx) {
|
|
folders, err := h.store.GetFolders(ctx)
|
|
if err != nil {
|
|
logger.Error("failed to get folders: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"folders": folders,
|
|
})
|
|
}
|
|
|
|
// getFolderByID handles GET /api/prompt-repo/folders/{id}
|
|
func (h *PromptsHandler) getFolderByID(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "folder ID is required")
|
|
return
|
|
}
|
|
id, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid folder ID")
|
|
return
|
|
}
|
|
|
|
folder, err := h.store.GetFolderByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "folder not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get folder: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"folder": folder,
|
|
})
|
|
}
|
|
|
|
// createFolder handles POST /api/prompt-repo/folders
|
|
func (h *PromptsHandler) createFolder(ctx *fasthttp.RequestCtx) {
|
|
var req CreateFolderRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.Name == "" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
|
|
folder := &tables.TableFolder{
|
|
ID: uuid.New().String(),
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
}
|
|
|
|
if err := h.store.CreateFolder(ctx, folder); err != nil {
|
|
logger.Error("failed to create folder: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"folder": folder,
|
|
})
|
|
}
|
|
|
|
// updateFolder handles PUT /api/prompt-repo/folders/{id}
|
|
func (h *PromptsHandler) updateFolder(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "folder ID is required")
|
|
return
|
|
}
|
|
id, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid folder ID")
|
|
return
|
|
}
|
|
|
|
var req UpdateFolderRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
folder, err := h.store.GetFolderByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "folder not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get folder: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
if req.Name != "" {
|
|
folder.Name = req.Name
|
|
}
|
|
if req.DescriptionExists {
|
|
folder.Description = req.Description
|
|
}
|
|
|
|
if err := h.store.UpdateFolder(ctx, folder); err != nil {
|
|
logger.Error("failed to update folder: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"folder": folder,
|
|
})
|
|
}
|
|
|
|
// deleteFolder handles DELETE /api/prompt-repo/folders/{id}
|
|
func (h *PromptsHandler) deleteFolder(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "folder ID is required")
|
|
return
|
|
}
|
|
id, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid folder ID")
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteFolder(ctx, id); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "folder not found")
|
|
return
|
|
}
|
|
logger.Error("failed to delete folder: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.reloadCache(ctx)
|
|
SendJSON(ctx, map[string]any{
|
|
"message": "folder deleted successfully",
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Prompt Handlers
|
|
// ============================================================================
|
|
|
|
// getPrompts handles GET /api/prompt-repo/prompts
|
|
func (h *PromptsHandler) getPrompts(ctx *fasthttp.RequestCtx) {
|
|
var folderID *string
|
|
if folderIDParam := string(ctx.QueryArgs().Peek("folder_id")); folderIDParam != "" {
|
|
folderID = &folderIDParam
|
|
}
|
|
|
|
prompts, err := h.store.GetPrompts(ctx, folderID)
|
|
if err != nil {
|
|
logger.Error("failed to get prompts: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"prompts": prompts,
|
|
})
|
|
}
|
|
|
|
// getPromptByID handles GET /api/prompt-repo/prompts/{id}
|
|
func (h *PromptsHandler) getPromptByID(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "prompt ID is required")
|
|
return
|
|
}
|
|
id, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid prompt ID")
|
|
return
|
|
}
|
|
|
|
prompt, err := h.store.GetPromptByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "prompt not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get prompt: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"prompt": prompt,
|
|
})
|
|
}
|
|
|
|
// createPrompt handles POST /api/prompt-repo/prompts
|
|
func (h *PromptsHandler) createPrompt(ctx *fasthttp.RequestCtx) {
|
|
var req CreatePromptRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.Name == "" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
// Normalize empty folder_id to nil (treat as root)
|
|
if req.FolderID != nil && *req.FolderID == "" {
|
|
req.FolderID = nil
|
|
}
|
|
// Verify folder exists if folder_id is provided
|
|
if req.FolderID != nil {
|
|
if _, err := h.store.GetFolderByID(ctx, *req.FolderID); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "folder not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get folder: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
prompt := &tables.TablePrompt{
|
|
ID: uuid.New().String(),
|
|
Name: req.Name,
|
|
FolderID: req.FolderID,
|
|
}
|
|
|
|
if err := h.store.CreatePrompt(ctx, prompt); err != nil {
|
|
logger.Error("failed to create prompt: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.reloadCache(ctx)
|
|
SendJSON(ctx, map[string]any{
|
|
"prompt": prompt,
|
|
})
|
|
}
|
|
|
|
// updatePrompt handles PUT /api/prompt-repo/prompts/{id}
|
|
func (h *PromptsHandler) updatePrompt(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "prompt ID is required")
|
|
return
|
|
}
|
|
id, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid prompt ID")
|
|
return
|
|
}
|
|
|
|
var req UpdatePromptRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
// Detect if folder_id key was present in JSON (even if null)
|
|
var rawFields map[string]json.RawMessage
|
|
if err := json.Unmarshal(ctx.PostBody(), &rawFields); err == nil {
|
|
if _, exists := rawFields["folder_id"]; exists {
|
|
req.FolderIDExists = true
|
|
}
|
|
}
|
|
|
|
prompt, err := h.store.GetPromptByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "prompt not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get prompt: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
if req.Name != "" {
|
|
prompt.Name = req.Name
|
|
}
|
|
if req.FolderIDExists {
|
|
if req.FolderID == nil {
|
|
// folder_id: null — move to root
|
|
prompt.FolderID = nil
|
|
} else if *req.FolderID == "" {
|
|
prompt.FolderID = nil
|
|
} else {
|
|
// Verify folder exists
|
|
if _, err := h.store.GetFolderByID(ctx, *req.FolderID); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "folder not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get folder: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
prompt.FolderID = req.FolderID
|
|
}
|
|
}
|
|
|
|
if err := h.store.UpdatePrompt(ctx, prompt); err != nil {
|
|
logger.Error("failed to update prompt: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.reloadCache(ctx)
|
|
SendJSON(ctx, map[string]any{
|
|
"prompt": prompt,
|
|
})
|
|
}
|
|
|
|
// deletePrompt handles DELETE /api/prompt-repo/prompts/{id}
|
|
func (h *PromptsHandler) deletePrompt(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "prompt ID is required")
|
|
return
|
|
}
|
|
id, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid prompt ID")
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeletePrompt(ctx, id); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "prompt not found")
|
|
return
|
|
}
|
|
logger.Error("failed to delete prompt: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.reloadCache(ctx)
|
|
SendJSON(ctx, map[string]any{
|
|
"message": "prompt deleted successfully",
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Version Handlers
|
|
// ============================================================================
|
|
|
|
// getPromptVersions handles GET /api/prompt-repo/prompts/{id}/versions
|
|
func (h *PromptsHandler) getPromptVersions(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "prompt ID is required")
|
|
return
|
|
}
|
|
promptID, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid prompt ID")
|
|
return
|
|
}
|
|
|
|
versions, err := h.store.GetPromptVersions(ctx, promptID)
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "prompt not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get versions: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"versions": versions,
|
|
})
|
|
}
|
|
|
|
// getVersionByID handles GET /api/prompt-repo/versions/{id}
|
|
func (h *PromptsHandler) getVersionByID(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "version ID is required")
|
|
return
|
|
}
|
|
idStr, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid version ID")
|
|
return
|
|
}
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid version ID")
|
|
return
|
|
}
|
|
|
|
version, err := h.store.GetPromptVersionByID(ctx, uint(id))
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "version not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get version: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"version": version,
|
|
})
|
|
}
|
|
|
|
// createVersion handles POST /api/prompt-repo/prompts/{id}/versions
|
|
func (h *PromptsHandler) createVersion(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "prompt ID is required")
|
|
return
|
|
}
|
|
promptID, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid prompt ID")
|
|
return
|
|
}
|
|
|
|
var req CreateVersionRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
// Verify prompt exists
|
|
if _, err := h.store.GetPromptByID(ctx, promptID); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "prompt not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get prompt: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert messages
|
|
var messages []tables.TablePromptVersionMessage
|
|
for _, msg := range req.Messages {
|
|
messages = append(messages, tables.TablePromptVersionMessage{
|
|
PromptID: promptID,
|
|
Message: msg,
|
|
})
|
|
}
|
|
|
|
// Strip variable values — versions store keys only; values live in sessions
|
|
var versionVars tables.PromptVariables
|
|
if len(req.Variables) > 0 {
|
|
versionVars = make(tables.PromptVariables, len(req.Variables))
|
|
for key := range req.Variables {
|
|
versionVars[key] = ""
|
|
}
|
|
}
|
|
|
|
version := &tables.TablePromptVersion{
|
|
PromptID: promptID,
|
|
CommitMessage: req.CommitMessage,
|
|
ModelParams: req.ModelParams,
|
|
Provider: req.Provider,
|
|
Model: req.Model,
|
|
Variables: versionVars,
|
|
Messages: messages,
|
|
}
|
|
|
|
if err := h.store.CreatePromptVersion(ctx, version); err != nil {
|
|
logger.Error("failed to create version: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.reloadCache(ctx)
|
|
SendJSON(ctx, map[string]any{
|
|
"version": version,
|
|
})
|
|
}
|
|
|
|
// deleteVersion handles DELETE /api/prompt-repo/versions/{id}
|
|
func (h *PromptsHandler) deleteVersion(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "version ID is required")
|
|
return
|
|
}
|
|
idStr, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid version ID")
|
|
return
|
|
}
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid version ID")
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeletePromptVersion(ctx, uint(id)); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "version not found")
|
|
return
|
|
}
|
|
logger.Error("failed to delete version: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.reloadCache(ctx)
|
|
SendJSON(ctx, map[string]any{
|
|
"message": "version deleted successfully",
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Session Handlers
|
|
// ============================================================================
|
|
|
|
// getPromptSessions handles GET /api/prompt-repo/prompts/{id}/sessions
|
|
func (h *PromptsHandler) getPromptSessions(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "prompt ID is required")
|
|
return
|
|
}
|
|
promptID, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid prompt ID")
|
|
return
|
|
}
|
|
|
|
sessions, err := h.store.GetPromptSessions(ctx, promptID)
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "prompt not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get sessions: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"sessions": sessions,
|
|
})
|
|
}
|
|
|
|
// getSessionByID handles GET /api/prompt-repo/sessions/{id}
|
|
func (h *PromptsHandler) getSessionByID(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "session ID is required")
|
|
return
|
|
}
|
|
idStr, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
|
|
session, err := h.store.GetPromptSessionByID(ctx, uint(id))
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "session not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"session": session,
|
|
})
|
|
}
|
|
|
|
// createSession handles POST /api/prompt-repo/prompts/{id}/sessions
|
|
func (h *PromptsHandler) createSession(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "prompt ID is required")
|
|
return
|
|
}
|
|
promptID, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid prompt ID")
|
|
return
|
|
}
|
|
|
|
var req CreateSessionRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
// Verify prompt exists
|
|
if _, err := h.store.GetPromptByID(ctx, promptID); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "prompt not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get prompt: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
// If version_id is provided, copy messages from that version
|
|
var messages []tables.TablePromptSessionMessage
|
|
if req.VersionID != nil {
|
|
version, err := h.store.GetPromptVersionByID(ctx, *req.VersionID)
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "version not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get version: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
// Verify version belongs to this prompt
|
|
if version.PromptID != promptID {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "version does not belong to this prompt")
|
|
return
|
|
}
|
|
// Copy messages from version
|
|
for _, msg := range version.Messages {
|
|
messages = append(messages, tables.TablePromptSessionMessage{
|
|
PromptID: promptID,
|
|
Message: msg.Message,
|
|
})
|
|
}
|
|
// Use version's model params, provider, model if not provided
|
|
if req.Provider == "" {
|
|
req.Provider = version.Provider
|
|
}
|
|
if req.Model == "" {
|
|
req.Model = version.Model
|
|
}
|
|
if len(req.ModelParams) == 0 {
|
|
req.ModelParams = version.ModelParams
|
|
}
|
|
if len(req.Variables) == 0 && len(version.Variables) > 0 {
|
|
req.Variables = make(tables.PromptVariables, len(version.Variables))
|
|
for key := range version.Variables {
|
|
req.Variables[key] = ""
|
|
}
|
|
}
|
|
} else {
|
|
// Use provided messages
|
|
for _, msg := range req.Messages {
|
|
messages = append(messages, tables.TablePromptSessionMessage{
|
|
PromptID: promptID,
|
|
Message: msg,
|
|
})
|
|
}
|
|
}
|
|
|
|
session := &tables.TablePromptSession{
|
|
PromptID: promptID,
|
|
VersionID: req.VersionID,
|
|
Name: req.Name,
|
|
ModelParams: req.ModelParams,
|
|
Provider: req.Provider,
|
|
Model: req.Model,
|
|
Variables: req.Variables,
|
|
Messages: messages,
|
|
}
|
|
|
|
if err := h.store.CreatePromptSession(ctx, session); err != nil {
|
|
logger.Error("failed to create session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"session": session,
|
|
})
|
|
}
|
|
|
|
// updateSession handles PUT /api/prompt-repo/sessions/{id}
|
|
func (h *PromptsHandler) updateSession(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "session ID is required")
|
|
return
|
|
}
|
|
idStr, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
|
|
var req UpdateSessionRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
session, err := h.store.GetPromptSessionByID(ctx, uint(id))
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "session not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
if req.Name != "" {
|
|
session.Name = req.Name
|
|
}
|
|
session.ModelParams = req.ModelParams
|
|
session.Provider = req.Provider
|
|
session.Model = req.Model
|
|
if req.Variables != nil {
|
|
session.Variables = req.Variables
|
|
}
|
|
|
|
// Update messages
|
|
var messages []tables.TablePromptSessionMessage
|
|
for _, msg := range req.Messages {
|
|
messages = append(messages, tables.TablePromptSessionMessage{
|
|
PromptID: session.PromptID,
|
|
Message: msg,
|
|
})
|
|
}
|
|
session.Messages = messages
|
|
|
|
if err := h.store.UpdatePromptSession(ctx, session); err != nil {
|
|
logger.Error("failed to update session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"session": session,
|
|
})
|
|
}
|
|
|
|
// deleteSession handles DELETE /api/prompt-repo/sessions/{id}
|
|
func (h *PromptsHandler) deleteSession(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "session ID is required")
|
|
return
|
|
}
|
|
idStr, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeletePromptSession(ctx, uint(id)); err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "session not found")
|
|
return
|
|
}
|
|
logger.Error("failed to delete session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"message": "session deleted successfully",
|
|
})
|
|
}
|
|
|
|
// renameSession handles PUT /api/prompt-repo/sessions/{id}/rename
|
|
func (h *PromptsHandler) renameSession(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "session ID is required")
|
|
return
|
|
}
|
|
idStr, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
|
|
var req RenameSessionRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
session, err := h.store.GetPromptSessionByID(ctx, uint(id))
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "session not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.store.RenamePromptSession(ctx, session.ID, req.Name); err != nil {
|
|
logger.Error("failed to rename session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
session.Name = req.Name
|
|
SendJSON(ctx, map[string]any{
|
|
"session": session,
|
|
})
|
|
}
|
|
|
|
// commitSession handles POST /api/prompt-repo/sessions/{id}/commit
|
|
func (h *PromptsHandler) commitSession(ctx *fasthttp.RequestCtx) {
|
|
idVal := ctx.UserValue("id")
|
|
if idVal == nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "session ID is required")
|
|
return
|
|
}
|
|
idStr, ok := idVal.(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid session ID")
|
|
return
|
|
}
|
|
|
|
var req CommitSessionRequest
|
|
if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.CommitMessage == "" {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "commit_message is required")
|
|
return
|
|
}
|
|
|
|
session, err := h.store.GetPromptSessionByID(ctx, uint(id))
|
|
if err != nil {
|
|
if errors.Is(err, configstore.ErrNotFound) {
|
|
SendError(ctx, fasthttp.StatusNotFound, "session not found")
|
|
return
|
|
}
|
|
logger.Error("failed to get session: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert session messages to version messages
|
|
var messages []tables.TablePromptVersionMessage
|
|
if req.MessageIndices != nil {
|
|
// Only include messages at the specified indices, deduplicating
|
|
seen := make(map[int]struct{})
|
|
for _, idx := range *req.MessageIndices {
|
|
if _, ok := seen[idx]; ok {
|
|
continue
|
|
}
|
|
seen[idx] = struct{}{}
|
|
if idx < 0 || idx >= len(session.Messages) {
|
|
SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("message index %d out of range (0-%d)", idx, len(session.Messages)-1))
|
|
return
|
|
}
|
|
msg := session.Messages[idx]
|
|
messages = append(messages, tables.TablePromptVersionMessage{
|
|
PromptID: session.PromptID,
|
|
Message: msg.Message,
|
|
})
|
|
}
|
|
} else {
|
|
for _, msg := range session.Messages {
|
|
messages = append(messages, tables.TablePromptVersionMessage{
|
|
PromptID: session.PromptID,
|
|
Message: msg.Message,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(messages) == 0 {
|
|
SendError(ctx, fasthttp.StatusBadRequest, "at least one message must be included in the version")
|
|
return
|
|
}
|
|
|
|
// Copy variable keys from session with empty values for the version
|
|
var versionVars tables.PromptVariables
|
|
if len(session.Variables) > 0 {
|
|
versionVars = make(tables.PromptVariables, len(session.Variables))
|
|
for key := range session.Variables {
|
|
versionVars[key] = ""
|
|
}
|
|
}
|
|
|
|
version := &tables.TablePromptVersion{
|
|
PromptID: session.PromptID,
|
|
CommitMessage: req.CommitMessage,
|
|
ModelParams: session.ModelParams,
|
|
Provider: session.Provider,
|
|
Model: session.Model,
|
|
Variables: versionVars,
|
|
Messages: messages,
|
|
}
|
|
|
|
if err := h.store.CreatePromptVersion(ctx, version); err != nil {
|
|
logger.Error("failed to create version: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.reloadCache(ctx)
|
|
SendJSON(ctx, map[string]any{
|
|
"version": version,
|
|
})
|
|
}
|