- 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)
111 lines
2.8 KiB
Go
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})
|
|
}
|