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/title.go
Jeffrey Smith 11fd8c1e57 step 1: rename module chat-switchboard → switchboard-core
- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
2026-03-25 19:48:04 -04:00

111 lines
2.8 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"switchboard-core/providers"
"switchboard-core/roles"
"switchboard-core/store"
)
// TitleHandler generates chat titles using the utility role.
type TitleHandler struct {
stores store.Stores
resolver *roles.Resolver
}
// NewTitleHandler creates a title generation handler.
func NewTitleHandler(s store.Stores, r *roles.Resolver) *TitleHandler {
return &TitleHandler{stores: s, resolver: r}
}
// GenerateTitle generates a title for a channel using the utility role.
// POST /channels/:id/generate-title
func (h *TitleHandler) GenerateTitle(c *gin.Context) {
channelID := c.Param("id")
userID := c.GetString("user_id")
teamID := c.GetString("team_id")
// Verify channel access
owns, err := h.stores.Channels.UserOwns(c.Request.Context(), channelID, userID)
if err != nil || !owns {
c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
return
}
ch, err := h.stores.Channels.GetByID(c.Request.Context(), channelID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
return
}
// Load first few messages
msgs, err := h.stores.Messages.ListForChannel(c.Request.Context(), channelID, store.ListOptions{
Limit: 4, Order: "asc",
})
if err != nil || len(msgs) == 0 {
c.JSON(http.StatusOK, gin.H{"title": ch.Title}) // keep existing
return
}
// Build a snippet from the first messages
var snippet strings.Builder
for _, m := range msgs {
content := m.Content
if len(content) > 200 {
content = content[:200] + "…"
}
fmt.Fprintf(&snippet, "%s: %s\n", m.Role, content)
}
// Call utility role
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
var tID *string
if teamID != "" {
tID = &teamID
}
result, err := h.resolver.Complete(ctx, roles.RoleUtility, userID, tID, []providers.Message{
{
Role: "system",
Content: "You are a title generator. Given a conversation snippet, produce a concise title of 6 words or fewer. Respond with ONLY the title, no quotes, no punctuation at the end, no explanation.",
},
{
Role: "user",
Content: "Title this conversation:\n\n" + snippet.String(),
},
})
if err != nil {
// Fallback: truncate first user message
c.JSON(http.StatusOK, gin.H{"title": ch.Title})
return
}
title := strings.TrimSpace(result.Content)
// Strip surrounding quotes if the model added them
title = strings.Trim(title, "\"'`\u201c\u201d\u2018\u2019")
if title == "" {
c.JSON(http.StatusOK, gin.H{"title": ch.Title})
return
}
// Cap at 100 chars
if len(title) > 100 {
title = title[:100]
}
// Update channel
_ = h.stores.Channels.Update(c.Request.Context(), channelID, map[string]interface{}{
"title": title,
})
c.JSON(http.StatusOK, gin.H{"title": title})
}