Files
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

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,
})
}