This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/summarize.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(
`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,
})
}