package service import ( "database/sql" "encoding/json/v2" "errors" "log/slog" "net/http" "time" "code.chimeric.al/chimerical/odidere/internal/conversation" "code.chimeric.al/chimerical/odidere/internal/db" "code.chimeric.al/chimerical/odidere/internal/llm" ) func (svc *Service) insertMessage(w http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() log = ctx.Value(logKey).(*slog.Logger) id = r.PathValue("id") req struct { Message *llm.Message `json:"message"` MessageId string `json:"message_id"` UpdatedAt time.Time `json:"updated_at"` } ) if _, err := svc.dbr.SelectConversationIdUserId( ctx, id, "default", ); err != nil { if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } log.ErrorContext( ctx, "failed to load conversation", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } c, err := svc.getConversation(ctx, id) if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } if err != nil { log.ErrorContext( ctx, "failed to get conversation handle", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } r.Body = http.MaxBytesReader(w, r.Body, 32<<20) if err := json.UnmarshalRead(r.Body, &req); err != nil { log.ErrorContext( ctx, "failed to decode request", slog.Any("error", err), ) http.Error(w, "invalid request", http.StatusBadRequest) return } if req.Message == nil { http.Error(w, "invalid request", http.StatusBadRequest) return } var saved *db.Message if req.Message.Role == llm.RoleTool { if req.MessageId == "" || req.UpdatedAt.IsZero() { http.Error(w, "invalid request", http.StatusBadRequest) return } saved, err = c.AddToolResult( ctx, req.MessageId, req.Message.ToolCallID, req.UpdatedAt, req.Message.Text(), ) } else { saved, err = c.InsertMessage(ctx, *req.Message) } if err != nil { switch { case errors.Is(err, conversation.ErrBusy), errors.Is(err, conversation.ErrRevisionConflict): http.Error( w, http.StatusText(http.StatusConflict), http.StatusConflict, ) case errors.Is(err, sql.ErrNoRows): http.NotFound(w, r) default: log.ErrorContext( ctx, "failed to insert message", slog.Any("error", err), ) http.Error(w, err.Error(), http.StatusBadRequest) } return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) if err := json.MarshalWrite(w, saved); err != nil { log.ErrorContext( ctx, "failed to encode message response", slog.Any("error", err), ) } } func (svc *Service) updateMessage(w http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() log = ctx.Value(logKey).(*slog.Logger) id = r.PathValue("id") mid = r.PathValue("mid") req struct { Message *llm.Message `json:"message"` UpdatedAt time.Time `json:"updated_at"` } ) if _, err := svc.dbr.SelectConversationIdUserId( ctx, id, "default", ); err != nil { if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } log.ErrorContext( ctx, "failed to load conversation", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } c, err := svc.getConversation(ctx, id) if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } if err != nil { log.ErrorContext( ctx, "failed to get conversation handle", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } r.Body = http.MaxBytesReader(w, r.Body, 32<<20) if err := json.UnmarshalRead(r.Body, &req); err != nil { log.ErrorContext( ctx, "failed to decode request", slog.Any("error", err), ) http.Error(w, "invalid request", http.StatusBadRequest) return } if req.Message == nil || req.UpdatedAt.IsZero() { http.Error(w, "invalid request", http.StatusBadRequest) return } var saved *db.Message if req.Message.Role == llm.RoleTool { // mid identifies the parent assistant message, while // UpdatedAt is the tool result message's updated_at. saved, err = c.UpdateToolResult( ctx, mid, req.Message.ToolCallID, req.UpdatedAt, req.Message.Text(), ) } else { saved, err = c.UpdateMessage( ctx, mid, req.UpdatedAt, *req.Message, ) } if err != nil { switch { case errors.Is(err, conversation.ErrBusy), errors.Is(err, conversation.ErrRevisionConflict): http.Error( w, http.StatusText(http.StatusConflict), http.StatusConflict, ) case errors.Is(err, sql.ErrNoRows): http.NotFound(w, r) default: log.ErrorContext( ctx, "failed to update message", slog.Any("error", err), ) http.Error(w, err.Error(), http.StatusBadRequest) } return } w.Header().Set("Content-Type", "application/json") if err := json.MarshalWrite(w, saved); err != nil { log.ErrorContext( ctx, "failed to encode message response", slog.Any("error", err), ) } } func (svc *Service) deleteMessage(w http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() log = ctx.Value(logKey).(*slog.Logger) id = r.PathValue("id") mid = r.PathValue("mid") req struct { UpdatedAt time.Time `json:"updated_at"` } ) if _, err := svc.dbr.SelectConversationIdUserId( ctx, id, "default", ); err != nil { if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } log.ErrorContext( ctx, "failed to load conversation", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } c, err := svc.getConversation(ctx, id) if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } if err != nil { log.ErrorContext( ctx, "failed to get conversation handle", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } r.Body = http.MaxBytesReader(w, r.Body, 32<<20) if err := json.UnmarshalRead(r.Body, &req); err != nil { log.ErrorContext( ctx, "failed to decode request", slog.Any("error", err), ) http.Error(w, "invalid request", http.StatusBadRequest) return } if req.UpdatedAt.IsZero() { http.Error(w, "invalid request", http.StatusBadRequest) return } if err := c.DeleteMessage(ctx, mid, req.UpdatedAt); err != nil { switch { case errors.Is(err, conversation.ErrBusy), errors.Is(err, conversation.ErrRevisionConflict): http.Error( w, http.StatusText(http.StatusConflict), http.StatusConflict, ) case errors.Is(err, sql.ErrNoRows): http.NotFound(w, r) default: log.ErrorContext( ctx, "failed to delete message", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) } return } w.WriteHeader(http.StatusNoContent) } func (svc *Service) addToolCall(w http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() log = ctx.Value(logKey).(*slog.Logger) id = r.PathValue("id") mid = r.PathValue("mid") req struct { Call *llm.ToolCall `json:"call"` UpdatedAt time.Time `json:"updated_at"` } ) if _, err := svc.dbr.SelectConversationIdUserId( ctx, id, "default", ); err != nil { if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } log.ErrorContext( ctx, "failed to load conversation", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } c, err := svc.getConversation(ctx, id) if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } if err != nil { log.ErrorContext( ctx, "failed to get conversation handle", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } r.Body = http.MaxBytesReader(w, r.Body, 32<<20) if err := json.UnmarshalRead(r.Body, &req); err != nil { log.ErrorContext( ctx, "failed to decode request", slog.Any("error", err), ) http.Error(w, "invalid request", http.StatusBadRequest) return } if req.Call == nil || req.UpdatedAt.IsZero() { http.Error(w, "invalid request", http.StatusBadRequest) return } saved, err := c.AddToolCall(ctx, mid, req.UpdatedAt, *req.Call) if err != nil { switch { case errors.Is(err, conversation.ErrBusy), errors.Is(err, conversation.ErrRevisionConflict): http.Error( w, http.StatusText(http.StatusConflict), http.StatusConflict, ) case errors.Is(err, sql.ErrNoRows): http.NotFound(w, r) default: log.ErrorContext( ctx, "failed to add tool call", slog.Any("error", err), ) http.Error(w, err.Error(), http.StatusBadRequest) } return } w.Header().Set("Content-Type", "application/json") if err := json.MarshalWrite(w, saved); err != nil { log.ErrorContext( ctx, "failed to encode message response", slog.Any("error", err), ) } } func (svc *Service) updateToolCall(w http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() log = ctx.Value(logKey).(*slog.Logger) id = r.PathValue("id") mid = r.PathValue("mid") cid = r.PathValue("cid") req struct { Call *llm.ToolCall `json:"call"` UpdatedAt time.Time `json:"updated_at"` } ) if _, err := svc.dbr.SelectConversationIdUserId( ctx, id, "default", ); err != nil { if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } log.ErrorContext( ctx, "failed to load conversation", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } c, err := svc.getConversation(ctx, id) if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } if err != nil { log.ErrorContext( ctx, "failed to get conversation handle", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } r.Body = http.MaxBytesReader(w, r.Body, 32<<20) if err := json.UnmarshalRead(r.Body, &req); err != nil { log.ErrorContext( ctx, "failed to decode request", slog.Any("error", err), ) http.Error(w, "invalid request", http.StatusBadRequest) return } if req.Call == nil || req.UpdatedAt.IsZero() || req.Call.ID != cid { http.Error(w, "invalid request", http.StatusBadRequest) return } saved, err := c.UpdateToolCall( ctx, mid, cid, req.UpdatedAt, *req.Call, ) if err != nil { switch { case errors.Is(err, conversation.ErrBusy), errors.Is(err, conversation.ErrRevisionConflict): http.Error( w, http.StatusText(http.StatusConflict), http.StatusConflict, ) case errors.Is(err, sql.ErrNoRows): http.NotFound(w, r) default: log.ErrorContext( ctx, "failed to update tool call", slog.Any("error", err), ) http.Error(w, err.Error(), http.StatusBadRequest) } return } w.Header().Set("Content-Type", "application/json") if err := json.MarshalWrite(w, saved); err != nil { log.ErrorContext( ctx, "failed to encode message response", slog.Any("error", err), ) } } func (svc *Service) deleteToolCall(w http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() log = ctx.Value(logKey).(*slog.Logger) id = r.PathValue("id") mid = r.PathValue("mid") cid = r.PathValue("cid") req struct { UpdatedAt time.Time `json:"updated_at"` } ) if _, err := svc.dbr.SelectConversationIdUserId( ctx, id, "default", ); err != nil { if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } log.ErrorContext( ctx, "failed to load conversation", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } c, err := svc.getConversation(ctx, id) if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) return } if err != nil { log.ErrorContext( ctx, "failed to get conversation handle", slog.Any("error", err), ) http.Error( w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, ) return } r.Body = http.MaxBytesReader(w, r.Body, 32<<20) if err := json.UnmarshalRead(r.Body, &req); err != nil { log.ErrorContext( ctx, "failed to decode request", slog.Any("error", err), ) http.Error(w, "invalid request", http.StatusBadRequest) return } if req.UpdatedAt.IsZero() { http.Error(w, "invalid request", http.StatusBadRequest) return } if err := c.DeleteToolCall(ctx, mid, cid, req.UpdatedAt); err != nil { switch { case errors.Is(err, conversation.ErrBusy), errors.Is(err, conversation.ErrRevisionConflict): http.Error( w, http.StatusText(http.StatusConflict), http.StatusConflict, ) case errors.Is(err, sql.ErrNoRows): http.NotFound(w, r) default: log.ErrorContext( ctx, "failed to delete tool call", slog.Any("error", err), ) http.Error(w, err.Error(), http.StatusBadRequest) } return } w.WriteHeader(http.StatusNoContent) }