96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/compaction"
|
|
"git.gobha.me/xcaliber/chat-switchboard/database"
|
|
"git.gobha.me/xcaliber/chat-switchboard/roles"
|
|
)
|
|
|
|
// SummarizeHandler handles manual conversation summarization via HTTP.
|
|
type SummarizeHandler struct {
|
|
compaction *compaction.Service
|
|
}
|
|
|
|
// NewSummarizeHandler creates a new handler backed by the compaction service.
|
|
func NewSummarizeHandler(svc *compaction.Service) *SummarizeHandler {
|
|
return &SummarizeHandler{compaction: svc}
|
|
}
|
|
|
|
// ── Summarize & Continue ──────────────────
|
|
// POST /channels/:id/summarize
|
|
//
|
|
// User-triggered compaction: calls the utility role to summarize the
|
|
// conversation history, inserts the summary as a tree node, and returns it.
|
|
|
|
func (h *SummarizeHandler) Summarize(c *gin.Context) {
|
|
channelID := c.Param("id")
|
|
userID := getUserID(c)
|
|
|
|
// ── Verify channel ownership ──
|
|
var ownerID string
|
|
err := database.DB.QueryRow(
|
|
database.Q(`SELECT user_id FROM channels WHERE id = $1`), channelID,
|
|
).Scan(&ownerID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
|
|
return
|
|
}
|
|
if ownerID != userID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "not your channel"})
|
|
return
|
|
}
|
|
|
|
// ── Check utility role is configured ──
|
|
resolver := h.compaction.Resolver()
|
|
if !resolver.IsConfigured(c.Request.Context(), roles.RoleUtility) {
|
|
if !resolver.IsPersonalOverride(c.Request.Context(), userID, roles.RoleUtility) {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "Utility model role is not configured. Ask your admin to set one, or configure your own in Settings → Model Roles.",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
// ── Rate limiting (org-funded calls only) ──
|
|
isPersonal := resolver.IsPersonalOverride(c.Request.Context(), userID, roles.RoleUtility)
|
|
if !isPersonal {
|
|
if err := h.compaction.CheckRateLimit(c.Request.Context(), userID); err != nil {
|
|
c.JSON(http.StatusTooManyRequests, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
// ── Compact ──
|
|
teamID := h.compaction.GetUserTeamID(c.Request.Context(), userID)
|
|
result, err := h.compaction.Compact(c.Request.Context(), compaction.CompactRequest{
|
|
ChannelID: channelID,
|
|
UserID: userID,
|
|
TeamID: teamID,
|
|
Trigger: "manual",
|
|
})
|
|
if err != nil {
|
|
// Map specific errors to HTTP status codes
|
|
switch err.Error() {
|
|
case "conversation too short to summarize":
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
case "not enough new messages since last summary":
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
default:
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
}
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"summary_id": result.SummaryID,
|
|
"summarized_count": result.SummarizedCount,
|
|
"model": result.Model,
|
|
"used_fallback": result.UsedFallback,
|
|
"content": result.Content,
|
|
})
|
|
}
|