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

@@ -137,7 +137,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
})
// Persist user message
if _, err := h.persistMessage(channelID, userID, "user", req.Content, "", 0, 0, nil); err != nil {
if _, err := h.persistMessage(channelID, userID, "user", req.Content, "", 0, 0, nil, nil); err != nil {
log.Printf("Failed to persist user message: %v", err)
}
@@ -210,159 +210,11 @@ func (h *CompletionHandler) streamCompletion(
req providers.CompletionRequest,
channelID, userID, model string,
) {
// Set SSE headers
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no") // Disable nginx buffering
result := streamWithToolLoop(c, provider, cfg, &req, model, userID, channelID)
c.Status(http.StatusOK)
flusher, _ := c.Writer.(http.Flusher)
flush := func() {
if flusher != nil {
flusher.Flush()
}
}
// sendSSE sends a standard OpenAI-compatible SSE data line.
sendSSE := func(data string) {
fmt.Fprintf(c.Writer, "data: %s\n\n", data)
flush()
}
// sendEvent sends a named SSE event (for tool status — non-standard).
sendEvent := func(event, data string) {
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, data)
flush()
}
var fullContent string
for iteration := 0; iteration < maxToolIterations; iteration++ {
ch, err := provider.StreamCompletion(c.Request.Context(), cfg, req)
if err != nil {
sendSSE(fmt.Sprintf(`{"error":"%s"}`, escapeJSON(err.Error())))
return
}
var iterContent string
var toolCalls []providers.ToolCall
for event := range ch {
if event.Error != nil {
sendSSE(fmt.Sprintf(`{"error":"%s"}`, escapeJSON(event.Error.Error())))
return
}
// Stream text deltas to client
if event.Delta != "" {
iterContent += event.Delta
sendSSE(fmt.Sprintf(
`{"choices":[{"delta":{"content":%q},"finish_reason":null}],"model":%q}`,
event.Delta, model))
}
if event.Done {
if event.FinishReason == "tool_calls" && len(event.ToolCalls) > 0 {
toolCalls = event.ToolCalls
} else {
// Normal completion — send finish event
finishReason := event.FinishReason
if finishReason == "" {
finishReason = "stop"
}
fullContent += iterContent
sendSSE(fmt.Sprintf(
`{"choices":[{"delta":{},"finish_reason":%q}],"model":%q}`,
finishReason, model))
sendSSE("[DONE]")
// Persist assistant response
if fullContent != "" {
if _, err := h.persistMessage(channelID, userID, "assistant", fullContent, model, 0, 0, nil); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
}
return
}
break
}
}
// ── Tool execution round ────────────────
if len(toolCalls) == 0 {
// Stream ended without tool calls or finish — shouldn't happen
sendSSE("[DONE]")
if iterContent != "" {
fullContent += iterContent
if _, err := h.persistMessage(channelID, userID, "assistant", fullContent, model, 0, 0, nil); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
}
return
}
// Notify client about tool calls
toolCallsJSON, _ := json.Marshal(toolCalls)
sendEvent("tool_use", string(toolCallsJSON))
// Append assistant message (with tool_calls, possibly with text) to conversation
assistantMsg := providers.Message{
Role: "assistant",
Content: iterContent,
}
for _, tc := range toolCalls {
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, tc)
}
req.Messages = append(req.Messages, assistantMsg)
// Execute all tool calls
execCtx := tools.ExecutionContext{
UserID: userID,
ChannelID: channelID,
}
for _, tc := range toolCalls {
call := tools.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
}
log.Printf("🔧 Executing tool: %s (call %s)", call.Name, call.ID)
result := tools.ExecuteCall(c.Request.Context(), execCtx, call)
// Notify client about tool result
resultJSON, _ := json.Marshal(map[string]interface{}{
"tool_call_id": result.ToolCallID,
"name": result.Name,
"content": result.Content,
"is_error": result.IsError,
})
sendEvent("tool_result", string(resultJSON))
// Append tool result to conversation for next iteration
req.Messages = append(req.Messages, providers.Message{
Role: "tool",
Content: result.Content,
ToolCallID: result.ToolCallID,
Name: result.Name,
})
}
fullContent += iterContent
// Loop: send updated messages back to provider
}
// If we hit max iterations, send what we have
log.Printf("⚠️ Tool loop hit max iterations (%d) for channel %s", maxToolIterations, channelID)
sendSSE(fmt.Sprintf(
`{"choices":[{"delta":{"content":"%s"},"finish_reason":"stop"}],"model":%q}`,
escapeJSON("[Tool execution limit reached]"), model))
sendSSE("[DONE]")
if fullContent != "" {
if _, err := h.persistMessage(channelID, userID, "assistant", fullContent, model, 0, 0, nil); err != nil {
// Persist assistant response
if result.Content != "" {
if _, err := h.persistMessage(channelID, userID, "assistant", result.Content, model, 0, 0, nil, toolActivityJSON(result.ToolActivity)); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
}
@@ -379,6 +231,7 @@ func (h *CompletionHandler) syncCompletion(
) {
var totalInput, totalOutput int
var finalContent string
var allToolActivity []map[string]interface{}
for iteration := 0; iteration < maxToolIterations; iteration++ {
resp, err := provider.ChatCompletion(c.Request.Context(), cfg, req)
@@ -395,7 +248,7 @@ func (h *CompletionHandler) syncCompletion(
finalContent = resp.Content
// Persist assistant response
if _, err := h.persistMessage(channelID, userID, "assistant", finalContent, model, totalInput, totalOutput, nil); err != nil {
if _, err := h.persistMessage(channelID, userID, "assistant", finalContent, model, totalInput, totalOutput, nil, toolActivityJSON(allToolActivity)); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
@@ -443,6 +296,15 @@ func (h *CompletionHandler) syncCompletion(
log.Printf("🔧 Executing tool: %s (call %s)", call.Name, call.ID)
result := tools.ExecuteCall(c.Request.Context(), execCtx, call)
// Collect for persistence
allToolActivity = append(allToolActivity, map[string]interface{}{
"id": tc.ID,
"name": tc.Function.Name,
"arguments": tc.Function.Arguments,
"result": result.Content,
"is_error": result.IsError,
})
req.Messages = append(req.Messages, providers.Message{
Role: "tool",
Content: result.Content,
@@ -632,7 +494,16 @@ func (h *CompletionHandler) loadConversation(channelID, userID, presetSystemProm
// - parentOverride (explicit parent for edit/regen operations)
// - cursor's active_leaf_id (normal conversation flow)
// - latest message by created_at (fallback for legacy/missing cursor)
func (h *CompletionHandler) persistMessage(channelID, userID, role, content, model string, inputTokens, outputTokens int, parentOverride *string) (string, error) {
// toolActivityJSON marshals tool activity for persistence. Returns nil if empty.
func toolActivityJSON(activity []map[string]interface{}) json.RawMessage {
if len(activity) == 0 {
return nil
}
b, _ := json.Marshal(activity)
return b
}
func (h *CompletionHandler) persistMessage(channelID, userID, role, content, model string, inputTokens, outputTokens int, parentOverride *string, toolCallsJSON json.RawMessage) (string, error) {
var tokensUsed *int
if inputTokens > 0 || outputTokens > 0 {
total := inputTokens + outputTokens
@@ -658,15 +529,21 @@ func (h *CompletionHandler) persistMessage(channelID, userID, role, content, mod
participantID = model
}
// Prepare tool_calls JSONB (nil → NULL)
var tcVal interface{}
if len(toolCallsJSON) > 0 {
tcVal = string(toolCallsJSON)
}
// Insert with RETURNING id
var newID string
err := database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, model, tokens_used,
parent_id, participant_type, participant_id, sibling_index)
VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8, $9)
tool_calls, parent_id, participant_type, participant_id, sibling_index)
VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8, $9, $10)
RETURNING id
`, channelID, role, content, model, tokensUsed,
parentID, participantType, participantID, siblingIdx,
tcVal, parentID, participantType, participantID, siblingIdx,
).Scan(&newID)
if err != nil {
return "", err