Changeset 0.9.3 (#53)

This commit is contained in:
2026-02-23 23:55:22 +00:00
parent ac7c7c7d59
commit 90021157e6
20 changed files with 897 additions and 414 deletions

View File

@@ -4,7 +4,6 @@ import (
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
@@ -14,6 +13,7 @@ import (
capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/providers"
"git.gobha.me/xcaliber/chat-switchboard/tools"
)
// ── Request / Response types ────────────────
@@ -443,74 +443,43 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
provReq.Temperature = temperature
}
// ── Stream the response ──
ch, err := provider.StreamCompletion(c.Request.Context(), providerCfg, provReq)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "provider error: " + err.Error()})
return
// Attach tool definitions (same as normal completion)
if caps.ToolCalling && tools.HasTools() {
provReq.Tools = comp.buildToolDefs()
}
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
c.Status(http.StatusOK)
// ── Stream the response (shared loop handles tools, reasoning, SSE) ──
var fullContent string
flusher, _ := c.Writer.(http.Flusher)
result := streamWithToolLoop(c, provider, providerCfg, &provReq, model, userID, channelID)
for event := range ch {
if event.Error != nil {
fmt.Fprintf(c.Writer, "data: {\"error\":\"%s\"}\n\n", event.Error.Error())
if flusher != nil {
flusher.Flush()
}
break
}
if event.Delta != "" {
fullContent += event.Delta
fmt.Fprintf(c.Writer, "data: {\"choices\":[{\"delta\":{\"content\":%q},\"finish_reason\":null}],\"model\":%q}\n\n",
event.Delta, model)
if flusher != nil {
flusher.Flush()
}
}
if event.Done {
finishReason := event.FinishReason
if finishReason == "" {
finishReason = "stop"
}
fmt.Fprintf(c.Writer, "data: {\"choices\":[{\"delta\":{},\"finish_reason\":%q}],\"model\":%q}\n\n",
finishReason, model)
io.WriteString(c.Writer, "data: [DONE]\n\n")
if flusher != nil {
flusher.Flush()
}
break
}
}
// Persist as child or sibling depending on target role
if fullContent != "" {
// Persist as sibling (regen) with tool activity
if result.Content != "" {
siblingIdx := nextSiblingIndex(channelID, newParentID)
// Match persistMessage pattern: nil interface{} → SQL NULL,
// non-nil string → valid JSONB. A nil json.RawMessage ([]byte)
// is sent by pq as empty bytes, not NULL, causing "invalid input
// syntax for type json".
var tcVal interface{}
if len(result.ToolActivity) > 0 {
b, _ := json.Marshal(result.ToolActivity)
tcVal = string(b)
}
var newID string
err := database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, model,
INSERT INTO messages (channel_id, role, content, model, tool_calls,
parent_id, participant_type, participant_id, sibling_index)
VALUES ($1, 'assistant', $2, $3, $4, 'model', $5, $6)
VALUES ($1, 'assistant', $2, $3, $4, $5, 'model', $6, $7)
RETURNING id
`, channelID, fullContent, model, newParentID, model, siblingIdx).Scan(&newID)
`, channelID, result.Content, model, tcVal, newParentID, model, siblingIdx).Scan(&newID)
if err != nil {
log.Printf("Failed to persist regenerated message: %v", err)
} else {
_ = updateCursor(channelID, userID, newID)
// Send message metadata in a final SSE event so frontend can update
flusher, _ := c.Writer.(http.Flusher)
msgJSON, _ := json.Marshal(gin.H{
"id": newID,
"parent_id": newParentID,